mirror of
https://github.com/openai/codex.git
synced 2026-05-30 07:50:17 +00:00
hook trust metadata and enforcement (#20321)
# Why We want shared hook trust that both the app and the TUI can build on, but the metadata is only useful if runtime behavior agrees with it. This PR adds a single backend trust model for hooks so unmanaged hooks cannot run until the current definition has been reviewed, while managed hooks remain runnable and non-configurable. # What - persist `trusted_hash` alongside hook state in `config.toml` - expose `currentHash` and derived `trustStatus` through `hooks/list` - derive trust from normalized hook definitions so equivalent hooks from `config.toml` and `hooks.json` share the same trust identity - gate unmanaged hooks on trust before they enter the runnable handler set # Reviewer Notes - key file to review is `codex-rs/hooks/src/engine/discovery.rs` - the only **core** change is schema related
This commit is contained in:
@@ -999,6 +999,9 @@
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"trusted_hash": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::hooks::trusted_config_layer_stack;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
@@ -163,13 +164,24 @@ print({hook_output:?})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("write hooks.json");
|
||||
let hook_list = codex_hooks::list_hooks(HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(turn_context.config.config_layer_stack.clone()),
|
||||
..HooksConfig::default()
|
||||
});
|
||||
assert_eq!(hook_list.hooks.len(), 1);
|
||||
let trusted_config_layer_stack = trusted_config_layer_stack(
|
||||
&turn_context.config.config_layer_stack,
|
||||
&turn_context.config.codex_home,
|
||||
hook_list.hooks,
|
||||
);
|
||||
|
||||
session
|
||||
.services
|
||||
.hooks
|
||||
.store(Arc::new(Hooks::new(HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(turn_context.config.config_layer_stack.clone()),
|
||||
config_layer_stack: Some(trusted_config_layer_stack),
|
||||
shell_program: (!cfg!(windows)).then_some("/bin/sh".to_string()),
|
||||
shell_args: if cfg!(windows) {
|
||||
Vec::new()
|
||||
|
||||
@@ -1173,15 +1173,17 @@ async fn reload_user_config_layer_refreshes_hooks() -> anyhow::Result<()> {
|
||||
.await?;
|
||||
let codex_home = session.codex_home().await;
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
std::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[hooks]
|
||||
|
||||
[[hooks.SessionStart]]
|
||||
hooks = [{ type = "command", command = "python3 /tmp/user.py" }]
|
||||
"#,
|
||||
)?;
|
||||
let config_toml_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let user_config: codex_config::TomlValue = serde_json::from_value(serde_json::json!({
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "python3 /tmp/user.py",
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}))?;
|
||||
|
||||
let request = codex_hooks::SessionStartRequest {
|
||||
session_id: session.conversation_id,
|
||||
@@ -1193,6 +1195,39 @@ hooks = [{ type = "command", command = "python3 /tmp/user.py" }]
|
||||
};
|
||||
assert!(session.hooks().preview_session_start(&request).is_empty());
|
||||
|
||||
let config = session.get_config().await;
|
||||
let hook_list = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(
|
||||
config
|
||||
.config_layer_stack
|
||||
.with_user_config(&config_toml_path, user_config.clone()),
|
||||
),
|
||||
..codex_hooks::HooksConfig::default()
|
||||
});
|
||||
assert_eq!(hook_list.hooks.len(), 1);
|
||||
assert_eq!(
|
||||
hook_list.hooks[0].trust_status,
|
||||
codex_protocol::protocol::HookTrustStatus::Untrusted
|
||||
);
|
||||
|
||||
let trusted_user_config: codex_config::TomlValue = serde_json::from_value(serde_json::json!({
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "python3 /tmp/user.py",
|
||||
}],
|
||||
}],
|
||||
"state": {
|
||||
hook_list.hooks[0].key.clone(): {
|
||||
"trusted_hash": hook_list.hooks[0].current_hash.clone(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))?;
|
||||
std::fs::write(&config_toml_path, toml::to_string(&trusted_user_config)?)?;
|
||||
|
||||
session.reload_user_config_layer().await;
|
||||
|
||||
assert_eq!(session.hooks().preview_session_start(&request).len(), 1);
|
||||
@@ -8568,17 +8603,27 @@ async fn session_start_hooks_only_load_from_trusted_project_layers() -> std::io:
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let preview = preview_session_start_hooks(&config).await?;
|
||||
let hook_list = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(config.config_layer_stack.clone()),
|
||||
..codex_hooks::HooksConfig::default()
|
||||
});
|
||||
let expected_source_path = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(
|
||||
nested_dot_codex.join("hooks.json"),
|
||||
)?;
|
||||
assert_eq!(
|
||||
preview
|
||||
hook_list
|
||||
.hooks
|
||||
.iter()
|
||||
.map(|run| &run.source_path)
|
||||
.map(|hook| &hook.source_path)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![&expected_source_path],
|
||||
);
|
||||
assert_eq!(
|
||||
hook_list.hooks[0].trust_status,
|
||||
codex_protocol::protocol::HookTrustStatus::Untrusted
|
||||
);
|
||||
assert!(preview_session_start_hooks(&config).await?.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -8618,11 +8663,23 @@ async fn session_start_hooks_require_project_trust_without_config_toml() -> std:
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let hook_list = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(config.config_layer_stack.clone()),
|
||||
..codex_hooks::HooksConfig::default()
|
||||
});
|
||||
assert_eq!(
|
||||
preview_session_start_hooks(&config).await?.len(),
|
||||
hook_list.hooks.len(),
|
||||
expected_hooks,
|
||||
"unexpected hook count for {name}",
|
||||
"unexpected discovered hook count for {name}",
|
||||
);
|
||||
assert!(preview_session_start_hooks(&config).await?.is_empty());
|
||||
if expected_hooks == 1 {
|
||||
assert_eq!(
|
||||
hook_list.hooks[0].trust_status,
|
||||
codex_protocol::protocol::HookTrustStatus::Untrusted
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -371,6 +371,29 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
|
||||
.to_string(),
|
||||
)
|
||||
.context("write hooks.json")?;
|
||||
let config_toml_path = turn_context
|
||||
.config
|
||||
.codex_home
|
||||
.join(codex_config::CONFIG_TOML_FILE);
|
||||
let hook_list = codex_hooks::list_hooks(HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(turn_context.config.config_layer_stack.clone()),
|
||||
..HooksConfig::default()
|
||||
});
|
||||
assert_eq!(hook_list.hooks.len(), 1);
|
||||
let trusted_config_layer_stack = turn_context.config.config_layer_stack.with_user_config(
|
||||
&config_toml_path,
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"hooks": {
|
||||
"state": {
|
||||
hook_list.hooks[0].key.clone(): {
|
||||
"trusted_hash": hook_list.hooks[0].current_hash.clone(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.context("build trusted hook state")?,
|
||||
);
|
||||
|
||||
let mut hook_shell_argv = session
|
||||
.user_shell()
|
||||
@@ -382,7 +405,7 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
|
||||
.hooks
|
||||
.store(Arc::new(Hooks::new(HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(turn_context.config.config_layer_stack.clone()),
|
||||
config_layer_stack: Some(trusted_config_layer_stack),
|
||||
shell_program: Some(hook_shell_program),
|
||||
shell_args: hook_shell_argv,
|
||||
..HooksConfig::default()
|
||||
|
||||
@@ -19,6 +19,7 @@ codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
|
||||
70
codex-rs/core/tests/common/hooks.rs
Normal file
70
codex-rs/core/tests/common/hooks.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::TomlValue;
|
||||
use codex_core::config::Config;
|
||||
use codex_features::Feature;
|
||||
use codex_hooks::HookListEntry;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
pub fn trust_discovered_hooks(config: &mut Config) {
|
||||
if let Err(err) = config.features.enable(Feature::CodexHooks) {
|
||||
panic!("test config should allow feature update: {err}");
|
||||
}
|
||||
|
||||
let listed = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(config.config_layer_stack.clone()),
|
||||
..codex_hooks::HooksConfig::default()
|
||||
});
|
||||
assert!(
|
||||
!listed.hooks.is_empty(),
|
||||
"trusted hook fixture should discover at least one hook"
|
||||
);
|
||||
trust_hooks(config, listed.hooks);
|
||||
}
|
||||
|
||||
pub fn trust_hooks(config: &mut Config, hooks: Vec<HookListEntry>) {
|
||||
config.config_layer_stack =
|
||||
trusted_config_layer_stack(&config.config_layer_stack, &config.codex_home, hooks);
|
||||
}
|
||||
|
||||
pub fn trusted_config_layer_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
codex_home: &AbsolutePathBuf,
|
||||
hooks: Vec<HookListEntry>,
|
||||
) -> ConfigLayerStack {
|
||||
let mut user_config = config_layer_stack
|
||||
.get_user_layer()
|
||||
.map(|layer| layer.config.clone())
|
||||
.unwrap_or_else(|| TomlValue::Table(Default::default()));
|
||||
let Some(user_table) = user_config.as_table_mut() else {
|
||||
panic!("user config should be a table");
|
||||
};
|
||||
let Some(hooks_table) = user_table
|
||||
.entry("hooks")
|
||||
.or_insert_with(|| TomlValue::Table(Default::default()))
|
||||
.as_table_mut()
|
||||
else {
|
||||
panic!("hooks config should be a table");
|
||||
};
|
||||
let Some(state_table) = hooks_table
|
||||
.entry("state")
|
||||
.or_insert_with(|| TomlValue::Table(Default::default()))
|
||||
.as_table_mut()
|
||||
else {
|
||||
panic!("hook state config should be a table");
|
||||
};
|
||||
for hook in hooks {
|
||||
let mut hook_state = TomlValue::Table(Default::default());
|
||||
let Some(hook_state_table) = hook_state.as_table_mut() else {
|
||||
panic!("hook state should be a table");
|
||||
};
|
||||
hook_state_table.insert(
|
||||
"trusted_hash".to_string(),
|
||||
TomlValue::String(hook.current_hash),
|
||||
);
|
||||
state_table.insert(hook.key, hook_state);
|
||||
}
|
||||
|
||||
config_layer_stack.with_user_config(&codex_home.join(CONFIG_TOML_FILE), user_config)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ use std::path::PathBuf;
|
||||
|
||||
pub mod apps_test_server;
|
||||
pub mod context_snapshot;
|
||||
pub mod hooks;
|
||||
pub mod process;
|
||||
pub mod responses;
|
||||
pub mod streaming_sse;
|
||||
|
||||
@@ -3,8 +3,11 @@ use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_features::Feature;
|
||||
use codex_plugin::PluginHookSource;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_protocol::items::parse_hook_prompt_fragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -16,6 +19,9 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::hooks::trust_discovered_hooks;
|
||||
use core_test_support::hooks::trust_hooks;
|
||||
use core_test_support::managed_network_requirements_loader;
|
||||
use core_test_support::responses::ev_apply_patch_function_call;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
@@ -67,6 +73,23 @@ fn network_workspace_write_profile() -> PermissionProfile {
|
||||
)
|
||||
}
|
||||
|
||||
fn trust_plugin_hooks(config: &mut Config, plugin_hook_sources: Vec<PluginHookSource>) {
|
||||
if let Err(err) = config.features.enable(Feature::CodexHooks) {
|
||||
panic!("test config should allow feature update: {err}");
|
||||
}
|
||||
let listed = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(config.config_layer_stack.clone()),
|
||||
plugin_hook_sources,
|
||||
..codex_hooks::HooksConfig::default()
|
||||
});
|
||||
assert!(
|
||||
!listed.hooks.is_empty(),
|
||||
"trusted plugin hook fixture should discover at least one hook"
|
||||
);
|
||||
trust_hooks(config, listed.hooks);
|
||||
}
|
||||
|
||||
fn write_stop_hook(home: &Path, block_prompts: &[&str]) -> Result<()> {
|
||||
let script_path = home.join("stop_hook.py");
|
||||
let log_path = home.join("stop_hook_log.jsonl");
|
||||
@@ -835,12 +858,7 @@ async fn stop_hook_can_block_multiple_times_in_same_turn() -> Result<()> {
|
||||
panic!("failed to write stop hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("hello from the sea").await?;
|
||||
@@ -934,12 +952,7 @@ async fn session_start_hook_sees_materialized_transcript_path() -> Result<()> {
|
||||
panic!("failed to write session start hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
@@ -984,12 +997,7 @@ async fn session_start_hook_spills_large_additional_context() -> Result<()> {
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
@@ -1041,12 +1049,7 @@ async fn stop_hook_spills_large_continuation_prompt() -> Result<()> {
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("hello from the sea").await?;
|
||||
@@ -1091,12 +1094,7 @@ async fn resumed_thread_keeps_stop_continuation_prompt_in_history() -> Result<()
|
||||
panic!("failed to write stop hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let initial = initial_builder.build(&server).await?;
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
@@ -1119,12 +1117,7 @@ async fn resumed_thread_keeps_stop_continuation_prompt_in_history() -> Result<()
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut resume_builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let mut resume_builder = test_codex().with_config(trust_discovered_hooks);
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
|
||||
resumed.submit_turn("and now continue").await?;
|
||||
@@ -1170,12 +1163,7 @@ async fn multiple_blocking_stop_hooks_persist_multiple_hook_prompt_fragments() -
|
||||
panic!("failed to write parallel stop hook fixtures: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("hello again").await?;
|
||||
@@ -1228,12 +1216,7 @@ async fn blocked_user_prompt_submit_persists_additional_context_for_next_turn()
|
||||
panic!("failed to write user prompt submit hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("blocked first prompt").await?;
|
||||
@@ -1335,12 +1318,7 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
|
||||
panic!("failed to write user prompt submit hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build_with_streaming_server(&server).await?;
|
||||
|
||||
test.codex
|
||||
@@ -1489,12 +1467,7 @@ async fn permission_request_hook_allows_shell_command_without_user_approval() ->
|
||||
panic!("failed to write permission request hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
fs::write(&marker, "seed").context("create permission request marker")?;
|
||||
@@ -1576,10 +1549,7 @@ async fn permission_request_hook_allows_apply_patch_with_write_alias() -> Result
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let target_path = test.workspace_path(&patch_path);
|
||||
@@ -1653,10 +1623,7 @@ async fn permission_request_hook_sees_raw_exec_command_input() -> Result<()> {
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
@@ -1741,10 +1708,7 @@ allow_local_binding = true
|
||||
})
|
||||
.with_cloud_requirements(managed_network_requirements_loader())
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
|
||||
config
|
||||
.permissions
|
||||
@@ -1853,12 +1817,7 @@ async fn permission_request_hook_sees_retry_context_after_sandbox_denial() -> Re
|
||||
panic!("failed to write permission request hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
let marker_path = test.workspace_path(marker);
|
||||
let _ = fs::remove_file(&marker_path);
|
||||
@@ -1925,12 +1884,7 @@ async fn pre_tool_use_blocks_shell_command_before_execution() -> Result<()> {
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
if marker.exists() {
|
||||
@@ -2027,12 +1981,7 @@ async fn pre_tool_use_records_additional_context_for_shell_command() -> Result<(
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("run the shell command with pre hook")
|
||||
@@ -2098,12 +2047,7 @@ async fn blocked_pre_tool_use_records_additional_context_for_shell_command() ->
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
if marker.exists() {
|
||||
@@ -2215,9 +2159,7 @@ print(json.dumps({{
|
||||
),
|
||||
)
|
||||
.context("write plugin pre tool use hook script")?;
|
||||
fs::write(
|
||||
hooks_dir.join("hooks.json"),
|
||||
r#"{
|
||||
let plugin_hooks_json = r#"{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "^Bash$",
|
||||
@@ -2227,21 +2169,34 @@ print(json.dumps({{
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.context("write plugin hooks config")?;
|
||||
}"#;
|
||||
let plugin_hooks_path = hooks_dir.join("hooks.json");
|
||||
fs::write(&plugin_hooks_path, plugin_hooks_json).context("write plugin hooks config")?;
|
||||
let plugin_root_abs =
|
||||
AbsolutePathBuf::try_from(plugin_root.clone()).context("absolute plugin root")?;
|
||||
let plugin_hooks_path_abs =
|
||||
AbsolutePathBuf::try_from(plugin_hooks_path).context("absolute plugin hooks path")?;
|
||||
let plugin_data_root =
|
||||
AbsolutePathBuf::try_from(plugin_root.join("data")).context("absolute plugin data root")?;
|
||||
let plugin_hook_sources = vec![PluginHookSource {
|
||||
plugin_id: PluginId::parse("sample@test").context("plugin id")?,
|
||||
plugin_root: plugin_root_abs,
|
||||
plugin_data_root,
|
||||
source_path: plugin_hooks_path_abs,
|
||||
source_relative_path: "hooks/hooks.json".to_string(),
|
||||
hooks: serde_json::from_str::<codex_config::HooksFile>(plugin_hooks_json)
|
||||
.context("parse plugin hooks")?
|
||||
.hooks,
|
||||
}];
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
.with_config(|config| {
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Plugins)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_plugin_hooks(config, plugin_hook_sources);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::PluginHooks)
|
||||
@@ -2315,18 +2270,20 @@ async fn pre_tool_use_blocks_shell_when_defined_in_config_toml() -> Result<()> {
|
||||
)
|
||||
.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 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}");
|
||||
}
|
||||
})
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
if marker.exists() {
|
||||
@@ -2397,21 +2354,23 @@ async fn pre_tool_use_merges_hooks_json_and_config_toml() -> Result<()> {
|
||||
)
|
||||
.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 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}");
|
||||
}
|
||||
})
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("run the shell command with merged hook sources")
|
||||
@@ -2510,12 +2469,7 @@ async fn pre_tool_use_blocks_local_shell_before_execution() -> Result<()> {
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
if marker.exists() {
|
||||
@@ -2603,10 +2557,7 @@ async fn pre_tool_use_blocks_exec_command_before_execution() -> Result<()> {
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
@@ -2693,10 +2644,7 @@ async fn pre_tool_use_blocks_apply_patch_before_execution() -> Result<()> {
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -2767,10 +2715,7 @@ async fn pre_tool_use_blocks_apply_patch_with_write_alias() -> Result<()> {
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -2843,12 +2788,7 @@ async fn pre_tool_use_does_not_fire_for_plan_tool() -> Result<()> {
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("update the plan").await?;
|
||||
@@ -2912,12 +2852,7 @@ async fn post_tool_use_records_additional_context_for_shell_command() -> Result<
|
||||
panic!("failed to write post tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("run the shell command with post hook")
|
||||
@@ -3009,12 +2944,7 @@ async fn post_tool_use_block_decision_replaces_shell_command_output_with_reason(
|
||||
panic!("failed to write post tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("run the shell command with blocking post hook")
|
||||
@@ -3078,12 +3008,7 @@ async fn post_tool_use_continue_false_replaces_shell_command_output_with_stop_re
|
||||
panic!("failed to write post tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("run the shell command with stop-style post hook")
|
||||
@@ -3149,12 +3074,7 @@ async fn post_tool_use_records_additional_context_for_local_shell() -> Result<()
|
||||
panic!("failed to write post tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("run the local shell command with post hook")
|
||||
@@ -3222,10 +3142,7 @@ async fn post_tool_use_exit_two_replaces_one_shot_exec_command_output_with_feedb
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
@@ -3300,10 +3217,7 @@ async fn post_tool_use_spills_large_feedback_message() -> Result<()> {
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
@@ -3388,10 +3302,7 @@ async fn post_tool_use_blocks_when_exec_session_completes_via_write_stdin() -> R
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
@@ -3475,10 +3386,7 @@ async fn post_tool_use_records_additional_context_for_apply_patch() -> Result<()
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -3566,10 +3474,7 @@ async fn post_tool_use_records_apply_patch_context_with_edit_alias() -> Result<(
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_apply_patch_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
trust_discovered_hooks(config);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -3642,12 +3547,7 @@ async fn post_tool_use_does_not_fire_for_plan_tool() -> Result<()> {
|
||||
panic!("failed to write post tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("update the plan").await?;
|
||||
|
||||
@@ -9,7 +9,7 @@ use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core::config::Config;
|
||||
use codex_features::Feature;
|
||||
use core_test_support::hooks::trust_discovered_hooks;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call_with_namespace;
|
||||
@@ -163,9 +163,7 @@ fn enable_hooks_and_rmcp_server(
|
||||
rmcp_test_server_bin: String,
|
||||
approval_mode: AppToolApproval,
|
||||
) {
|
||||
if let Err(err) = config.features.enable(Feature::CodexHooks) {
|
||||
panic!("test config should allow feature update: {err}");
|
||||
}
|
||||
trust_discovered_hooks(config);
|
||||
insert_rmcp_test_server(config, rmcp_test_server_bin, approval_mode);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::apps_test_server::DOCUMENT_EXTRACT_TEXT_RESOURCE_URI;
|
||||
use core_test_support::hooks::trust_discovered_hooks;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call_with_namespace;
|
||||
@@ -162,9 +163,7 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
|
||||
})
|
||||
.with_config(move |config| {
|
||||
configure_apps(config, apps_server.chatgpt_base_url.as_str());
|
||||
if let Err(err) = config.features.enable(Feature::CodexHooks) {
|
||||
panic!("test config should allow feature update: {err}");
|
||||
}
|
||||
trust_discovered_hooks(config);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
tokio::fs::write(test.cwd.path().join("report.txt"), b"hello world").await?;
|
||||
|
||||
Reference in New Issue
Block a user