mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix(core): emit hooks for apply_patch edits (#18391)
Fixes https://github.com/openai/codex/issues/16732. ## Why `apply_patch` is Codex's primary file edit path, but it was not emitting `PreToolUse` or `PostToolUse` hook events. That meant hook-based policy, auditing, and write coordination could observe shell commands while missing the actual file mutation performed by `apply_patch`. The issue also exposed that the hook runtime serialized command hook payloads with `tool_name: "Bash"` unconditionally. Even if `apply_patch` supplied hook payloads, hooks would either fail to match it directly or receive misleading stdin that identified the edit as a Bash tool call. ## What Changed - Added `PreToolUse` and `PostToolUse` payload support to `ApplyPatchHandler`. - Exposed the raw patch body as `tool_input.command` for both JSON/function and freeform `apply_patch` calls. - Taught tool hook payloads to carry a handler-supplied hook-facing `tool_name`. - Preserved existing shell compatibility by continuing to emit `Bash` for shell-like tools. - Serialized the selected hook `tool_name` into hook stdin instead of hardcoding `Bash`. - Relaxed the generated hook command input schema so `tool_name` can represent tools other than `Bash`. ## Verification Added focused handler coverage for: - JSON/function `apply_patch` calls producing a `PreToolUse` payload. - Freeform `apply_patch` calls producing a `PreToolUse` payload. - Successful `apply_patch` output producing a `PostToolUse` payload. - Shell and `exec_command` handlers continuing to expose `Bash`. Added end-to-end hook coverage for: - A `PreToolUse` hook matching `^apply_patch$` blocking the patch before the target file is created. - A `PostToolUse` hook matching `^apply_patch$` receiving the patch input and tool response, then adding context to the follow-up model request. - Non-participating tools such as the plan tool continuing not to emit `PreToolUse`/`PostToolUse` hook events. Also validated manually with a live `codex exec` smoke test using an isolated temp workspace and temp `CODEX_HOME`. The smoke test confirmed that a real `apply_patch` edit emits `PreToolUse`/`PostToolUse` with `tool_name: "apply_patch"`, a shell command still emits `tool_name: "Bash"`, and a denying `PreToolUse` hook prevents the blocked patch file from being created.
This commit is contained in:
committed by
GitHub
Unverified
parent
1d4cc494c9
commit
09ebc34f17
@@ -128,10 +128,17 @@ pub(crate) async fn run_pending_session_start_hooks(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Runs matching `PreToolUse` hooks before a tool executes.
|
||||
///
|
||||
/// `tool_name` is the canonical name serialized to hook stdin. Matcher aliases
|
||||
/// are internal compatibility names used only for selecting configured hook
|
||||
/// handlers.
|
||||
pub(crate) async fn run_pre_tool_use_hooks(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
tool_use_id: String,
|
||||
tool_name: String,
|
||||
matcher_aliases: Vec<String>,
|
||||
command: String,
|
||||
) -> Option<String> {
|
||||
let request = PreToolUseRequest {
|
||||
@@ -141,7 +148,8 @@ pub(crate) async fn run_pre_tool_use_hooks(
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
permission_mode: hook_permission_mode(turn_context),
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_name,
|
||||
matcher_aliases,
|
||||
tool_use_id,
|
||||
command,
|
||||
};
|
||||
@@ -174,7 +182,8 @@ pub(crate) async fn run_permission_request_hooks(
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
permission_mode: hook_permission_mode(turn_context),
|
||||
tool_name: payload.tool_name,
|
||||
tool_name: payload.tool_name.name().to_string(),
|
||||
matcher_aliases: payload.tool_name.matcher_aliases().to_vec(),
|
||||
run_id_suffix: run_id_suffix.to_string(),
|
||||
command: payload.command,
|
||||
description: payload.description,
|
||||
@@ -191,10 +200,18 @@ pub(crate) async fn run_permission_request_hooks(
|
||||
decision
|
||||
}
|
||||
|
||||
/// Runs matching `PostToolUse` hooks after a tool has produced a successful output.
|
||||
///
|
||||
/// The `tool_name`, matcher aliases, `command`, and `tool_response` values are
|
||||
/// already adapted by the tool handler into the stable hook contract. Passing
|
||||
/// raw internal tool data here would leak implementation details into user hook
|
||||
/// matchers and hook logs.
|
||||
pub(crate) async fn run_post_tool_use_hooks(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
tool_use_id: String,
|
||||
tool_name: String,
|
||||
matcher_aliases: Vec<String>,
|
||||
command: String,
|
||||
tool_response: Value,
|
||||
) -> PostToolUseOutcome {
|
||||
@@ -205,7 +222,8 @@ pub(crate) async fn run_post_tool_use_hooks(
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
permission_mode: hook_permission_mode(turn_context),
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_name,
|
||||
matcher_aliases,
|
||||
tool_use_id,
|
||||
command,
|
||||
tool_response,
|
||||
|
||||
@@ -88,6 +88,13 @@ pub trait ToolOutput: Send {
|
||||
|
||||
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem;
|
||||
|
||||
/// Returns the stable value exposed to `PostToolUse` hooks for this tool output.
|
||||
///
|
||||
/// Tool handlers decide whether a tool participates in `PostToolUse`, but
|
||||
/// this method lets the output type own any conversion from model-facing
|
||||
/// response content to hook-facing data. Returning `None` means the output
|
||||
/// should not produce a post-use hook payload, not merely that the tool had
|
||||
/// empty output.
|
||||
fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue> {
|
||||
None
|
||||
}
|
||||
@@ -306,6 +313,10 @@ impl ToolOutput for ApplyPatchToolOutput {
|
||||
)
|
||||
}
|
||||
|
||||
fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue> {
|
||||
Some(JsonValue::String(self.text.clone()))
|
||||
}
|
||||
|
||||
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
|
||||
JsonValue::Object(serde_json::Map::new())
|
||||
}
|
||||
|
||||
@@ -16,12 +16,16 @@ use crate::tools::context::ApplyPatchToolOutput;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::SharedTurnDiffTracker;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::events::ToolEmitter;
|
||||
use crate::tools::events::ToolEventCtx;
|
||||
use crate::tools::handlers::apply_granted_turn_permissions;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::orchestrator::ToolOrchestrator;
|
||||
use crate::tools::registry::PostToolUsePayload;
|
||||
use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::tools::registry::ToolArgumentDiffConsumer;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
@@ -239,6 +243,21 @@ fn write_permissions_for_paths(
|
||||
normalize_additional_permissions(permissions).ok()
|
||||
}
|
||||
|
||||
/// Extracts the raw patch text used as the command-shaped hook input for apply_patch.
|
||||
///
|
||||
/// The apply_patch tool can arrive as the older JSON/function shape or as a
|
||||
/// freeform custom tool call. Both represent the same file edit operation, so
|
||||
/// hooks see the raw patch body in `tool_input.command` either way.
|
||||
fn apply_patch_payload_command(payload: &ToolPayload) -> Option<String> {
|
||||
match payload {
|
||||
ToolPayload::Function { arguments } => parse_arguments::<ApplyPatchToolArgs>(arguments)
|
||||
.ok()
|
||||
.map(|args| args.input),
|
||||
ToolPayload::Custom { input } => Some(input.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn effective_patch_permissions(
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
@@ -294,6 +313,27 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
Some(Box::<ApplyPatchArgumentDiffConsumer>::default())
|
||||
}
|
||||
|
||||
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
|
||||
apply_patch_payload_command(&invocation.payload).map(|command| PreToolUsePayload {
|
||||
tool_name: HookToolName::apply_patch(),
|
||||
command,
|
||||
})
|
||||
}
|
||||
|
||||
fn post_tool_use_payload(
|
||||
&self,
|
||||
call_id: &str,
|
||||
payload: &ToolPayload,
|
||||
result: &dyn ToolOutput,
|
||||
) -> Option<PostToolUsePayload> {
|
||||
let tool_response = result.post_tool_use_response(call_id, payload)?;
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: HookToolName::apply_patch(),
|
||||
command: apply_patch_payload_command(payload)?,
|
||||
tool_response,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
|
||||
@@ -7,9 +7,94 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::registry::PostToolUsePayload;
|
||||
use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
|
||||
fn sample_patch() -> &'static str {
|
||||
r#"*** Begin Patch
|
||||
*** Add File: hello.txt
|
||||
+hello
|
||||
*** End Patch"#
|
||||
}
|
||||
|
||||
async fn invocation_for_payload(payload: ToolPayload) -> ToolInvocation {
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
ToolInvocation {
|
||||
session: session.into(),
|
||||
turn: turn.into(),
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "call-apply-patch".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("apply_patch"),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_tool_use_payload_uses_json_patch_input() {
|
||||
let patch = sample_patch();
|
||||
let payload = ToolPayload::Function {
|
||||
arguments: json!({ "input": patch }).to_string(),
|
||||
};
|
||||
let invocation = invocation_for_payload(payload).await;
|
||||
let handler = ApplyPatchHandler;
|
||||
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&invocation),
|
||||
Some(PreToolUsePayload {
|
||||
tool_name: HookToolName::apply_patch(),
|
||||
command: patch.to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_tool_use_payload_uses_freeform_patch_input() {
|
||||
let patch = sample_patch();
|
||||
let payload = ToolPayload::Custom {
|
||||
input: patch.to_string(),
|
||||
};
|
||||
let invocation = invocation_for_payload(payload).await;
|
||||
let handler = ApplyPatchHandler;
|
||||
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&invocation),
|
||||
Some(PreToolUsePayload {
|
||||
tool_name: HookToolName::apply_patch(),
|
||||
command: patch.to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_tool_use_payload_uses_patch_input_and_tool_output() {
|
||||
let patch = sample_patch();
|
||||
let payload = ToolPayload::Custom {
|
||||
input: patch.to_string(),
|
||||
};
|
||||
let output = ApplyPatchToolOutput::from_text("Success. Updated files.".to_string());
|
||||
let handler = ApplyPatchHandler;
|
||||
|
||||
assert_eq!(
|
||||
handler.post_tool_use_payload("call-apply-patch", &payload, &output),
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: HookToolName::apply_patch(),
|
||||
command: patch.to_string(),
|
||||
tool_response: json!("Success. Updated files."),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_consumer_does_not_stream_json_tool_call_arguments() {
|
||||
|
||||
@@ -25,6 +25,7 @@ use crate::tools::handlers::normalize_and_validate_additional_permissions;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::handlers::parse_arguments_with_base_path;
|
||||
use crate::tools::handlers::resolve_workdir_base_path;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::orchestrator::ToolOrchestrator;
|
||||
use crate::tools::registry::PostToolUsePayload;
|
||||
use crate::tools::registry::PreToolUsePayload;
|
||||
@@ -205,7 +206,10 @@ impl ToolHandler for ShellHandler {
|
||||
}
|
||||
|
||||
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
|
||||
shell_payload_command(&invocation.payload).map(|command| PreToolUsePayload { command })
|
||||
shell_payload_command(&invocation.payload).map(|command| PreToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command,
|
||||
})
|
||||
}
|
||||
|
||||
fn post_tool_use_payload(
|
||||
@@ -216,6 +220,7 @@ impl ToolHandler for ShellHandler {
|
||||
) -> Option<PostToolUsePayload> {
|
||||
let tool_response = result.post_tool_use_response(call_id, payload)?;
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: shell_payload_command(payload)?,
|
||||
tool_response,
|
||||
})
|
||||
@@ -313,8 +318,10 @@ impl ToolHandler for ShellCommandHandler {
|
||||
}
|
||||
|
||||
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
|
||||
shell_command_payload_command(&invocation.payload)
|
||||
.map(|command| PreToolUsePayload { command })
|
||||
shell_command_payload_command(&invocation.payload).map(|command| PreToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command,
|
||||
})
|
||||
}
|
||||
|
||||
fn post_tool_use_payload(
|
||||
@@ -325,6 +332,7 @@ impl ToolHandler for ShellCommandHandler {
|
||||
) -> Option<PostToolUsePayload> {
|
||||
let tool_response = result.post_tool_use_response(call_id, payload)?;
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: shell_command_payload_command(payload)?,
|
||||
tool_response,
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::ShellHandler;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_shell_command::is_safe_command::is_known_safe_command;
|
||||
@@ -232,6 +233,7 @@ async fn shell_pre_tool_use_payload_uses_joined_command() {
|
||||
payload,
|
||||
}),
|
||||
Some(crate::tools::registry::PreToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: "bash -lc 'printf hi'".to_string(),
|
||||
})
|
||||
);
|
||||
@@ -258,6 +260,7 @@ async fn shell_command_pre_tool_use_payload_uses_raw_command() {
|
||||
payload,
|
||||
}),
|
||||
Some(crate::tools::registry::PreToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: "printf shell command".to_string(),
|
||||
})
|
||||
);
|
||||
@@ -280,6 +283,7 @@ fn build_post_tool_use_payload_uses_tool_output_wire_value() {
|
||||
assert_eq!(
|
||||
handler.post_tool_use_payload("call-42", &payload, &output),
|
||||
Some(crate::tools::registry::PostToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: "printf shell command".to_string(),
|
||||
tool_response: json!("shell output"),
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::tools::handlers::normalize_and_validate_additional_permissions;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::handlers::parse_arguments_with_base_path;
|
||||
use crate::tools::handlers::resolve_workdir_base_path;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::registry::PostToolUsePayload;
|
||||
use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
@@ -136,7 +137,10 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
|
||||
parse_arguments::<ExecCommandArgs>(arguments)
|
||||
.ok()
|
||||
.map(|args| PreToolUsePayload { command: args.cmd })
|
||||
.map(|args| PreToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: args.cmd,
|
||||
})
|
||||
}
|
||||
|
||||
fn post_tool_use_payload(
|
||||
@@ -156,6 +160,7 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
|
||||
let tool_response = result.post_tool_use_response(call_id, payload)?;
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: args.cmd,
|
||||
tool_response,
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::session::tests::make_session_and_context;
|
||||
use crate::tools::context::ExecCommandToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -217,6 +218,7 @@ async fn exec_command_pre_tool_use_payload_uses_raw_command() {
|
||||
payload,
|
||||
}),
|
||||
Some(crate::tools::registry::PreToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: "printf exec command".to_string(),
|
||||
})
|
||||
);
|
||||
@@ -268,6 +270,7 @@ fn exec_command_post_tool_use_payload_uses_output_for_noninteractive_one_shot_co
|
||||
assert_eq!(
|
||||
UnifiedExecHandler.post_tool_use_payload("call-43", &payload, &output),
|
||||
Some(crate::tools::registry::PostToolUsePayload {
|
||||
tool_name: HookToolName::bash(),
|
||||
command: "echo three".to_string(),
|
||||
tool_response: serde_json::json!("three"),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Hook-facing tool names and matcher compatibility aliases.
|
||||
//!
|
||||
//! Hook stdin exposes one canonical `tool_name`, but matcher selection may also
|
||||
//! need to recognize names from adjacent tool ecosystems. Keeping those two
|
||||
//! concepts together prevents handlers from accidentally serializing a
|
||||
//! compatibility alias, such as `Write`, as the stable hook payload name.
|
||||
|
||||
/// Identifies a tool in hook payloads and hook matcher selection.
|
||||
///
|
||||
/// `name` is the canonical value serialized into hook stdin. Matcher aliases are
|
||||
/// internal-only compatibility names that may select the same hook handlers but
|
||||
/// must not change the payload seen by hook processes.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct HookToolName {
|
||||
name: String,
|
||||
matcher_aliases: Vec<String>,
|
||||
}
|
||||
|
||||
impl HookToolName {
|
||||
/// Builds a hook tool name with no matcher aliases.
|
||||
pub(crate) fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
matcher_aliases: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hook identity for file edits performed through `apply_patch`.
|
||||
///
|
||||
/// The serialized name remains `apply_patch` so logs and policies can key
|
||||
/// off the actual Codex tool. `Write` and `Edit` are accepted as matcher
|
||||
/// aliases for compatibility with hook configurations that describe edits
|
||||
/// using Claude Code-style names.
|
||||
pub(crate) fn apply_patch() -> Self {
|
||||
Self {
|
||||
name: "apply_patch".to_string(),
|
||||
matcher_aliases: vec!["Write".to_string(), "Edit".to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hook identity historically used for shell-like tools.
|
||||
pub(crate) fn bash() -> Self {
|
||||
Self::new("Bash")
|
||||
}
|
||||
|
||||
/// Returns the canonical hook name serialized into hook stdin.
|
||||
pub(crate) fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns additional matcher inputs that should select the same handlers.
|
||||
pub(crate) fn matcher_aliases(&self) -> &[String] {
|
||||
&self.matcher_aliases
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub(crate) mod code_mode;
|
||||
pub(crate) mod context;
|
||||
pub(crate) mod events;
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod hook_names;
|
||||
pub(crate) mod js_repl;
|
||||
pub(crate) mod network_approval;
|
||||
pub(crate) mod orchestrator;
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::hook_runtime::run_permission_request_hooks;
|
||||
use crate::network_policy_decision::denied_network_policy_message;
|
||||
use crate::session::session::Session;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use codex_hooks::PermissionRequestDecision;
|
||||
@@ -383,7 +384,7 @@ impl NetworkApprovalService {
|
||||
&turn_context,
|
||||
&guardian_approval_id,
|
||||
PermissionRequestPayload {
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_name: HookToolName::bash(),
|
||||
command,
|
||||
description: Some(format!("network-access {target}")),
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use codex_hooks::HookEvent;
|
||||
use codex_hooks::HookEventAfterToolUse;
|
||||
use codex_hooks::HookPayload;
|
||||
@@ -129,12 +130,25 @@ impl AnyToolResult {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct PreToolUsePayload {
|
||||
/// Hook-facing tool name model.
|
||||
///
|
||||
/// The canonical name is serialized to hook stdin, while aliases are used
|
||||
/// only for matcher compatibility.
|
||||
pub(crate) tool_name: HookToolName,
|
||||
/// Command-shaped input exposed at `tool_input.command`.
|
||||
pub(crate) command: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct PostToolUsePayload {
|
||||
/// Hook-facing tool name model.
|
||||
///
|
||||
/// Keep this aligned with the corresponding pre-use payload so external
|
||||
/// hook consumers can pair events by `tool_use_id`.
|
||||
pub(crate) tool_name: HookToolName,
|
||||
/// Command-shaped input exposed at `tool_input.command`.
|
||||
pub(crate) command: String,
|
||||
/// Tool result exposed at `tool_response`.
|
||||
pub(crate) tool_response: Value,
|
||||
}
|
||||
|
||||
@@ -330,6 +344,8 @@ impl ToolRegistry {
|
||||
&invocation.session,
|
||||
&invocation.turn,
|
||||
invocation.call_id.clone(),
|
||||
pre_tool_use_payload.tool_name.name().to_string(),
|
||||
pre_tool_use_payload.tool_name.matcher_aliases().to_vec(),
|
||||
pre_tool_use_payload.command.clone(),
|
||||
)
|
||||
.await
|
||||
@@ -400,6 +416,8 @@ impl ToolRegistry {
|
||||
&invocation.session,
|
||||
&invocation.turn,
|
||||
invocation.call_id.clone(),
|
||||
post_tool_use_payload.tool_name.name().to_string(),
|
||||
post_tool_use_payload.tool_name.matcher_aliases().to_vec(),
|
||||
post_tool_use_payload.command,
|
||||
post_tool_use_payload.tool_response,
|
||||
)
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
use crate::exec::is_likely_sandbox_denied;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::sandboxing::Approvable;
|
||||
use crate::tools::sandboxing::ApprovalCtx;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use crate::tools::sandboxing::Sandboxable;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
@@ -198,6 +200,17 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
) -> Option<ExecApprovalRequirement> {
|
||||
Some(req.exec_approval_requirement.clone())
|
||||
}
|
||||
|
||||
fn permission_request_payload(
|
||||
&self,
|
||||
req: &ApplyPatchRequest,
|
||||
) -> Option<PermissionRequestPayload> {
|
||||
Some(PermissionRequestPayload {
|
||||
tool_name: HookToolName::apply_patch(),
|
||||
command: req.action.patch.clone(),
|
||||
description: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
|
||||
|
||||
@@ -78,6 +78,39 @@ fn guardian_review_request_includes_patch_context() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_request_payload_uses_apply_patch_hook_name_and_aliases() {
|
||||
let runtime = ApplyPatchRuntime::new();
|
||||
let path = std::env::temp_dir()
|
||||
.join("apply-patch-permission-request-payload.txt")
|
||||
.abs();
|
||||
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
|
||||
let expected_patch = action.patch.clone();
|
||||
let req = ApplyPatchRequest {
|
||||
action,
|
||||
file_paths: vec![path],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
},
|
||||
additional_permissions: None,
|
||||
permissions_preapproved: false,
|
||||
};
|
||||
|
||||
let payload = runtime
|
||||
.permission_request_payload(&req)
|
||||
.expect("permission request payload");
|
||||
|
||||
assert_eq!(payload.tool_name.name(), "apply_patch");
|
||||
assert_eq!(
|
||||
payload.tool_name.matcher_aliases(),
|
||||
&["Write".to_string(), "Edit".to_string()]
|
||||
);
|
||||
assert_eq!(payload.command, expected_patch);
|
||||
assert_eq!(payload.description, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_sandbox_context_uses_active_attempt() {
|
||||
let path = std::env::temp_dir()
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::sandboxing::execute_env;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
@@ -201,7 +202,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
|
||||
fn permission_request_payload(&self, req: &ShellRequest) -> Option<PermissionRequestPayload> {
|
||||
Some(PermissionRequestPayload {
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_name: HookToolName::bash(),
|
||||
command: req.hook_command.clone(),
|
||||
description: req.justification.clone(),
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
@@ -402,7 +403,7 @@ impl CoreShellActionProvider {
|
||||
.pause_for(async move {
|
||||
// 1) Run PermissionRequest hooks
|
||||
let permission_request = PermissionRequestPayload {
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_name: HookToolName::bash(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
description: None,
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::ExecServerEnvConfig;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
@@ -186,7 +187,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
req: &UnifiedExecRequest,
|
||||
) -> Option<PermissionRequestPayload> {
|
||||
Some(PermissionRequestPayload {
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_name: HookToolName::bash(),
|
||||
command: req.hook_command.clone(),
|
||||
description: req.justification.clone(),
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::state::SessionServices;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
@@ -133,7 +134,7 @@ pub(crate) struct ApprovalCtx<'a> {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PermissionRequestPayload {
|
||||
pub tool_name: String,
|
||||
pub tool_name: HookToolName,
|
||||
pub command: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
@@ -1809,6 +1810,7 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file()
|
||||
.with_config(move |config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
|
||||
config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config);
|
||||
config.approvals_reviewer = ApprovalsReviewer::User;
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses::ev_apply_patch_function_call;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
@@ -505,11 +506,12 @@ fn read_permission_request_hook_inputs(home: &Path) -> Result<Vec<serde_json::Va
|
||||
|
||||
fn assert_permission_request_hook_input(
|
||||
hook_input: &Value,
|
||||
tool_name: &str,
|
||||
command: &str,
|
||||
description: Option<&str>,
|
||||
) {
|
||||
assert_eq!(hook_input["hook_event_name"], "PermissionRequest");
|
||||
assert_eq!(hook_input["tool_name"], "Bash");
|
||||
assert_eq!(hook_input["tool_name"], tool_name);
|
||||
assert_eq!(hook_input["tool_input"]["command"], command);
|
||||
assert_eq!(
|
||||
hook_input["tool_input"]["description"],
|
||||
@@ -527,10 +529,19 @@ fn assert_single_permission_request_hook_input(
|
||||
home: &Path,
|
||||
command: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<Vec<serde_json::Value>> {
|
||||
assert_single_permission_request_hook_input_for_tool(home, "Bash", command, description)
|
||||
}
|
||||
|
||||
fn assert_single_permission_request_hook_input_for_tool(
|
||||
home: &Path,
|
||||
tool_name: &str,
|
||||
command: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<Vec<serde_json::Value>> {
|
||||
let hook_inputs = read_permission_request_hook_inputs(home)?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_permission_request_hook_input(&hook_inputs[0], command, description);
|
||||
assert_permission_request_hook_input(&hook_inputs[0], tool_name, command, description);
|
||||
Ok(hook_inputs)
|
||||
}
|
||||
|
||||
@@ -1223,6 +1234,89 @@ async fn permission_request_hook_allows_shell_command_without_user_approval() ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn permission_request_hook_allows_apply_patch_with_write_alias() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "permissionrequest-apply-patch";
|
||||
let file_name = "permission_request_apply_patch.txt";
|
||||
let patch_path = format!("../{file_name}");
|
||||
let patch = format!(
|
||||
r#"*** Begin Patch
|
||||
*** Add File: {patch_path}
|
||||
+approved
|
||||
*** End Patch"#
|
||||
);
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_apply_patch_function_call(call_id, &patch),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "permission request hook allowed apply_patch"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) = write_permission_request_hook(
|
||||
home,
|
||||
Some("^Write$"),
|
||||
"allow",
|
||||
PERMISSION_REQUEST_ALLOW_REASON,
|
||||
) {
|
||||
panic!("failed to write permission request hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let target_path = test.workspace_path(&patch_path);
|
||||
|
||||
test.submit_turn_with_policies(
|
||||
"apply the patch after hook approval",
|
||||
AskForApproval::OnRequest,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: Default::default(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
requests[1].function_call_output(call_id);
|
||||
assert!(
|
||||
target_path.exists(),
|
||||
"approved apply_patch should create the out-of-workspace file"
|
||||
);
|
||||
|
||||
assert_single_permission_request_hook_input_for_tool(
|
||||
test.codex_home_path(),
|
||||
"apply_patch",
|
||||
&patch,
|
||||
/*description*/ None,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn permission_request_hook_sees_raw_exec_command_input() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -1814,7 +1908,159 @@ async fn pre_tool_use_blocks_exec_command_before_execution() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_does_not_fire_for_non_shell_tools() -> Result<()> {
|
||||
async fn pre_tool_use_blocks_apply_patch_before_execution() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "pretooluse-apply-patch";
|
||||
let file_name = "pre_tool_use_apply_patch.txt";
|
||||
let patch = format!(
|
||||
r#"*** Begin Patch
|
||||
*** Add File: {file_name}
|
||||
+blocked
|
||||
*** End Patch"#
|
||||
);
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_apply_patch_function_call(call_id, &patch),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "apply_patch blocked"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) = write_pre_tool_use_hook(
|
||||
home,
|
||||
Some("^apply_patch$"),
|
||||
"json_deny",
|
||||
"blocked apply_patch",
|
||||
) {
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("apply the blocked patch").await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let output_item = requests[1].function_call_output(call_id);
|
||||
let output = output_item
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("apply_patch output string");
|
||||
assert!(
|
||||
output.contains("Command blocked by PreToolUse hook: blocked apply_patch"),
|
||||
"blocked apply_patch output should surface the hook reason",
|
||||
);
|
||||
assert!(
|
||||
!test.workspace_path(file_name).exists(),
|
||||
"blocked apply_patch should not create the file"
|
||||
);
|
||||
|
||||
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "apply_patch");
|
||||
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], patch);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_blocks_apply_patch_with_write_alias() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "pretooluse-apply-patch-write";
|
||||
let file_name = "pre_tool_use_apply_patch_write.txt";
|
||||
let patch = format!(
|
||||
r#"*** Begin Patch
|
||||
*** Add File: {file_name}
|
||||
+blocked
|
||||
*** End Patch"#
|
||||
);
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_apply_patch_function_call(call_id, &patch),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "apply_patch blocked by Write alias"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) =
|
||||
write_pre_tool_use_hook(home, Some("^Write$"), "json_deny", "blocked write alias")
|
||||
{
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("apply the patch blocked by Write alias")
|
||||
.await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let output_item = requests[1].function_call_output(call_id);
|
||||
let output = output_item
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("apply_patch output string");
|
||||
assert!(
|
||||
output.contains("Command blocked by PreToolUse hook: blocked write alias"),
|
||||
"blocked apply_patch output should surface the hook reason",
|
||||
);
|
||||
assert!(
|
||||
!test.workspace_path(file_name).exists(),
|
||||
"blocked apply_patch should not create the file"
|
||||
);
|
||||
|
||||
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "apply_patch");
|
||||
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], patch);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_does_not_fire_for_plan_tool() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -1879,7 +2125,7 @@ async fn pre_tool_use_does_not_fire_for_non_shell_tools() -> Result<()> {
|
||||
let hook_log_path = test.codex_home_path().join("pre_tool_use_hook_log.jsonl");
|
||||
assert!(
|
||||
!hook_log_path.exists(),
|
||||
"non-shell tools should not trigger pre tool use hooks",
|
||||
"plan tool should not trigger pre tool use hooks",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -2269,7 +2515,171 @@ async fn post_tool_use_exit_two_replaces_one_shot_exec_command_output_with_feedb
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn post_tool_use_does_not_fire_for_non_shell_tools() -> Result<()> {
|
||||
async fn post_tool_use_records_additional_context_for_apply_patch() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "posttooluse-apply-patch";
|
||||
let file_name = "post_tool_use_apply_patch.txt";
|
||||
let patch = format!(
|
||||
r#"*** Begin Patch
|
||||
*** Add File: {file_name}
|
||||
+patched
|
||||
*** End Patch"#
|
||||
);
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_apply_patch_function_call(call_id, &patch),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "apply_patch post hook context observed"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let post_context = "Remember the apply_patch post-tool note.";
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) =
|
||||
write_post_tool_use_hook(home, Some("^apply_patch$"), "context", post_context)
|
||||
{
|
||||
panic!("failed to write post tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("apply the patch with post hook").await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(
|
||||
requests[1]
|
||||
.message_input_texts("developer")
|
||||
.contains(&post_context.to_string()),
|
||||
"follow-up request should include apply_patch post tool use context",
|
||||
);
|
||||
assert!(
|
||||
test.workspace_path(file_name).exists(),
|
||||
"apply_patch should create the file"
|
||||
);
|
||||
|
||||
let hook_inputs = read_post_tool_use_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "apply_patch");
|
||||
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], patch);
|
||||
let tool_response = hook_inputs[0]["tool_response"]
|
||||
.as_str()
|
||||
.context("apply_patch tool_response should be a string")?;
|
||||
let mut parsed_tool_response = serde_json::from_str::<Value>(tool_response)?;
|
||||
if let Some(metadata) = parsed_tool_response
|
||||
.get_mut("metadata")
|
||||
.and_then(Value::as_object_mut)
|
||||
{
|
||||
let _ = metadata.remove("duration_seconds");
|
||||
}
|
||||
assert_eq!(
|
||||
parsed_tool_response,
|
||||
serde_json::json!({
|
||||
"output": "Success. Updated the following files:\nA post_tool_use_apply_patch.txt\n",
|
||||
"metadata": {
|
||||
"exit_code": 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn post_tool_use_records_apply_patch_context_with_edit_alias() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "posttooluse-apply-patch-edit";
|
||||
let file_name = "post_tool_use_apply_patch_edit.txt";
|
||||
let patch = format!(
|
||||
r#"*** Begin Patch
|
||||
*** Add File: {file_name}
|
||||
+patched
|
||||
*** End Patch"#
|
||||
);
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_apply_patch_function_call(call_id, &patch),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "apply_patch edit hook context observed"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let post_context = "Remember the edit alias post-tool note.";
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) =
|
||||
write_post_tool_use_hook(home, Some("^Edit$"), "context", post_context)
|
||||
{
|
||||
panic!("failed to write post tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("apply the patch with edit alias post hook")
|
||||
.await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(
|
||||
requests[1]
|
||||
.message_input_texts("developer")
|
||||
.contains(&post_context.to_string()),
|
||||
"follow-up request should include apply_patch post tool use context",
|
||||
);
|
||||
assert!(
|
||||
test.workspace_path(file_name).exists(),
|
||||
"apply_patch should create the file"
|
||||
);
|
||||
|
||||
let hook_inputs = read_post_tool_use_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "apply_patch");
|
||||
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], patch);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn post_tool_use_does_not_fire_for_plan_tool() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -2337,7 +2747,7 @@ async fn post_tool_use_does_not_fire_for_non_shell_tools() -> Result<()> {
|
||||
let hook_log_path = test.codex_home_path().join("post_tool_use_hook_log.jsonl");
|
||||
assert!(
|
||||
!hook_log_path.exists(),
|
||||
"non-shell tools should not trigger post tool use hooks",
|
||||
"plan tool should not trigger post tool use hooks",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"$ref": "#/definitions/PermissionRequestToolInput"
|
||||
},
|
||||
"tool_name": {
|
||||
"const": "Bash",
|
||||
"type": "string"
|
||||
},
|
||||
"transcript_path": {
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
"$ref": "#/definitions/PostToolUseToolInput"
|
||||
},
|
||||
"tool_name": {
|
||||
"const": "Bash",
|
||||
"type": "string"
|
||||
},
|
||||
"tool_response": true,
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
"$ref": "#/definitions/PreToolUseToolInput"
|
||||
},
|
||||
"tool_name": {
|
||||
"const": "Bash",
|
||||
"type": "string"
|
||||
},
|
||||
"tool_use_id": {
|
||||
|
||||
@@ -27,6 +27,18 @@ pub(crate) fn select_handlers(
|
||||
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_handlers_for_matcher_inputs(
|
||||
handlers: &[ConfiguredHandler],
|
||||
event_name: HookEventName,
|
||||
matcher_inputs: &[&str],
|
||||
) -> Vec<ConfiguredHandler> {
|
||||
// Check each configured handler once, even when several compatibility names
|
||||
// match the same regex. A hook like `apply_patch|Write|Edit` should run a
|
||||
// single time for one tool call, not once per matching alias.
|
||||
handlers
|
||||
.iter()
|
||||
.filter(|handler| handler.event_name == event_name)
|
||||
@@ -35,7 +47,13 @@ pub(crate) fn select_handlers(
|
||||
| HookEventName::PermissionRequest
|
||||
| HookEventName::PostToolUse
|
||||
| HookEventName::SessionStart => {
|
||||
matches_matcher(handler.matcher.as_deref(), matcher_input)
|
||||
if matcher_inputs.is_empty() {
|
||||
matches_matcher(handler.matcher.as_deref(), /*input*/ None)
|
||||
} else {
|
||||
matcher_inputs
|
||||
.iter()
|
||||
.any(|input| matches_matcher(handler.matcher.as_deref(), Some(input)))
|
||||
}
|
||||
}
|
||||
HookEventName::UserPromptSubmit | HookEventName::Stop => true,
|
||||
})
|
||||
@@ -128,6 +146,7 @@ mod tests {
|
||||
|
||||
use super::ConfiguredHandler;
|
||||
use super::select_handlers;
|
||||
use super::select_handlers_for_matcher_inputs;
|
||||
|
||||
fn make_handler(
|
||||
event_name: HookEventName,
|
||||
@@ -282,6 +301,51 @@ mod tests {
|
||||
assert_eq!(selected_bash.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_tool_use_aliases_match_once_per_handler() {
|
||||
let handlers = vec![
|
||||
make_handler(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^apply_patch$"),
|
||||
"echo apply_patch",
|
||||
/*display_order*/ 0,
|
||||
),
|
||||
make_handler(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^Write$"),
|
||||
"echo write",
|
||||
/*display_order*/ 1,
|
||||
),
|
||||
make_handler(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^Edit$"),
|
||||
"echo edit",
|
||||
/*display_order*/ 2,
|
||||
),
|
||||
make_handler(
|
||||
HookEventName::PreToolUse,
|
||||
Some("apply_patch|Write|Edit"),
|
||||
"echo combined",
|
||||
/*display_order*/ 3,
|
||||
),
|
||||
];
|
||||
|
||||
let selected = select_handlers_for_matcher_inputs(
|
||||
&handlers,
|
||||
HookEventName::PreToolUse,
|
||||
&["apply_patch", "Write", "Edit"],
|
||||
);
|
||||
|
||||
assert_eq!(selected.len(), 4);
|
||||
assert_eq!(
|
||||
selected
|
||||
.iter()
|
||||
.map(|handler| handler.display_order)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![0, 1, 2, 3],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_prompt_submit_ignores_matcher() {
|
||||
let handlers = vec![
|
||||
|
||||
@@ -129,6 +129,17 @@ pub(crate) fn matches_matcher(matcher: Option<&str>, input: Option<&str>) -> boo
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn matcher_inputs<'a>(
|
||||
tool_name: &'a str,
|
||||
matcher_aliases: &'a [String],
|
||||
) -> Vec<&'a str> {
|
||||
// Keep the canonical name first so matcher previews and execution preserve
|
||||
// the same primary identity that hook stdin will serialize.
|
||||
std::iter::once(tool_name)
|
||||
.chain(matcher_aliases.iter().map(String::as_str))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_match_all_matcher(matcher: &str) -> bool {
|
||||
matcher.is_empty() || matcher == "*"
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct PermissionRequestRequest {
|
||||
pub model: String,
|
||||
pub permission_mode: String,
|
||||
pub tool_name: String,
|
||||
pub matcher_aliases: Vec<String>,
|
||||
pub run_id_suffix: String,
|
||||
pub command: String,
|
||||
pub description: Option<String>,
|
||||
@@ -66,10 +67,11 @@ pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
request: &PermissionRequestRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_handlers(
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PermissionRequest,
|
||||
Some(&request.tool_name),
|
||||
&matcher_inputs,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| {
|
||||
@@ -86,10 +88,11 @@ pub(crate) async fn run(
|
||||
shell: &CommandShell,
|
||||
request: PermissionRequestRequest,
|
||||
) -> PermissionRequestOutcome {
|
||||
let matched = dispatcher::select_handlers(
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
let matched = dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PermissionRequest,
|
||||
Some(&request.tool_name),
|
||||
&matcher_inputs,
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return PermissionRequestOutcome {
|
||||
|
||||
@@ -28,6 +28,7 @@ pub struct PostToolUseRequest {
|
||||
pub model: String,
|
||||
pub permission_mode: String,
|
||||
pub tool_name: String,
|
||||
pub matcher_aliases: Vec<String>,
|
||||
pub tool_use_id: String,
|
||||
pub command: String,
|
||||
pub tool_response: Value,
|
||||
@@ -54,10 +55,11 @@ pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
request: &PostToolUseRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_handlers(
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PostToolUse,
|
||||
Some(&request.tool_name),
|
||||
&matcher_inputs,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| {
|
||||
@@ -71,10 +73,11 @@ pub(crate) async fn run(
|
||||
shell: &CommandShell,
|
||||
request: PostToolUseRequest,
|
||||
) -> PostToolUseOutcome {
|
||||
let matched = dispatcher::select_handlers(
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
let matched = dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PostToolUse,
|
||||
Some(&request.tool_name),
|
||||
&matcher_inputs,
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return PostToolUseOutcome {
|
||||
@@ -86,21 +89,7 @@ pub(crate) async fn run(
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = match serde_json::to_string(&PostToolUseCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PostToolUse".to_string(),
|
||||
model: request.model.clone(),
|
||||
permission_mode: request.permission_mode.clone(),
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_input: PostToolUseToolInput {
|
||||
command: request.command.clone(),
|
||||
},
|
||||
tool_response: request.tool_response.clone(),
|
||||
tool_use_id: request.tool_use_id.clone(),
|
||||
}) {
|
||||
let input_json = match command_input_json(&request) {
|
||||
Ok(input_json) => input_json,
|
||||
Err(error) => {
|
||||
let hook_events = common::serialization_failure_hook_events_for_tool_use(
|
||||
@@ -153,6 +142,29 @@ pub(crate) async fn run(
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes 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.
|
||||
fn command_input_json(request: &PostToolUseRequest) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(&PostToolUseCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PostToolUse".to_string(),
|
||||
model: request.model.clone(),
|
||||
permission_mode: request.permission_mode.clone(),
|
||||
tool_name: request.tool_name.clone(),
|
||||
tool_input: PostToolUseToolInput {
|
||||
command: request.command.clone(),
|
||||
},
|
||||
tool_response: request.tool_response.clone(),
|
||||
tool_use_id: request.tool_use_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_completed(
|
||||
handler: &ConfiguredHandler,
|
||||
run_result: CommandRunResult,
|
||||
@@ -314,12 +326,25 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::PostToolUseHandlerData;
|
||||
use super::command_input_json;
|
||||
use super::parse_completed;
|
||||
use super::preview;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::events::common;
|
||||
|
||||
#[test]
|
||||
fn command_input_uses_request_tool_name() {
|
||||
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");
|
||||
|
||||
assert_eq!(input["tool_name"], "apply_patch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_decision_stops_normal_processing() {
|
||||
let parsed = parse_completed(
|
||||
@@ -551,6 +576,7 @@ mod tests {
|
||||
model: "gpt-test".to_string(),
|
||||
permission_mode: "default".to_string(),
|
||||
tool_name: "Bash".to_string(),
|
||||
matcher_aliases: Vec::new(),
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
command: "echo hello".to_string(),
|
||||
tool_response: json!({"ok": true}),
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct PreToolUseRequest {
|
||||
pub model: String,
|
||||
pub permission_mode: String,
|
||||
pub tool_name: String,
|
||||
pub matcher_aliases: Vec<String>,
|
||||
pub tool_use_id: String,
|
||||
pub command: String,
|
||||
}
|
||||
@@ -47,10 +48,11 @@ pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
request: &PreToolUseRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_handlers(
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PreToolUse,
|
||||
Some(&request.tool_name),
|
||||
&matcher_inputs,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| {
|
||||
@@ -64,10 +66,11 @@ pub(crate) async fn run(
|
||||
shell: &CommandShell,
|
||||
request: PreToolUseRequest,
|
||||
) -> PreToolUseOutcome {
|
||||
let matched = dispatcher::select_handlers(
|
||||
let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
|
||||
let matched = dispatcher::select_handlers_for_matcher_inputs(
|
||||
handlers,
|
||||
HookEventName::PreToolUse,
|
||||
Some(&request.tool_name),
|
||||
&matcher_inputs,
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return PreToolUseOutcome {
|
||||
@@ -77,20 +80,7 @@ pub(crate) async fn run(
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = match serde_json::to_string(&PreToolUseCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PreToolUse".to_string(),
|
||||
model: request.model.clone(),
|
||||
permission_mode: request.permission_mode.clone(),
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_input: crate::schema::PreToolUseToolInput {
|
||||
command: request.command.clone(),
|
||||
},
|
||||
tool_use_id: request.tool_use_id.clone(),
|
||||
}) {
|
||||
let input_json = match command_input_json(&request) {
|
||||
Ok(input_json) => input_json,
|
||||
Err(error) => {
|
||||
let hook_events = common::serialization_failure_hook_events_for_tool_use(
|
||||
@@ -130,6 +120,28 @@ pub(crate) async fn run(
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes 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.
|
||||
fn command_input_json(request: &PreToolUseRequest) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(&PreToolUseCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PreToolUse".to_string(),
|
||||
model: request.model.clone(),
|
||||
permission_mode: request.permission_mode.clone(),
|
||||
tool_name: request.tool_name.clone(),
|
||||
tool_input: crate::schema::PreToolUseToolInput {
|
||||
command: request.command.clone(),
|
||||
},
|
||||
tool_use_id: request.tool_use_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_completed(
|
||||
handler: &ConfiguredHandler,
|
||||
run_result: CommandRunResult,
|
||||
@@ -250,12 +262,25 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::PreToolUseHandlerData;
|
||||
use super::command_input_json;
|
||||
use super::parse_completed;
|
||||
use super::preview;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::events::common;
|
||||
|
||||
#[test]
|
||||
fn command_input_uses_request_tool_name() {
|
||||
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");
|
||||
|
||||
assert_eq!(input["tool_name"], "apply_patch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_decision_deny_blocks_processing() {
|
||||
let parsed = parse_completed(
|
||||
@@ -540,6 +565,7 @@ mod tests {
|
||||
model: "gpt-test".to_string(),
|
||||
permission_mode: "default".to_string(),
|
||||
tool_name: "Bash".to_string(),
|
||||
matcher_aliases: Vec::new(),
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
command: "echo hello".to_string(),
|
||||
}
|
||||
|
||||
@@ -231,7 +231,6 @@ pub(crate) struct PreToolUseCommandInput {
|
||||
pub model: String,
|
||||
#[schemars(schema_with = "permission_mode_schema")]
|
||||
pub permission_mode: String,
|
||||
#[schemars(schema_with = "pre_tool_use_tool_name_schema")]
|
||||
pub tool_name: String,
|
||||
pub tool_input: PreToolUseToolInput,
|
||||
pub tool_use_id: String,
|
||||
@@ -260,7 +259,6 @@ pub(crate) struct PermissionRequestCommandInput {
|
||||
pub model: String,
|
||||
#[schemars(schema_with = "permission_mode_schema")]
|
||||
pub permission_mode: String,
|
||||
#[schemars(schema_with = "permission_request_tool_name_schema")]
|
||||
pub tool_name: String,
|
||||
pub tool_input: PermissionRequestToolInput,
|
||||
}
|
||||
@@ -286,7 +284,6 @@ pub(crate) struct PostToolUseCommandInput {
|
||||
pub model: String,
|
||||
#[schemars(schema_with = "permission_mode_schema")]
|
||||
pub permission_mode: String,
|
||||
#[schemars(schema_with = "post_tool_use_tool_name_schema")]
|
||||
pub tool_name: String,
|
||||
pub tool_input: PostToolUseToolInput,
|
||||
pub tool_response: Value,
|
||||
@@ -545,10 +542,6 @@ fn post_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("PostToolUse")
|
||||
}
|
||||
|
||||
fn post_tool_use_tool_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("Bash")
|
||||
}
|
||||
|
||||
fn pre_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("PreToolUse")
|
||||
}
|
||||
@@ -557,14 +550,6 @@ fn permission_request_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Sche
|
||||
string_const_schema("PermissionRequest")
|
||||
}
|
||||
|
||||
fn pre_tool_use_tool_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("Bash")
|
||||
}
|
||||
|
||||
fn permission_request_tool_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("Bash")
|
||||
}
|
||||
|
||||
fn user_prompt_submit_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("UserPromptSubmit")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user