Reject sessions with unloadable required managed hooks (#38394)

## Why

Hooks supplied as managed requirements must not be silently skipped when their
handlers cannot be loaded.

## What changed

- Fail session and app-server thread startup when an enabled managed requirement
  contains an invalid matcher, an empty command, or an unsupported handler type.
- Keep load failures for ordinary managed configuration hooks as warnings, and do
  not enforce managed hook requirements while the hooks feature is disabled.

## Testing

Added hook-engine, core session, and app-server coverage for valid and invalid
managed hook requirements.

GitOrigin-RevId: 91a23b0c1d85e143aba6aabe874c5274e52fc6b2
This commit is contained in:
viyatb-oai authored and copyberry committed 2026-08-13 15:23:09 +00:00
1 parent 683716cee9
commit ef596c68ca
9 files changed
+440 -40

No files matched your search

@@ -1083,6 +1083,87 @@ async fn thread_start_fails_when_required_mcp_server_fails_to_initialize() -> Re
Ok(())
}
#[tokio::test]
async fn thread_start_fails_when_managed_hook_matcher_is_invalid() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
std::fs::write(
codex_home.path().join("requirements.toml"),
r#"[hooks]
[[hooks.PreToolUse]]
matcher = "["
[[hooks.PreToolUse.hooks]]
type = "command"
command = "echo managed"
"#,
)?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
let request_id = app_server
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert!(
error.error.message.contains("managed") && error.error.message.contains("invalid matcher"),
"unexpected error message: {}",
error.error.message
);
Ok(())
}
#[tokio::test]
async fn thread_start_fails_when_managed_hook_handler_is_unsupported() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
std::fs::write(
codex_home.path().join("requirements.toml"),
r#"[hooks]
[[hooks.PreToolUse]]
matcher = "^Bash$"
[[hooks.PreToolUse.hooks]]
type = "mcp_tool"
server = "security"
tool = "scan"
"#,
)?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
let request_id = app_server
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert!(
error.error.message.contains("MCP tool hook"),
"unexpected error message: {}",
error.error.message
);
Ok(())
}
#[tokio::test]
async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;