mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
## Why After `hooks/list` exposes the hook inventory, clients need a way to persist user hook preferences, make those changes effective in already-open sessions, and distinguish user-controllable hooks from managed requirements without adding another bespoke app-server write API. ## What - Extends `hooks/list` entries with effective `enabled` state. - Persists user-level hook state under `hooks.state.<hook-id>` so the model can grow beyond a single boolean over time. - Uses the existing `config/batchWrite` path for hook state updates instead of introducing a dedicated hook write RPC. - Refreshes live session hook engines after config writes so already-open threads observe updated enablement without a restart. ## Stack 1. openai/codex#19705 2. openai/codex#19778 3. This PR - openai/codex#19840 4. openai/codex#19882 ## Reviewer Notes The generated schema files account for much of the raw diff. The core behavior is in: - `hooks/src/config_rules.rs`, which resolves per-hook user state from the config layer stack. - `hooks/src/engine/discovery.rs`, which projects effective enablement into `hooks/list` from source-derived managedness. - `config/src/hook_config.rs`, which defines the new `hooks.state` representation. - `core/src/session/mod.rs`, which rebuilds live hook state after user config reloads. --------- Co-authored-by: Codex <noreply@openai.com>
165 lines
4.3 KiB
Rust
165 lines
4.3 KiB
Rust
use pretty_assertions::assert_eq;
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use super::HookEventsToml;
|
|
use super::HookHandlerConfig;
|
|
use super::HooksFile;
|
|
use super::HooksToml;
|
|
use super::ManagedHooksRequirementsToml;
|
|
use super::MatcherGroup;
|
|
|
|
#[test]
|
|
fn hooks_file_deserializes_existing_json_shape() {
|
|
let parsed: HooksFile = serde_json::from_str(
|
|
r#"{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "^Bash$",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "python3 /tmp/pre.py",
|
|
"timeout": 10,
|
|
"statusMessage": "checking"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}"#,
|
|
)
|
|
.expect("hooks.json should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HooksFile {
|
|
hooks: HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "python3 /tmp/pre.py".to_string(),
|
|
timeout_sec: Some(10),
|
|
r#async: false,
|
|
status_message: Some("checking".to_string()),
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hook_events_deserialize_from_toml_arrays_of_tables() {
|
|
let parsed: HookEventsToml = toml::from_str(
|
|
r#"
|
|
[[PreToolUse]]
|
|
matcher = "^Bash$"
|
|
|
|
[[PreToolUse.hooks]]
|
|
type = "command"
|
|
command = "python3 /tmp/pre.py"
|
|
timeout = 10
|
|
statusMessage = "checking"
|
|
"#,
|
|
)
|
|
.expect("hook events TOML should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "python3 /tmp/pre.py".to_string(),
|
|
timeout_sec: Some(10),
|
|
r#async: false,
|
|
status_message: Some("checking".to_string()),
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hooks_toml_deserializes_inline_events_and_state_map() {
|
|
let parsed: HooksToml = toml::from_str(
|
|
r#"
|
|
[state."/tmp/hooks.json:pre_tool_use:0:0"]
|
|
enabled = false
|
|
|
|
[[PreToolUse]]
|
|
matcher = "^Bash$"
|
|
|
|
[[PreToolUse.hooks]]
|
|
type = "command"
|
|
command = "python3 /tmp/pre.py"
|
|
"#,
|
|
)
|
|
.expect("hooks TOML should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HooksToml {
|
|
events: HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "python3 /tmp/pre.py".to_string(),
|
|
timeout_sec: None,
|
|
r#async: false,
|
|
status_message: None,
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
},
|
|
state: BTreeMap::from([(
|
|
"/tmp/hooks.json:pre_tool_use:0:0".to_string(),
|
|
super::HookStateToml {
|
|
enabled: Some(false),
|
|
},
|
|
)]),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn managed_hooks_requirements_flatten_hook_events() {
|
|
let parsed: ManagedHooksRequirementsToml = toml::from_str(
|
|
r#"
|
|
managed_dir = "/enterprise/place"
|
|
|
|
[[PreToolUse]]
|
|
matcher = "^Bash$"
|
|
|
|
[[PreToolUse.hooks]]
|
|
type = "command"
|
|
command = "python3 /enterprise/place/pre.py"
|
|
"#,
|
|
)
|
|
.expect("requirements hooks TOML should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
ManagedHooksRequirementsToml {
|
|
managed_dir: Some(std::path::PathBuf::from("/enterprise/place")),
|
|
windows_managed_dir: None,
|
|
hooks: HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "python3 /enterprise/place/pre.py".to_string(),
|
|
timeout_sec: None,
|
|
r#async: false,
|
|
status_message: None,
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
},
|
|
}
|
|
);
|
|
}
|