mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why This PR prepares the stack to enable Clippy await-holding lints that were left disabled in #18178. The mechanical lock-scope cleanup is handled separately; this PR is the documentation/configuration layer for the remaining await-across-guard sites. Without explicit annotations, reviewers and future maintainers cannot tell whether an await-holding warning is a real concurrency smell or an intentional serialization boundary. ## What changed - Configures `clippy.toml` so `await_holding_invalid_type` also covers `tokio::sync::{MutexGuard,RwLockReadGuard,RwLockWriteGuard}`. - Adds targeted `#[expect(clippy::await_holding_invalid_type, reason = ...)]` annotations for intentional async guard lifetimes. - Documents the main categories of intentional cases: active-turn state transitions that must remain atomic, session-owned MCP manager accesses, remote-control websocket serialization, JS REPL kernel/process serialization, OAuth persistence, external bearer token refresh serialization, and tests that intentionally serialize shared global or session-owned state. - For external bearer token refresh, documents the existing serialization boundary: holding `cached_token` across the provider command prevents concurrent cache misses from starting duplicate refresh commands, and the current behavior is small enough that an explicit expectation is easier to maintain than adding another synchronization primitive. ## Verification - `cargo clippy -p codex-login --all-targets` - `cargo clippy -p codex-connectors --all-targets` - `cargo clippy -p codex-core --all-targets` - The follow-up PR #18698 enables `await_holding_invalid_type` and `await_holding_lock` as workspace `deny` lints, so any undocumented remaining offender will fail Clippy. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18423). * #18698 * __->__ #18423
675 lines
22 KiB
Rust
675 lines
22 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
|
|
use crate::function_tool::FunctionCallError;
|
|
use crate::hook_runtime::record_additional_contexts;
|
|
use crate::hook_runtime::run_post_tool_use_hooks;
|
|
use crate::hook_runtime::run_pre_tool_use_hooks;
|
|
use crate::memories::usage::emit_metric_for_tool_read;
|
|
use crate::sandbox_tags::sandbox_tag;
|
|
use crate::session::turn_context::TurnContext;
|
|
use crate::tools::context::FunctionToolOutput;
|
|
use crate::tools::context::ToolInvocation;
|
|
use crate::tools::context::ToolOutput;
|
|
use crate::tools::context::ToolPayload;
|
|
use codex_hooks::HookEvent;
|
|
use codex_hooks::HookEventAfterToolUse;
|
|
use codex_hooks::HookPayload;
|
|
use codex_hooks::HookResult;
|
|
use codex_hooks::HookToolInput;
|
|
use codex_hooks::HookToolInputLocalShell;
|
|
use codex_hooks::HookToolKind;
|
|
use codex_protocol::models::ResponseInputItem;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_tools::ConfiguredToolSpec;
|
|
use codex_tools::ToolName;
|
|
use codex_tools::ToolSpec;
|
|
use codex_utils_readiness::Readiness;
|
|
use futures::future::BoxFuture;
|
|
use serde_json::Value;
|
|
use tracing::warn;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum ToolKind {
|
|
Function,
|
|
Mcp,
|
|
}
|
|
|
|
pub trait ToolHandler: Send + Sync {
|
|
type Output: ToolOutput + 'static;
|
|
|
|
fn kind(&self) -> ToolKind;
|
|
|
|
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
|
matches!(
|
|
(self.kind(), payload),
|
|
(ToolKind::Function, ToolPayload::Function { .. })
|
|
| (ToolKind::Function, ToolPayload::ToolSearch { .. })
|
|
| (ToolKind::Mcp, ToolPayload::Mcp { .. })
|
|
)
|
|
}
|
|
|
|
/// Returns `true` if the [ToolInvocation] *might* mutate the environment of the
|
|
/// user (through file system, OS operations, ...).
|
|
/// This function must remains defensive and return `true` if a doubt exist on the
|
|
/// exact effect of a ToolInvocation.
|
|
fn is_mutating(
|
|
&self,
|
|
_invocation: &ToolInvocation,
|
|
) -> impl std::future::Future<Output = bool> + Send {
|
|
async { false }
|
|
}
|
|
|
|
fn pre_tool_use_payload(&self, _invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
|
|
None
|
|
}
|
|
|
|
fn post_tool_use_payload(
|
|
&self,
|
|
_call_id: &str,
|
|
_payload: &ToolPayload,
|
|
_result: &dyn ToolOutput,
|
|
) -> Option<PostToolUsePayload> {
|
|
None
|
|
}
|
|
|
|
/// Creates an optional consumer for streamed tool argument diffs.
|
|
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
|
|
None
|
|
}
|
|
|
|
/// Perform the actual [ToolInvocation] and returns a [ToolOutput] containing
|
|
/// the final output to return to the model.
|
|
fn handle(
|
|
&self,
|
|
invocation: ToolInvocation,
|
|
) -> impl std::future::Future<Output = Result<Self::Output, FunctionCallError>> + Send;
|
|
}
|
|
|
|
/// Consumes streamed argument diffs for a tool call and emits protocol events
|
|
/// derived from partial tool input.
|
|
pub(crate) trait ToolArgumentDiffConsumer: Send {
|
|
/// Consume the next argument diff for a tool call.
|
|
fn consume_diff(&mut self, turn: &TurnContext, call_id: String, diff: &str)
|
|
-> Option<EventMsg>;
|
|
|
|
/// Flush any buffered event before the tool call completes.
|
|
fn flush_on_complete(&mut self) -> Option<EventMsg> {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) struct AnyToolResult {
|
|
pub(crate) call_id: String,
|
|
pub(crate) payload: ToolPayload,
|
|
pub(crate) result: Box<dyn ToolOutput>,
|
|
}
|
|
|
|
impl AnyToolResult {
|
|
pub(crate) fn into_response(self) -> ResponseInputItem {
|
|
let Self {
|
|
call_id,
|
|
payload,
|
|
result,
|
|
..
|
|
} = self;
|
|
result.to_response_item(&call_id, &payload)
|
|
}
|
|
|
|
pub(crate) fn code_mode_result(self) -> serde_json::Value {
|
|
let Self {
|
|
payload, result, ..
|
|
} = self;
|
|
result.code_mode_result(&payload)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct PreToolUsePayload {
|
|
pub(crate) command: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub(crate) struct PostToolUsePayload {
|
|
pub(crate) command: String,
|
|
pub(crate) tool_response: Value,
|
|
}
|
|
|
|
trait AnyToolHandler: Send + Sync {
|
|
fn matches_kind(&self, payload: &ToolPayload) -> bool;
|
|
|
|
fn is_mutating<'a>(&'a self, invocation: &'a ToolInvocation) -> BoxFuture<'a, bool>;
|
|
|
|
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload>;
|
|
|
|
fn post_tool_use_payload(
|
|
&self,
|
|
call_id: &str,
|
|
payload: &ToolPayload,
|
|
result: &dyn ToolOutput,
|
|
) -> Option<PostToolUsePayload>;
|
|
|
|
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>>;
|
|
|
|
fn handle_any<'a>(
|
|
&'a self,
|
|
invocation: ToolInvocation,
|
|
) -> BoxFuture<'a, Result<AnyToolResult, FunctionCallError>>;
|
|
}
|
|
|
|
impl<T> AnyToolHandler for T
|
|
where
|
|
T: ToolHandler,
|
|
{
|
|
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
|
ToolHandler::matches_kind(self, payload)
|
|
}
|
|
|
|
fn is_mutating<'a>(&'a self, invocation: &'a ToolInvocation) -> BoxFuture<'a, bool> {
|
|
Box::pin(ToolHandler::is_mutating(self, invocation))
|
|
}
|
|
|
|
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
|
|
ToolHandler::pre_tool_use_payload(self, invocation)
|
|
}
|
|
|
|
fn post_tool_use_payload(
|
|
&self,
|
|
call_id: &str,
|
|
payload: &ToolPayload,
|
|
result: &dyn ToolOutput,
|
|
) -> Option<PostToolUsePayload> {
|
|
ToolHandler::post_tool_use_payload(self, call_id, payload, result)
|
|
}
|
|
|
|
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
|
|
ToolHandler::create_diff_consumer(self)
|
|
}
|
|
|
|
fn handle_any<'a>(
|
|
&'a self,
|
|
invocation: ToolInvocation,
|
|
) -> BoxFuture<'a, Result<AnyToolResult, FunctionCallError>> {
|
|
Box::pin(async move {
|
|
let call_id = invocation.call_id.clone();
|
|
let payload = invocation.payload.clone();
|
|
let output = self.handle(invocation).await?;
|
|
Ok(AnyToolResult {
|
|
call_id,
|
|
payload,
|
|
result: Box::new(output),
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct ToolRegistry {
|
|
handlers: HashMap<ToolName, Arc<dyn AnyToolHandler>>,
|
|
}
|
|
|
|
impl ToolRegistry {
|
|
fn new(handlers: HashMap<ToolName, Arc<dyn AnyToolHandler>>) -> Self {
|
|
Self { handlers }
|
|
}
|
|
|
|
fn handler(&self, name: &ToolName) -> Option<Arc<dyn AnyToolHandler>> {
|
|
self.handlers.get(name).map(Arc::clone)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn has_handler(&self, name: &ToolName) -> bool {
|
|
self.handler(name).is_some()
|
|
}
|
|
|
|
pub(crate) fn create_diff_consumer(
|
|
&self,
|
|
name: &ToolName,
|
|
) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
|
|
self.handler(name)?.create_diff_consumer()
|
|
}
|
|
|
|
// TODO(jif) for dynamic tools.
|
|
// pub fn register(&mut self, name: impl Into<String>, handler: Arc<dyn ToolHandler>) {
|
|
// let name = name.into();
|
|
// if self.handlers.insert(name.clone(), handler).is_some() {
|
|
// warn!("overwriting handler for tool {name}");
|
|
// }
|
|
// }
|
|
|
|
#[expect(
|
|
clippy::await_holding_invalid_type,
|
|
reason = "tool dispatch must keep active-turn accounting atomic"
|
|
)]
|
|
pub(crate) async fn dispatch_any(
|
|
&self,
|
|
invocation: ToolInvocation,
|
|
) -> Result<AnyToolResult, FunctionCallError> {
|
|
let tool_name = invocation.tool_name.clone();
|
|
let display_name = tool_name.display();
|
|
let call_id_owned = invocation.call_id.clone();
|
|
let otel = invocation.turn.session_telemetry.clone();
|
|
let payload_for_response = invocation.payload.clone();
|
|
let log_payload = payload_for_response.log_payload();
|
|
let metric_tags = [
|
|
(
|
|
"sandbox",
|
|
sandbox_tag(
|
|
&invocation.turn.sandbox_policy,
|
|
invocation.turn.windows_sandbox_level,
|
|
),
|
|
),
|
|
(
|
|
"sandbox_policy",
|
|
sandbox_policy_tag(&invocation.turn.sandbox_policy),
|
|
),
|
|
];
|
|
let (mcp_server, mcp_server_origin) = match &invocation.payload {
|
|
ToolPayload::Mcp { server, .. } => {
|
|
let manager = invocation
|
|
.session
|
|
.services
|
|
.mcp_connection_manager
|
|
.read()
|
|
.await;
|
|
let origin = manager.server_origin(server).map(str::to_owned);
|
|
(Some(server.clone()), origin)
|
|
}
|
|
_ => (None, None),
|
|
};
|
|
let mcp_server_ref = mcp_server.as_deref();
|
|
let mcp_server_origin_ref = mcp_server_origin.as_deref();
|
|
|
|
{
|
|
let mut active = invocation.session.active_turn.lock().await;
|
|
if let Some(active_turn) = active.as_mut() {
|
|
let mut turn_state = active_turn.turn_state.lock().await;
|
|
turn_state.tool_calls = turn_state.tool_calls.saturating_add(1);
|
|
}
|
|
}
|
|
|
|
let handler = match self.handler(&tool_name) {
|
|
Some(handler) => handler,
|
|
None => {
|
|
let message = unsupported_tool_call_message(&invocation.payload, &tool_name);
|
|
otel.tool_result_with_tags(
|
|
&display_name,
|
|
&call_id_owned,
|
|
log_payload.as_ref(),
|
|
Duration::ZERO,
|
|
/*success*/ false,
|
|
&message,
|
|
&metric_tags,
|
|
mcp_server_ref,
|
|
mcp_server_origin_ref,
|
|
);
|
|
return Err(FunctionCallError::RespondToModel(message));
|
|
}
|
|
};
|
|
|
|
if !handler.matches_kind(&invocation.payload) {
|
|
let message = format!("tool {display_name} invoked with incompatible payload");
|
|
otel.tool_result_with_tags(
|
|
&display_name,
|
|
&call_id_owned,
|
|
log_payload.as_ref(),
|
|
Duration::ZERO,
|
|
/*success*/ false,
|
|
&message,
|
|
&metric_tags,
|
|
mcp_server_ref,
|
|
mcp_server_origin_ref,
|
|
);
|
|
return Err(FunctionCallError::Fatal(message));
|
|
}
|
|
|
|
if let Some(pre_tool_use_payload) = handler.pre_tool_use_payload(&invocation)
|
|
&& let Some(reason) = run_pre_tool_use_hooks(
|
|
&invocation.session,
|
|
&invocation.turn,
|
|
invocation.call_id.clone(),
|
|
pre_tool_use_payload.command.clone(),
|
|
)
|
|
.await
|
|
{
|
|
return Err(FunctionCallError::RespondToModel(format!(
|
|
"Command blocked by PreToolUse hook: {reason}. Command: {}",
|
|
pre_tool_use_payload.command
|
|
)));
|
|
}
|
|
|
|
let is_mutating = handler.is_mutating(&invocation).await;
|
|
let response_cell = tokio::sync::Mutex::new(None);
|
|
let invocation_for_tool = invocation.clone();
|
|
|
|
let started = Instant::now();
|
|
let result = otel
|
|
.log_tool_result_with_tags(
|
|
&display_name,
|
|
&call_id_owned,
|
|
log_payload.as_ref(),
|
|
&metric_tags,
|
|
mcp_server_ref,
|
|
mcp_server_origin_ref,
|
|
|| {
|
|
let handler = handler.clone();
|
|
let response_cell = &response_cell;
|
|
async move {
|
|
if is_mutating {
|
|
tracing::trace!("waiting for tool gate");
|
|
invocation_for_tool.turn.tool_call_gate.wait_ready().await;
|
|
tracing::trace!("tool gate released");
|
|
}
|
|
match handler.handle_any(invocation_for_tool).await {
|
|
Ok(result) => {
|
|
let preview = result.result.log_preview();
|
|
let success = result.result.success_for_logging();
|
|
let mut guard = response_cell.lock().await;
|
|
*guard = Some(result);
|
|
Ok((preview, success))
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
},
|
|
)
|
|
.await;
|
|
let duration = started.elapsed();
|
|
let (output_preview, success) = match &result {
|
|
Ok((preview, success)) => (preview.clone(), *success),
|
|
Err(err) => (err.to_string(), false),
|
|
};
|
|
emit_metric_for_tool_read(&invocation, success).await;
|
|
let post_tool_use_payload = if success {
|
|
let guard = response_cell.lock().await;
|
|
guard.as_ref().and_then(|result| {
|
|
handler.post_tool_use_payload(
|
|
&result.call_id,
|
|
&result.payload,
|
|
result.result.as_ref(),
|
|
)
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
let post_tool_use_outcome = if let Some(post_tool_use_payload) = post_tool_use_payload {
|
|
Some(
|
|
run_post_tool_use_hooks(
|
|
&invocation.session,
|
|
&invocation.turn,
|
|
invocation.call_id.clone(),
|
|
post_tool_use_payload.command,
|
|
post_tool_use_payload.tool_response,
|
|
)
|
|
.await,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
// Deprecated: this is the legacy AfterToolUse hook. Prefer the new PostToolUse
|
|
let hook_abort_error = dispatch_after_tool_use_hook(AfterToolUseHookDispatch {
|
|
invocation: &invocation,
|
|
output_preview,
|
|
success,
|
|
executed: true,
|
|
duration,
|
|
mutating: is_mutating,
|
|
})
|
|
.await;
|
|
|
|
if let Some(err) = hook_abort_error {
|
|
return Err(err);
|
|
}
|
|
|
|
if let Some(outcome) = &post_tool_use_outcome {
|
|
record_additional_contexts(
|
|
&invocation.session,
|
|
&invocation.turn,
|
|
outcome.additional_contexts.clone(),
|
|
)
|
|
.await;
|
|
|
|
let replacement_text = if outcome.should_stop {
|
|
Some(
|
|
outcome
|
|
.feedback_message
|
|
.clone()
|
|
.or_else(|| outcome.stop_reason.clone())
|
|
.unwrap_or_else(|| "PostToolUse hook stopped execution".to_string()),
|
|
)
|
|
} else {
|
|
outcome.feedback_message.clone()
|
|
};
|
|
if let Some(replacement_text) = replacement_text {
|
|
let mut guard = response_cell.lock().await;
|
|
if let Some(result) = guard.as_mut() {
|
|
result.result = Box::new(FunctionToolOutput::from_text(
|
|
replacement_text,
|
|
/*success*/ None,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
match result {
|
|
Ok(_) => {
|
|
let mut guard = response_cell.lock().await;
|
|
let result = guard.take().ok_or_else(|| {
|
|
FunctionCallError::Fatal("tool produced no output".to_string())
|
|
})?;
|
|
Ok(result)
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ToolRegistryBuilder {
|
|
handlers: HashMap<ToolName, Arc<dyn AnyToolHandler>>,
|
|
specs: Vec<ConfiguredToolSpec>,
|
|
}
|
|
|
|
impl ToolRegistryBuilder {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
handlers: HashMap::new(),
|
|
specs: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn push_spec(&mut self, spec: ToolSpec) {
|
|
self.push_spec_with_parallel_support(spec, /*supports_parallel_tool_calls*/ false);
|
|
}
|
|
|
|
pub fn push_spec_with_parallel_support(
|
|
&mut self,
|
|
spec: ToolSpec,
|
|
supports_parallel_tool_calls: bool,
|
|
) {
|
|
self.specs
|
|
.push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls));
|
|
}
|
|
|
|
pub fn register_handler<H>(&mut self, name: impl Into<ToolName>, handler: Arc<H>)
|
|
where
|
|
H: ToolHandler + 'static,
|
|
{
|
|
let name = name.into();
|
|
let display_name = name.display();
|
|
let handler: Arc<dyn AnyToolHandler> = handler;
|
|
if self.handlers.insert(name, handler).is_some() {
|
|
warn!("overwriting handler for tool {display_name}");
|
|
}
|
|
}
|
|
|
|
// TODO(jif) for dynamic tools.
|
|
// pub fn register_many<I>(&mut self, names: I, handler: Arc<dyn ToolHandler>)
|
|
// where
|
|
// I: IntoIterator,
|
|
// I::Item: Into<String>,
|
|
// {
|
|
// for name in names {
|
|
// let name = name.into();
|
|
// if self
|
|
// .handlers
|
|
// .insert(name.clone(), handler.clone())
|
|
// .is_some()
|
|
// {
|
|
// warn!("overwriting handler for tool {name}");
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
pub fn build(self) -> (Vec<ConfiguredToolSpec>, ToolRegistry) {
|
|
let registry = ToolRegistry::new(self.handlers);
|
|
(self.specs, registry)
|
|
}
|
|
}
|
|
|
|
fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &ToolName) -> String {
|
|
let tool_name = tool_name.display();
|
|
match payload {
|
|
ToolPayload::Custom { .. } => format!("unsupported custom tool call: {tool_name}"),
|
|
_ => format!("unsupported call: {tool_name}"),
|
|
}
|
|
}
|
|
|
|
fn sandbox_policy_tag(policy: &SandboxPolicy) -> &'static str {
|
|
match policy {
|
|
SandboxPolicy::ReadOnly { .. } => "read-only",
|
|
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
|
|
SandboxPolicy::DangerFullAccess => "danger-full-access",
|
|
SandboxPolicy::ExternalSandbox { .. } => "external-sandbox",
|
|
}
|
|
}
|
|
|
|
// Hooks use a separate wire-facing input type so hook payload JSON stays stable
|
|
// and decoupled from core's internal tool runtime representation.
|
|
impl From<&ToolPayload> for HookToolInput {
|
|
fn from(payload: &ToolPayload) -> Self {
|
|
match payload {
|
|
ToolPayload::Function { arguments } => HookToolInput::Function {
|
|
arguments: arguments.clone(),
|
|
},
|
|
ToolPayload::ToolSearch { arguments } => HookToolInput::Function {
|
|
arguments: serde_json::json!({
|
|
"query": arguments.query,
|
|
"limit": arguments.limit,
|
|
})
|
|
.to_string(),
|
|
},
|
|
ToolPayload::Custom { input } => HookToolInput::Custom {
|
|
input: input.clone(),
|
|
},
|
|
ToolPayload::LocalShell { params } => HookToolInput::LocalShell {
|
|
params: HookToolInputLocalShell {
|
|
command: params.command.clone(),
|
|
workdir: params.workdir.clone(),
|
|
timeout_ms: params.timeout_ms,
|
|
sandbox_permissions: params.sandbox_permissions,
|
|
prefix_rule: params.prefix_rule.clone(),
|
|
justification: params.justification.clone(),
|
|
},
|
|
},
|
|
ToolPayload::Mcp {
|
|
server,
|
|
tool,
|
|
raw_arguments,
|
|
} => HookToolInput::Mcp {
|
|
server: server.clone(),
|
|
tool: tool.clone(),
|
|
arguments: raw_arguments.clone(),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn hook_tool_kind(tool_input: &HookToolInput) -> HookToolKind {
|
|
match tool_input {
|
|
HookToolInput::Function { .. } => HookToolKind::Function,
|
|
HookToolInput::Custom { .. } => HookToolKind::Custom,
|
|
HookToolInput::LocalShell { .. } => HookToolKind::LocalShell,
|
|
HookToolInput::Mcp { .. } => HookToolKind::Mcp,
|
|
}
|
|
}
|
|
|
|
struct AfterToolUseHookDispatch<'a> {
|
|
invocation: &'a ToolInvocation,
|
|
output_preview: String,
|
|
success: bool,
|
|
executed: bool,
|
|
duration: Duration,
|
|
mutating: bool,
|
|
}
|
|
|
|
async fn dispatch_after_tool_use_hook(
|
|
dispatch: AfterToolUseHookDispatch<'_>,
|
|
) -> Option<FunctionCallError> {
|
|
let AfterToolUseHookDispatch { invocation, .. } = dispatch;
|
|
let session = invocation.session.as_ref();
|
|
let turn = invocation.turn.as_ref();
|
|
let tool_input = HookToolInput::from(&invocation.payload);
|
|
let hook_outcomes = session
|
|
.hooks()
|
|
.dispatch(HookPayload {
|
|
session_id: session.conversation_id,
|
|
cwd: turn.cwd.clone(),
|
|
client: turn.app_server_client_name.clone(),
|
|
triggered_at: chrono::Utc::now(),
|
|
hook_event: HookEvent::AfterToolUse {
|
|
event: HookEventAfterToolUse {
|
|
turn_id: turn.sub_id.clone(),
|
|
call_id: invocation.call_id.clone(),
|
|
tool_name: invocation.tool_name.display(),
|
|
tool_kind: hook_tool_kind(&tool_input),
|
|
tool_input,
|
|
executed: dispatch.executed,
|
|
success: dispatch.success,
|
|
duration_ms: u64::try_from(dispatch.duration.as_millis()).unwrap_or(u64::MAX),
|
|
mutating: dispatch.mutating,
|
|
sandbox: sandbox_tag(&turn.sandbox_policy, turn.windows_sandbox_level)
|
|
.to_string(),
|
|
sandbox_policy: sandbox_policy_tag(&turn.sandbox_policy).to_string(),
|
|
output_preview: dispatch.output_preview.clone(),
|
|
},
|
|
},
|
|
})
|
|
.await;
|
|
|
|
for hook_outcome in hook_outcomes {
|
|
let hook_name = hook_outcome.hook_name;
|
|
match hook_outcome.result {
|
|
HookResult::Success => {}
|
|
HookResult::FailedContinue(error) => {
|
|
warn!(
|
|
call_id = %invocation.call_id,
|
|
tool_name = %invocation.tool_name.display(),
|
|
hook_name = %hook_name,
|
|
error = %error,
|
|
"after_tool_use hook failed; continuing"
|
|
);
|
|
}
|
|
HookResult::FailedAbort(error) => {
|
|
warn!(
|
|
call_id = %invocation.call_id,
|
|
tool_name = %invocation.tool_name.display(),
|
|
hook_name = %hook_name,
|
|
error = %error,
|
|
"after_tool_use hook failed; aborting operation"
|
|
);
|
|
return Some(FunctionCallError::Fatal(format!(
|
|
"after_tool_use hook '{hook_name}' failed and aborted operation: {error}"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "registry_tests.rs"]
|
|
mod tests;
|