mirror of
https://github.com/openai/codex.git
synced 2026-05-04 21:32:21 +03: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:
@@ -4,6 +4,7 @@ use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::ManagedHooksRequirementsToml;
|
||||
use codex_config::NetworkConstraints;
|
||||
use codex_config::NetworkDomainPermissionToml;
|
||||
use codex_config::NetworkUnixSocketPermissionToml;
|
||||
@@ -168,6 +169,17 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(hooks) = requirements_toml.hooks.as_ref() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"hooks",
|
||||
format_managed_hooks_requirements(hooks),
|
||||
requirements
|
||||
.managed_hooks
|
||||
.as_ref()
|
||||
.and_then(|managed_hooks| managed_hooks.source.as_ref()),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(servers) = requirements_toml.mcp_servers.as_ref() {
|
||||
let value = join_or_empty(servers.keys().cloned().collect::<Vec<_>>());
|
||||
requirement_lines.push(requirement_line(
|
||||
@@ -257,6 +269,23 @@ fn render_session_flag_details(config: &TomlValue) -> Vec<Line<'static>> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn format_managed_hooks_requirements(hooks: &ManagedHooksRequirementsToml) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
if let Some(managed_dir) = hooks.managed_dir.as_ref() {
|
||||
parts.push(format!("managed_dir={}", managed_dir.display()));
|
||||
}
|
||||
if let Some(windows_managed_dir) = hooks.windows_managed_dir.as_ref() {
|
||||
parts.push(format!(
|
||||
"windows_managed_dir={}",
|
||||
windows_managed_dir.display()
|
||||
));
|
||||
}
|
||||
parts.push(format!("handlers={}", hooks.handler_count()));
|
||||
|
||||
join_or_empty(parts)
|
||||
}
|
||||
|
||||
fn render_mdm_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
|
||||
let value = layer
|
||||
.raw_toml()
|
||||
@@ -484,6 +513,10 @@ mod tests {
|
||||
use codex_config::ConstrainedWithSource;
|
||||
use codex_config::FeatureRequirementsToml;
|
||||
use codex_config::FilesystemConstraints;
|
||||
use codex_config::HookEventsToml;
|
||||
use codex_config::HookHandlerConfig;
|
||||
use codex_config::ManagedHooksRequirementsToml;
|
||||
use codex_config::MatcherGroup;
|
||||
use codex_config::McpServerIdentity;
|
||||
use codex_config::McpServerRequirement;
|
||||
use codex_config::NetworkConstraints;
|
||||
@@ -655,6 +688,7 @@ mod tests {
|
||||
feature_requirements: Some(FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
||||
}),
|
||||
hooks: None,
|
||||
mcp_servers: Some(BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerRequirement {
|
||||
@@ -860,6 +894,7 @@ approval_policy = "never"
|
||||
allowed_web_search_modes: Some(Vec::new()),
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -877,6 +912,50 @@ approval_policy = "never"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_config_output_lists_managed_hooks_requirement() {
|
||||
let requirements = ConfigRequirements {
|
||||
managed_hooks: Some(ConstrainedWithSource::new(
|
||||
Constrained::allow_any(ManagedHooksRequirementsToml {
|
||||
managed_dir: Some(if cfg!(windows) {
|
||||
std::path::PathBuf::from(r"C:\enterprise\hooks")
|
||||
} else {
|
||||
std::path::PathBuf::from("/enterprise/hooks")
|
||||
}),
|
||||
windows_managed_dir: Some(std::path::PathBuf::from(r"C:\enterprise\hooks")),
|
||||
hooks: HookEventsToml {
|
||||
pre_tool_use: vec![MatcherGroup {
|
||||
matcher: Some("^Bash$".to_string()),
|
||||
hooks: vec![HookHandlerConfig::Command {
|
||||
command: "python3 /enterprise/hooks/pre.py".to_string(),
|
||||
timeout_sec: Some(10),
|
||||
r#async: false,
|
||||
status_message: Some("checking".to_string()),
|
||||
}],
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
}),
|
||||
Some(RequirementSource::CloudRequirements),
|
||||
)),
|
||||
..ConfigRequirements::default()
|
||||
};
|
||||
let requirements_toml = ConfigRequirementsToml {
|
||||
hooks: requirements
|
||||
.managed_hooks
|
||||
.as_ref()
|
||||
.map(|hooks| hooks.get().clone()),
|
||||
..ConfigRequirementsToml::default()
|
||||
};
|
||||
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||
.expect("config layer stack");
|
||||
|
||||
let rendered = render_to_text(&render_debug_config_lines(&stack));
|
||||
assert!(rendered.contains("hooks:"));
|
||||
assert!(rendered.contains("handlers=1"));
|
||||
assert!(rendered.contains("(source: cloud requirements)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_all_proxy_url_uses_socks_when_enabled() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user