mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
codex: support hooks in config.toml and requirements.toml (#18893)
## Summary Support the existing hooks schema in inline TOML so hooks can be configured from both `config.toml` and enterprise-managed `requirements.toml` without requiring a separate `hooks.json` payload. This gives enterprise admins a way to ship managed hook policy through the existing requirements channel while still leaving script delivery to MDM or other device-management tooling, and it keeps `hooks.json` working unchanged for existing users. This also lays the groundwork for follow-on managed filtering work such as #15937, while continuing to respect project trust gating from #14718. It does **not** implement `allow_managed_hooks_only` itself. NOTE: yes, it's a bit unfortunate that the toml isn't formatted as closely as normal to our default styling. This is because we're trying to stay compatible with the spec for plugins/hooks that we'll need to support & the main usecase here is embedding into requirements.toml ## What changed - moved the shared hook serde model out of `codex-rs/hooks` into `codex-rs/config` so the same schema can power `hooks.json`, inline `config.toml` hooks, and managed `requirements.toml` hooks - added `hooks` support to both `ConfigToml` and `ConfigRequirementsToml`, including requirements-side `managed_dir` / `windows_managed_dir` - treated requirements-managed hooks as one constrained value via `Constrained`, so managed hook policy is merged atomically and cannot drift across requirement sources - updated hook discovery to load requirements-managed hooks first, then per-layer `hooks.json`, then per-layer inline TOML hooks, with a warning when a single layer defines both representations - threaded managed hook metadata through discovered handlers and exposed requirements hooks in app-server responses, generated schemas, and `/debug-config` - added hook/config coverage in `codex-rs/config`, `codex-rs/hooks`, `codex-rs/core/src/config_loader/tests.rs`, and `codex-rs/core/tests/suite/hooks.rs` ## Testing - `cargo test -p codex-config` - `cargo test -p codex-hooks` - `cargo test -p codex-app-server config_api` ## Documentation Companion updates are needed in the developers website repo for: - the hooks guide - the config reference, sample, basic, and advanced pages - the enterprise managed configuration guide --------- Co-authored-by: Michael Bolin <mbolin@openai.com>
This commit is contained in:
co-authored by
Michael Bolin
parent
9955eacd22
commit
2b2de3f38b
@@ -848,6 +848,117 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"HookEventsToml": {
|
||||
"properties": {
|
||||
"PermissionRequest": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"PostToolUse": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"PreToolUse": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"SessionStart": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"Stop": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookHandlerConfig": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"async": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"statusMessage": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"default": null,
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"command"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"prompt"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"agent"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MarketplaceConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -898,6 +1009,22 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MatcherGroup": {
|
||||
"properties": {
|
||||
"hooks": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookHandlerConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"matcher": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerEnvVar": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2634,6 +2761,14 @@
|
||||
"default": null,
|
||||
"description": "Settings that govern if and what will be written to `~/.codex/history.jsonl`."
|
||||
},
|
||||
"hooks": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HookEventsToml"
|
||||
}
|
||||
],
|
||||
"description": "Lifecycle hooks configured inline in TOML."
|
||||
},
|
||||
"include_apps_instructions": {
|
||||
"description": "Whether to inject the `<apps_instructions>` developer block.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -5732,6 +5732,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
crate::config_loader::WebSearchModeRequirement::Cached,
|
||||
]),
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -6407,6 +6408,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
|
||||
@@ -1577,6 +1577,7 @@ impl Config {
|
||||
sandbox_policy: mut constrained_sandbox_policy,
|
||||
web_search_mode: mut constrained_web_search_mode,
|
||||
feature_requirements,
|
||||
managed_hooks: _,
|
||||
mcp_servers,
|
||||
exec_policy: _,
|
||||
enforce_residency,
|
||||
|
||||
@@ -45,7 +45,11 @@ pub use codex_config::ConstrainedWithSource;
|
||||
pub use codex_config::FeatureRequirementsToml;
|
||||
pub use codex_config::FilesystemConstraints;
|
||||
pub use codex_config::FilesystemDenyReadPattern;
|
||||
pub use codex_config::HookEventsToml;
|
||||
pub use codex_config::HookHandlerConfig;
|
||||
pub use codex_config::LoaderOverrides;
|
||||
pub use codex_config::ManagedHooksRequirementsToml;
|
||||
pub use codex_config::MatcherGroup;
|
||||
pub use codex_config::McpServerIdentity;
|
||||
pub use codex_config::McpServerRequirement;
|
||||
pub use codex_config::NetworkConstraints;
|
||||
|
||||
@@ -777,6 +777,7 @@ allowed_approval_policies = ["on-request"]
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -833,6 +834,7 @@ allowed_approval_policies = ["on-request"]
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -1041,6 +1043,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -1084,6 +1087,62 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
let managed_dir = tmp.path().join("managed-hooks");
|
||||
tokio::fs::create_dir_all(&managed_dir).await?;
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?;
|
||||
|
||||
let requirements = ConfigRequirementsToml {
|
||||
hooks: Some(codex_config::ManagedHooksRequirementsToml {
|
||||
managed_dir: Some(managed_dir.clone()),
|
||||
windows_managed_dir: None,
|
||||
hooks: codex_config::HookEventsToml {
|
||||
pre_tool_use: vec![codex_config::MatcherGroup {
|
||||
matcher: Some("^Bash$".to_string()),
|
||||
hooks: vec![codex_config::HookHandlerConfig::Command {
|
||||
command: format!("python3 {}/pre.py", managed_dir.display()),
|
||||
timeout_sec: Some(10),
|
||||
r#async: false,
|
||||
status_message: Some("checking".to_string()),
|
||||
}],
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
}),
|
||||
..ConfigRequirementsToml::default()
|
||||
};
|
||||
let expected = requirements.clone();
|
||||
let cloud_requirements = CloudRequirementsLoader::new(async move { Ok(Some(requirements)) });
|
||||
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(cwd),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
cloud_requirements,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(layers.requirements_toml().hooks, expected.hooks);
|
||||
assert_eq!(
|
||||
layers
|
||||
.requirements()
|
||||
.managed_hooks
|
||||
.as_ref()
|
||||
.map(|hooks| hooks.source.clone()),
|
||||
Some(Some(RequirementSource::CloudRequirements))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_applies_matching_remote_sandbox_config() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
|
||||
@@ -482,12 +482,12 @@ impl TestCodexBuilder {
|
||||
..built_in_model_providers(/*openai_base_url*/ None)["openai"].clone()
|
||||
};
|
||||
let cwd = Arc::new(TempDir::new()?);
|
||||
let mut config = load_default_config_for_test(home).await;
|
||||
config.cwd = cwd_override;
|
||||
config.model_provider = model_provider;
|
||||
for hook in self.pre_build_hooks.drain(..) {
|
||||
hook(home.path());
|
||||
}
|
||||
let mut config = load_default_config_for_test(home).await;
|
||||
config.cwd = cwd_override;
|
||||
config.model_provider = model_provider;
|
||||
if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex") {
|
||||
config.codex_self_exe = Some(path);
|
||||
} else if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex-exec") {
|
||||
|
||||
@@ -254,6 +254,74 @@ elif mode == "exit_2":
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_pre_tool_use_hook_toml(
|
||||
home: &Path,
|
||||
script_name: &str,
|
||||
log_name: &str,
|
||||
matcher: Option<&str>,
|
||||
mode: &str,
|
||||
reason: &str,
|
||||
) -> Result<()> {
|
||||
let script_path = home.join(script_name);
|
||||
let log_path = home.join(log_name);
|
||||
let mode_json = serde_json::to_string(mode).context("serialize pre tool use mode")?;
|
||||
let reason_json = serde_json::to_string(reason).context("serialize pre tool use reason")?;
|
||||
let script = format!(
|
||||
r#"import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
log_path = Path(r"{log_path}")
|
||||
mode = {mode_json}
|
||||
reason = {reason_json}
|
||||
|
||||
payload = json.load(sys.stdin)
|
||||
|
||||
with log_path.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
|
||||
if mode == "json_deny":
|
||||
print(json.dumps({{
|
||||
"hookSpecificOutput": {{
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": reason
|
||||
}}
|
||||
}}))
|
||||
elif mode == "exit_2":
|
||||
sys.stderr.write(reason + "\n")
|
||||
raise SystemExit(2)
|
||||
"#,
|
||||
log_path = log_path.display(),
|
||||
mode_json = mode_json,
|
||||
reason_json = reason_json,
|
||||
);
|
||||
let matcher_line = matcher
|
||||
.map(|matcher| format!("matcher = '{matcher}'\n"))
|
||||
.unwrap_or_default();
|
||||
let config_toml = format!(
|
||||
r#"[features]
|
||||
codex_hooks = true
|
||||
|
||||
[hooks]
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
{matcher_line}
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = 'python3 {script_path}'
|
||||
statusMessage = "running pre tool use hook"
|
||||
"#,
|
||||
matcher_line = matcher_line,
|
||||
script_path = script_path.display(),
|
||||
);
|
||||
|
||||
fs::write(&script_path, script).context("write TOML pre tool use hook script")?;
|
||||
fs::write(home.join("config.toml"), config_toml).context("write config.toml hooks")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_permission_request_hook(
|
||||
home: &Path,
|
||||
matcher: Option<&str>,
|
||||
@@ -546,12 +614,7 @@ fn read_stop_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
}
|
||||
|
||||
fn read_pre_tool_use_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
fs::read_to_string(home.join("pre_tool_use_hook_log.jsonl"))
|
||||
.context("read pre tool use hook log")?
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| serde_json::from_str(line).context("parse pre tool use hook log line"))
|
||||
.collect()
|
||||
read_hook_inputs_from_log(home.join("pre_tool_use_hook_log.jsonl").as_path())
|
||||
}
|
||||
|
||||
fn read_permission_request_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
@@ -605,11 +668,15 @@ fn assert_single_permission_request_hook_input_for_tool(
|
||||
}
|
||||
|
||||
fn read_post_tool_use_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
fs::read_to_string(home.join("post_tool_use_hook_log.jsonl"))
|
||||
.context("read post tool use hook log")?
|
||||
read_hook_inputs_from_log(home.join("post_tool_use_hook_log.jsonl").as_path())
|
||||
}
|
||||
|
||||
fn read_hook_inputs_from_log(log_path: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
fs::read_to_string(log_path)
|
||||
.with_context(|| format!("read hook log {}", log_path.display()))?
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| serde_json::from_str(line).context("parse post tool use hook log line"))
|
||||
.map(|line| serde_json::from_str(line).context("parse hook log line"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1785,6 +1852,190 @@ async fn pre_tool_use_blocks_shell_command_before_execution() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_blocks_shell_when_defined_in_config_toml() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "pretooluse-config-toml";
|
||||
let marker = std::env::temp_dir().join("pretooluse-config-toml-marker");
|
||||
let command = format!("printf blocked > {}", marker.display());
|
||||
let args = serde_json::json!({ "command": command });
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
core_test_support::responses::ev_function_call(
|
||||
call_id,
|
||||
"shell_command",
|
||||
&serde_json::to_string(&args)?,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "config.toml hook blocked it"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_pre_build_hook(|home| {
|
||||
if let Err(error) = write_pre_tool_use_hook_toml(
|
||||
home,
|
||||
"pre_tool_use_config_hook.py",
|
||||
"pre_tool_use_config_hook_log.jsonl",
|
||||
Some("^Bash$"),
|
||||
"json_deny",
|
||||
"blocked by config toml hook",
|
||||
) {
|
||||
panic!("failed to write config.toml hook test fixture: {error}");
|
||||
}
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
if marker.exists() {
|
||||
fs::remove_file(&marker).context("remove leftover config.toml marker")?;
|
||||
}
|
||||
|
||||
test.submit_turn_with_policy(
|
||||
"run the blocked shell command from config toml",
|
||||
codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.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("shell command output string");
|
||||
assert!(
|
||||
output.contains("Command blocked by PreToolUse hook: blocked by config toml hook"),
|
||||
"blocked tool output should surface the config.toml hook reason",
|
||||
);
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"config.toml hook should block command execution"
|
||||
);
|
||||
|
||||
let hook_inputs = read_hook_inputs_from_log(
|
||||
test.codex_home_path()
|
||||
.join("pre_tool_use_config_hook_log.jsonl")
|
||||
.as_path(),
|
||||
)?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["hook_event_name"], "PreToolUse");
|
||||
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_merges_hooks_json_and_config_toml() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "pretooluse-merged-sources";
|
||||
let command = "printf merged-hooks".to_string();
|
||||
let args = serde_json::json!({ "command": command });
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
core_test_support::responses::ev_function_call(
|
||||
call_id,
|
||||
"shell_command",
|
||||
&serde_json::to_string(&args)?,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "merged hook context observed"),
|
||||
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("^Bash$"), "allow", "unused") {
|
||||
panic!("failed to write hooks.json hook fixture: {error}");
|
||||
}
|
||||
if let Err(error) = write_pre_tool_use_hook_toml(
|
||||
home,
|
||||
"pre_tool_use_toml_hook.py",
|
||||
"pre_tool_use_toml_hook_log.jsonl",
|
||||
Some("^Bash$"),
|
||||
"allow",
|
||||
"unused",
|
||||
) {
|
||||
panic!("failed to write config.toml hook fixture: {error}");
|
||||
}
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("run the shell command with merged hook sources")
|
||||
.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("shell command output string");
|
||||
assert!(
|
||||
output.contains("merged-hooks"),
|
||||
"shell command output should still reach the model",
|
||||
);
|
||||
|
||||
let json_hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?
|
||||
.into_iter()
|
||||
.map(|hook_input| {
|
||||
serde_json::json!({
|
||||
"hook_event_name": hook_input["hook_event_name"],
|
||||
"tool_name": hook_input["tool_name"],
|
||||
"tool_use_id": hook_input["tool_use_id"],
|
||||
"tool_input": hook_input["tool_input"],
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let toml_hook_inputs = read_hook_inputs_from_log(
|
||||
test.codex_home_path()
|
||||
.join("pre_tool_use_toml_hook_log.jsonl")
|
||||
.as_path(),
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|hook_input| {
|
||||
serde_json::json!({
|
||||
"hook_event_name": hook_input["hook_event_name"],
|
||||
"tool_name": hook_input["tool_name"],
|
||||
"tool_use_id": hook_input["tool_use_id"],
|
||||
"tool_input": hook_input["tool_input"],
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let expected_hook_inputs = vec![serde_json::json!({
|
||||
"hook_event_name": "PreToolUse",
|
||||
"tool_name": "Bash",
|
||||
"tool_use_id": call_id,
|
||||
"tool_input": {
|
||||
"command": command,
|
||||
},
|
||||
})];
|
||||
assert_eq!(expected_hook_inputs, json_hook_inputs);
|
||||
assert_eq!(expected_hook_inputs, toml_hook_inputs);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_blocks_local_shell_before_execution() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user