mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08: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:
committed by
GitHub
Unverified
parent
78421face0
commit
0452dca986
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user