mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Recognize MCP tool hook configurations (#37363)
## What changed - Parse `mcp_tool` hook handlers with a server, tool, JSON input, and optional timeout and status message. - Return managed MCP tool hooks from `configRequirements/read` and include the handler in the generated protocol schemas. - Skip MCP tool hooks during discovery with an explicit warning until execution is supported. ## Testing - Cover MCP tool hook deserialization, including nested and null JSON input values. - Extend the config requirements RPC test to verify the new handler shape. GitOrigin-RevId: ff7e2d12b75b486c54d63469af547a29f9b31418
This commit is contained in:
+43
@@ -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": {
|
||||
|
||||
+43
@@ -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": {
|
||||
|
||||
+43
@@ -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": {
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
-1
@@ -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", };
|
||||
|
||||
@@ -519,6 +519,19 @@ pub enum ConfiguredHookHandler {
|
||||
#[ts(rename = "additionalContextLimit")]
|
||||
additional_context_limit: Option<usize>,
|
||||
},
|
||||
#[serde(rename = "mcp_tool")]
|
||||
#[ts(rename = "mcp_tool")]
|
||||
McpTool {
|
||||
server: String,
|
||||
tool: String,
|
||||
input: serde_json::Map<String, serde_json::Value>,
|
||||
#[serde(rename = "timeoutSec")]
|
||||
#[ts(rename = "timeoutSec")]
|
||||
timeout_sec: Option<u64>,
|
||||
#[serde(rename = "statusMessage")]
|
||||
#[ts(rename = "statusMessage")]
|
||||
status_message: Option<String>,
|
||||
},
|
||||
#[serde(rename = "prompt")]
|
||||
#[ts(rename = "prompt")]
|
||||
Prompt {},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {},
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -169,6 +169,17 @@ pub enum HookHandlerConfig {
|
||||
)]
|
||||
additional_context_limit: Option<usize>,
|
||||
},
|
||||
#[serde(rename = "mcp_tool")]
|
||||
McpTool {
|
||||
server: String,
|
||||
tool: String,
|
||||
#[serde(default)]
|
||||
input: serde_json::Map<String, serde_json::Value>,
|
||||
#[serde(default, rename = "timeout")]
|
||||
timeout_sec: Option<u64>,
|
||||
#[serde(default, rename = "statusMessage")]
|
||||
status_message: Option<String>,
|
||||
},
|
||||
#[serde(rename = "prompt")]
|
||||
Prompt {},
|
||||
#[serde(rename = "agent")]
|
||||
|
||||
@@ -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::<HooksFile>(
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user