diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index ea55a572d..af0a0c776 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -1765,6 +1765,30 @@ "title": "DynamicToolCallRequestEventMsg", "type": "object" }, + { + "properties": { + "item_id": { + "type": "string" + }, + "skill_name": { + "type": "string" + }, + "type": { + "enum": [ + "skill_request_approval" + ], + "title": "SkillRequestApprovalEventMsgType", + "type": "string" + } + }, + "required": [ + "item_id", + "skill_name", + "type" + ], + "title": "SkillRequestApprovalEventMsg", + "type": "object" + }, { "properties": { "id": { @@ -7042,6 +7066,30 @@ "title": "DynamicToolCallRequestEventMsg", "type": "object" }, + { + "properties": { + "item_id": { + "type": "string" + }, + "skill_name": { + "type": "string" + }, + "type": { + "enum": [ + "skill_request_approval" + ], + "title": "SkillRequestApprovalEventMsgType", + "type": "string" + } + }, + "required": [ + "item_id", + "skill_name", + "type" + ], + "title": "SkillRequestApprovalEventMsg", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 553f189dc..fb73dc0ed 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -576,6 +576,21 @@ } ] }, + "SkillRequestApprovalParams": { + "properties": { + "itemId": { + "type": "string" + }, + "skillName": { + "type": "string" + } + }, + "required": [ + "itemId", + "skillName" + ], + "type": "object" + }, "ThreadId": { "type": "string" }, @@ -737,6 +752,30 @@ "title": "Item/tool/requestUserInputRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skill/requestApproval" + ], + "title": "Skill/requestApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillRequestApprovalParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skill/requestApprovalRequest", + "type": "object" + }, { "description": "Execute a dynamic tool call on the client.", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalParams.json new file mode 100644 index 000000000..eebe6570b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalParams.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "itemId": { + "type": "string" + }, + "skillName": { + "type": "string" + } + }, + "required": [ + "itemId", + "skillName" + ], + "title": "SkillRequestApprovalParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalResponse.json new file mode 100644 index 000000000..0bea163cc --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalResponse.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "SkillApprovalDecision": { + "enum": [ + "approve", + "decline" + ], + "type": "string" + } + }, + "properties": { + "decision": { + "$ref": "#/definitions/SkillApprovalDecision" + } + }, + "required": [ + "decision" + ], + "title": "SkillRequestApprovalResponse", + "type": "object" +} \ No newline at end of file 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 fb9019aeb..02df08274 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 @@ -2826,6 +2826,30 @@ "title": "DynamicToolCallRequestEventMsg", "type": "object" }, + { + "properties": { + "item_id": { + "type": "string" + }, + "skill_name": { + "type": "string" + }, + "type": { + "enum": [ + "skill_request_approval" + ], + "title": "SkillRequestApprovalEventMsgType", + "type": "string" + } + }, + "required": [ + "item_id", + "skill_name", + "type" + ], + "title": "SkillRequestApprovalEventMsg", + "type": "object" + }, { "properties": { "id": { @@ -6248,6 +6272,30 @@ "title": "Item/tool/requestUserInputRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skill/requestApproval" + ], + "title": "Skill/requestApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillRequestApprovalParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skill/requestApprovalRequest", + "type": "object" + }, { "description": "Execute a dynamic tool call on the client.", "properties": { @@ -6369,6 +6417,43 @@ ], "type": "object" }, + "SkillApprovalDecision": { + "enum": [ + "approve", + "decline" + ], + "type": "string" + }, + "SkillRequestApprovalParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "itemId": { + "type": "string" + }, + "skillName": { + "type": "string" + } + }, + "required": [ + "itemId", + "skillName" + ], + "title": "SkillRequestApprovalParams", + "type": "object" + }, + "SkillRequestApprovalResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "decision": { + "$ref": "#/definitions/SkillApprovalDecision" + } + }, + "required": [ + "decision" + ], + "title": "SkillRequestApprovalResponse", + "type": "object" + }, "StepStatus": { "enum": [ "pending", diff --git a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts index 3aa06058e..cc1009bfd 100644 --- a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts +++ b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts @@ -56,6 +56,7 @@ import type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent"; import type { RequestUserInputEvent } from "./RequestUserInputEvent"; import type { ReviewRequest } from "./ReviewRequest"; import type { SessionConfiguredEvent } from "./SessionConfiguredEvent"; +import type { SkillRequestApprovalEvent } from "./SkillRequestApprovalEvent"; import type { StreamErrorEvent } from "./StreamErrorEvent"; import type { TerminalInteractionEvent } from "./TerminalInteractionEvent"; import type { ThreadNameUpdatedEvent } from "./ThreadNameUpdatedEvent"; @@ -78,4 +79,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent"; * Response event from the agent * NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen. */ -export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "realtime_conversation_started" } & RealtimeConversationStartedEvent | { "type": "realtime_conversation_realtime" } & RealtimeConversationRealtimeEvent | { "type": "realtime_conversation_closed" } & RealtimeConversationClosedEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent; +export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "realtime_conversation_started" } & RealtimeConversationStartedEvent | { "type": "realtime_conversation_realtime" } & RealtimeConversationRealtimeEvent | { "type": "realtime_conversation_closed" } & RealtimeConversationClosedEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "skill_request_approval" } & SkillRequestApprovalEvent | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts index 17c66959a..1f1bc2c77 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts @@ -8,9 +8,10 @@ import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefre import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams"; import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams"; import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams"; +import type { SkillRequestApprovalParams } from "./v2/SkillRequestApprovalParams"; import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams"; /** * Request initiated from the server and sent to the client. */ -export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, }; +export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "skill/requestApproval", id: RequestId, params: SkillRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/SkillRequestApprovalEvent.ts b/codex-rs/app-server-protocol/schema/typescript/SkillRequestApprovalEvent.ts new file mode 100644 index 000000000..6699eadac --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/SkillRequestApprovalEvent.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 SkillRequestApprovalEvent = { item_id: string, skill_name: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index e38f97df4..e5c856695 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -203,6 +203,7 @@ export type { SkillDependencies } from "./SkillDependencies"; export type { SkillErrorInfo } from "./SkillErrorInfo"; export type { SkillInterface } from "./SkillInterface"; export type { SkillMetadata } from "./SkillMetadata"; +export type { SkillRequestApprovalEvent } from "./SkillRequestApprovalEvent"; export type { SkillScope } from "./SkillScope"; export type { SkillToolDependency } from "./SkillToolDependency"; export type { SkillsListEntry } from "./SkillsListEntry"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillApprovalDecision.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillApprovalDecision.ts new file mode 100644 index 000000000..6773b0f1e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillApprovalDecision.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 SkillApprovalDecision = "approve" | "decline"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalParams.ts new file mode 100644 index 000000000..3f70fea59 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalParams.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 SkillRequestApprovalParams = { itemId: string, skillName: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalResponse.ts new file mode 100644 index 000000000..9ce454ff4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalResponse.ts @@ -0,0 +1,6 @@ +// 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 { SkillApprovalDecision } from "./SkillApprovalDecision"; + +export type SkillRequestApprovalResponse = { decision: SkillApprovalDecision, }; 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 ce2289c38..12e6e9adb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -128,10 +128,13 @@ export type { SandboxMode } from "./SandboxMode"; export type { SandboxPolicy } from "./SandboxPolicy"; export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; export type { SessionSource } from "./SessionSource"; +export type { SkillApprovalDecision } from "./SkillApprovalDecision"; export type { SkillDependencies } from "./SkillDependencies"; export type { SkillErrorInfo } from "./SkillErrorInfo"; export type { SkillInterface } from "./SkillInterface"; export type { SkillMetadata } from "./SkillMetadata"; +export type { SkillRequestApprovalParams } from "./SkillRequestApprovalParams"; +export type { SkillRequestApprovalResponse } from "./SkillRequestApprovalResponse"; export type { SkillScope } from "./SkillScope"; export type { SkillToolDependency } from "./SkillToolDependency"; export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index a9ce27ed0..f32925746 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -671,6 +671,11 @@ server_request_definitions! { response: v2::ToolRequestUserInputResponse, }, + SkillRequestApproval => "skill/requestApproval" { + params: v2::SkillRequestApprovalParams, + response: v2::SkillRequestApprovalResponse, + }, + /// Execute a dynamic tool call on the client. DynamicToolCall => "item/tool/call" { params: v2::DynamicToolCallParams, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 43038d351..b1f6ceb5c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3431,6 +3431,29 @@ pub struct FileChangeRequestApprovalResponse { pub decision: FileChangeApprovalDecision, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillRequestApprovalParams { + pub item_id: String, + pub skill_name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum SkillApprovalDecision { + Approve, + Decline, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillRequestApprovalResponse { + pub decision: SkillApprovalDecision, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index a1d10be33..1cba215e2 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -57,6 +57,9 @@ use codex_app_server_protocol::SendUserMessageParams; use codex_app_server_protocol::SendUserMessageResponse; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::SkillApprovalDecision; +use codex_app_server_protocol::SkillRequestApprovalParams; +use codex_app_server_protocol::SkillRequestApprovalResponse; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadListResponse; @@ -1472,6 +1475,9 @@ impl CodexClient { ServerRequest::FileChangeRequestApproval { request_id, params } => { self.approve_file_change_request(request_id, params)?; } + ServerRequest::SkillRequestApproval { request_id, params } => { + self.approve_skill_request(request_id, params)?; + } other => { bail!("received unsupported server request: {other:?}"); } @@ -1543,6 +1549,22 @@ impl CodexClient { Ok(()) } + fn approve_skill_request( + &mut self, + request_id: RequestId, + params: SkillRequestApprovalParams, + ) -> Result<()> { + println!( + "\n< skill approval requested for item {}, skill {}", + params.item_id, params.skill_name + ); + let response = SkillRequestApprovalResponse { + decision: SkillApprovalDecision::Approve, + }; + self.send_server_request_response(request_id, &response)?; + Ok(()) + } + fn approve_file_change_request( &mut self, request_id: RequestId, diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index fe3eb9685..2a5557b5b 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -53,6 +53,9 @@ use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification; use codex_app_server_protocol::ReasoningTextDeltaNotification; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestPayload; +use codex_app_server_protocol::SkillApprovalDecision as V2SkillApprovalDecision; +use codex_app_server_protocol::SkillRequestApprovalParams; +use codex_app_server_protocol::SkillRequestApprovalResponse; use codex_app_server_protocol::TerminalInteractionNotification; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadNameUpdatedNotification; @@ -97,6 +100,7 @@ use codex_protocol::protocol::TokenCountEvent; use codex_protocol::protocol::TurnDiffEvent; use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse; +use codex_protocol::skill_approval::SkillApprovalResponse as CoreSkillApprovalResponse; use codex_shell_command::parse_command::shlex_join; use std::collections::HashMap; use std::convert::TryFrom; @@ -423,6 +427,37 @@ pub(crate) async fn apply_bespoke_event_handling( } } } + EventMsg::SkillRequestApproval(request) => { + if matches!(api_version, ApiVersion::V2) { + let item_id = request.item_id; + let skill_name = request.skill_name; + let params = SkillRequestApprovalParams { + item_id: item_id.clone(), + skill_name, + }; + let rx = outgoing + .send_request(ServerRequestPayload::SkillRequestApproval(params)) + .await; + tokio::spawn(async move { + let approved = match rx.await { + Ok(Ok(value)) => { + serde_json::from_value::(value) + .map(|response| { + matches!(response.decision, V2SkillApprovalDecision::Approve) + }) + .unwrap_or(false) + } + _ => false, + }; + let _ = conversation + .submit(Op::SkillApproval { + id: item_id, + response: CoreSkillApprovalResponse { approved }, + }) + .await; + }); + } + } EventMsg::DynamicToolCallRequest(request) => { if matches!(api_version, ApiVersion::V2) { let call_id = request.call_id; diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index ac8f9c988..0e428536c 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -16,6 +16,7 @@ mod rate_limits; mod request_user_input; mod review; mod safety_check_downgrade; +mod skill_approval; mod skills_list; mod thread_archive; mod thread_fork; diff --git a/codex-rs/app-server/tests/suite/v2/skill_approval.rs b/codex-rs/app-server/tests/suite/v2/skill_approval.rs new file mode 100644 index 000000000..945a545a8 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/skill_approval.rs @@ -0,0 +1,109 @@ +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::create_final_assistant_message_sse_response; +use app_test_support::create_mock_responses_server_sequence; +use app_test_support::to_response; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::UserInput as V2UserInput; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn skill_request_approval_round_trip() -> Result<()> { + let codex_home = tempfile::TempDir::new()?; + let server = + create_mock_responses_server_sequence(vec![create_final_assistant_message_sse_response( + "done", + )?]) + .await; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_start_id = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_start_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?; + + let turn_start_id = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: "ask something".to_string(), + text_elements: Vec::new(), + }], + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let turn_start_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_start_id)), + ) + .await??; + let TurnStartResponse { turn, .. } = to_response(turn_start_resp)?; + + let server_req = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_request_message(), + ) + .await??; + let ServerRequest::SkillRequestApproval { request_id, params } = server_req else { + panic!("expected SkillRequestApproval request, got: {server_req:?}"); + }; + + assert_eq!(params.item_id, turn.id); + assert_eq!(params.skill_name, "test-skill"); + + mcp.send_response(request_id, serde_json::json!({ "decision": "approve" })) + .await?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + Ok(()) +} + +fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> { + let config_toml = codex_home.join("config.toml"); + std::fs::write( + config_toml, + format!( + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "read-only" + +model_provider = "mock_provider" + +[features] +skill_approval = true + +[model_providers.mock_provider] +name = "Mock provider for test" +base_url = "{server_uri}/v1" +request_max_retries = 0 +stream_max_retries = 0 +"# + ), + ) +} diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 6c29fa55c..de3353e30 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -394,6 +394,9 @@ "shell_zsh_fork": { "type": "boolean" }, + "skill_approval": { + "type": "boolean" + }, "skill_env_var_dependency_prompt": { "type": "boolean" }, @@ -1678,6 +1681,9 @@ "shell_zsh_fork": { "type": "boolean" }, + "skill_approval": { + "type": "boolean" + }, "skill_env_var_dependency_prompt": { "type": "boolean" }, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 01d9851d5..94d2cde3c 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -88,6 +88,7 @@ use codex_protocol::protocol::TurnContextNetworkItem; use codex_protocol::protocol::TurnStartedEvent; use codex_protocol::request_user_input::RequestUserInputArgs; use codex_protocol::request_user_input::RequestUserInputResponse; +use codex_protocol::skill_approval::SkillApprovalResponse; use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::OAuthCredentialsStoreMode; use futures::future::BoxFuture; @@ -2637,6 +2638,40 @@ impl Session { rx_response.await.ok() } + pub async fn request_skill_approval( + &self, + turn_context: &TurnContext, + item_id: String, + skill_name: String, + ) -> Option { + let (tx_response, rx_response) = oneshot::channel(); + let prev_entry = { + let mut active = self.active_turn.lock().await; + match active.as_mut() { + Some(at) => { + let mut ts = at.turn_state.lock().await; + ts.insert_pending_skill_approval(item_id.clone(), tx_response) + } + None => None, + } + }; + if prev_entry.is_some() { + warn!("Overwriting existing pending skill approval for item_id: {item_id}"); + } + + self.send_event( + turn_context, + EventMsg::SkillRequestApproval( + codex_protocol::skill_approval::SkillRequestApprovalEvent { + item_id, + skill_name, + }, + ), + ) + .await; + rx_response.await.ok() + } + pub async fn notify_user_input_response( &self, sub_id: &str, @@ -2683,6 +2718,31 @@ impl Session { } } + pub async fn notify_skill_approval_response( + &self, + item_id: &str, + response: SkillApprovalResponse, + ) { + let entry = { + let mut active = self.active_turn.lock().await; + match active.as_mut() { + Some(at) => { + let mut ts = at.turn_state.lock().await; + ts.remove_pending_skill_approval(item_id) + } + None => None, + } + }; + match entry { + Some(tx_response) => { + tx_response.send(response).ok(); + } + None => { + warn!("No pending skill approval found for item_id: {item_id}"); + } + } + } + pub async fn notify_approval(&self, approval_id: &str, decision: ReviewDecision) { let entry = { let mut active = self.active_turn.lock().await; @@ -3585,6 +3645,9 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv Op::DynamicToolResponse { id, response } => { handlers::dynamic_tool_response(&sess, id, response).await; } + Op::SkillApproval { id, response } => { + handlers::skill_approval_response(&sess, id, response).await; + } Op::AddToHistory { text } => { handlers::add_to_history(&sess, &config, text).await; } @@ -3708,6 +3771,7 @@ mod handlers { use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::WarningEvent; use codex_protocol::request_user_input::RequestUserInputResponse; + use codex_protocol::skill_approval::SkillApprovalResponse; use crate::context_manager::is_user_turn_boundary; use codex_protocol::config_types::CollaborationMode; @@ -3948,6 +4012,14 @@ mod handlers { sess.notify_dynamic_tool_response(&id, response).await; } + pub async fn skill_approval_response( + sess: &Arc, + id: String, + response: SkillApprovalResponse, + ) { + sess.notify_skill_approval_response(&id, response).await; + } + pub async fn add_to_history(sess: &Arc, config: &Arc, text: String) { let id = sess.conversation_id; let config = Arc::clone(config); @@ -4661,6 +4733,15 @@ pub(crate) async fn run_turn( collaboration_mode_kind: turn_context.collaboration_mode.mode, }); sess.send_event(&turn_context, event).await; + if turn_context.config.features.enabled(Feature::SkillApproval) { + let _ = sess + .request_skill_approval( + turn_context.as_ref(), + turn_context.sub_id.clone(), + "test-skill".to_string(), + ) + .await; + } // TODO(ccunningham): Pre-turn compaction runs before context updates and the // new user message are recorded. Estimate pending incoming items (context // diffs/full reinjection + user input) and trigger compaction preemptively diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 151917f50..521eb7146 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -131,6 +131,8 @@ pub enum Feature { SkillMcpDependencyInstall, /// Prompt for missing skill env var dependencies. SkillEnvVarDependencyPrompt, + /// Emit skill approval test prompts/events. + SkillApproval, /// Steer feature flag - when enabled, Enter submits immediately instead of queuing. Steer, /// Enable collaboration modes (Plan, Default). @@ -611,6 +613,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::SkillApproval, + key: "skill_approval", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::Steer, key: "steer", diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 7899f0e83..823233023 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -139,6 +139,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::TerminalInteraction(_) | EventMsg::ExecCommandOutputDelta(_) | EventMsg::ExecApprovalRequest(_) + | EventMsg::SkillRequestApproval(_) | EventMsg::RequestUserInput(_) | EventMsg::DynamicToolCallRequest(_) | EventMsg::ElicitationRequest(_) diff --git a/codex-rs/core/src/state/turn.rs b/codex-rs/core/src/state/turn.rs index ccc50d066..330174086 100644 --- a/codex-rs/core/src/state/turn.rs +++ b/codex-rs/core/src/state/turn.rs @@ -11,6 +11,7 @@ use tokio_util::task::AbortOnDropHandle; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::models::ResponseInputItem; use codex_protocol::request_user_input::RequestUserInputResponse; +use codex_protocol::skill_approval::SkillApprovalResponse; use tokio::sync::oneshot; use crate::codex::TurnContext; @@ -71,6 +72,7 @@ impl ActiveTurn { pub(crate) struct TurnState { pending_approvals: HashMap>, pending_user_input: HashMap>, + pending_skill_approvals: HashMap>, pending_dynamic_tools: HashMap>, pending_input: Vec, } @@ -94,6 +96,7 @@ impl TurnState { pub(crate) fn clear_pending(&mut self) { self.pending_approvals.clear(); self.pending_user_input.clear(); + self.pending_skill_approvals.clear(); self.pending_dynamic_tools.clear(); self.pending_input.clear(); } @@ -121,6 +124,21 @@ impl TurnState { self.pending_dynamic_tools.insert(key, tx) } + pub(crate) fn insert_pending_skill_approval( + &mut self, + key: String, + tx: oneshot::Sender, + ) -> Option> { + self.pending_skill_approvals.insert(key, tx) + } + + pub(crate) fn remove_pending_skill_approval( + &mut self, + key: &str, + ) -> Option> { + self.pending_skill_approvals.remove(key) + } + pub(crate) fn remove_pending_dynamic_tool( &mut self, key: &str, diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 7d73e4668..4bd6badb8 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -111,6 +111,7 @@ mod seatbelt; mod shell_command; mod shell_serialization; mod shell_snapshot; +mod skill_approval; mod skills; mod sqlite_state; mod stream_error_allows_next_turn; diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs new file mode 100644 index 000000000..acbc0323f --- /dev/null +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -0,0 +1,88 @@ +#![allow(clippy::unwrap_used)] + +use codex_core::features::Feature; +use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::skill_approval::SkillApprovalResponse; +use codex_protocol::user_input::UserInput; +use core_test_support::responses; +use core_test_support::responses::ev_assistant_message; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::skip_if_no_network; +use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use core_test_support::wait_for_event_match; +use pretty_assertions::assert_eq; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn skill_approval_event_round_trip_unblocks_turn() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + responses::mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-1"), + ev_assistant_message("msg-1", "approved"), + ev_completed("resp-1"), + ]), + ) + .await; + + let builder = test_codex(); + let TestCodex { + codex, + cwd, + session_configured, + .. + } = builder + .with_config(|config| { + config.features.enable(Feature::SkillApproval); + }) + .build(&server) + .await?; + + let turn_id = codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "trigger skill approval test".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: cwd.path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::DangerFullAccess, + model: session_configured.model.clone(), + effort: None, + summary: ReasoningSummary::Auto, + collaboration_mode: None, + personality: None, + }) + .await?; + + let request = wait_for_event_match(&codex, |event| match event { + EventMsg::SkillRequestApproval(request) => Some(request.clone()), + _ => None, + }) + .await; + assert_eq!(request.item_id, turn_id); + assert_eq!(request.skill_name, "test-skill"); + + codex + .submit(Op::SkillApproval { + id: request.item_id, + response: SkillApprovalResponse { approved: true }, + }) + .await?; + + wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; + + Ok(()) +} diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 13599294d..f3c92b76d 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -811,7 +811,8 @@ impl EventProcessor for EventProcessorWithHumanOutput { | EventMsg::RealtimeConversationStarted(_) | EventMsg::RealtimeConversationRealtime(_) | EventMsg::RealtimeConversationClosed(_) - | EventMsg::DynamicToolCallRequest(_) => {} + | EventMsg::DynamicToolCallRequest(_) + | EventMsg::SkillRequestApproval(_) => {} } CodexStatus::Running } diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index f1dedb11c..409cc7116 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -362,6 +362,7 @@ async fn run_codex_tool_session_inner( | EventMsg::ExitedReviewMode(_) | EventMsg::RequestUserInput(_) | EventMsg::DynamicToolCallRequest(_) + | EventMsg::SkillRequestApproval(_) | EventMsg::ContextCompacted(_) | EventMsg::ModelReroute(_) | EventMsg::ThreadRolledBack(_) diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index 5841b1187..c942a717d 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -15,4 +15,5 @@ pub mod parse_command; pub mod plan_tool; pub mod protocol; pub mod request_user_input; +pub mod skill_approval; pub mod user_input; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index da49fe384..a8770cc90 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -40,6 +40,7 @@ use crate::openai_models::ReasoningEffort as ReasoningEffortConfig; use crate::parse_command::ParsedCommand; use crate::plan_tool::UpdatePlanArgs; use crate::request_user_input::RequestUserInputResponse; +use crate::skill_approval::SkillApprovalResponse; use crate::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; use schemars::JsonSchema; @@ -60,6 +61,7 @@ pub use crate::approvals::NetworkApprovalProtocol; pub use crate::approvals::NetworkPolicyAmendment; pub use crate::approvals::NetworkPolicyRuleAction; pub use crate::request_user_input::RequestUserInputEvent; +pub use crate::skill_approval::SkillRequestApprovalEvent; /// Open/close tags for special user-input blocks. Used across crates to avoid /// duplicated hardcoded strings. @@ -293,6 +295,14 @@ pub enum Op { response: DynamicToolResponse, }, + /// Resolve a skill approval request. + SkillApproval { + /// Item id for the in-flight request. + id: String, + /// User decision. + response: SkillApprovalResponse, + }, + /// Append an entry to the persistent cross-session message history. /// /// Note the entry is not guaranteed to be logged if the user has @@ -1043,6 +1053,8 @@ pub enum EventMsg { DynamicToolCallRequest(DynamicToolCallRequest), + SkillRequestApproval(SkillRequestApprovalEvent), + ElicitationRequest(ElicitationRequestEvent), ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent), diff --git a/codex-rs/protocol/src/skill_approval.rs b/codex-rs/protocol/src/skill_approval.rs new file mode 100644 index 000000000..152e24818 --- /dev/null +++ b/codex-rs/protocol/src/skill_approval.rs @@ -0,0 +1,15 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +pub struct SkillRequestApprovalEvent { + pub item_id: String, + pub skill_name: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +pub struct SkillApprovalResponse { + pub approved: bool, +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b6e1f5b16..fee60fb1a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4340,7 +4340,8 @@ impl ChatWidget { | EventMsg::RealtimeConversationStarted(_) | EventMsg::RealtimeConversationRealtime(_) | EventMsg::RealtimeConversationClosed(_) - | EventMsg::DynamicToolCallRequest(_) => {} + | EventMsg::DynamicToolCallRequest(_) + | EventMsg::SkillRequestApproval(_) => {} EventMsg::ItemCompleted(event) => { let item = event.item; if let codex_protocol::items::TurnItem::Plan(plan_item) = &item {