diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 115907527c..17f8cf9b33 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9069,6 +9069,49 @@ "title": "CommandConfiguredHookHandler", "type": "object" }, + { + "properties": { + "input": { + "additionalProperties": true, + "type": "object" + }, + "server": { + "type": "string" + }, + "statusMessage": { + "type": [ + "string", + "null" + ] + }, + "timeoutSec": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "mcp_tool" + ], + "title": "McpToolConfiguredHookHandlerType", + "type": "string" + } + }, + "required": [ + "input", + "server", + "tool", + "type" + ], + "title": "McpToolConfiguredHookHandler", + "type": "object" + }, { "properties": { "type": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 97c7f79ba1..79660f0d49 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -5234,6 +5234,49 @@ "title": "CommandConfiguredHookHandler", "type": "object" }, + { + "properties": { + "input": { + "additionalProperties": true, + "type": "object" + }, + "server": { + "type": "string" + }, + "statusMessage": { + "type": [ + "string", + "null" + ] + }, + "timeoutSec": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "mcp_tool" + ], + "title": "McpToolConfiguredHookHandlerType", + "type": "string" + } + }, + "required": [ + "input", + "server", + "tool", + "type" + ], + "title": "McpToolConfiguredHookHandler", + "type": "object" + }, { "properties": { "type": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index db82430b6b..d29ccb7174 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -305,6 +305,49 @@ "title": "CommandConfiguredHookHandler", "type": "object" }, + { + "properties": { + "input": { + "additionalProperties": true, + "type": "object" + }, + "server": { + "type": "string" + }, + "statusMessage": { + "type": [ + "string", + "null" + ] + }, + "timeoutSec": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "mcp_tool" + ], + "title": "McpToolConfiguredHookHandlerType", + "type": "string" + } + }, + "required": [ + "input", + "server", + "tool", + "type" + ], + "title": "McpToolConfiguredHookHandler", + "type": "object" + }, { "properties": { "type": { diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 720e287b04..9ecf2ae3c9 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 9ec0af06a1..dbbe54b60f 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts index 38adeb2b1c..6ee1b16ba3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts @@ -1,6 +1,7 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; export type ConfiguredHookHandler = { "type": "command", command: string, commandWindows: string | null, timeoutSec: bigint | null, async: boolean, statusMessage: string | null, /** @@ -9,4 +10,4 @@ export type ConfiguredHookHandler = { "type": "command", command: string, comman * evaluated against the original context; a spilled preview also includes recovery * metadata. */ -additionalContextLimit: number | null, } | { "type": "prompt", } | { "type": "agent", }; +additionalContextLimit: number | null, } | { "type": "mcp_tool", server: string, tool: string, input: { [key in string]?: JsonValue }, timeoutSec: bigint | null, statusMessage: string | null, } | { "type": "prompt", } | { "type": "agent", }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index 5dcd22cc52..842e29d516 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -519,6 +519,19 @@ pub enum ConfiguredHookHandler { #[ts(rename = "additionalContextLimit")] additional_context_limit: Option, }, + #[serde(rename = "mcp_tool")] + #[ts(rename = "mcp_tool")] + McpTool { + server: String, + tool: String, + input: serde_json::Map, + #[serde(rename = "timeoutSec")] + #[ts(rename = "timeoutSec")] + timeout_sec: Option, + #[serde(rename = "statusMessage")] + #[ts(rename = "statusMessage")] + status_message: Option, + }, #[serde(rename = "prompt")] #[ts(rename = "prompt")] Prompt {}, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 5054606e2a..d05cf4a5cf 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -277,7 +277,7 @@ Example with notification opt-out: - `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Successful session entries include the original imported title when one was available. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection. - `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. Writes that overlap a managed requirement are rejected with `configRequirementReadonly`. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults do not reload existing threads. -- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), Browser Use policy (`browserUse.disableAutoReview`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including each command handler's optional `additionalContextLimit`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), Browser Use policy (`browserUse.disableAutoReview`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including command handlers with optional `additionalContextLimit` and `mcp_tool` handlers with `server`, `tool`, `input`, `timeoutSec`, and `statusMessage`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index dbd6060404..b8f082325a 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -524,6 +524,19 @@ fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHand status_message, additional_context_limit, }, + CoreHookHandlerConfig::McpTool { + server, + tool, + input, + timeout_sec, + status_message, + } => ConfiguredHookHandler::McpTool { + server, + tool, + input, + timeout_sec, + status_message, + }, CoreHookHandlerConfig::Prompt {} => ConfiguredHookHandler::Prompt {}, CoreHookHandlerConfig::Agent {} => ConfiguredHookHandler::Agent {}, } diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index cd95082905..d6104f8432 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -62,6 +62,14 @@ async fn config_requirements_read_includes_remote_control_and_managed_hooks() -> type = "command" command = "echo managed" additionalContextLimit = 4096 + +[[hooks.SessionStart.hooks]] +type = "mcp_tool" +server = "security" +tool = "scan" +input = { path = "${tool_input.file_path}", metadata = { enabled = true, retries = 2 } } +timeout = 30 +statusMessage = "Scanning file" "#, )?; let mut mcp = TestAppServer::builder() @@ -83,14 +91,26 @@ additionalContextLimit = 4096 .expect("managed hooks should be returned") .session_start[0] .hooks, - vec![ConfiguredHookHandler::Command { - command: "echo managed".to_string(), - command_windows: None, - timeout_sec: None, - r#async: false, - status_message: None, - additional_context_limit: Some(4_096), - }] + vec![ + ConfiguredHookHandler::Command { + command: "echo managed".to_string(), + command_windows: None, + timeout_sec: None, + r#async: false, + status_message: None, + additional_context_limit: Some(4_096), + }, + ConfiguredHookHandler::McpTool { + server: "security".to_string(), + tool: "scan".to_string(), + input: serde_json::from_value(json!({ + "path": "${tool_input.file_path}", + "metadata": { "enabled": true, "retries": 2 }, + }))?, + timeout_sec: Some(30), + status_message: Some("Scanning file".to_string()), + }, + ] ); Ok(()) } diff --git a/codex-rs/config/src/hook_config.rs b/codex-rs/config/src/hook_config.rs index 935e2e1d63..b667179855 100644 --- a/codex-rs/config/src/hook_config.rs +++ b/codex-rs/config/src/hook_config.rs @@ -169,6 +169,17 @@ pub enum HookHandlerConfig { )] additional_context_limit: Option, }, + #[serde(rename = "mcp_tool")] + McpTool { + server: String, + tool: String, + #[serde(default)] + input: serde_json::Map, + #[serde(default, rename = "timeout")] + timeout_sec: Option, + #[serde(default, rename = "statusMessage")] + status_message: Option, + }, #[serde(rename = "prompt")] Prompt {}, #[serde(rename = "agent")] diff --git a/codex-rs/config/src/hooks_tests.rs b/codex-rs/config/src/hooks_tests.rs index b869fe2561..fcf7b91901 100644 --- a/codex-rs/config/src/hooks_tests.rs +++ b/codex-rs/config/src/hooks_tests.rs @@ -56,6 +56,46 @@ fn hooks_file_deserializes_existing_json_shape() { ); } +#[test] +fn hooks_file_deserializes_mcp_tool_handler_with_json_inputs() { + let parsed: HooksFile = serde_json::from_value(serde_json::json!({ + "hooks": { + "PostToolUse": [{ + "matcher": "Write|Edit", + "hooks": [{ + "type": "mcp_tool", + "server": "security", + "tool": "scan", + "input": { + "file_path": "${tool_input.file_path}", + "optional": null, + }, + "timeout": 30, + "statusMessage": "Scanning file", + }], + }], + }, + })) + .expect("MCP tool hooks should deserialize"); + + assert_eq!( + parsed.hooks.post_tool_use[0].hooks, + vec![HookHandlerConfig::McpTool { + server: "security".to_string(), + tool: "scan".to_string(), + input: serde_json::Map::from_iter([ + ( + "file_path".to_string(), + serde_json::Value::String("${tool_input.file_path}".to_string()), + ), + ("optional".to_string(), serde_json::Value::Null), + ]), + timeout_sec: Some(30), + status_message: Some("Scanning file".to_string()), + }] + ); +} + #[test] fn hooks_file_rejects_events_outside_hooks_object() { let error = serde_json::from_str::( diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index f9277577cf..72543f224f 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1307,6 +1307,43 @@ ], "type": "object" }, + { + "properties": { + "input": { + "additionalProperties": true, + "default": {}, + "type": "object" + }, + "server": { + "type": "string" + }, + "statusMessage": { + "default": null, + "type": "string" + }, + "timeout": { + "default": null, + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "mcp_tool" + ], + "type": "string" + } + }, + "required": [ + "server", + "tool", + "type" + ], + "type": "object" + }, { "properties": { "type": { diff --git a/codex-rs/hooks/src/engine/discovery.rs b/codex-rs/hooks/src/engine/discovery.rs index 79f904bed8..9ed6a903ef 100644 --- a/codex-rs/hooks/src/engine/discovery.rs +++ b/codex-rs/hooks/src/engine/discovery.rs @@ -583,6 +583,10 @@ fn append_matcher_groups( } *display_order += 1; } + HookHandlerConfig::McpTool { .. } => warnings.push(format!( + "skipping MCP tool hook in {}: MCP tool hooks are not supported yet", + source.path.display() + )), HookHandlerConfig::Prompt {} => warnings.push(format!( "skipping prompt hook in {}: prompt hooks are not supported yet", source.path.display()