mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Simplify async hook dispatch
This commit is contained in:
@@ -6,6 +6,7 @@ use std::sync::MutexGuard;
|
||||
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::approx_bytes_for_tokens;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
use codex_utils_output_truncation::formatted_truncate_text;
|
||||
use codex_utils_output_truncation::truncate_text;
|
||||
@@ -46,15 +47,13 @@ struct AsyncCommandState {
|
||||
tasks: TaskTracker,
|
||||
}
|
||||
|
||||
/// A rendered snapshot of the oldest deliverable completions in the queue.
|
||||
/// Single-use marker for the completions ready before a real user turn began.
|
||||
///
|
||||
/// Preparing a batch does not consume it. The caller commits it only after the
|
||||
/// synchronous `UserPromptSubmit` lane accepts the turn, so a blocked prompt
|
||||
/// cannot lose completed async output.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct AsyncOutputBatch {
|
||||
/// Delivery is single-consumer, and producers only append between capturing and
|
||||
/// flushing a boundary. Therefore the marked prefix cannot move or shrink.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct AsyncOutputBoundary {
|
||||
completion_count: usize,
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl AsyncCommandRuntime {
|
||||
@@ -122,36 +121,66 @@ impl AsyncCommandRuntime {
|
||||
.push_back(AsyncCommandCompletion { event_name, text });
|
||||
}
|
||||
|
||||
/// Prepares the oldest contiguous queue prefix that fits the per-turn flush budget.
|
||||
///
|
||||
/// The selected completions remain queued until [`Self::commit`] is called.
|
||||
/// Later completions stay in FIFO order for a subsequent real user turn.
|
||||
pub(crate) fn prepare_batch(&self) -> Option<AsyncOutputBatch> {
|
||||
let pending = self.lock_pending();
|
||||
let mut selected = Vec::new();
|
||||
for completion in pending.iter() {
|
||||
selected.push(completion.clone());
|
||||
// Measure the fully rendered developer injection so wrapper overhead
|
||||
// counts toward the flush budget. The first item that does not fit,
|
||||
// and every item after it, remains queued.
|
||||
let text = render_batch(&selected);
|
||||
if approx_token_count(&text) > ASYNC_HOOK_FLUSH_TOKEN_LIMIT {
|
||||
selected.pop();
|
||||
break;
|
||||
}
|
||||
/// Captures the queue boundary before the current user turn can spawn more hooks.
|
||||
pub(super) fn ready_boundary(&self) -> AsyncOutputBoundary {
|
||||
AsyncOutputBoundary {
|
||||
completion_count: self.lock_pending().len(),
|
||||
}
|
||||
(!selected.is_empty()).then(|| AsyncOutputBatch {
|
||||
completion_count: selected.len(),
|
||||
text: render_batch(&selected),
|
||||
})
|
||||
}
|
||||
|
||||
/// Consumes a prepared prefix and returns its merged developer-context payload.
|
||||
pub(crate) fn commit(&self, batch: AsyncOutputBatch) -> String {
|
||||
// Producers only append, so completions arriving after preparation cannot
|
||||
// disturb the prefix identified by `completion_count`.
|
||||
self.lock_pending().drain(..batch.completion_count);
|
||||
batch.text
|
||||
/// Flushes the bounded FIFO prefix that was ready at `boundary`.
|
||||
///
|
||||
/// Callers invoke this only after the real user turn is accepted. Completions
|
||||
/// appended after the boundary, including hooks fired by the current turn,
|
||||
/// remain queued for a later turn.
|
||||
pub(super) fn flush_through(&self, boundary: AsyncOutputBoundary) -> Option<String> {
|
||||
let mut pending = self.lock_pending();
|
||||
debug_assert!(
|
||||
boundary.completion_count <= pending.len(),
|
||||
"async output can only be appended between boundary capture and flush"
|
||||
);
|
||||
// Keep release builds resilient if the single-consumer contract is ever
|
||||
// violated rather than allowing an out-of-bounds prefix.
|
||||
let eligible_count = boundary.completion_count.min(pending.len());
|
||||
let max_bytes = approx_bytes_for_tokens(ASYNC_HOOK_FLUSH_TOKEN_LIMIT);
|
||||
let closing_tag = "\n</async_hook_outputs>";
|
||||
let mut completion_count = 0;
|
||||
let mut text = String::from("<async_hook_outputs>\n");
|
||||
|
||||
for completion in pending.iter().take(eligible_count) {
|
||||
let rendered = format!(
|
||||
"<async_hook_output event=\"{:?}\">\n{}\n</async_hook_output>",
|
||||
completion.event_name, completion.text
|
||||
);
|
||||
let separator_len = usize::from(completion_count > 0);
|
||||
|
||||
// Include the outer closing tag in the budget. The first completion
|
||||
// that does not fit, and every completion after it, stays queued.
|
||||
if text
|
||||
.len()
|
||||
.saturating_add(separator_len)
|
||||
.saturating_add(rendered.len())
|
||||
.saturating_add(closing_tag.len())
|
||||
> max_bytes
|
||||
{
|
||||
break;
|
||||
}
|
||||
if completion_count > 0 {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push_str(&rendered);
|
||||
completion_count += 1;
|
||||
}
|
||||
|
||||
if completion_count == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
text.push_str(closing_tag);
|
||||
// Producers only append, so arrivals after the boundary cannot disturb
|
||||
// the prefix selected above.
|
||||
pending.drain(..completion_count);
|
||||
Some(text)
|
||||
}
|
||||
|
||||
/// Cancels in-flight handlers, closes the tracker for waiting, and joins its tasks.
|
||||
@@ -170,21 +199,6 @@ impl AsyncCommandRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders several completed firings as one ordered developer-context injection.
|
||||
fn render_batch(completions: &[AsyncCommandCompletion]) -> String {
|
||||
let outputs = completions
|
||||
.iter()
|
||||
.map(|completion| {
|
||||
format!(
|
||||
"<async_hook_output event=\"{:?}\">\n{}\n</async_hook_output>",
|
||||
completion.event_name, completion.text
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!("<async_hook_outputs>\n{outputs}\n</async_hook_outputs>")
|
||||
}
|
||||
|
||||
/// Converts a command result into informational text suitable for later delivery.
|
||||
///
|
||||
/// Successful output contributes only event-supported informational content.
|
||||
|
||||
@@ -78,22 +78,31 @@ fn async_output_surfaces_parse_and_runtime_failures() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_preserves_duplicate_completions_until_commit() {
|
||||
fn queue_preserves_duplicate_completions_and_holds_arrivals_after_boundary() {
|
||||
let runtime = AsyncCommandRuntime::default();
|
||||
runtime.push(HookEventName::PreToolUse, "same".to_string());
|
||||
runtime.push(HookEventName::PostToolUse, "same".to_string());
|
||||
runtime.push(HookEventName::Stop, "last".to_string());
|
||||
|
||||
let batch = runtime.prepare_batch().expect("queued output batch");
|
||||
assert_eq!(runtime.prepare_batch(), Some(batch.clone()));
|
||||
let text = runtime.commit(batch);
|
||||
let boundary = runtime.ready_boundary();
|
||||
runtime.push(HookEventName::SessionStart, "next-turn".to_string());
|
||||
|
||||
let text = runtime
|
||||
.flush_through(boundary)
|
||||
.expect("queued async output");
|
||||
assert_eq!(text.matches("same").count(), 2);
|
||||
assert!(
|
||||
["PreToolUse", "PostToolUse", "last"]
|
||||
.map(|needle| text.find(needle).expect("queued output"))
|
||||
.is_sorted()
|
||||
);
|
||||
assert!(runtime.prepare_batch().is_none());
|
||||
assert!(!text.contains("next-turn"));
|
||||
|
||||
let text = runtime
|
||||
.flush_through(runtime.ready_boundary())
|
||||
.expect("completion after boundary");
|
||||
assert!(text.contains("next-turn"));
|
||||
assert!(runtime.flush_through(runtime.ready_boundary()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -114,9 +123,10 @@ fn queue_bounds_items_and_flushes_a_contiguous_prefix() {
|
||||
|
||||
let mut completed = 0;
|
||||
loop {
|
||||
let batch = runtime.prepare_batch().expect("bounded output batch");
|
||||
completed += batch.completion_count;
|
||||
let text = runtime.commit(batch);
|
||||
let text = runtime
|
||||
.flush_through(runtime.ready_boundary())
|
||||
.expect("bounded async output");
|
||||
completed += text.matches("<async_hook_output event=").count();
|
||||
assert!(approx_token_count(&text) <= ASYNC_HOOK_FLUSH_TOKEN_LIMIT);
|
||||
if let Some(tail) = text.find("small-tail") {
|
||||
if let Some(large) = text.find("tokens truncated") {
|
||||
@@ -126,7 +136,7 @@ fn queue_bounds_items_and_flushes_a_contiguous_prefix() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(runtime.prepare_batch().is_none());
|
||||
assert!(runtime.flush_through(runtime.ready_boundary()).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::Path;
|
||||
|
||||
use futures::StreamExt;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use serde::Serialize;
|
||||
|
||||
use codex_protocol::protocol::HookCompletedEvent;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
@@ -11,9 +12,8 @@ use codex_protocol::protocol::HookRunStatus;
|
||||
use codex_protocol::protocol::HookRunSummary;
|
||||
use codex_protocol::protocol::HookScope;
|
||||
|
||||
use super::CommandShell;
|
||||
use super::ClaudeHooksEngine;
|
||||
use super::ConfiguredHandler;
|
||||
use super::async_output::AsyncCommandRuntime;
|
||||
use super::command_runner::CommandRunResult;
|
||||
use super::command_runner::run_command;
|
||||
use crate::events::common::matches_matcher;
|
||||
@@ -25,27 +25,7 @@ pub(crate) struct ParsedHandler<T> {
|
||||
pub completion_order: usize,
|
||||
}
|
||||
|
||||
pub(crate) fn select_handlers(
|
||||
handlers: &[ConfiguredHandler],
|
||||
event_name: HookEventName,
|
||||
matcher_input: Option<&str>,
|
||||
) -> Vec<ConfiguredHandler> {
|
||||
let matcher_inputs = matcher_input.into_iter().collect::<Vec<_>>();
|
||||
select_handlers_for_matcher_inputs(handlers, event_name, &matcher_inputs)
|
||||
}
|
||||
|
||||
pub(crate) fn select_sync_handlers(
|
||||
handlers: &[ConfiguredHandler],
|
||||
event_name: HookEventName,
|
||||
matcher_input: Option<&str>,
|
||||
) -> Vec<ConfiguredHandler> {
|
||||
select_handlers(handlers, event_name, matcher_input)
|
||||
.into_iter()
|
||||
.filter(|handler| !handler.r#async)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn select_handlers_for_matcher_inputs(
|
||||
fn select_handlers_for_matcher_inputs(
|
||||
handlers: &[ConfiguredHandler],
|
||||
event_name: HookEventName,
|
||||
matcher_inputs: &[&str],
|
||||
@@ -79,18 +59,7 @@ pub(crate) fn select_handlers_for_matcher_inputs(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn select_sync_handlers_for_matcher_inputs(
|
||||
handlers: &[ConfiguredHandler],
|
||||
event_name: HookEventName,
|
||||
matcher_inputs: &[&str],
|
||||
) -> Vec<ConfiguredHandler> {
|
||||
select_handlers_for_matcher_inputs(handlers, event_name, matcher_inputs)
|
||||
.into_iter()
|
||||
.filter(|handler| !handler.r#async)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn running_summary(handler: &ConfiguredHandler) -> HookRunSummary {
|
||||
fn running_summary(handler: &ConfiguredHandler) -> HookRunSummary {
|
||||
HookRunSummary {
|
||||
id: handler.run_id(),
|
||||
event_name: handler.event_name,
|
||||
@@ -109,49 +78,86 @@ pub(crate) fn running_summary(handler: &ConfiguredHandler) -> HookRunSummary {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_handlers<T>(
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
handlers: Vec<ConfiguredHandler>,
|
||||
input_json: Result<String, String>,
|
||||
cwd: &Path,
|
||||
turn_id: Option<String>,
|
||||
parse: fn(&ConfiguredHandler, CommandRunResult, Option<String>) -> ParsedHandler<T>,
|
||||
) -> Vec<ParsedHandler<T>> {
|
||||
let (handlers, asynchronous) = handlers
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|handler| !handler.r#async);
|
||||
for handler in asynchronous {
|
||||
async_runtime.spawn_handler(
|
||||
shell.clone(),
|
||||
handler,
|
||||
input_json.clone(),
|
||||
cwd.to_path_buf(),
|
||||
);
|
||||
impl ClaudeHooksEngine {
|
||||
pub(crate) fn preview_commands(
|
||||
&self,
|
||||
event_name: HookEventName,
|
||||
matcher_inputs: &[&str],
|
||||
) -> Vec<HookRunSummary> {
|
||||
select_handlers_for_matcher_inputs(&self.handlers, event_name, matcher_inputs)
|
||||
.into_iter()
|
||||
.filter(|handler| !handler.r#async)
|
||||
.map(|handler| running_summary(&handler))
|
||||
.collect()
|
||||
}
|
||||
|
||||
let mut pending = FuturesUnordered::new();
|
||||
for (configured_order, handler) in handlers.into_iter().enumerate() {
|
||||
let input_json = input_json.clone();
|
||||
let turn_id = turn_id.clone();
|
||||
pending.push(async move {
|
||||
let result = match input_json {
|
||||
Ok(input_json) => run_command(shell, &handler, &input_json, cwd).await,
|
||||
Err(error) => CommandRunResult::failed(error),
|
||||
};
|
||||
(configured_order, parse(&handler, result, turn_id))
|
||||
});
|
||||
}
|
||||
pub(crate) async fn execute_commands<T, I>(
|
||||
&self,
|
||||
event_name: HookEventName,
|
||||
matcher_inputs: &[&str],
|
||||
input: &I,
|
||||
cwd: &Path,
|
||||
turn_id: Option<String>,
|
||||
parse: fn(&ConfiguredHandler, CommandRunResult, Option<String>) -> ParsedHandler<T>,
|
||||
) -> Vec<ParsedHandler<T>>
|
||||
where
|
||||
I: Serialize + ?Sized,
|
||||
{
|
||||
let handlers =
|
||||
select_handlers_for_matcher_inputs(&self.handlers, event_name, matcher_inputs);
|
||||
if handlers.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut completed = Vec::new();
|
||||
let mut completion_order = 0;
|
||||
while let Some((configured_order, mut parsed)) = pending.next().await {
|
||||
parsed.completion_order = completion_order;
|
||||
completion_order += 1;
|
||||
completed.push((configured_order, parsed));
|
||||
let input_label = match event_name {
|
||||
HookEventName::PreToolUse => "pre tool use",
|
||||
HookEventName::PermissionRequest => "permission request",
|
||||
HookEventName::PostToolUse => "post tool use",
|
||||
HookEventName::PreCompact => "pre compact",
|
||||
HookEventName::PostCompact => "post compact",
|
||||
HookEventName::SessionStart => "session start",
|
||||
HookEventName::UserPromptSubmit => "user prompt submit",
|
||||
HookEventName::SubagentStart => "subagent start",
|
||||
HookEventName::SubagentStop => "subagent stop",
|
||||
HookEventName::Stop => "stop",
|
||||
};
|
||||
let input_json = serde_json::to_string(input)
|
||||
.map_err(|error| format!("failed to serialize {input_label} hook input: {error}"));
|
||||
let (handlers, asynchronous) = handlers
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|handler| !handler.r#async);
|
||||
for handler in asynchronous {
|
||||
self.async_runtime.spawn_handler(
|
||||
self.shell.clone(),
|
||||
handler,
|
||||
input_json.clone(),
|
||||
cwd.to_path_buf(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut pending = FuturesUnordered::new();
|
||||
for (configured_order, handler) in handlers.into_iter().enumerate() {
|
||||
let input_json = input_json.clone();
|
||||
let turn_id = turn_id.clone();
|
||||
pending.push(async move {
|
||||
let result = match input_json {
|
||||
Ok(input_json) => run_command(&self.shell, &handler, &input_json, cwd).await,
|
||||
Err(error) => CommandRunResult::failed(error),
|
||||
};
|
||||
(configured_order, parse(&handler, result, turn_id))
|
||||
});
|
||||
}
|
||||
|
||||
let mut completed = Vec::new();
|
||||
let mut completion_order = 0;
|
||||
while let Some((configured_order, mut parsed)) = pending.next().await {
|
||||
parsed.completion_order = completion_order;
|
||||
completion_order += 1;
|
||||
completed.push((configured_order, parsed));
|
||||
}
|
||||
completed.sort_by_key(|(configured_order, _)| *configured_order);
|
||||
completed.into_iter().map(|(_, parsed)| parsed).collect()
|
||||
}
|
||||
completed.sort_by_key(|(configured_order, _)| *configured_order);
|
||||
completed.into_iter().map(|(_, parsed)| parsed).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn completed_summary(
|
||||
@@ -202,16 +208,19 @@ mod tests {
|
||||
use codex_protocol::protocol::HookSource;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use serde::ser::Error as _;
|
||||
|
||||
use super::ClaudeHooksEngine;
|
||||
use super::CommandRunResult;
|
||||
use super::CommandShell;
|
||||
use super::ConfiguredHandler;
|
||||
use super::ParsedHandler;
|
||||
use super::completed_summary;
|
||||
use super::execute_handlers;
|
||||
use super::select_handlers;
|
||||
use super::select_handlers_for_matcher_inputs;
|
||||
use crate::engine::CommandShell;
|
||||
use crate::engine::async_output::AsyncCommandRuntime;
|
||||
use crate::output_spill::HookOutputSpiller;
|
||||
|
||||
fn make_handler(
|
||||
event_name: HookEventName,
|
||||
@@ -233,6 +242,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn engine_with_handlers(
|
||||
handlers: Vec<ConfiguredHandler>,
|
||||
async_runtime: AsyncCommandRuntime,
|
||||
) -> ClaudeHooksEngine {
|
||||
ClaudeHooksEngine {
|
||||
handlers,
|
||||
warnings: Vec::new(),
|
||||
shell: CommandShell {
|
||||
program: String::new(),
|
||||
args: Vec::new(),
|
||||
},
|
||||
async_runtime,
|
||||
output_spiller: HookOutputSpiller::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_handlers_keeps_duplicate_stop_handlers() {
|
||||
let handlers = vec![
|
||||
@@ -250,7 +275,7 @@ mod tests {
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers(&handlers, HookEventName::Stop, /*matcher_input*/ None);
|
||||
let selected = select_handlers_for_matcher_inputs(&handlers, HookEventName::Stop, &[]);
|
||||
|
||||
assert_eq!(selected.len(), 2);
|
||||
assert_eq!(selected[0].display_order, 0);
|
||||
@@ -274,7 +299,11 @@ mod tests {
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers(&handlers, HookEventName::SessionStart, Some("startup"));
|
||||
let selected = select_handlers_for_matcher_inputs(
|
||||
&handlers,
|
||||
HookEventName::SessionStart,
|
||||
&["startup"],
|
||||
);
|
||||
|
||||
assert_eq!(selected.len(), 2);
|
||||
assert_eq!(selected[0].display_order, 0);
|
||||
@@ -298,7 +327,8 @@ mod tests {
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers(&handlers, HookEventName::PreCompact, Some("manual"));
|
||||
let selected =
|
||||
select_handlers_for_matcher_inputs(&handlers, HookEventName::PreCompact, &["manual"]);
|
||||
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].display_order, 0);
|
||||
@@ -321,7 +351,8 @@ mod tests {
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers(&handlers, HookEventName::PreToolUse, Some("Bash"));
|
||||
let selected =
|
||||
select_handlers_for_matcher_inputs(&handlers, HookEventName::PreToolUse, &["Bash"]);
|
||||
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].display_order, 0);
|
||||
@@ -344,7 +375,8 @@ mod tests {
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers(&handlers, HookEventName::PostToolUse, Some("Bash"));
|
||||
let selected =
|
||||
select_handlers_for_matcher_inputs(&handlers, HookEventName::PostToolUse, &["Bash"]);
|
||||
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].display_order, 0);
|
||||
@@ -367,7 +399,8 @@ mod tests {
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers(&handlers, HookEventName::PreToolUse, Some("Bash"));
|
||||
let selected =
|
||||
select_handlers_for_matcher_inputs(&handlers, HookEventName::PreToolUse, &["Bash"]);
|
||||
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].display_order, 0);
|
||||
@@ -382,9 +415,12 @@ mod tests {
|
||||
/*display_order*/ 0,
|
||||
)];
|
||||
|
||||
let selected_edit = select_handlers(&handlers, HookEventName::PreToolUse, Some("Edit"));
|
||||
let selected_write = select_handlers(&handlers, HookEventName::PreToolUse, Some("Write"));
|
||||
let selected_bash = select_handlers(&handlers, HookEventName::PreToolUse, Some("Bash"));
|
||||
let selected_edit =
|
||||
select_handlers_for_matcher_inputs(&handlers, HookEventName::PreToolUse, &["Edit"]);
|
||||
let selected_write =
|
||||
select_handlers_for_matcher_inputs(&handlers, HookEventName::PreToolUse, &["Write"]);
|
||||
let selected_bash =
|
||||
select_handlers_for_matcher_inputs(&handlers, HookEventName::PreToolUse, &["Bash"]);
|
||||
|
||||
assert_eq!(selected_edit.len(), 1);
|
||||
assert_eq!(selected_write.len(), 1);
|
||||
@@ -453,11 +489,8 @@ mod tests {
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers(
|
||||
&handlers,
|
||||
HookEventName::UserPromptSubmit,
|
||||
/*matcher_input*/ None,
|
||||
);
|
||||
let selected =
|
||||
select_handlers_for_matcher_inputs(&handlers, HookEventName::UserPromptSubmit, &[]);
|
||||
|
||||
assert_eq!(selected.len(), 2);
|
||||
assert_eq!(selected[0].display_order, 0);
|
||||
@@ -488,12 +521,9 @@ mod tests {
|
||||
];
|
||||
handlers[1].r#async = true;
|
||||
|
||||
let selected = select_handlers(&handlers, HookEventName::Stop, /*matcher_input*/ None);
|
||||
let synchronous = super::select_sync_handlers(
|
||||
&handlers,
|
||||
HookEventName::Stop,
|
||||
/*matcher_input*/ None,
|
||||
);
|
||||
let selected = select_handlers_for_matcher_inputs(&handlers, HookEventName::Stop, &[]);
|
||||
let engine = engine_with_handlers(handlers, AsyncCommandRuntime::default());
|
||||
let synchronous = engine.preview_commands(HookEventName::Stop, &[]);
|
||||
|
||||
assert_eq!(selected.len(), 3);
|
||||
assert_eq!(selected[0].command, "first");
|
||||
@@ -502,9 +532,9 @@ mod tests {
|
||||
assert_eq!(
|
||||
synchronous
|
||||
.iter()
|
||||
.map(|handler| handler.command.as_str())
|
||||
.map(|run| run.display_order)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["first", "third"],
|
||||
vec![0, 2],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -521,41 +551,55 @@ mod tests {
|
||||
asynchronous.display_order = 1;
|
||||
asynchronous.r#async = true;
|
||||
let runtime = AsyncCommandRuntime::default();
|
||||
let engine = engine_with_handlers(vec![synchronous, asynchronous], runtime.clone());
|
||||
let cwd = test_path_buf("/tmp").abs();
|
||||
|
||||
let results = execute_handlers(
|
||||
&CommandShell {
|
||||
program: String::new(),
|
||||
args: Vec::new(),
|
||||
},
|
||||
&runtime,
|
||||
vec![synchronous, asynchronous],
|
||||
Err("serialize failed".to_string()),
|
||||
cwd.as_path(),
|
||||
Some("turn-1".to_string()),
|
||||
parse_failure,
|
||||
)
|
||||
.await;
|
||||
let results = engine
|
||||
.execute_commands(
|
||||
HookEventName::PreToolUse,
|
||||
&["Bash"],
|
||||
&SerializationFailure,
|
||||
cwd.as_path(),
|
||||
Some("turn-1".to_string()),
|
||||
parse_failure,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].completed.run.status, HookRunStatus::Failed);
|
||||
assert_eq!(results[0].data, "serialize failed");
|
||||
let batch = tokio::time::timeout(Duration::from_secs(1), async {
|
||||
assert_eq!(
|
||||
results[0].data,
|
||||
"failed to serialize pre tool use hook input: serialize failed"
|
||||
);
|
||||
let output = tokio::time::timeout(Duration::from_secs(1), async {
|
||||
loop {
|
||||
if let Some(batch) = runtime.prepare_batch() {
|
||||
break batch;
|
||||
let boundary = runtime.ready_boundary();
|
||||
if let Some(output) = runtime.flush_through(boundary) {
|
||||
break output;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("async serialization failure completion");
|
||||
let output = runtime.commit(batch);
|
||||
assert!(output.contains("Async hook failed to run: serialize failed"));
|
||||
assert!(output.contains(
|
||||
"Async hook failed to run: failed to serialize pre tool use hook input: serialize failed"
|
||||
));
|
||||
assert!(output.contains("event=\"PreToolUse\""));
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
|
||||
struct SerializationFailure;
|
||||
|
||||
impl Serialize for SerializationFailure {
|
||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Err(S::Error::custom("serialize failed"))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_failure(
|
||||
handler: &ConfiguredHandler,
|
||||
run_result: CommandRunResult,
|
||||
|
||||
@@ -196,25 +196,25 @@ impl ClaudeHooksEngine {
|
||||
&self,
|
||||
request: &SessionStartRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
crate::events::session_start::preview(&self.handlers, request)
|
||||
crate::events::session_start::preview(self, request)
|
||||
}
|
||||
|
||||
pub(crate) fn preview_pre_tool_use(&self, request: &PreToolUseRequest) -> Vec<HookRunSummary> {
|
||||
crate::events::pre_tool_use::preview(&self.handlers, request)
|
||||
crate::events::pre_tool_use::preview(self, request)
|
||||
}
|
||||
|
||||
pub(crate) fn preview_permission_request(
|
||||
&self,
|
||||
request: &PermissionRequestRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
crate::events::permission_request::preview(&self.handlers, request)
|
||||
crate::events::permission_request::preview(self, request)
|
||||
}
|
||||
|
||||
pub(crate) fn preview_post_tool_use(
|
||||
&self,
|
||||
request: &PostToolUseRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
crate::events::post_tool_use::preview(&self.handlers, request)
|
||||
crate::events::post_tool_use::preview(self, request)
|
||||
}
|
||||
|
||||
pub(crate) async fn run_session_start(
|
||||
@@ -223,14 +223,7 @@ impl ClaudeHooksEngine {
|
||||
turn_id: Option<String>,
|
||||
) -> SessionStartOutcome {
|
||||
let session_id = request.session_id;
|
||||
let mut outcome = crate::events::session_start::run(
|
||||
&self.handlers,
|
||||
&self.shell,
|
||||
&self.async_runtime,
|
||||
request,
|
||||
turn_id,
|
||||
)
|
||||
.await;
|
||||
let mut outcome = crate::events::session_start::run(self, request, turn_id).await;
|
||||
outcome.additional_contexts = self
|
||||
.maybe_spill_texts(session_id, outcome.additional_contexts)
|
||||
.await;
|
||||
@@ -239,13 +232,7 @@ impl ClaudeHooksEngine {
|
||||
|
||||
pub(crate) async fn run_pre_tool_use(&self, request: PreToolUseRequest) -> PreToolUseOutcome {
|
||||
let session_id = request.session_id;
|
||||
let mut outcome = crate::events::pre_tool_use::run(
|
||||
&self.handlers,
|
||||
&self.shell,
|
||||
&self.async_runtime,
|
||||
request,
|
||||
)
|
||||
.await;
|
||||
let mut outcome = crate::events::pre_tool_use::run(self, request).await;
|
||||
outcome.additional_contexts = self
|
||||
.maybe_spill_texts(session_id, outcome.additional_contexts)
|
||||
.await;
|
||||
@@ -256,13 +243,7 @@ impl ClaudeHooksEngine {
|
||||
&self,
|
||||
request: PermissionRequestRequest,
|
||||
) -> PermissionRequestOutcome {
|
||||
crate::events::permission_request::run(
|
||||
&self.handlers,
|
||||
&self.shell,
|
||||
&self.async_runtime,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
crate::events::permission_request::run(self, request).await
|
||||
}
|
||||
|
||||
pub(crate) async fn run_post_tool_use(
|
||||
@@ -270,13 +251,7 @@ impl ClaudeHooksEngine {
|
||||
request: PostToolUseRequest,
|
||||
) -> PostToolUseOutcome {
|
||||
let session_id = request.session_id;
|
||||
let mut outcome = crate::events::post_tool_use::run(
|
||||
&self.handlers,
|
||||
&self.shell,
|
||||
&self.async_runtime,
|
||||
request,
|
||||
)
|
||||
.await;
|
||||
let mut outcome = crate::events::post_tool_use::run(self, request).await;
|
||||
outcome.additional_contexts = self
|
||||
.maybe_spill_texts(session_id, outcome.additional_contexts)
|
||||
.await;
|
||||
@@ -287,31 +262,29 @@ impl ClaudeHooksEngine {
|
||||
}
|
||||
|
||||
pub(crate) fn preview_pre_compact(&self, request: &PreCompactRequest) -> Vec<HookRunSummary> {
|
||||
crate::events::compact::preview_pre(&self.handlers, request)
|
||||
crate::events::compact::preview_pre(self, request)
|
||||
}
|
||||
|
||||
pub(crate) async fn run_pre_compact(&self, request: PreCompactRequest) -> PreCompactOutcome {
|
||||
crate::events::compact::run_pre(&self.handlers, &self.shell, &self.async_runtime, request)
|
||||
.await
|
||||
crate::events::compact::run_pre(self, request).await
|
||||
}
|
||||
|
||||
pub(crate) fn preview_post_compact(&self, request: &PostCompactRequest) -> Vec<HookRunSummary> {
|
||||
crate::events::compact::preview_post(&self.handlers, request)
|
||||
crate::events::compact::preview_post(self, request)
|
||||
}
|
||||
|
||||
pub(crate) async fn run_post_compact(
|
||||
&self,
|
||||
request: PostCompactRequest,
|
||||
) -> StatelessHookOutcome {
|
||||
crate::events::compact::run_post(&self.handlers, &self.shell, &self.async_runtime, request)
|
||||
.await
|
||||
crate::events::compact::run_post(self, request).await
|
||||
}
|
||||
|
||||
pub(crate) fn preview_user_prompt_submit(
|
||||
&self,
|
||||
request: &UserPromptSubmitRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
crate::events::user_prompt_submit::preview(&self.handlers, request)
|
||||
crate::events::user_prompt_submit::preview(self, request)
|
||||
}
|
||||
|
||||
pub(crate) async fn run_user_prompt_submit(
|
||||
@@ -319,35 +292,26 @@ impl ClaudeHooksEngine {
|
||||
request: UserPromptSubmitRequest,
|
||||
) -> UserPromptSubmitOutcome {
|
||||
let session_id = request.session_id;
|
||||
let async_output_batch = self.async_runtime.prepare_batch();
|
||||
let mut outcome = crate::events::user_prompt_submit::run(
|
||||
&self.handlers,
|
||||
&self.shell,
|
||||
&self.async_runtime,
|
||||
request,
|
||||
)
|
||||
.await;
|
||||
let async_output_boundary = self.async_runtime.ready_boundary();
|
||||
let mut outcome = crate::events::user_prompt_submit::run(self, request).await;
|
||||
outcome.additional_contexts = self
|
||||
.maybe_spill_texts(session_id, outcome.additional_contexts)
|
||||
.await;
|
||||
if !outcome.should_stop
|
||||
&& let Some(batch) = async_output_batch
|
||||
&& let Some(text) = self.async_runtime.flush_through(async_output_boundary)
|
||||
{
|
||||
let text = self.async_runtime.commit(batch);
|
||||
outcome.additional_contexts.insert(0, text);
|
||||
}
|
||||
outcome
|
||||
}
|
||||
|
||||
pub(crate) fn preview_stop(&self, request: &StopRequest) -> Vec<HookRunSummary> {
|
||||
crate::events::stop::preview(&self.handlers, request)
|
||||
crate::events::stop::preview(self, request)
|
||||
}
|
||||
|
||||
pub(crate) async fn run_stop(&self, request: StopRequest) -> StopOutcome {
|
||||
let session_id = request.session_id;
|
||||
let mut outcome =
|
||||
crate::events::stop::run(&self.handlers, &self.shell, &self.async_runtime, request)
|
||||
.await;
|
||||
let mut outcome = crate::events::stop::run(self, request).await;
|
||||
outcome.continuation_fragments = self
|
||||
.maybe_spill_prompt_fragments(session_id, outcome.continuation_fragments)
|
||||
.await;
|
||||
|
||||
@@ -32,6 +32,7 @@ use tempfile::tempdir;
|
||||
use super::ClaudeHooksEngine;
|
||||
use super::CommandShell;
|
||||
use super::ConfiguredHandler;
|
||||
use crate::events::post_tool_use::PostToolUseRequest;
|
||||
use crate::events::pre_tool_use::PreToolUseRequest;
|
||||
use crate::events::user_prompt_submit::UserPromptSubmitRequest;
|
||||
|
||||
@@ -66,6 +67,45 @@ fn user_prompt_request() -> UserPromptSubmitRequest {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_tool_use_preview_id_includes_tool_use_id() {
|
||||
let source_path = cwd();
|
||||
let mut engine = disabled_engine();
|
||||
engine.handlers.push(ConfiguredHandler {
|
||||
event_name: HookEventName::PostToolUse,
|
||||
matcher: Some("^Bash$".to_string()),
|
||||
command: "echo hook".to_string(),
|
||||
timeout_sec: 5,
|
||||
r#async: false,
|
||||
status_message: None,
|
||||
source_path: source_path.clone(),
|
||||
source: HookSource::User,
|
||||
display_order: 0,
|
||||
env: HashMap::new(),
|
||||
});
|
||||
|
||||
let preview = engine.preview_post_tool_use(&PostToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: cwd(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
permission_mode: "default".to_string(),
|
||||
tool_name: "Bash".to_string(),
|
||||
matcher_aliases: Vec::new(),
|
||||
tool_use_id: "tool-call-456".to_string(),
|
||||
tool_input: serde_json::json!({ "command": "echo hello" }),
|
||||
tool_response: serde_json::json!({ "output": "hello" }),
|
||||
});
|
||||
|
||||
assert_eq!(preview.len(), 1);
|
||||
assert_eq!(
|
||||
preview[0].id,
|
||||
format!("post-tool-use:0:{}:tool-call-456", source_path.display())
|
||||
);
|
||||
}
|
||||
|
||||
fn managed_hooks_for_current_platform(
|
||||
managed_dir: impl AsRef<Path>,
|
||||
hooks: HookEventsToml,
|
||||
@@ -1272,6 +1312,7 @@ print(json.dumps({
|
||||
.await;
|
||||
|
||||
assert_eq!(outcome.hook_events.len(), 1);
|
||||
assert_eq!(outcome.hook_events[0].run.id, preview[0].id);
|
||||
assert_eq!(outcome.hook_events[0].run.source, HookSource::Plugin);
|
||||
assert_eq!(
|
||||
outcome.hook_events[0].run.status,
|
||||
@@ -1415,7 +1456,8 @@ async fn reconfiguration_preserves_pending_async_output() {
|
||||
assert_eq!(outcome.hook_events, Vec::new());
|
||||
assert_eq!(outcome.additional_contexts.len(), 1);
|
||||
assert!(outcome.additional_contexts[0].contains("queued context"));
|
||||
assert!(reconfigured.async_runtime.prepare_batch().is_none());
|
||||
let boundary = reconfigured.async_runtime.ready_boundary();
|
||||
assert!(reconfigured.async_runtime.flush_through(boundary).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1444,11 +1486,11 @@ async fn blocked_prompt_preserves_pending_async_output() {
|
||||
|
||||
let blocked = engine.run_user_prompt_submit(request.clone()).await;
|
||||
assert!(blocked.should_stop);
|
||||
assert!(engine.async_runtime.prepare_batch().is_some());
|
||||
|
||||
engine.handlers.clear();
|
||||
let accepted = engine.run_user_prompt_submit(request).await;
|
||||
assert!(!accepted.should_stop);
|
||||
assert!(accepted.additional_contexts[0].contains("queued context"));
|
||||
assert!(engine.async_runtime.prepare_batch().is_none());
|
||||
let boundary = engine.async_runtime.ready_boundary();
|
||||
assert!(engine.async_runtime.flush_through(boundary).is_none());
|
||||
}
|
||||
|
||||
@@ -10,9 +10,8 @@ use codex_protocol::protocol::HookRunSummary;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::common;
|
||||
use crate::engine::CommandShell;
|
||||
use crate::engine::ClaudeHooksEngine;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::async_output::AsyncCommandRuntime;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
@@ -57,51 +56,27 @@ pub struct PreCompactOutcome {
|
||||
}
|
||||
|
||||
pub(crate) fn preview_pre(
|
||||
handlers: &[ConfiguredHandler],
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: &PreCompactRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_sync_handlers(
|
||||
handlers,
|
||||
HookEventName::PreCompact,
|
||||
Some(request.trigger.as_str()),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| dispatcher::running_summary(&handler))
|
||||
.collect()
|
||||
engine.preview_commands(HookEventName::PreCompact, &[request.trigger.as_str()])
|
||||
}
|
||||
|
||||
pub(crate) async fn run_pre(
|
||||
handlers: &[ConfiguredHandler],
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: PreCompactRequest,
|
||||
) -> PreCompactOutcome {
|
||||
let matched = dispatcher::select_handlers(
|
||||
handlers,
|
||||
HookEventName::PreCompact,
|
||||
Some(request.trigger.as_str()),
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return PreCompactOutcome {
|
||||
hook_events: Vec::new(),
|
||||
should_stop: false,
|
||||
stop_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = pre_command_input_json(&request)
|
||||
.map_err(|error| format!("failed to serialize pre compact hook input: {error}"));
|
||||
|
||||
let results = dispatcher::execute_handlers(
|
||||
shell,
|
||||
async_runtime,
|
||||
matched,
|
||||
input_json,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_pre_completed,
|
||||
)
|
||||
.await;
|
||||
let input = pre_command_input(&request);
|
||||
let results = engine
|
||||
.execute_commands(
|
||||
HookEventName::PreCompact,
|
||||
&[request.trigger.as_str()],
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_pre_completed,
|
||||
)
|
||||
.await;
|
||||
let should_stop = results.iter().any(|result| result.data.should_stop);
|
||||
let stop_reason = results
|
||||
.iter()
|
||||
@@ -113,9 +88,9 @@ pub(crate) async fn run_pre(
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_command_input_json(request: &PreCompactRequest) -> Result<String, serde_json::Error> {
|
||||
fn pre_command_input(request: &PreCompactRequest) -> PreCompactCommandInput {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
serde_json::to_string(&PreCompactCommandInput {
|
||||
PreCompactCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
@@ -125,55 +100,31 @@ fn pre_command_input_json(request: &PreCompactRequest) -> Result<String, serde_j
|
||||
hook_event_name: "PreCompact".to_string(),
|
||||
model: request.model.clone(),
|
||||
trigger: request.trigger.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn preview_post(
|
||||
handlers: &[ConfiguredHandler],
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: &PostCompactRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_sync_handlers(
|
||||
handlers,
|
||||
HookEventName::PostCompact,
|
||||
Some(request.trigger.as_str()),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| dispatcher::running_summary(&handler))
|
||||
.collect()
|
||||
engine.preview_commands(HookEventName::PostCompact, &[request.trigger.as_str()])
|
||||
}
|
||||
|
||||
pub(crate) async fn run_post(
|
||||
handlers: &[ConfiguredHandler],
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: PostCompactRequest,
|
||||
) -> StatelessHookOutcome {
|
||||
let matched = dispatcher::select_handlers(
|
||||
handlers,
|
||||
HookEventName::PostCompact,
|
||||
Some(request.trigger.as_str()),
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return StatelessHookOutcome {
|
||||
hook_events: Vec::new(),
|
||||
should_stop: false,
|
||||
stop_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = post_command_input_json(&request)
|
||||
.map_err(|error| format!("failed to serialize post compact hook input: {error}"));
|
||||
|
||||
let results = dispatcher::execute_handlers(
|
||||
shell,
|
||||
async_runtime,
|
||||
matched,
|
||||
input_json,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_post_completed,
|
||||
)
|
||||
.await;
|
||||
let input = post_command_input(&request);
|
||||
let results = engine
|
||||
.execute_commands(
|
||||
HookEventName::PostCompact,
|
||||
&[request.trigger.as_str()],
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_post_completed,
|
||||
)
|
||||
.await;
|
||||
let should_stop = results.iter().any(|result| result.data.should_stop);
|
||||
let stop_reason = results
|
||||
.iter()
|
||||
@@ -185,9 +136,9 @@ pub(crate) async fn run_post(
|
||||
}
|
||||
}
|
||||
|
||||
fn post_command_input_json(request: &PostCompactRequest) -> Result<String, serde_json::Error> {
|
||||
fn post_command_input(request: &PostCompactRequest) -> PostCompactCommandInput {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
serde_json::to_string(&PostCompactCommandInput {
|
||||
PostCompactCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
@@ -197,7 +148,7 @@ fn post_command_input_json(request: &PostCompactRequest) -> Result<String, serde
|
||||
hook_event_name: "PostCompact".to_string(),
|
||||
model: request.model.clone(),
|
||||
trigger: request.trigger.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -410,16 +361,15 @@ mod tests {
|
||||
|
||||
use super::parse_post_completed;
|
||||
use super::parse_pre_completed;
|
||||
use super::post_command_input_json;
|
||||
use super::pre_command_input_json;
|
||||
use super::post_command_input;
|
||||
use super::pre_command_input;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
|
||||
#[test]
|
||||
fn pre_compact_input_includes_lifecycle_metadata() {
|
||||
let input_json = pre_command_input_json(&pre_request()).expect("serialize command input");
|
||||
let input: serde_json::Value =
|
||||
serde_json::from_str(&input_json).expect("parse command input");
|
||||
let input = serde_json::to_value(pre_command_input(&pre_request()))
|
||||
.expect("serialize command input");
|
||||
|
||||
assert_eq!(
|
||||
input,
|
||||
@@ -437,9 +387,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn post_compact_input_includes_lifecycle_metadata() {
|
||||
let input_json = post_command_input_json(&post_request()).expect("serialize command input");
|
||||
let input: serde_json::Value =
|
||||
serde_json::from_str(&input_json).expect("parse command input");
|
||||
let input = serde_json::to_value(post_command_input(&post_request()))
|
||||
.expect("serialize command input");
|
||||
|
||||
assert_eq!(
|
||||
input,
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::common;
|
||||
use crate::engine::CommandShell;
|
||||
use crate::engine::ClaudeHooksEngine;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::async_output::AsyncCommandRuntime;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
@@ -66,57 +65,33 @@ struct PermissionRequestHandlerData {
|
||||
}
|
||||
|
||||
pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: &PermissionRequestRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
dispatcher::select_sync_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PermissionRequest,
|
||||
&matcher_inputs,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| {
|
||||
common::hook_run_for_tool_use(
|
||||
dispatcher::running_summary(&handler),
|
||||
&request.run_id_suffix,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
engine
|
||||
.preview_commands(HookEventName::PermissionRequest, &matcher_inputs)
|
||||
.into_iter()
|
||||
.map(|run| common::hook_run_for_tool_use(run, &request.run_id_suffix))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
handlers: &[ConfiguredHandler],
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: PermissionRequestRequest,
|
||||
) -> PermissionRequestOutcome {
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
let matched = dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PermissionRequest,
|
||||
&matcher_inputs,
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return PermissionRequestOutcome {
|
||||
hook_events: Vec::new(),
|
||||
decision: None,
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = serde_json::to_string(&build_command_input(&request))
|
||||
.map_err(|error| format!("failed to serialize permission request hook input: {error}"));
|
||||
|
||||
let results = dispatcher::execute_handlers(
|
||||
shell,
|
||||
async_runtime,
|
||||
matched,
|
||||
input_json,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id.clone()),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
let input = build_command_input(&request);
|
||||
let results = engine
|
||||
.execute_commands(
|
||||
HookEventName::PermissionRequest,
|
||||
&matcher_inputs,
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id.clone()),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Preserve the most specific matching allow, but treat any deny as final so
|
||||
// broader policy layers cannot accidentally overrule a more specific block.
|
||||
|
||||
@@ -11,9 +11,8 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::common;
|
||||
use crate::engine::CommandShell;
|
||||
use crate::engine::ClaudeHooksEngine;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::async_output::AsyncCommandRuntime;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
@@ -54,57 +53,33 @@ struct PostToolUseHandlerData {
|
||||
}
|
||||
|
||||
pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: &PostToolUseRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
dispatcher::select_sync_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PostToolUse,
|
||||
&matcher_inputs,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| {
|
||||
common::hook_run_for_tool_use(dispatcher::running_summary(&handler), &request.tool_use_id)
|
||||
})
|
||||
.collect()
|
||||
engine
|
||||
.preview_commands(HookEventName::PostToolUse, &matcher_inputs)
|
||||
.into_iter()
|
||||
.map(|run| common::hook_run_for_tool_use(run, &request.tool_use_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
handlers: &[ConfiguredHandler],
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: PostToolUseRequest,
|
||||
) -> PostToolUseOutcome {
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
let matched = dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PostToolUse,
|
||||
&matcher_inputs,
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return PostToolUseOutcome {
|
||||
hook_events: Vec::new(),
|
||||
should_stop: false,
|
||||
stop_reason: None,
|
||||
additional_contexts: Vec::new(),
|
||||
feedback_message: None,
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = command_input_json(&request)
|
||||
.map_err(|error| format!("failed to serialize post tool use hook input: {error}"));
|
||||
|
||||
let results = dispatcher::execute_handlers(
|
||||
shell,
|
||||
async_runtime,
|
||||
matched,
|
||||
input_json,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id.clone()),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
let input = command_input(&request);
|
||||
let results = engine
|
||||
.execute_commands(
|
||||
HookEventName::PostToolUse,
|
||||
&matcher_inputs,
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id.clone()),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
|
||||
let additional_contexts = common::flatten_additional_contexts(
|
||||
results
|
||||
@@ -136,15 +111,15 @@ pub(crate) async fn run(
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes command stdin for a selected `PostToolUse` hook.
|
||||
/// Builds command stdin for a selected `PostToolUse` hook.
|
||||
///
|
||||
/// Handler selection may include internal matcher aliases, but hook stdin keeps
|
||||
/// the canonical `tool_name` for logs and for consumers that pair pre/post
|
||||
/// events across processes. Shell-like tools pass `{ "command": ... }` as
|
||||
/// `tool_input`; MCP tools pass their resolved JSON arguments.
|
||||
fn command_input_json(request: &PostToolUseRequest) -> Result<String, serde_json::Error> {
|
||||
fn command_input(request: &PostToolUseRequest) -> PostToolUseCommandInput {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
serde_json::to_string(&PostToolUseCommandInput {
|
||||
PostToolUseCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
@@ -158,7 +133,7 @@ fn command_input_json(request: &PostToolUseRequest) -> Result<String, serde_json
|
||||
tool_input: request.tool_input.clone(),
|
||||
tool_response: request.tool_response.clone(),
|
||||
tool_use_id: request.tool_use_id.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_completed(
|
||||
@@ -313,9 +288,8 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::PostToolUseHandlerData;
|
||||
use super::command_input_json;
|
||||
use super::command_input;
|
||||
use super::parse_completed;
|
||||
use super::preview;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::events::common;
|
||||
@@ -325,9 +299,7 @@ mod tests {
|
||||
let mut request = request_for_tool_use("call-apply-patch");
|
||||
request.tool_name = "apply_patch".to_string();
|
||||
|
||||
let input_json = command_input_json(&request).expect("serialize command input");
|
||||
let input: serde_json::Value =
|
||||
serde_json::from_str(&input_json).expect("parse command input");
|
||||
let input = serde_json::to_value(command_input(&request)).expect("serialize command input");
|
||||
|
||||
assert_eq!(input["tool_name"], "apply_patch");
|
||||
}
|
||||
@@ -490,19 +462,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_and_completed_run_ids_include_tool_use_id() {
|
||||
fn completed_run_id_includes_tool_use_id() {
|
||||
let request = request_for_tool_use("tool-call-456");
|
||||
let runs = preview(&[handler()], &request);
|
||||
|
||||
assert_eq!(runs.len(), 1);
|
||||
assert_eq!(
|
||||
runs[0].id,
|
||||
format!(
|
||||
"post-tool-use:0:{}:tool-call-456",
|
||||
test_path_buf("/tmp/hooks.json").display()
|
||||
)
|
||||
);
|
||||
|
||||
let parsed = parse_completed(
|
||||
&handler(),
|
||||
run_result(Some(0), "", ""),
|
||||
@@ -510,7 +471,13 @@ mod tests {
|
||||
);
|
||||
let completed = common::hook_completed_for_tool_use(parsed.completed, &request.tool_use_id);
|
||||
|
||||
assert_eq!(completed.run.id, runs[0].id);
|
||||
assert_eq!(
|
||||
completed.run.id,
|
||||
format!(
|
||||
"post-tool-use:0:{}:tool-call-456",
|
||||
test_path_buf("/tmp/hooks.json").display()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn handler() -> ConfiguredHandler {
|
||||
|
||||
@@ -11,9 +11,8 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::common;
|
||||
use crate::engine::CommandShell;
|
||||
use crate::engine::ClaudeHooksEngine;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::async_output::AsyncCommandRuntime;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
@@ -53,57 +52,33 @@ struct PreToolUseHandlerData {
|
||||
}
|
||||
|
||||
pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: &PreToolUseRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
dispatcher::select_sync_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PreToolUse,
|
||||
&matcher_inputs,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| {
|
||||
common::hook_run_for_tool_use(dispatcher::running_summary(&handler), &request.tool_use_id)
|
||||
})
|
||||
.collect()
|
||||
engine
|
||||
.preview_commands(HookEventName::PreToolUse, &matcher_inputs)
|
||||
.into_iter()
|
||||
.map(|run| common::hook_run_for_tool_use(run, &request.tool_use_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
handlers: &[ConfiguredHandler],
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: PreToolUseRequest,
|
||||
) -> PreToolUseOutcome {
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
let matched = dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PreToolUse,
|
||||
&matcher_inputs,
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return PreToolUseOutcome {
|
||||
hook_events: Vec::new(),
|
||||
should_block: false,
|
||||
block_reason: None,
|
||||
additional_contexts: Vec::new(),
|
||||
updated_input: None,
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = command_input_json(&request)
|
||||
.map_err(|error| format!("failed to serialize pre tool use hook input: {error}"));
|
||||
|
||||
let results = dispatcher::execute_handlers(
|
||||
shell,
|
||||
async_runtime,
|
||||
matched,
|
||||
input_json,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id.clone()),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
let input = command_input(&request);
|
||||
let results = engine
|
||||
.execute_commands(
|
||||
HookEventName::PreToolUse,
|
||||
&matcher_inputs,
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id.clone()),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
|
||||
let should_block = results.iter().any(|result| result.data.should_block);
|
||||
let block_reason = results
|
||||
@@ -154,15 +129,15 @@ fn latest_updated_input(
|
||||
.map(|(_, updated_input)| updated_input)
|
||||
}
|
||||
|
||||
/// Serializes command stdin for a selected `PreToolUse` hook.
|
||||
/// Builds command stdin for a selected `PreToolUse` hook.
|
||||
///
|
||||
/// Handler selection may include internal matcher aliases, but hook stdin keeps
|
||||
/// the canonical `tool_name` so audit logs and downstream policy decisions stay
|
||||
/// stable. Shell-like tools pass `{ "command": ... }` as `tool_input`; MCP
|
||||
/// tools pass their resolved JSON arguments.
|
||||
fn command_input_json(request: &PreToolUseRequest) -> Result<String, serde_json::Error> {
|
||||
fn command_input(request: &PreToolUseRequest) -> PreToolUseCommandInput {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
serde_json::to_string(&PreToolUseCommandInput {
|
||||
PreToolUseCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
@@ -175,7 +150,7 @@ fn command_input_json(request: &PreToolUseRequest) -> Result<String, serde_json:
|
||||
tool_name: request.tool_name.clone(),
|
||||
tool_input: request.tool_input.clone(),
|
||||
tool_use_id: request.tool_use_id.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_completed(
|
||||
@@ -307,10 +282,9 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::PreToolUseHandlerData;
|
||||
use super::command_input_json;
|
||||
use super::command_input;
|
||||
use super::latest_updated_input;
|
||||
use super::parse_completed;
|
||||
use super::preview;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::events::common;
|
||||
@@ -320,9 +294,7 @@ mod tests {
|
||||
let mut request = request_for_tool_use("call-apply-patch");
|
||||
request.tool_name = "apply_patch".to_string();
|
||||
|
||||
let input_json = command_input_json(&request).expect("serialize command input");
|
||||
let input: serde_json::Value =
|
||||
serde_json::from_str(&input_json).expect("parse command input");
|
||||
let input = serde_json::to_value(command_input(&request)).expect("serialize command input");
|
||||
|
||||
assert_eq!(input["tool_name"], "apply_patch");
|
||||
}
|
||||
@@ -682,19 +654,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_and_completed_run_ids_include_tool_use_id() {
|
||||
fn completed_run_id_includes_tool_use_id() {
|
||||
let request = request_for_tool_use("tool-call-123");
|
||||
let runs = preview(&[handler()], &request);
|
||||
|
||||
assert_eq!(runs.len(), 1);
|
||||
assert_eq!(
|
||||
runs[0].id,
|
||||
format!(
|
||||
"pre-tool-use:0:{}:tool-call-123",
|
||||
test_path_buf("/tmp/hooks.json").display()
|
||||
)
|
||||
);
|
||||
|
||||
let parsed = parse_completed(
|
||||
&handler(),
|
||||
run_result(Some(0), "", ""),
|
||||
@@ -702,7 +663,13 @@ mod tests {
|
||||
);
|
||||
let completed = common::hook_completed_for_tool_use(parsed.completed, &request.tool_use_id);
|
||||
|
||||
assert_eq!(completed.run.id, runs[0].id);
|
||||
assert_eq!(
|
||||
completed.run.id,
|
||||
format!(
|
||||
"pre-tool-use:0:{}:tool-call-123",
|
||||
test_path_buf("/tmp/hooks.json").display()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn handler() -> ConfiguredHandler {
|
||||
|
||||
@@ -10,9 +10,8 @@ use codex_protocol::protocol::HookRunSummary;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::common;
|
||||
use crate::engine::CommandShell;
|
||||
use crate::engine::ClaudeHooksEngine;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::async_output::AsyncCommandRuntime;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
@@ -93,53 +92,41 @@ struct SessionStartHandlerData {
|
||||
}
|
||||
|
||||
pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: &SessionStartRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_sync_handlers(
|
||||
handlers,
|
||||
engine.preview_commands(
|
||||
request.target.event_name(),
|
||||
Some(request.target.matcher_input()),
|
||||
&[request.target.matcher_input()],
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| dispatcher::running_summary(&handler))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
handlers: &[ConfiguredHandler],
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: SessionStartRequest,
|
||||
turn_id: Option<String>,
|
||||
) -> SessionStartOutcome {
|
||||
let matched = dispatcher::select_handlers(
|
||||
handlers,
|
||||
request.target.event_name(),
|
||||
Some(request.target.matcher_input()),
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return SessionStartOutcome {
|
||||
hook_events: Vec::new(),
|
||||
should_stop: false,
|
||||
stop_reason: None,
|
||||
additional_contexts: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let (input_json, turn_id) = match request.target {
|
||||
StartHookTarget::SessionStart { source } => (
|
||||
serde_json::to_string(&SessionStartCommandInput::new(
|
||||
let results = match request.target {
|
||||
StartHookTarget::SessionStart { source } => {
|
||||
let input = SessionStartCommandInput::new(
|
||||
request.session_id.to_string(),
|
||||
request.transcript_path.clone(),
|
||||
request.cwd.display().to_string(),
|
||||
request.model.clone(),
|
||||
request.permission_mode.clone(),
|
||||
source.as_str().to_string(),
|
||||
))
|
||||
.map_err(|error| format!("failed to serialize session start hook input: {error}")),
|
||||
turn_id,
|
||||
),
|
||||
);
|
||||
engine
|
||||
.execute_commands(
|
||||
HookEventName::SessionStart,
|
||||
&[source.as_str()],
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
turn_id,
|
||||
parse_completed,
|
||||
)
|
||||
.await
|
||||
}
|
||||
StartHookTarget::SubagentStart {
|
||||
turn_id: subagent_turn_id,
|
||||
agent_id,
|
||||
@@ -156,23 +143,19 @@ pub(crate) async fn run(
|
||||
agent_id,
|
||||
agent_type,
|
||||
};
|
||||
let input_json = serde_json::to_string(&input)
|
||||
.map_err(|error| format!("failed to serialize subagent start hook input: {error}"));
|
||||
(input_json, Some(subagent_turn_id))
|
||||
engine
|
||||
.execute_commands(
|
||||
HookEventName::SubagentStart,
|
||||
&[input.agent_type.as_str()],
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(subagent_turn_id),
|
||||
parse_completed,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
let results = dispatcher::execute_handlers(
|
||||
shell,
|
||||
async_runtime,
|
||||
matched,
|
||||
input_json,
|
||||
request.cwd.as_path(),
|
||||
turn_id,
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
|
||||
let should_stop = results.iter().any(|result| result.data.should_stop);
|
||||
let stop_reason = results
|
||||
.iter()
|
||||
|
||||
@@ -11,9 +11,8 @@ use codex_protocol::protocol::HookRunSummary;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::common;
|
||||
use crate::engine::CommandShell;
|
||||
use crate::engine::ClaudeHooksEngine;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::async_output::AsyncCommandRuntime;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
@@ -79,43 +78,13 @@ struct StopHandlerData {
|
||||
continuation_fragments: Vec<HookPromptFragment>,
|
||||
}
|
||||
|
||||
pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
request: &StopRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_sync_handlers(
|
||||
handlers,
|
||||
request.target.event_name(),
|
||||
request.target.matcher_input(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| dispatcher::running_summary(&handler))
|
||||
.collect()
|
||||
pub(crate) fn preview(engine: &ClaudeHooksEngine, request: &StopRequest) -> Vec<HookRunSummary> {
|
||||
let matcher_input = request.target.matcher_input();
|
||||
engine.preview_commands(request.target.event_name(), matcher_input.as_slice())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
handlers: &[ConfiguredHandler],
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
request: StopRequest,
|
||||
) -> StopOutcome {
|
||||
let matched = dispatcher::select_handlers(
|
||||
handlers,
|
||||
request.target.event_name(),
|
||||
request.target.matcher_input(),
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return StopOutcome {
|
||||
hook_events: Vec::new(),
|
||||
should_stop: false,
|
||||
stop_reason: None,
|
||||
should_block: false,
|
||||
block_reason: None,
|
||||
continuation_fragments: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = match request.target {
|
||||
pub(crate) async fn run(engine: &ClaudeHooksEngine, request: StopRequest) -> StopOutcome {
|
||||
let results = match request.target {
|
||||
StopHookTarget::Stop => {
|
||||
let input = StopCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
@@ -130,8 +99,16 @@ pub(crate) async fn run(
|
||||
request.last_assistant_message.clone(),
|
||||
),
|
||||
};
|
||||
serde_json::to_string(&input)
|
||||
.map_err(|error| format!("failed to serialize stop hook input: {error}"))
|
||||
engine
|
||||
.execute_commands(
|
||||
HookEventName::Stop,
|
||||
&[],
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_completed,
|
||||
)
|
||||
.await
|
||||
}
|
||||
StopHookTarget::SubagentStop {
|
||||
agent_id,
|
||||
@@ -154,22 +131,19 @@ pub(crate) async fn run(
|
||||
request.last_assistant_message.clone(),
|
||||
),
|
||||
};
|
||||
serde_json::to_string(&input)
|
||||
.map_err(|error| format!("failed to serialize subagent stop hook input: {error}"))
|
||||
engine
|
||||
.execute_commands(
|
||||
HookEventName::SubagentStop,
|
||||
&[input.agent_type.as_str()],
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_completed,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
let results = dispatcher::execute_handlers(
|
||||
shell,
|
||||
async_runtime,
|
||||
matched,
|
||||
input_json,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
|
||||
let aggregate = aggregate_results(results.iter().map(|result| &result.data));
|
||||
|
||||
StopOutcome {
|
||||
|
||||
@@ -10,9 +10,8 @@ use codex_protocol::protocol::HookRunSummary;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::common;
|
||||
use crate::engine::CommandShell;
|
||||
use crate::engine::ClaudeHooksEngine;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::async_output::AsyncCommandRuntime;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
@@ -48,41 +47,18 @@ struct UserPromptSubmitHandlerData {
|
||||
}
|
||||
|
||||
pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
engine: &ClaudeHooksEngine,
|
||||
_request: &UserPromptSubmitRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_sync_handlers(
|
||||
handlers,
|
||||
HookEventName::UserPromptSubmit,
|
||||
/*matcher_input*/ None,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| dispatcher::running_summary(&handler))
|
||||
.collect()
|
||||
engine.preview_commands(HookEventName::UserPromptSubmit, &[])
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
handlers: &[ConfiguredHandler],
|
||||
shell: &CommandShell,
|
||||
async_runtime: &AsyncCommandRuntime,
|
||||
engine: &ClaudeHooksEngine,
|
||||
request: UserPromptSubmitRequest,
|
||||
) -> UserPromptSubmitOutcome {
|
||||
let matched = dispatcher::select_handlers(
|
||||
handlers,
|
||||
HookEventName::UserPromptSubmit,
|
||||
/*matcher_input*/ None,
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return UserPromptSubmitOutcome {
|
||||
hook_events: Vec::new(),
|
||||
should_stop: false,
|
||||
stop_reason: None,
|
||||
additional_contexts: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
let input_json = serde_json::to_string(&UserPromptSubmitCommandInput {
|
||||
let input = UserPromptSubmitCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
@@ -93,19 +69,18 @@ pub(crate) async fn run(
|
||||
model: request.model.clone(),
|
||||
permission_mode: request.permission_mode.clone(),
|
||||
prompt: request.prompt.clone(),
|
||||
})
|
||||
.map_err(|error| format!("failed to serialize user prompt submit hook input: {error}"));
|
||||
};
|
||||
|
||||
let results = dispatcher::execute_handlers(
|
||||
shell,
|
||||
async_runtime,
|
||||
matched,
|
||||
input_json,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
let results = engine
|
||||
.execute_commands(
|
||||
HookEventName::UserPromptSubmit,
|
||||
&[],
|
||||
&input,
|
||||
request.cwd.as_path(),
|
||||
Some(request.turn_id),
|
||||
parse_completed,
|
||||
)
|
||||
.await;
|
||||
|
||||
let should_stop = results.iter().any(|result| result.data.should_stop);
|
||||
let stop_reason = results
|
||||
|
||||
Reference in New Issue
Block a user