mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
Add allow_managed_hooks_only hook requirement (#20319)
## Why Enterprise-managed hook policy needs a narrow way to require Codex to ignore user-controlled lifecycle hooks without adopting the broader trust-precedence model from earlier hook work. This keeps the policy anchored in `requirements.toml`, so admins can opt into managed hooks only while normal `config.toml` files cannot enable the restriction themselves. ## What changed - Added `allow_managed_hooks_only` to the requirements data flow and preserved explicit `false` values. - Also adds it to /debug-config - Marked MDM, system, and legacy managed config layers as managed for hook discovery. - Updated hook discovery so `allow_managed_hooks_only = true`: - keeps managed requirements hooks and managed config-layer hooks, - skips user/project/session `hooks.json` and `[hooks]` entries with concise startup warnings, - skips current unmanaged plugin hooks, - ignores any `allow_managed_hooks_only` key placed in ordinary `config.toml` layers.
This commit is contained in:
@@ -228,6 +228,76 @@ async fn returns_config_error_for_schema_error_in_user_config() {
|
||||
assert_eq!(config_error, &expected_config_error);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn top_level_allow_managed_hooks_only_in_user_config_does_not_enable_requirements_policy()
|
||||
-> std::io::Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(
|
||||
tmp.path().join(CONFIG_TOML_FILE),
|
||||
"allow_managed_hooks_only = true",
|
||||
)
|
||||
.expect("write config");
|
||||
|
||||
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
tmp.path(),
|
||||
Some(cwd),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(layers.requirements_toml().allow_managed_hooks_only, None);
|
||||
assert!(layers.requirements().allow_managed_hooks_only.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hooks_allow_managed_hooks_only_in_user_config_does_not_enable_requirements_policy()
|
||||
-> std::io::Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let contents = r#"
|
||||
[hooks]
|
||||
allow_managed_hooks_only = true
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "python3 /tmp/user-hook.py"
|
||||
"#;
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), contents).expect("write config");
|
||||
|
||||
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
tmp.path(),
|
||||
Some(cwd),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
layers
|
||||
.get_user_layer()
|
||||
.and_then(|layer| layer.config.get("hooks"))
|
||||
.is_some(),
|
||||
"hooks should still deserialize from config.toml"
|
||||
);
|
||||
assert_eq!(layers.requirements_toml().allow_managed_hooks_only, None);
|
||||
assert!(layers.requirements().allow_managed_hooks_only.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_error_points_to_feature_value() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
@@ -777,6 +847,7 @@ allowed_approval_policies = ["on-request"]
|
||||
allowed_sandbox_modes: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
@@ -834,6 +905,7 @@ allowed_approval_policies = ["on-request"]
|
||||
allowed_sandbox_modes: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
@@ -1042,6 +1114,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
allowed_sandbox_modes: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
|
||||
@@ -8167,6 +8167,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
allowed_sandbox_modes: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||
allow_managed_hooks_only: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
@@ -8879,6 +8880,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
||||
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
|
||||
@@ -2125,6 +2125,7 @@ impl Config {
|
||||
approvals_reviewer: mut constrained_approvals_reviewer,
|
||||
permission_profile: mut constrained_permission_profile,
|
||||
web_search_mode: mut constrained_web_search_mode,
|
||||
allow_managed_hooks_only: _,
|
||||
feature_requirements,
|
||||
managed_hooks: _,
|
||||
mcp_servers,
|
||||
|
||||
Reference in New Issue
Block a user