From 340f9c9ecb0b02a89e88c6dc068809007185f645 Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Sun, 8 Mar 2026 18:07:46 -0700 Subject: [PATCH] app-server: include experimental skill metadata in exec approval requests (#13929) ## Summary This change surfaces skill metadata on command approval requests so app-server clients can tell when an approval came from a skill script and identify the originating `SKILL.md`. - add `skill_metadata` to exec approval events in the shared protocol - thread skill metadata through core shell escalation and delegated approval handling for skill-triggered approvals - expose the field in app-server v2 as experimental `skillMetadata` - regenerate the JSON/TypeScript schemas and cover the new field in protocol, transport, core, and TUI tests ## Why Skill-triggered approvals already carry skill context inside core, but app-server clients could not see which skill caused the prompt. Sending the skill metadata with the approval request makes it possible for clients to present better approval UX and connect the prompt back to the relevant skill definition. ## example event in app-server-v2 verified that we see this event when experimental api is on: ``` < { < "id": 11, < "method": "item/commandExecution/requestApproval", < "params": { < "additionalPermissions": { < "fileSystem": null, < "macos": { < "accessibility": false, < "automations": { < "bundle_ids": [ < "com.apple.Notes" < ] < }, < "calendar": false, < "preferences": "read_only" < }, < "network": null < }, < "approvalId": "25d600ee-5a3c-4746-8d17-e2e61fb4c563", < "availableDecisions": [ < "accept", < "acceptForSession", < "cancel" < ], < "command": "/Applications/ChatGPT.app/Contents/Resources/CodexAppServer_CodexAppServerBundledSkills.bundle/Contents/Resources/skills/apple-notes/scripts/notes_info", < "commandActions": [ < { < "command": "/Applications/ChatGPT.app/Contents/Resources/CodexAppServer_CodexAppServerBundledSkills.bundle/Contents/Resources/skills/apple-notes/scripts/notes_info", < "type": "unknown" < } < ], < "cwd": "/Applications/ChatGPT.app/Contents/Resources/CodexAppServer_CodexAppServerBundledSkills.bundle/Contents/Resources/skills/apple-notes", < "itemId": "call_jZp3xFpNg4D8iKAD49cvEvZy", < "skillMetadata": { < "pathToSkillsMd": "/Applications/ChatGPT.app/Contents/Resources/CodexAppServer_CodexAppServerBundledSkills.bundle/Contents/Resources/skills/apple-notes/SKILL.md" < }, < "threadId": "019ccc10-b7d3-7ff2-84fe-3a75e7681e69", < "turnId": "019ccc10-b848-76f1-81b3-4a1fa225493f" < } < }` ``` & verified that this is the event when experimental api is off: ``` < { < "id": 13, < "method": "item/commandExecution/requestApproval", < "params": { < "approvalId": "5fbbf776-261b-4cf8-899b-c125b547f2c0", < "availableDecisions": [ < "accept", < "acceptForSession", < "cancel" < ], < "command": "/Applications/ChatGPT.app/Contents/Resources/CodexAppServer_CodexAppServerBundledSkills.bundle/Contents/Resources/skills/apple-notes/scripts/notes_info", < "commandActions": [ < { < "command": "/Applications/ChatGPT.app/Contents/Resources/CodexAppServer_CodexAppServerBundledSkills.bundle/Contents/Resources/skills/apple-notes/scripts/notes_info", < "type": "unknown" < } < ], < "cwd": "/Users/celia/code/codex/codex-rs", < "itemId": "call_OV2DHzTgYcbYtWaTTBWlocOt", < "threadId": "019ccc16-2a2b-7be1-8500-e00d45b892d4", < "turnId": "019ccc16-2a8e-7961-98ec-649600e7d06a" < } < } ``` --- .../schema/json/ClientRequest.json | 2 +- ...CommandExecutionRequestApprovalParams.json | 11 ++++ .../schema/json/EventMsg.json | 33 ++++++++++++ .../schema/json/ServerRequest.json | 11 ++++ .../codex_app_server_protocol.schemas.json | 35 +++++++++++- .../codex_app_server_protocol.v2.schemas.json | 24 ++++++++- .../v2/WindowsSandboxSetupStartParams.json | 2 +- .../typescript/ExecApprovalRequestEvent.ts | 5 ++ .../ExecApprovalRequestSkillMetadata.ts | 5 ++ .../schema/typescript/index.ts | 1 + .../CommandExecutionRequestApprovalParams.ts | 5 ++ ...ndExecutionRequestApprovalSkillMetadata.ts | 5 ++ .../schema/typescript/v2/index.ts | 1 + codex-rs/app-server-protocol/src/export.rs | 10 ++++ .../src/protocol/common.rs | 28 ++++++++++ .../app-server-protocol/src/protocol/v2.rs | 53 +++++++++++++++++++ codex-rs/app-server-test-client/src/lib.rs | 4 ++ .../app-server/src/bespoke_event_handling.rs | 5 ++ codex-rs/app-server/src/transport.rs | 14 +++++ codex-rs/core/src/codex.rs | 3 ++ codex-rs/core/src/codex_delegate.rs | 2 + codex-rs/core/src/tools/network_approval.rs | 1 + codex-rs/core/src/tools/runtimes/shell.rs | 1 + .../tools/runtimes/shell/unix_escalation.rs | 10 ++++ .../core/src/tools/runtimes/unified_exec.rs | 1 + codex-rs/core/tests/suite/skill_approval.rs | 9 ++++ codex-rs/mcp-server/src/codex_tool_runner.rs | 1 + codex-rs/protocol/src/approvals.rs | 11 ++++ codex-rs/protocol/src/protocol.rs | 1 + codex-rs/tui/src/app.rs | 3 ++ .../tui/src/app/pending_interactive_replay.rs | 3 ++ codex-rs/tui/src/chatwidget/tests.rs | 8 +++ 32 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestSkillMetadata.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalSkillMetadata.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index f9118b9dd0..059ba533d0 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -4221,4 +4221,4 @@ } ], "title": "ClientRequest" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index fad972da4e..befa086b3b 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -286,6 +286,17 @@ } ] }, + "CommandExecutionRequestApprovalSkillMetadata": { + "properties": { + "pathToSkillsMd": { + "type": "string" + } + }, + "required": [ + "pathToSkillsMd" + ], + "type": "object" + }, "MacOsAutomationPermission": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 70ddde458e..7396e358e2 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -1866,6 +1866,17 @@ "null" ] }, + "skill_metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ExecApprovalRequestSkillMetadata" + }, + { + "type": "null" + } + ], + "description": "Optional skill metadata when the approval was triggered by a skill script." + }, "turn_id": { "default": "", "description": "Turn ID that this command belongs to. Uses `#[serde(default)]` for backwards compatibility.", @@ -3442,6 +3453,17 @@ } ] }, + "ExecApprovalRequestSkillMetadata": { + "properties": { + "path_to_skills_md": { + "type": "string" + } + }, + "required": [ + "path_to_skills_md" + ], + "type": "object" + }, "ExecCommandSource": { "enum": [ "agent", @@ -7678,6 +7700,17 @@ "null" ] }, + "skill_metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ExecApprovalRequestSkillMetadata" + }, + { + "type": "null" + } + ], + "description": "Optional skill metadata when the approval was triggered by a skill script." + }, "turn_id": { "default": "", "description": "Turn ID that this command belongs to. Uses `#[serde(default)]` for backwards compatibility.", diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index ae0c4ee506..7183de5d3b 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -440,6 +440,17 @@ ], "type": "object" }, + "CommandExecutionRequestApprovalSkillMetadata": { + "properties": { + "pathToSkillsMd": { + "type": "string" + } + }, + "required": [ + "pathToSkillsMd" + ], + "type": "object" + }, "DynamicToolCallParams": { "properties": { "arguments": true, 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 6206539dfd..7129d3a4be 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 @@ -1762,6 +1762,17 @@ "title": "CommandExecutionRequestApprovalResponse", "type": "object" }, + "CommandExecutionRequestApprovalSkillMetadata": { + "properties": { + "pathToSkillsMd": { + "type": "string" + } + }, + "required": [ + "pathToSkillsMd" + ], + "type": "object" + }, "CustomPrompt": { "properties": { "argument_hint": { @@ -3182,6 +3193,17 @@ "null" ] }, + "skill_metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ExecApprovalRequestSkillMetadata" + }, + { + "type": "null" + } + ], + "description": "Optional skill metadata when the approval was triggered by a skill script." + }, "turn_id": { "default": "", "description": "Turn ID that this command belongs to. Uses `#[serde(default)]` for backwards compatibility.", @@ -4759,6 +4781,17 @@ ], "title": "EventMsg" }, + "ExecApprovalRequestSkillMetadata": { + "properties": { + "path_to_skills_md": { + "type": "string" + } + }, + "required": [ + "path_to_skills_md" + ], + "type": "object" + }, "ExecCommandApprovalParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -17075,4 +17108,4 @@ }, "title": "CodexAppServerProtocol", "type": "object" -} +} \ No newline at end of file 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 d6a948b6a6..88f4d1de2a 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 @@ -4999,6 +4999,17 @@ "null" ] }, + "skill_metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ExecApprovalRequestSkillMetadata" + }, + { + "type": "null" + } + ], + "description": "Optional skill metadata when the approval was triggered by a skill script." + }, "turn_id": { "default": "", "description": "Turn ID that this command belongs to. Uses `#[serde(default)]` for backwards compatibility.", @@ -6576,6 +6587,17 @@ ], "title": "EventMsg" }, + "ExecApprovalRequestSkillMetadata": { + "properties": { + "path_to_skills_md": { + "type": "string" + } + }, + "required": [ + "path_to_skills_md" + ], + "type": "object" + }, "ExecCommandSource": { "enum": [ "agent", @@ -15330,4 +15352,4 @@ }, "title": "CodexAppServerProtocolV2", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json index 6e2e8baebf..ed93913cb3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json @@ -33,4 +33,4 @@ ], "title": "WindowsSandboxSetupStartParams", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts index cc13400900..5f305f521d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.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 { ExecApprovalRequestSkillMetadata } from "./ExecApprovalRequestSkillMetadata"; import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; import type { NetworkApprovalContext } from "./NetworkApprovalContext"; import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; @@ -53,6 +54,10 @@ proposed_network_policy_amendments?: Array, * Optional additional filesystem permissions requested for this command. */ additional_permissions?: PermissionProfile, +/** + * Optional skill metadata when the approval was triggered by a skill script. + */ +skill_metadata?: ExecApprovalRequestSkillMetadata, /** * Ordered list of decisions the client may present for this prompt. * diff --git a/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestSkillMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestSkillMetadata.ts new file mode 100644 index 0000000000..1121e214eb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestSkillMetadata.ts @@ -0,0 +1,5 @@ +// 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. + +export type ExecApprovalRequestSkillMetadata = { path_to_skills_md: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index af0db3367e..1a53466b45 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -53,6 +53,7 @@ export type { ElicitationRequestEvent } from "./ElicitationRequestEvent"; export type { ErrorEvent } from "./ErrorEvent"; export type { EventMsg } from "./EventMsg"; export type { ExecApprovalRequestEvent } from "./ExecApprovalRequestEvent"; +export type { ExecApprovalRequestSkillMetadata } from "./ExecApprovalRequestSkillMetadata"; export type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams"; export type { ExecCommandApprovalResponse } from "./ExecCommandApprovalResponse"; export type { ExecCommandBeginEvent } from "./ExecCommandBeginEvent"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts index 8fb6375e69..623fb971c1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts @@ -4,6 +4,7 @@ import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; import type { CommandAction } from "./CommandAction"; import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; +import type { CommandExecutionRequestApprovalSkillMetadata } from "./CommandExecutionRequestApprovalSkillMetadata"; import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; import type { NetworkApprovalContext } from "./NetworkApprovalContext"; import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; @@ -43,6 +44,10 @@ commandActions?: Array | null, * Optional additional permissions requested for this command. */ additionalPermissions?: AdditionalPermissionProfile | null, +/** + * Optional skill metadata when the approval was triggered by a skill script. + */ +skillMetadata?: CommandExecutionRequestApprovalSkillMetadata | null, /** * Optional proposed execpolicy amendment to allow similar commands without prompting. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalSkillMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalSkillMetadata.ts new file mode 100644 index 0000000000..dcfd7c2916 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalSkillMetadata.ts @@ -0,0 +1,5 @@ +// 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. + +export type CommandExecutionRequestApprovalSkillMetadata = { pathToSkillsMd: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index ac81e35a5a..e233c50e59 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -53,6 +53,7 @@ export type { CommandExecutionApprovalDecision } from "./CommandExecutionApprova export type { CommandExecutionOutputDeltaNotification } from "./CommandExecutionOutputDeltaNotification"; export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRequestApprovalParams"; export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse"; +export type { CommandExecutionRequestApprovalSkillMetadata } from "./CommandExecutionRequestApprovalSkillMetadata"; export type { CommandExecutionStatus } from "./CommandExecutionStatus"; export type { Config } from "./Config"; export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams"; diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index ba151046a1..0588e983a4 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -2206,6 +2206,10 @@ mod tests { command_execution_request_approval_ts.contains("additionalPermissions"), true ); + assert_eq!( + command_execution_request_approval_ts.contains("skillMetadata"), + true + ); Ok(()) } @@ -2668,6 +2672,10 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k command_execution_request_approval_json.contains("additionalPermissions"), false ); + assert_eq!( + command_execution_request_approval_json.contains("skillMetadata"), + false + ); let client_request_json = fs::read_to_string(output_dir.join("ClientRequest.json"))?; assert_eq!( @@ -2679,6 +2687,7 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k fs::read_to_string(output_dir.join("codex_app_server_protocol.schemas.json"))?; assert_eq!(bundle_json.contains("mockExperimentalField"), false); assert_eq!(bundle_json.contains("additionalPermissions"), false); + assert_eq!(bundle_json.contains("skillMetadata"), false); assert_eq!(bundle_json.contains("MockExperimentalMethodParams"), false); assert_eq!( bundle_json.contains("MockExperimentalMethodResponse"), @@ -2688,6 +2697,7 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k fs::read_to_string(output_dir.join("codex_app_server_protocol.v2.schemas.json"))?; assert_eq!(flat_v2_bundle_json.contains("mockExperimentalField"), false); assert_eq!(flat_v2_bundle_json.contains("additionalPermissions"), false); + assert_eq!(flat_v2_bundle_json.contains("skillMetadata"), false); assert_eq!( flat_v2_bundle_json.contains("MockExperimentalMethodParams"), false diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 8e87aa9ecb..d32ebd9fbc 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1562,6 +1562,7 @@ mod tests { }), macos: None, }), + skill_metadata: None, proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, available_decisions: None, @@ -1572,4 +1573,31 @@ mod tests { Some("item/commandExecution/requestApproval.additionalPermissions") ); } + + #[test] + fn command_execution_request_approval_skill_metadata_is_marked_experimental() { + let params = v2::CommandExecutionRequestApprovalParams { + thread_id: "thr_123".to_string(), + turn_id: "turn_123".to_string(), + item_id: "call_123".to_string(), + approval_id: None, + reason: None, + network_approval_context: None, + command: Some("cat file".to_string()), + cwd: None, + command_actions: None, + additional_permissions: None, + skill_metadata: Some(v2::CommandExecutionRequestApprovalSkillMetadata { + path_to_skills_md: PathBuf::from("/tmp/SKILLS.md"), + }), + proposed_execpolicy_amendment: None, + proposed_network_policy_amendments: None, + available_decisions: None, + }; + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¶ms); + assert_eq!( + reason, + Some("item/commandExecution/requestApproval.skillMetadata") + ); + } } diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index f953bd1be9..d75c0b1a43 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -7,6 +7,7 @@ use crate::protocol::common::AuthMode; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::account::PlanType; use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest; +use codex_protocol::approvals::ExecApprovalRequestSkillMetadata as CoreExecApprovalRequestSkillMetadata; use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext; use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol; @@ -2827,6 +2828,14 @@ impl From for SkillMetadata { } } +impl From for CommandExecutionRequestApprovalSkillMetadata { + fn from(value: CoreExecApprovalRequestSkillMetadata) -> Self { + Self { + path_to_skills_md: value.path_to_skills_md, + } + } +} + impl From for SkillInterface { fn from(value: CoreSkillInterface) -> Self { Self { @@ -4300,6 +4309,11 @@ pub struct CommandExecutionRequestApprovalParams { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub additional_permissions: Option, + /// Optional skill metadata when the approval was triggered by a skill script. + #[experimental("item/commandExecution/requestApproval.skillMetadata")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub skill_metadata: Option, /// Optional proposed execpolicy amendment to allow similar commands without prompting. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] @@ -4321,9 +4335,17 @@ impl CommandExecutionRequestApprovalParams { // We need a generic outbound compatibility design for stripping or // otherwise handling experimental server->client payloads. self.additional_permissions = None; + self.skill_metadata = None; } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecutionRequestApprovalSkillMetadata { + pub path_to_skills_md: PathBuf, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -5098,6 +5120,7 @@ mod tests { }, "macos": null }, + "skillMetadata": null, "proposedExecpolicyAmendment": null, "proposedNetworkPolicyAmendments": null, "availableDecisions": null @@ -5133,6 +5156,7 @@ mod tests { "calendar": false } }, + "skillMetadata": null, "proposedExecpolicyAmendment": null, "proposedNetworkPolicyAmendments": null, "availableDecisions": null @@ -5150,6 +5174,35 @@ mod tests { ); } + #[test] + fn command_execution_request_approval_accepts_skill_metadata() { + let params = serde_json::from_value::(json!({ + "threadId": "thr_123", + "turnId": "turn_123", + "itemId": "call_123", + "command": "cat file", + "cwd": "/tmp", + "commandActions": null, + "reason": null, + "networkApprovalContext": null, + "additionalPermissions": null, + "skillMetadata": { + "pathToSkillsMd": "/tmp/SKILLS.md" + }, + "proposedExecpolicyAmendment": null, + "proposedNetworkPolicyAmendments": null, + "availableDecisions": null + })) + .expect("skill metadata should deserialize"); + + assert_eq!( + params.skill_metadata, + Some(CommandExecutionRequestApprovalSkillMetadata { + path_to_skills_md: PathBuf::from("/tmp/SKILLS.md"), + }) + ); + } + #[test] fn command_exec_params_default_optional_streaming_flags() { let params = serde_json::from_value::(json!({ diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 51931d6a80..c5a917b64c 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1507,6 +1507,7 @@ impl CodexClient { cwd, command_actions, additional_permissions, + skill_metadata, proposed_execpolicy_amendment, proposed_network_policy_amendments, available_decisions, @@ -1541,6 +1542,9 @@ impl CodexClient { if let Some(additional_permissions) = additional_permissions.as_ref() { println!("< additional permissions: {additional_permissions:?}"); } + if let Some(skill_metadata) = skill_metadata.as_ref() { + println!("< skill metadata: {skill_metadata:?}"); + } if let Some(execpolicy_amendment) = proposed_execpolicy_amendment.as_ref() { println!("< proposed execpolicy amendment: {execpolicy_amendment:?}"); } diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 499ac9ffc7..c8665daef6 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -26,6 +26,7 @@ use codex_app_server_protocol::CommandExecutionApprovalDecision; use codex_app_server_protocol::CommandExecutionOutputDeltaNotification; use codex_app_server_protocol::CommandExecutionRequestApprovalParams; use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; +use codex_app_server_protocol::CommandExecutionRequestApprovalSkillMetadata; use codex_app_server_protocol::CommandExecutionStatus; use codex_app_server_protocol::ContextCompactedNotification; use codex_app_server_protocol::DeprecationNoticeNotification; @@ -437,6 +438,7 @@ pub(crate) async fn apply_bespoke_event_handling( proposed_execpolicy_amendment, proposed_network_policy_amendments, additional_permissions, + skill_metadata, parsed_cmd, .. } = ev; @@ -508,6 +510,8 @@ pub(crate) async fn apply_bespoke_event_handling( }); let additional_permissions = additional_permissions.map(V2AdditionalPermissionProfile::from); + let skill_metadata = + skill_metadata.map(CommandExecutionRequestApprovalSkillMetadata::from); let params = CommandExecutionRequestApprovalParams { thread_id: conversation_id.to_string(), @@ -520,6 +524,7 @@ pub(crate) async fn apply_bespoke_event_handling( cwd, command_actions, additional_permissions, + skill_metadata, proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2, proposed_network_policy_amendments: proposed_network_policy_amendments_v2, available_decisions: Some(available_decisions), diff --git a/codex-rs/app-server/src/transport.rs b/codex-rs/app-server/src/transport.rs index 019d00e45e..4e8bdd7e16 100644 --- a/codex-rs/app-server/src/transport.rs +++ b/codex-rs/app-server/src/transport.rs @@ -671,6 +671,7 @@ pub(crate) async fn route_outgoing_envelope( mod tests { use super::*; use crate::error_code::OVERLOADED_ERROR_CODE; + use codex_app_server_protocol::CommandExecutionRequestApprovalSkillMetadata; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::json; @@ -991,6 +992,9 @@ mod tests { macos: None, }, ), + skill_metadata: Some(CommandExecutionRequestApprovalSkillMetadata { + path_to_skills_md: PathBuf::from("/tmp/SKILLS.md"), + }), proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, available_decisions: None, @@ -1006,6 +1010,7 @@ mod tests { .expect("request should be delivered to the connection"); let json = serde_json::to_value(message).expect("request should serialize"); assert_eq!(json["params"].get("additionalPermissions"), None); + assert_eq!(json["params"].get("skillMetadata"), None); } #[tokio::test] @@ -1053,6 +1058,9 @@ mod tests { macos: None, }, ), + skill_metadata: Some(CommandExecutionRequestApprovalSkillMetadata { + path_to_skills_md: PathBuf::from("/tmp/SKILLS.md"), + }), proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, available_decisions: None, @@ -1079,6 +1087,12 @@ mod tests { "macos": null, }) ); + assert_eq!( + json["params"]["skillMetadata"], + json!({ + "pathToSkillsMd": "/tmp/SKILLS.md", + }) + ); } #[tokio::test] diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 66985375e8..c545848f51 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -71,6 +71,7 @@ use codex_otel::current_span_w3c_trace_context; use codex_otel::set_parent_from_w3c_trace_context; use codex_protocol::ThreadId; use codex_protocol::approvals::ElicitationRequestEvent; +use codex_protocol::approvals::ExecApprovalRequestSkillMetadata; use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::approvals::NetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyRuleAction; @@ -2721,6 +2722,7 @@ impl Session { network_approval_context: Option, proposed_execpolicy_amendment: Option, additional_permissions: Option, + skill_metadata: Option, available_decisions: Option>, ) -> ReviewDecision { // command-level approvals use `call_id`. @@ -2774,6 +2776,7 @@ impl Session { proposed_execpolicy_amendment, proposed_network_policy_amendments, additional_permissions, + skill_metadata, available_decisions: Some(available_decisions), parsed_cmd, }); diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 8e41ad4a7e..9d83a44298 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -332,6 +332,7 @@ async fn handle_exec_approval( network_approval_context, proposed_execpolicy_amendment, additional_permissions, + skill_metadata, available_decisions, .. } = event; @@ -346,6 +347,7 @@ async fn handle_exec_approval( network_approval_context, proposed_execpolicy_amendment, additional_permissions, + skill_metadata, available_decisions, ); let decision = await_approval_with_cancel( diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 24502cde8d..cd223b1690 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -371,6 +371,7 @@ impl NetworkApprovalService { Some(network_approval_context.clone()), None, None, + None, available_decisions, ) .await diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index c5a883438c..388573eb4d 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -188,6 +188,7 @@ impl Approvable for ShellRuntime { .proposed_execpolicy_amendment() .cloned(), req.additional_permissions.clone(), + None, available_decisions, ) .await diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index b5a906eb07..35db88d960 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -31,6 +31,7 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::ExecApprovalRequestSkillMetadata; use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; @@ -410,6 +411,14 @@ impl CoreShellActionProvider { .into_iter() .flatten() .collect(); + let skill_metadata = match decision_source { + DecisionSource::SkillScript { skill } => { + Some(ExecApprovalRequestSkillMetadata { + path_to_skills_md: skill.path_to_skills_md.clone(), + }) + } + DecisionSource::PrefixRule | DecisionSource::UnmatchedCommandFallback => None, + }; session .request_command_approval( &turn, @@ -421,6 +430,7 @@ impl CoreShellActionProvider { None, None, additional_permissions, + skill_metadata, Some(available_decisions), ) .await diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 2a775e26c0..3afbd9ce99 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -154,6 +154,7 @@ impl Approvable for UnifiedExecRuntime<'_> { .proposed_execpolicy_amendment() .cloned(), req.additional_permissions.clone(), + None, available_decisions, ) .await diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index 27341ec24c..6ec6f597f3 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -7,6 +7,7 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; +use codex_protocol::protocol::ExecApprovalRequestSkillMetadata; use codex_protocol::protocol::Op; use codex_protocol::protocol::RejectConfig; use codex_protocol::protocol::ReviewDecision; @@ -242,6 +243,14 @@ permissions: ..Default::default() }) ); + assert_eq!( + approval.skill_metadata, + Some(ExecApprovalRequestSkillMetadata { + path_to_skills_md: test + .codex_home_path() + .join("skills/mbolin-test-skill/agents/openai.yaml"), + }) + ); test.codex .submit(Op::ExecApproval { diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index d0cfaf909a..30b194ae5e 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -228,6 +228,7 @@ async fn run_codex_tool_session_inner( parsed_cmd, network_approval_context: _, additional_permissions: _, + skill_metadata: _, available_decisions: _, } = ev; handle_exec_approval_request( diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 4116777afb..74aadb7570 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -90,6 +90,13 @@ pub struct NetworkPolicyAmendment { pub action: NetworkPolicyRuleAction, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case")] +pub struct ExecApprovalRequestSkillMetadata { + pub path_to_skills_md: PathBuf, +} + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ExecApprovalRequestEvent { /// Identifier for the associated command execution item. @@ -128,6 +135,10 @@ pub struct ExecApprovalRequestEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub additional_permissions: Option, + /// Optional skill metadata when the approval was triggered by a skill script. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub skill_metadata: Option, /// Ordered list of decisions the client may present for this prompt. /// /// When absent, clients should derive the legacy default set from the diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f51e3e6b72..e30c2a0af6 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -56,6 +56,7 @@ use ts_rs::TS; pub use crate::approvals::ApplyPatchApprovalRequestEvent; pub use crate::approvals::ElicitationAction; pub use crate::approvals::ExecApprovalRequestEvent; +pub use crate::approvals::ExecApprovalRequestSkillMetadata; pub use crate::approvals::ExecPolicyAmendment; pub use crate::approvals::NetworkApprovalContext; pub use crate::approvals::NetworkApprovalProtocol; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index e649455da6..4ad16d634d 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -3915,6 +3915,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: Vec::new(), }, @@ -5078,6 +5079,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: Vec::new(), }, @@ -5169,6 +5171,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: Vec::new(), }, diff --git a/codex-rs/tui/src/app/pending_interactive_replay.rs b/codex-rs/tui/src/app/pending_interactive_replay.rs index 8ed6d2653a..4443b5ef26 100644 --- a/codex-rs/tui/src/app/pending_interactive_replay.rs +++ b/codex-rs/tui/src/app/pending_interactive_replay.rs @@ -402,6 +402,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: Vec::new(), }, @@ -545,6 +546,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: Vec::new(), }, @@ -634,6 +636,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: Vec::new(), }, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 7307891106..43ec2b798e 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -3188,6 +3188,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: vec![], }; @@ -3238,6 +3239,7 @@ async fn exec_approval_uses_approval_id_when_present() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: vec![], }), @@ -3279,6 +3281,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: vec![], }; @@ -3334,6 +3337,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: vec![], }; @@ -7965,6 +7969,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> { ])), proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: vec![], }; @@ -8026,6 +8031,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> { ])), proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: vec![], }; @@ -8074,6 +8080,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot() proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)), proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: vec![], }; @@ -8441,6 +8448,7 @@ async fn status_widget_and_approval_modal_snapshot() { ])), proposed_network_policy_amendments: None, additional_permissions: None, + skill_metadata: None, available_decisions: None, parsed_cmd: vec![], };