diff --git a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json index 37380f5988..84842fd194 100644 --- a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json @@ -59,7 +59,7 @@ "type": "object" }, { - "description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.", + "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.", "enum": [ "approved_for_session" ], diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index 2122ad6521..4e2106bbbc 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -202,6 +202,85 @@ } ] }, + "CommandExecutionApprovalDecision": { + "oneOf": [ + { + "description": "User approved the command.", + "enum": [ + "accept" + ], + "type": "string" + }, + { + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", + "enum": [ + "acceptForSession" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.", + "properties": { + "acceptWithExecpolicyAmendment": { + "properties": { + "execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "execpolicy_amendment" + ], + "type": "object" + } + }, + "required": [ + "acceptWithExecpolicyAmendment" + ], + "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "applyNetworkPolicyAmendment" + ], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "description": "User denied the command. The agent will continue the turn.", + "enum": [ + "decline" + ], + "type": "string" + }, + { + "description": "User denied the command. The turn will also be immediately interrupted.", + "enum": [ + "cancel" + ], + "type": "string" + } + ] + }, "MacOsAutomationValue": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json index 04e914a390..0b7986fba9 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json @@ -11,7 +11,7 @@ "type": "string" }, { - "description": "User approved the command and future identical commands should run without prompting.", + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", "enum": [ "acceptForSession" ], diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 5139e5da66..76a9725f2e 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -500,6 +500,50 @@ ], "type": "object" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, "EventMsg": { "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", "oneOf": [ @@ -1631,6 +1675,16 @@ "null" ] }, + "available_decisions": { + "description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.", + "items": { + "$ref": "#/definitions/ReviewDecision" + }, + "type": [ + "array", + "null" + ] + }, "call_id": { "description": "Identifier for the associated command execution item.", "type": "string" @@ -1776,6 +1830,70 @@ "title": "DynamicToolCallRequestEventMsg", "type": "object" }, + { + "properties": { + "arguments": { + "description": "Dynamic tool call arguments." + }, + "call_id": { + "description": "Identifier for the corresponding DynamicToolCallRequest.", + "type": "string" + }, + "content_items": { + "description": "Dynamic tool response content items.", + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": "array" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ], + "description": "The duration of the dynamic tool call." + }, + "error": { + "description": "Optional error text when the tool call failed before producing a response.", + "type": [ + "string", + "null" + ] + }, + "success": { + "description": "Whether the tool call succeeded.", + "type": "boolean" + }, + "tool": { + "description": "Dynamic tool name.", + "type": "string" + }, + "turn_id": { + "description": "Turn ID that this dynamic tool call belongs to.", + "type": "string" + }, + "type": { + "enum": [ + "dynamic_tool_call_response" + ], + "title": "DynamicToolCallResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "arguments", + "call_id", + "content_items", + "duration", + "success", + "tool", + "turn_id", + "type" + ], + "title": "DynamicToolCallResponseEventMsg", + "type": "object" + }, { "properties": { "item_id": { @@ -4887,6 +5005,86 @@ ], "type": "object" }, + "ReviewDecision": { + "description": "User's decision in response to an ExecApprovalRequest.", + "oneOf": [ + { + "description": "User has approved this command and the agent should execute it.", + "enum": [ + "approved" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.", + "properties": { + "approved_execpolicy_amendment": { + "properties": { + "proposed_execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "proposed_execpolicy_amendment" + ], + "type": "object" + } + }, + "required": [ + "approved_execpolicy_amendment" + ], + "title": "ApprovedExecpolicyAmendmentReviewDecision", + "type": "object" + }, + { + "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.", + "enum": [ + "approved_for_session" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.", + "properties": { + "network_policy_amendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "network_policy_amendment" + ], + "title": "NetworkPolicyAmendmentReviewDecision", + "type": "object" + }, + { + "description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.", + "enum": [ + "denied" + ], + "type": "string" + }, + { + "description": "User has denied this command and the agent should not do anything until the user's next command.", + "enum": [ + "abort" + ], + "type": "string" + } + ] + }, "ReviewFinding": { "description": "A single review finding describing an observed issue or recommendation.", "properties": { @@ -7057,6 +7255,16 @@ "null" ] }, + "available_decisions": { + "description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.", + "items": { + "$ref": "#/definitions/ReviewDecision" + }, + "type": [ + "array", + "null" + ] + }, "call_id": { "description": "Identifier for the associated command execution item.", "type": "string" @@ -7202,6 +7410,70 @@ "title": "DynamicToolCallRequestEventMsg", "type": "object" }, + { + "properties": { + "arguments": { + "description": "Dynamic tool call arguments." + }, + "call_id": { + "description": "Identifier for the corresponding DynamicToolCallRequest.", + "type": "string" + }, + "content_items": { + "description": "Dynamic tool response content items.", + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": "array" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ], + "description": "The duration of the dynamic tool call." + }, + "error": { + "description": "Optional error text when the tool call failed before producing a response.", + "type": [ + "string", + "null" + ] + }, + "success": { + "description": "Whether the tool call succeeded.", + "type": "boolean" + }, + "tool": { + "description": "Dynamic tool name.", + "type": "string" + }, + "turn_id": { + "description": "Turn ID that this dynamic tool call belongs to.", + "type": "string" + }, + "type": { + "enum": [ + "dynamic_tool_call_response" + ], + "title": "DynamicToolCallResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "arguments", + "call_id", + "content_items", + "duration", + "success", + "tool", + "turn_id", + "type" + ], + "title": "DynamicToolCallResponseEventMsg", + "type": "object" + }, { "properties": { "item_id": { diff --git a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json index ac15f23658..baedafa403 100644 --- a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json @@ -59,7 +59,7 @@ "type": "object" }, { - "description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.", + "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.", "enum": [ "approved_for_session" ], diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 47cdc64723..4de83a9ff3 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -778,6 +778,58 @@ ], "type": "object" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "ErrorNotification": { "properties": { "error": { @@ -1965,6 +2017,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index ba94b889cf..75e8394954 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -268,6 +268,85 @@ } ] }, + "CommandExecutionApprovalDecision": { + "oneOf": [ + { + "description": "User approved the command.", + "enum": [ + "accept" + ], + "type": "string" + }, + { + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", + "enum": [ + "acceptForSession" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.", + "properties": { + "acceptWithExecpolicyAmendment": { + "properties": { + "execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "execpolicy_amendment" + ], + "type": "object" + } + }, + "required": [ + "acceptWithExecpolicyAmendment" + ], + "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "applyNetworkPolicyAmendment" + ], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "description": "User denied the command. The agent will continue the turn.", + "enum": [ + "decline" + ], + "type": "string" + }, + { + "description": "User denied the command. The turn will also be immediately interrupted.", + "enum": [ + "cancel" + ], + "type": "string" + } + ] + }, "CommandExecutionRequestApprovalParams": { "properties": { "approvalId": { 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 4bceb1d98d..a9dc645af9 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 @@ -1418,7 +1418,7 @@ "type": "string" }, { - "description": "User approved the command and future identical commands should run without prompting.", + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", "enum": [ "acceptForSession" ], @@ -1640,50 +1640,6 @@ ], "type": "object" }, - "DynamicToolCallOutputContentItem": { - "oneOf": [ - { - "properties": { - "text": { - "type": "string" - }, - "type": { - "enum": [ - "inputText" - ], - "title": "InputTextDynamicToolCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "title": "InputTextDynamicToolCallOutputContentItem", - "type": "object" - }, - { - "properties": { - "imageUrl": { - "type": "string" - }, - "type": { - "enum": [ - "inputImage" - ], - "title": "InputImageDynamicToolCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "imageUrl", - "type" - ], - "title": "InputImageDynamicToolCallOutputContentItem", - "type": "object" - } - ] - }, "DynamicToolCallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -1716,7 +1672,7 @@ "properties": { "contentItems": { "items": { - "$ref": "#/definitions/DynamicToolCallOutputContentItem" + "$ref": "#/definitions/v2/DynamicToolCallOutputContentItem" }, "type": "array" }, @@ -2863,6 +2819,16 @@ "null" ] }, + "available_decisions": { + "description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.", + "items": { + "$ref": "#/definitions/ReviewDecision" + }, + "type": [ + "array", + "null" + ] + }, "call_id": { "description": "Identifier for the associated command execution item.", "type": "string" @@ -3008,6 +2974,70 @@ "title": "DynamicToolCallRequestEventMsg", "type": "object" }, + { + "properties": { + "arguments": { + "description": "Dynamic tool call arguments." + }, + "call_id": { + "description": "Identifier for the corresponding DynamicToolCallRequest.", + "type": "string" + }, + "content_items": { + "description": "Dynamic tool response content items.", + "items": { + "$ref": "#/definitions/v2/DynamicToolCallOutputContentItem" + }, + "type": "array" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ], + "description": "The duration of the dynamic tool call." + }, + "error": { + "description": "Optional error text when the tool call failed before producing a response.", + "type": [ + "string", + "null" + ] + }, + "success": { + "description": "Whether the tool call succeeded.", + "type": "boolean" + }, + "tool": { + "description": "Dynamic tool name.", + "type": "string" + }, + "turn_id": { + "description": "Turn ID that this dynamic tool call belongs to.", + "type": "string" + }, + "type": { + "enum": [ + "dynamic_tool_call_response" + ], + "title": "DynamicToolCallResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "arguments", + "call_id", + "content_items", + "duration", + "success", + "tool", + "turn_id", + "type" + ], + "title": "DynamicToolCallResponseEventMsg", + "type": "object" + }, { "properties": { "item_id": { @@ -5656,7 +5686,7 @@ "type": "object" }, { - "description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.", + "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.", "enum": [ "approved_for_session" ], @@ -9094,6 +9124,58 @@ "title": "DeprecationNoticeNotification", "type": "object" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "DynamicToolSpec": { "properties": { "description": { @@ -12895,6 +12977,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/v2/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/v2/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 98d8a631e9..c8824f560e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -185,6 +185,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -633,6 +685,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index ee10e470e0..04dbc07389 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -185,6 +185,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -633,6 +685,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 760477095f..61449aa785 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -747,6 +799,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 71f9da2f3b..c1dee7a589 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -345,6 +345,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -1207,6 +1259,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 5907d13cea..e9bce99fbd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 9944457c3e..e487ad0d8d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 7778773a16..b606e34a28 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -345,6 +345,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -1207,6 +1259,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index c4bcdf7092..faea619708 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 6b3d2ebc58..a1de9f9efa 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -345,6 +345,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -1207,6 +1259,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 2fbd9538f3..f8faff0101 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index df3e7beee5..b1be7b2de6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 2c8eec7f90..84bb60c8e8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -747,6 +799,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 5585cd455a..863814e1b4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -747,6 +799,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 4462c314d2..aba5d973ab 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -747,6 +799,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallOutputContentItem.ts b/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallOutputContentItem.ts new file mode 100644 index 0000000000..8f432109d1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallOutputContentItem.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 DynamicToolCallOutputContentItem = { "type": "inputText", text: string, } | { "type": "inputImage", imageUrl: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallResponseEvent.ts b/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallResponseEvent.ts new file mode 100644 index 0000000000..442c0ce6f3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallResponseEvent.ts @@ -0,0 +1,39 @@ +// 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 { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; +import type { JsonValue } from "./serde_json/JsonValue"; + +export type DynamicToolCallResponseEvent = { +/** + * Identifier for the corresponding DynamicToolCallRequest. + */ +call_id: string, +/** + * Turn ID that this dynamic tool call belongs to. + */ +turn_id: string, +/** + * Dynamic tool name. + */ +tool: string, +/** + * Dynamic tool call arguments. + */ +arguments: JsonValue, +/** + * Dynamic tool response content items. + */ +content_items: Array, +/** + * Whether the tool call succeeded. + */ +success: boolean, +/** + * Optional error text when the tool call failed before producing a response. + */ +error: string | null, +/** + * The duration of the dynamic tool call. + */ +duration: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts index cc1009bfd5..f1e528d7db 100644 --- a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts +++ b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts @@ -24,6 +24,7 @@ import type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent"; import type { ContextCompactedEvent } from "./ContextCompactedEvent"; import type { DeprecationNoticeEvent } from "./DeprecationNoticeEvent"; import type { DynamicToolCallRequest } from "./DynamicToolCallRequest"; +import type { DynamicToolCallResponseEvent } from "./DynamicToolCallResponseEvent"; import type { ElicitationRequestEvent } from "./ElicitationRequestEvent"; import type { ErrorEvent } from "./ErrorEvent"; import type { ExecApprovalRequestEvent } from "./ExecApprovalRequestEvent"; @@ -79,4 +80,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": "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; +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": "dynamic_tool_call_response" } & DynamicToolCallResponseEvent | { "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/ExecApprovalRequestEvent.ts b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts index 5144dab6e6..cc13400900 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts @@ -6,6 +6,7 @@ import type { NetworkApprovalContext } from "./NetworkApprovalContext"; import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; import type { ParsedCommand } from "./ParsedCommand"; import type { PermissionProfile } from "./PermissionProfile"; +import type { ReviewDecision } from "./ReviewDecision"; export type ExecApprovalRequestEvent = { /** @@ -51,4 +52,11 @@ proposed_network_policy_amendments?: Array, /** * Optional additional filesystem permissions requested for this command. */ -additional_permissions?: PermissionProfile, parsed_cmd: Array, }; +additional_permissions?: PermissionProfile, +/** + * Ordered list of decisions the client may present for this prompt. + * + * When absent, clients should derive the legacy default set from the + * other fields on this request. + */ +available_decisions?: Array, parsed_cmd: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index cc323e963d..8b1347d026 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -53,7 +53,9 @@ export type { ConversationSummary } from "./ConversationSummary"; export type { CreditsSnapshot } from "./CreditsSnapshot"; export type { CustomPrompt } from "./CustomPrompt"; export type { DeprecationNoticeEvent } from "./DeprecationNoticeEvent"; +export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallRequest } from "./DynamicToolCallRequest"; +export type { DynamicToolCallResponseEvent } from "./DynamicToolCallResponseEvent"; export type { ElicitationRequestEvent } from "./ElicitationRequestEvent"; export type { ErrorEvent } from "./ErrorEvent"; export type { EventMsg } from "./EventMsg"; 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 70cb8e1521..8fb6375e69 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts @@ -3,6 +3,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; import type { CommandAction } from "./CommandAction"; +import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; import type { NetworkApprovalContext } from "./NetworkApprovalContext"; import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; @@ -49,4 +50,8 @@ proposedExecpolicyAmendment?: ExecPolicyAmendment | null, /** * Optional proposed network policy amendments (allow/deny host) for future requests. */ -proposedNetworkPolicyAmendments?: Array | null, }; +proposedNetworkPolicyAmendments?: Array | null, +/** + * Ordered list of decisions the client may present for this prompt. + */ +availableDecisions?: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DynamicToolCallStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DynamicToolCallStatus.ts new file mode 100644 index 0000000000..04f44ec0a8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/DynamicToolCallStatus.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 DynamicToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index dee39cd5db..d937d53b44 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -8,6 +8,8 @@ import type { CollabAgentTool } from "./CollabAgentTool"; import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; import type { CommandAction } from "./CommandAction"; import type { CommandExecutionStatus } from "./CommandExecutionStatus"; +import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; +import type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; import type { FileUpdateChange } from "./FileUpdateChange"; import type { McpToolCallError } from "./McpToolCallError"; import type { McpToolCallResult } from "./McpToolCallResult"; @@ -50,6 +52,10 @@ durationMs: number | null, } | { "type": "fileChange", id: string, changes: Arra /** * The duration of the MCP tool call in milliseconds. */ +durationMs: number | null, } | { "type": "dynamicToolCall", id: string, tool: string, arguments: JsonValue, status: DynamicToolCallStatus, contentItems: Array | null, success: boolean | null, +/** + * The duration of the dynamic tool call in milliseconds. + */ durationMs: number | null, } | { "type": "collabAgentToolCall", /** * Unique identifier for this collab tool call. 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 3f9041dcfe..c7d62cc074 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -61,6 +61,7 @@ export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotificat export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallParams } from "./DynamicToolCallParams"; export type { DynamicToolCallResponse } from "./DynamicToolCallResponse"; +export type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; export type { DynamicToolSpec } from "./DynamicToolSpec"; export type { ErrorNotification } from "./ErrorNotification"; export type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7a918ada09..a1f21e36ab 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1540,6 +1540,7 @@ mod tests { }), proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, + available_decisions: None, }; let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¶ms); assert_eq!( diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index c6d0cc00fe..97ad8d521f 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -3,6 +3,8 @@ use crate::protocol::v2::CollabAgentTool; use crate::protocol::v2::CollabAgentToolCallStatus; use crate::protocol::v2::CommandAction; use crate::protocol::v2::CommandExecutionStatus; +use crate::protocol::v2::DynamicToolCallOutputContentItem; +use crate::protocol::v2::DynamicToolCallStatus; use crate::protocol::v2::FileUpdateChange; use crate::protocol::v2::McpToolCallError; use crate::protocol::v2::McpToolCallResult; @@ -22,6 +24,7 @@ use codex_protocol::protocol::AgentReasoningRawContentEvent; use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::CompactedItem; use codex_protocol::protocol::ContextCompactedEvent; +use codex_protocol::protocol::DynamicToolCallResponseEvent; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandBeginEvent; @@ -125,6 +128,12 @@ impl ThreadHistoryBuilder { EventMsg::ExecCommandEnd(payload) => self.handle_exec_command_end(payload), EventMsg::PatchApplyBegin(payload) => self.handle_patch_apply_begin(payload), EventMsg::PatchApplyEnd(payload) => self.handle_patch_apply_end(payload), + EventMsg::DynamicToolCallRequest(payload) => { + self.handle_dynamic_tool_call_request(payload) + } + EventMsg::DynamicToolCallResponse(payload) => { + self.handle_dynamic_tool_call_response(payload) + } EventMsg::McpToolCallBegin(payload) => self.handle_mcp_tool_call_begin(payload), EventMsg::McpToolCallEnd(payload) => self.handle_mcp_tool_call_end(payload), EventMsg::ViewImageToolCall(payload) => self.handle_view_image_tool_call(payload), @@ -382,6 +391,49 @@ impl ThreadHistoryBuilder { } } + fn handle_dynamic_tool_call_request( + &mut self, + payload: &codex_protocol::dynamic_tools::DynamicToolCallRequest, + ) { + let item = ThreadItem::DynamicToolCall { + id: payload.call_id.clone(), + tool: payload.tool.clone(), + arguments: payload.arguments.clone(), + status: DynamicToolCallStatus::InProgress, + content_items: None, + success: None, + duration_ms: None, + }; + if payload.turn_id.is_empty() { + self.upsert_item_in_current_turn(item); + } else { + self.upsert_item_in_turn_id(&payload.turn_id, item); + } + } + + fn handle_dynamic_tool_call_response(&mut self, payload: &DynamicToolCallResponseEvent) { + let status = if payload.success { + DynamicToolCallStatus::Completed + } else { + DynamicToolCallStatus::Failed + }; + let duration_ms = i64::try_from(payload.duration.as_millis()).ok(); + let item = ThreadItem::DynamicToolCall { + id: payload.call_id.clone(), + tool: payload.tool.clone(), + arguments: payload.arguments.clone(), + status, + content_items: Some(convert_dynamic_tool_content_items(&payload.content_items)), + success: Some(payload.success), + duration_ms, + }; + if payload.turn_id.is_empty() { + self.upsert_item_in_current_turn(item); + } else { + self.upsert_item_in_turn_id(&payload.turn_id, item); + } + } + fn handle_mcp_tool_call_begin(&mut self, payload: &McpToolCallBeginEvent) { let item = ThreadItem::McpToolCall { id: payload.call_id.clone(), @@ -913,6 +965,23 @@ pub fn convert_patch_changes( converted } +fn convert_dynamic_tool_content_items( + items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem], +) -> Vec { + items + .iter() + .cloned() + .map(|item| match item { + codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputText { text } => { + DynamicToolCallOutputContentItem::InputText { text } + } + codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputImage { + image_url, + } => DynamicToolCallOutputContentItem::InputImage { image_url }, + }) + .collect() +} + fn map_patch_change_kind(change: &codex_protocol::protocol::FileChange) -> PatchChangeKind { match change { codex_protocol::protocol::FileChange::Add { .. } => PatchChangeKind::Add, @@ -1002,6 +1071,7 @@ impl From<&PendingTurn> for Turn { mod tests { use super::*; use codex_protocol::ThreadId; + use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::items::UserMessageItem as CoreUserMessageItem; use codex_protocol::models::MessagePhase as CoreMessagePhase; @@ -1012,6 +1082,7 @@ mod tests { use codex_protocol::protocol::AgentReasoningRawContentEvent; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::CompactedItem; + use codex_protocol::protocol::DynamicToolCallResponseEvent; use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::ItemStartedEvent; @@ -1606,6 +1677,65 @@ mod tests { ); } + #[test] + fn reconstructs_dynamic_tool_items_from_request_and_response_events() { + let events = vec![ + EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".into(), + model_context_window: None, + collaboration_mode_kind: Default::default(), + }), + EventMsg::UserMessage(UserMessageEvent { + message: "run dynamic tool".into(), + images: None, + text_elements: Vec::new(), + local_images: Vec::new(), + }), + EventMsg::DynamicToolCallRequest( + codex_protocol::dynamic_tools::DynamicToolCallRequest { + call_id: "dyn-1".into(), + turn_id: "turn-1".into(), + tool: "lookup_ticket".into(), + arguments: serde_json::json!({"id":"ABC-123"}), + }, + ), + EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { + call_id: "dyn-1".into(), + turn_id: "turn-1".into(), + tool: "lookup_ticket".into(), + arguments: serde_json::json!({"id":"ABC-123"}), + content_items: vec![CoreDynamicToolCallOutputContentItem::InputText { + text: "Ticket is open".into(), + }], + success: true, + error: None, + duration: Duration::from_millis(42), + }), + ]; + + let items = events + .into_iter() + .map(RolloutItem::EventMsg) + .collect::>(); + let turns = build_turns_from_rollout_items(&items); + assert_eq!(turns.len(), 1); + assert_eq!(turns[0].items.len(), 2); + assert_eq!( + turns[0].items[1], + ThreadItem::DynamicToolCall { + id: "dyn-1".into(), + tool: "lookup_ticket".into(), + arguments: serde_json::json!({"id":"ABC-123"}), + status: DynamicToolCallStatus::Completed, + content_items: Some(vec![DynamicToolCallOutputContentItem::InputText { + text: "Ticket is open".into(), + }]), + success: Some(true), + duration_ms: Some(42), + } + ); + } + #[test] fn reconstructs_declined_exec_and_patch_items() { let events = vec![ diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 1e4e5d0c31..b031e6864c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -48,6 +48,7 @@ use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess; use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame; use codex_protocol::protocol::RejectConfig as CoreRejectConfig; +use codex_protocol::protocol::ReviewDecision as CoreReviewDecision; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; use codex_protocol::protocol::SkillErrorInfo as CoreSkillErrorInfo; @@ -740,7 +741,8 @@ pub struct ConfigEdit { pub enum CommandExecutionApprovalDecision { /// User approved the command. Accept, - /// User approved the command and future identical commands should run without prompting. + /// User approved the command and future prompts in the same session-scoped + /// approval cache should run without prompting. AcceptForSession, /// User approved the command, and wants to apply the proposed execpolicy amendment so future /// matching commands can run without prompting. @@ -757,6 +759,27 @@ pub enum CommandExecutionApprovalDecision { Cancel, } +impl From for CommandExecutionApprovalDecision { + fn from(value: CoreReviewDecision) -> Self { + match value { + CoreReviewDecision::Approved => Self::Accept, + CoreReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment, + } => Self::AcceptWithExecpolicyAmendment { + execpolicy_amendment: proposed_execpolicy_amendment.into(), + }, + CoreReviewDecision::ApprovedForSession => Self::AcceptForSession, + CoreReviewDecision::NetworkPolicyAmendment { + network_policy_amendment, + } => Self::ApplyNetworkPolicyAmendment { + network_policy_amendment: network_policy_amendment.into(), + }, + CoreReviewDecision::Denied => Self::Decline, + CoreReviewDecision::Abort => Self::Cancel, + } + } +} + v2_enum_from_core! { pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol { Http, @@ -3057,6 +3080,19 @@ pub enum ThreadItem { }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] + DynamicToolCall { + id: String, + tool: String, + arguments: JsonValue, + status: DynamicToolCallStatus, + content_items: Option>, + success: Option, + /// The duration of the dynamic tool call in milliseconds. + #[ts(type = "number | null")] + duration_ms: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] CollabAgentToolCall { /// Unique identifier for this collab tool call. id: String, @@ -3105,6 +3141,7 @@ impl ThreadItem { | ThreadItem::CommandExecution { id, .. } | ThreadItem::FileChange { id, .. } | ThreadItem::McpToolCall { id, .. } + | ThreadItem::DynamicToolCall { id, .. } | ThreadItem::CollabAgentToolCall { id, .. } | ThreadItem::WebSearch { id, .. } | ThreadItem::ImageView { id, .. } @@ -3285,6 +3322,15 @@ pub enum McpToolCallStatus { Failed, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum DynamicToolCallStatus { + InProgress, + Completed, + Failed, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3724,6 +3770,11 @@ pub struct CommandExecutionRequestApprovalParams { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub proposed_network_policy_amendments: Option>, + /// Ordered list of decisions the client may present for this prompt. + #[experimental("item/commandExecution/requestApproval.availableDecisions")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub available_decisions: Option>, } impl CommandExecutionRequestApprovalParams { diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index a9225b0426..c4f0eb3a03 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1540,6 +1540,7 @@ impl CodexClient { additional_permissions, proposed_execpolicy_amendment, proposed_network_policy_amendments, + available_decisions, } = params; println!( @@ -1554,6 +1555,9 @@ impl CodexClient { if let Some(network_approval_context) = network_approval_context.as_ref() { println!("< network approval context: {network_approval_context:?}"); } + if let Some(available_decisions) = available_decisions.as_ref() { + println!("< available decisions: {available_decisions:?}"); + } if let Some(command) = command.as_deref() { println!("< command: {command}"); } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 08b7c8df0d..9391bad9ae 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -689,7 +689,7 @@ Certain actions (shell commands or modifying files) may require explicit user ap Order of messages: 1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action. -2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. +2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted. 3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`. 4. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result. @@ -724,6 +724,13 @@ When a dynamic tool is invoked during a turn, the server sends an `item/tool/cal } ``` +The server also emits item lifecycle notifications around the request: + +1. `item/started` with `item.type = "dynamicToolCall"`, `status = "inProgress"`, plus `tool` and `arguments`. +2. `item/tool/call` request. +3. Client response. +4. `item/completed` with `item.type = "dynamicToolCall"`, final `status`, and the returned `contentItems`/`success`. + The client must respond with content items. Use `inputText` for text and `inputImage` for image URLs/data URLs: ```json diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 56e55823c8..5bd0e16ba9 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -27,7 +27,9 @@ use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; use codex_app_server_protocol::CommandExecutionStatus; use codex_app_server_protocol::ContextCompactedNotification; use codex_app_server_protocol::DeprecationNoticeNotification; +use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::DynamicToolCallParams; +use codex_app_server_protocol::DynamicToolCallStatus; use codex_app_server_protocol::ErrorNotification; use codex_app_server_protocol::ExecCommandApprovalParams; use codex_app_server_protocol::ExecCommandApprovalResponse; @@ -334,6 +336,11 @@ pub(crate) async fn apply_bespoke_event_handling( .note_permission_requested(&conversation_id.to_string()) .await; let approval_id_for_op = ev.effective_approval_id(); + let available_decisions = ev + .effective_available_decisions() + .into_iter() + .map(CommandExecutionApprovalDecision::from) + .collect::>(); let ExecApprovalRequestEvent { call_id, approval_id, @@ -430,6 +437,7 @@ pub(crate) async fn apply_bespoke_event_handling( additional_permissions, proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2, proposed_network_policy_amendments: proposed_network_policy_amendments_v2, + available_decisions: Some(available_decisions), }; let rx = outgoing .send_request(ServerRequestPayload::CommandExecutionRequestApproval( @@ -550,12 +558,32 @@ pub(crate) async fn apply_bespoke_event_handling( EventMsg::DynamicToolCallRequest(request) => { if matches!(api_version, ApiVersion::V2) { let call_id = request.call_id; + let turn_id = request.turn_id; + let tool = request.tool; + let arguments = request.arguments; + let item = ThreadItem::DynamicToolCall { + id: call_id.clone(), + tool: tool.clone(), + arguments: arguments.clone(), + status: DynamicToolCallStatus::InProgress, + content_items: None, + success: None, + duration_ms: None, + }; + let notification = ItemStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemStarted(notification)) + .await; let params = DynamicToolCallParams { thread_id: conversation_id.to_string(), - turn_id: request.turn_id, + turn_id: turn_id.clone(), call_id: call_id.clone(), - tool: request.tool, - arguments: request.arguments, + tool: tool.clone(), + arguments: arguments.clone(), }; let rx = outgoing .send_request(ServerRequestPayload::DynamicToolCall(params)) @@ -582,6 +610,46 @@ pub(crate) async fn apply_bespoke_event_handling( .await; } } + EventMsg::DynamicToolCallResponse(response) => { + if matches!(api_version, ApiVersion::V2) { + let status = if response.success { + DynamicToolCallStatus::Completed + } else { + DynamicToolCallStatus::Failed + }; + let duration_ms = i64::try_from(response.duration.as_millis()).ok(); + let item = ThreadItem::DynamicToolCall { + id: response.call_id, + tool: response.tool, + arguments: response.arguments, + status, + content_items: Some( + response + .content_items + .into_iter() + .map(|item| match item { + CoreDynamicToolCallOutputContentItem::InputText { text } => { + DynamicToolCallOutputContentItem::InputText { text } + } + CoreDynamicToolCallOutputContentItem::InputImage { image_url } => { + DynamicToolCallOutputContentItem::InputImage { image_url } + } + }) + .collect(), + ), + success: Some(response.success), + duration_ms, + }; + let notification = ItemCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: response.turn_id, + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemCompleted(notification)) + .await; + } + } // TODO(celia): properly construct McpToolCall TurnItem in core. EventMsg::McpToolCallBegin(begin_event) => { let notification = construct_mcp_tool_call_notification( diff --git a/codex-rs/app-server/src/dynamic_tools.rs b/codex-rs/app-server/src/dynamic_tools.rs index 31374d80b6..522bcfdcac 100644 --- a/codex-rs/app-server/src/dynamic_tools.rs +++ b/codex-rs/app-server/src/dynamic_tools.rs @@ -1,3 +1,4 @@ +use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::DynamicToolCallResponse; use codex_core::CodexThread; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; @@ -15,65 +16,23 @@ pub(crate) async fn on_call_response( conversation: Arc, ) { let response = receiver.await; - let value = match response { - Ok(Ok(value)) => value, + let (response, _error) = match response { + Ok(Ok(value)) => decode_response(value), Ok(Err(err)) => { error!("request failed with client error: {err:?}"); - let fallback = CoreDynamicToolResponse { - content_items: vec![CoreDynamicToolCallOutputContentItem::InputText { - text: "dynamic tool request failed".to_string(), - }], - success: false, - }; - if let Err(err) = conversation - .submit(Op::DynamicToolResponse { - id: call_id.clone(), - response: fallback, - }) - .await - { - error!("failed to submit DynamicToolResponse: {err}"); - } - return; + fallback_response("dynamic tool request failed") } Err(err) => { error!("request failed: {err:?}"); - let fallback = CoreDynamicToolResponse { - content_items: vec![CoreDynamicToolCallOutputContentItem::InputText { - text: "dynamic tool request failed".to_string(), - }], - success: false, - }; - if let Err(err) = conversation - .submit(Op::DynamicToolResponse { - id: call_id.clone(), - response: fallback, - }) - .await - { - error!("failed to submit DynamicToolResponse: {err}"); - } - return; + fallback_response("dynamic tool request failed") } }; - let response = serde_json::from_value::(value).unwrap_or_else(|err| { - error!("failed to deserialize DynamicToolCallResponse: {err}"); - DynamicToolCallResponse { - content_items: vec![ - codex_app_server_protocol::DynamicToolCallOutputContentItem::InputText { - text: "dynamic tool response was invalid".to_string(), - }, - ], - success: false, - } - }); - let DynamicToolCallResponse { content_items, success, - } = response; - let response = CoreDynamicToolResponse { + } = response.clone(); + let core_response = CoreDynamicToolResponse { content_items: content_items .into_iter() .map(CoreDynamicToolCallOutputContentItem::from) @@ -82,11 +41,33 @@ pub(crate) async fn on_call_response( }; if let Err(err) = conversation .submit(Op::DynamicToolResponse { - id: call_id, - response, + id: call_id.clone(), + response: core_response, }) .await { error!("failed to submit DynamicToolResponse: {err}"); } } + +fn decode_response(value: serde_json::Value) -> (DynamicToolCallResponse, Option) { + match serde_json::from_value::(value) { + Ok(response) => (response, None), + Err(err) => { + error!("failed to deserialize DynamicToolCallResponse: {err}"); + fallback_response("dynamic tool response was invalid") + } + } +} + +fn fallback_response(message: &str) -> (DynamicToolCallResponse, Option) { + ( + DynamicToolCallResponse { + content_items: vec![DynamicToolCallOutputContentItem::InputText { + text: message.to_string(), + }], + success: false, + }, + Some(message.to_string()), + ) +} diff --git a/codex-rs/app-server/src/transport.rs b/codex-rs/app-server/src/transport.rs index 8c36df9822..6ac3e0d466 100644 --- a/codex-rs/app-server/src/transport.rs +++ b/codex-rs/app-server/src/transport.rs @@ -986,6 +986,7 @@ mod tests { ), proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, + available_decisions: None, }, }), }, @@ -1047,6 +1048,7 @@ mod tests { ), proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, + available_decisions: None, }, }), }, diff --git a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs index f4cd85a61d..52a597f66b 100644 --- a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs +++ b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs @@ -7,10 +7,15 @@ use app_test_support::to_response; use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::DynamicToolCallParams; use codex_app_server_protocol::DynamicToolCallResponse; +use codex_app_server_protocol::DynamicToolCallStatus; use codex_app_server_protocol::DynamicToolSpec; +use codex_app_server_protocol::ItemCompletedNotification; +use codex_app_server_protocol::ItemStartedNotification; +use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -163,11 +168,12 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res ) .await??; let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + let thread_id = thread.id.clone(); // Start a turn so the tool call is emitted. let turn_req = mcp .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), + thread_id: thread_id.clone(), input: vec![V2UserInput::Text { text: "Run the tool".to_string(), text_elements: Vec::new(), @@ -181,6 +187,30 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res ) .await??; let TurnStartResponse { turn } = to_response::(turn_resp)?; + let turn_id = turn.id.clone(); + + let started = wait_for_dynamic_tool_started(&mut mcp, call_id).await?; + assert_eq!(started.thread_id, thread_id); + assert_eq!(started.turn_id, turn_id.clone()); + let ThreadItem::DynamicToolCall { + id, + tool, + arguments, + status, + content_items, + success, + duration_ms, + } = started.item + else { + panic!("expected dynamic tool call item"); + }; + assert_eq!(id, call_id); + assert_eq!(tool, tool_name); + assert_eq!(arguments, tool_args); + assert_eq!(status, DynamicToolCallStatus::InProgress); + assert_eq!(content_items, None); + assert_eq!(success, None); + assert_eq!(duration_ms, None); // Read the tool call request from the app server. let request = timeout( @@ -194,8 +224,8 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res }; let expected = DynamicToolCallParams { - thread_id: thread.id, - turn_id: turn.id, + thread_id: thread_id.clone(), + turn_id: turn_id.clone(), call_id: call_id.to_string(), tool: tool_name.to_string(), arguments: tool_args.clone(), @@ -212,6 +242,34 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res mcp.send_response(request_id, serde_json::to_value(response)?) .await?; + let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?; + assert_eq!(completed.thread_id, thread_id); + assert_eq!(completed.turn_id, turn_id); + let ThreadItem::DynamicToolCall { + id, + tool, + arguments, + status, + content_items, + success, + duration_ms, + } = completed.item + else { + panic!("expected dynamic tool call item"); + }; + assert_eq!(id, call_id); + assert_eq!(tool, tool_name); + assert_eq!(arguments, tool_args); + assert_eq!(status, DynamicToolCallStatus::Completed); + assert_eq!( + content_items, + Some(vec![DynamicToolCallOutputContentItem::InputText { + text: "dynamic-ok".to_string(), + }]) + ); + assert_eq!(success, Some(true)); + assert!(duration_ms.is_some()); + timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_notification_message("turn/completed"), @@ -282,10 +340,11 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( ) .await??; let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + let thread_id = thread.id.clone(); let turn_req = mcp .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), + thread_id: thread_id.clone(), input: vec![V2UserInput::Text { text: "Run the tool".to_string(), text_elements: Vec::new(), @@ -299,6 +358,11 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( ) .await??; let TurnStartResponse { turn } = to_response::(turn_resp)?; + let turn_id = turn.id.clone(); + + let started = wait_for_dynamic_tool_started(&mut mcp, call_id).await?; + assert_eq!(started.thread_id, thread_id.clone()); + assert_eq!(started.turn_id, turn_id.clone()); let request = timeout( DEFAULT_READ_TIMEOUT, @@ -311,8 +375,8 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( }; let expected = DynamicToolCallParams { - thread_id: thread.id, - turn_id: turn.id, + thread_id, + turn_id: turn_id.clone(), call_id: call_id.to_string(), tool: tool_name.to_string(), arguments: tool_args, @@ -346,6 +410,32 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( mcp.send_response(request_id, serde_json::to_value(response)?) .await?; + let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?; + assert_eq!(completed.thread_id, expected.thread_id.clone()); + assert_eq!(completed.turn_id, turn_id); + let ThreadItem::DynamicToolCall { + status, + content_items: completed_content_items, + success, + .. + } = completed.item + else { + panic!("expected dynamic tool call item"); + }; + assert_eq!(status, DynamicToolCallStatus::Completed); + assert_eq!( + completed_content_items, + Some(vec![ + DynamicToolCallOutputContentItem::InputText { + text: "dynamic-ok".to_string(), + }, + DynamicToolCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,AAA".to_string(), + }, + ]) + ); + assert_eq!(success, Some(true)); + timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_notification_message("turn/completed"), @@ -432,6 +522,46 @@ fn function_call_output_raw_output(body: &Value, call_id: &str) -> Option .cloned() } +async fn wait_for_dynamic_tool_started( + mcp: &mut McpProcess, + call_id: &str, +) -> Result { + loop { + let notification: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("item/started"), + ) + .await??; + let Some(params) = notification.params else { + continue; + }; + let started: ItemStartedNotification = serde_json::from_value(params)?; + if matches!(&started.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) { + return Ok(started); + } + } +} + +async fn wait_for_dynamic_tool_completed( + mcp: &mut McpProcess, + call_id: &str, +) -> Result { + loop { + let notification: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("item/completed"), + ) + .await??; + let Some(params) = notification.params else { + continue; + }; + let completed: ItemCompletedNotification = serde_json::from_value(params)?; + if matches!(&completed.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) { + return Ok(completed); + } + } +} + fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); std::fs::write( diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 74a2f15c19..25de3d33ea 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -315,7 +315,10 @@ impl ModelClient { instructions: &instructions, }; - let extra_headers = self.build_subagent_headers(); + let mut extra_headers = self.build_subagent_headers(); + extra_headers.extend(build_conversation_headers(Some( + self.state.conversation_id.to_string(), + ))); client .compact_input(&payload, extra_headers) .await diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 5a555d6a72..35aee5bb6a 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1379,7 +1379,7 @@ impl Session { otel_manager, models_manager: Arc::clone(&models_manager), tool_approvals: Mutex::new(ApprovalStore::default()), - execve_session_approvals: RwLock::new(HashSet::new()), + execve_session_approvals: RwLock::new(HashMap::new()), skills_manager, file_watcher, agent_control, @@ -2604,6 +2604,35 @@ impl Session { network_approval_context: Option, proposed_execpolicy_amendment: Option, additional_permissions: Option, + ) -> ReviewDecision { + self.request_command_approval_with_options( + turn_context, + call_id, + approval_id, + command, + cwd, + reason, + network_approval_context, + proposed_execpolicy_amendment, + additional_permissions, + None, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + pub async fn request_command_approval_with_options( + &self, + turn_context: &TurnContext, + call_id: String, + approval_id: Option, + command: Vec, + cwd: PathBuf, + reason: Option, + network_approval_context: Option, + proposed_execpolicy_amendment: Option, + additional_permissions: Option, + available_decisions: Option>, ) -> ReviewDecision { // command-level approvals use `call_id`. // `approval_id` is only present for subcommand callbacks (execve intercept) @@ -2637,6 +2666,14 @@ impl Session { }, ] }); + let available_decisions = available_decisions.unwrap_or_else(|| { + ExecApprovalRequestEvent::default_available_decisions( + network_approval_context.as_ref(), + proposed_execpolicy_amendment.as_ref(), + proposed_network_policy_amendments.as_deref(), + additional_permissions.as_ref(), + ) + }); let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { call_id, approval_id, @@ -2648,6 +2685,7 @@ impl Session { proposed_execpolicy_amendment, proposed_network_policy_amendments, additional_permissions, + available_decisions: Some(available_decisions), parsed_cmd, }); self.send_event(turn_context, event).await; @@ -8412,7 +8450,7 @@ mod tests { otel_manager: otel_manager.clone(), models_manager: Arc::clone(&models_manager), tool_approvals: Mutex::new(ApprovalStore::default()), - execve_session_approvals: RwLock::new(HashSet::new()), + execve_session_approvals: RwLock::new(HashMap::new()), skills_manager, file_watcher, agent_control, @@ -8570,7 +8608,7 @@ mod tests { otel_manager: otel_manager.clone(), models_manager: Arc::clone(&models_manager), tool_approvals: Mutex::new(ApprovalStore::default()), - execve_session_approvals: RwLock::new(HashSet::new()), + execve_session_approvals: RwLock::new(HashMap::new()), skills_manager, file_watcher, agent_control, diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index c1fb31ebd1..308899d9c3 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -322,10 +322,11 @@ async fn handle_exec_approval( network_approval_context, proposed_execpolicy_amendment, additional_permissions, + available_decisions, .. } = event; // Race approval with cancellation and timeout to avoid hangs. - let approval_fut = parent_session.request_command_approval( + let approval_fut = parent_session.request_command_approval_with_options( parent_ctx, call_id, approval_id, @@ -335,6 +336,7 @@ async fn handle_exec_approval( network_approval_context, proposed_execpolicy_amendment, additional_permissions, + available_decisions, ); let decision = await_approval_with_cancel( approval_fut, diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 47a4354e2d..1b858aedcf 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -29,6 +29,7 @@ use serde_json::Value; use std::sync::Arc; use tokio::sync::Mutex; use tokio::task::JoinHandle; +use tracing::debug; use tracing::error; use tracing::warn; @@ -217,6 +218,7 @@ pub(crate) async fn handle_start( _ => None, }; if let Some(text) = maybe_routed_text { + debug!(text = %text, "[realtime-text] realtime conversation text output"); let sess_for_routed_text = Arc::clone(&sess_clone); sess_for_routed_text.route_realtime_text_input(text).await; } @@ -279,6 +281,8 @@ pub(crate) async fn handle_text( sub_id: String, params: ConversationTextParams, ) { + debug!(text = %params.text, "[realtime-text] appending realtime conversation text input"); + if let Err(err) = sess.conversation.text_in(params.text).await { send_conversation_error(sess, sub_id, err.to_string(), CodexErrorInfo::BadRequest).await; } diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 8232330232..a1e77ba4fc 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -120,7 +120,9 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::CollabAgentInteractionEnd(_) | EventMsg::CollabWaitingEnd(_) | EventMsg::CollabCloseEnd(_) - | EventMsg::CollabResumeEnd(_) => Some(EventPersistenceMode::Extended), + | EventMsg::CollabResumeEnd(_) + | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) => Some(EventPersistenceMode::Extended), EventMsg::Warning(_) | EventMsg::RealtimeConversationStarted(_) | EventMsg::RealtimeConversationRealtime(_) @@ -141,7 +143,6 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::ExecApprovalRequest(_) | EventMsg::SkillRequestApproval(_) | EventMsg::RequestUserInput(_) - | EventMsg::DynamicToolCallRequest(_) | EventMsg::ElicitationRequest(_) | EventMsg::ApplyPatchApprovalRequest(_) | EventMsg::BackgroundEvent(_) diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index 0ec03b139e..f501551f95 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::HashMap; use std::sync::Arc; use crate::AuthManager; @@ -14,6 +14,7 @@ use crate::models_manager::manager::ModelsManager; use crate::skills::SkillsManager; use crate::state_db::StateDbHandle; use crate::tools::network_approval::NetworkApprovalService; +use crate::tools::runtimes::ExecveSessionApproval; use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; use codex_hooks::Hooks; @@ -45,7 +46,7 @@ pub(crate) struct SessionServices { pub(crate) otel_manager: OtelManager, pub(crate) tool_approvals: Mutex, #[cfg_attr(not(unix), allow(dead_code))] - pub(crate) execve_session_approvals: RwLock>, + pub(crate) execve_session_approvals: RwLock>, pub(crate) skills_manager: Arc, pub(crate) file_watcher: Arc, pub(crate) agent_control: AgentControl, diff --git a/codex-rs/core/src/tools/handlers/dynamic.rs b/codex-rs/core/src/tools/handlers/dynamic.rs index 51dc8db56f..23c8474b9f 100644 --- a/codex-rs/core/src/tools/handlers/dynamic.rs +++ b/codex-rs/core/src/tools/handlers/dynamic.rs @@ -12,8 +12,10 @@ use codex_protocol::dynamic_tools::DynamicToolCallRequest; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; +use codex_protocol::protocol::DynamicToolCallResponseEvent; use codex_protocol::protocol::EventMsg; use serde_json::Value; +use std::time::Instant; use tokio::sync::oneshot; use tracing::warn; @@ -81,7 +83,7 @@ async fn request_dynamic_tool( tool: String, arguments: Value, ) -> Option { - let _sub_id = turn_context.sub_id.clone(); + let turn_id = turn_context.sub_id.clone(); let (tx_response, rx_response) = oneshot::channel(); let event_id = call_id.clone(); let prev_entry = { @@ -98,12 +100,39 @@ async fn request_dynamic_tool( warn!("Overwriting existing pending dynamic tool call for call_id: {event_id}"); } + let started_at = Instant::now(); let event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest { - call_id, - turn_id: turn_context.sub_id.clone(), - tool, - arguments, + call_id: call_id.clone(), + turn_id: turn_id.clone(), + tool: tool.clone(), + arguments: arguments.clone(), }); session.send_event(turn_context, event).await; - rx_response.await.ok() + let response = rx_response.await.ok(); + + let response_event = match &response { + Some(response) => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { + call_id, + turn_id, + tool, + arguments, + content_items: response.content_items.clone(), + success: response.success, + error: None, + duration: started_at.elapsed(), + }), + None => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { + call_id, + turn_id, + tool, + arguments, + content_items: Vec::new(), + success: false, + error: Some("dynamic tool call was cancelled before receiving a response".to_string()), + duration: started_at.elapsed(), + }), + }; + session.send_event(turn_context, response_event).await; + + response } diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 937ab341d8..6fbc695322 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -9,6 +9,7 @@ use crate::path_utils; use crate::sandboxing::CommandSpec; use crate::sandboxing::SandboxPermissions; use crate::shell::Shell; +use crate::skills::SkillMetadata; use crate::tools::sandboxing::ToolError; use codex_protocol::models::PermissionProfile; use std::collections::HashMap; @@ -18,6 +19,13 @@ pub mod apply_patch; pub mod shell; pub mod unified_exec; +#[derive(Debug, Clone)] +pub(crate) struct ExecveSessionApproval { + /// If this exeve session approval is associated with a skill script, this + /// field contains metadata about the skill. + pub skill: Option, +} + /// Shared helper to construct a CommandSpec from a tokenized command line. /// Validates that at least a program is present. pub(crate) fn build_command_spec( 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 83f4fa32ac..a3ae5e32f1 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -10,6 +10,7 @@ use crate::sandboxing::SandboxPermissions; use crate::shell::ShellType; use crate::skills::SkillMetadata; use crate::tools::runtimes::build_command_spec; +use crate::tools::runtimes::ExecveSessionApproval; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; @@ -164,7 +165,9 @@ struct CoreShellActionProvider { } enum DecisionSource { - SkillScript, + SkillScript { + skill: SkillMetadata, + }, PrefixRule, /// Often, this is `is_safe_command()`. UnmatchedCommandFallback, @@ -263,13 +266,7 @@ impl CoreShellActionProvider { } } else { match self - .prompt( - program, - argv, - workdir, - &self.stopwatch, - additional_permissions, - ) + .prompt(program, argv, workdir, &self.stopwatch, additional_permissions) .await? { ReviewDecision::Approved @@ -284,13 +281,21 @@ impl CoreShellActionProvider { // Currently, we only add session approvals for // skill scripts because we are storing only the // `program` whereas prefix rules may be restricted by a longer prefix. - if matches!(decision_source, DecisionSource::SkillScript) { + if let DecisionSource::SkillScript { skill } = decision_source { + tracing::debug!( + "Adding session approval for {program:?} due to user approval of skill script {skill:?}" + ); self.session .services .execve_session_approvals .write() .await - .insert(program.clone()); + .insert( + program.clone(), + ExecveSessionApproval { + skill: Some(skill.clone()), + }, + ); } if needs_escalation { @@ -351,37 +356,61 @@ impl EscalationPolicy for CoreShellActionProvider { // In the usual case, the execve wrapper reports the command being // executed in `program`, so a direct skill lookup is sufficient. + let approval = { + self.session + .services + .execve_session_approvals + .read() + .await + .get(program) + .cloned() + }; + if let Some(approval) = approval { + tracing::debug!( + "Found session approval for {program:?}, allowing execution without further checks" + ); + let needs_escalation = true; + let (permissions, decision_source) = if let Some(skill) = approval.skill.as_ref() { + ( + skill.permission_profile.clone(), + DecisionSource::SkillScript { + skill: skill.clone(), + }, + ) + } else { + (None, DecisionSource::UnmatchedCommandFallback) + }; + return self + .process_decision( + Decision::Allow, + needs_escalation, + program, + argv, + workdir, + permissions, + decision_source, + ) + .await; + } + if let Some(skill) = self.find_skill(program).await { // For now, we always prompt for scripts that look like they belong // to skills, which means we ignore exec policy rules for those // scripts. tracing::debug!("Matched {program:?} to skill {skill:?}, prompting for approval"); - // TODO(mbolin): We should read the permissions associated with the - // skill and use those specific permissions in the - // EscalateAction::Run case, rather than always escalating when a - // skill matches. let needs_escalation = true; - let is_approved_for_session = self - .session - .services - .execve_session_approvals - .read() - .await - .contains(program); - let decision = if is_approved_for_session { - Decision::Allow - } else { - Decision::Prompt + let decision_source = DecisionSource::SkillScript { + skill: skill.clone(), }; return self .process_decision( - decision, + Decision::Prompt, needs_escalation, program, argv, workdir, skill.permission_profile.clone(), - DecisionSource::SkillScript, + decision_source, ) .await; } diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 1d450f9b11..573f38930b 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -77,6 +77,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { ) .await?; let codex = harness.test().codex.clone(); + let session_id = harness.test().session_configured.session_id.to_string(); let responses_mock = responses::mount_sse_sequence( harness.server(), @@ -137,6 +138,10 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { compact_request.header("authorization").as_deref(), Some("Bearer Access Token") ); + assert_eq!( + compact_request.header("session_id").as_deref(), + Some(session_id.as_str()) + ); let compact_body = compact_request.body_json(); assert_eq!( compact_body.get("model").and_then(|v| v.as_str()), @@ -195,6 +200,7 @@ async fn remote_compact_runs_automatically() -> Result<()> { ) .await?; let codex = harness.test().codex.clone(); + let session_id = harness.test().session_configured.session_id.to_string(); mount_sse_once( harness.server(), @@ -237,6 +243,13 @@ async fn remote_compact_runs_automatically() -> Result<()> { wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; assert!(message); assert_eq!(compact_mock.requests().len(), 1); + assert_eq!( + compact_mock + .single_request() + .header("session_id") + .as_deref(), + Some(session_id.as_str()) + ); let follow_up_request = responses_mock.single_request(); let follow_up_body = follow_up_request.body_json().to_string(); assert!(follow_up_body.contains("REMOTE_COMPACTED_SUMMARY")); 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 6e679052e0..eac514a8f1 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -854,6 +854,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { | EventMsg::RealtimeConversationRealtime(_) | EventMsg::RealtimeConversationClosed(_) | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) | EventMsg::SkillRequestApproval(_) => {} } CodexStatus::Running @@ -925,6 +926,7 @@ impl EventProcessorWithHumanOutput { | EventMsg::ThreadRolledBack(_) | EventMsg::RequestUserInput(_) | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) ) } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index cd577ca647..fb8b5e9976 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -696,6 +696,7 @@ fn should_suppress_agent_job_event(msg: &EventMsg) -> bool { | EventMsg::ApplyPatchApprovalRequest(_) | EventMsg::RequestUserInput(_) | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) | EventMsg::ElicitationRequest(_) | EventMsg::Error(_) | EventMsg::Warning(_) diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index eff8d8e13e..19fc41a089 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -227,6 +227,7 @@ async fn run_codex_tool_session_inner( parsed_cmd, network_approval_context: _, additional_permissions: _, + available_decisions: _, } = ev; handle_exec_approval_request( command, @@ -363,6 +364,7 @@ async fn run_codex_tool_session_inner( | EventMsg::ExitedReviewMode(_) | EventMsg::RequestUserInput(_) | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) | EventMsg::SkillRequestApproval(_) | EventMsg::ContextCompacted(_) | EventMsg::ModelReroute(_) diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index f742dc6328..49c0186e78 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -5,6 +5,7 @@ use crate::mcp::RequestId; use crate::models::PermissionProfile; use crate::parse_command::ParsedCommand; use crate::protocol::FileChange; +use crate::protocol::ReviewDecision; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -107,6 +108,13 @@ pub struct ExecApprovalRequestEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub additional_permissions: Option, + /// Ordered list of decisions the client may present for this prompt. + /// + /// When absent, clients should derive the legacy default set from the + /// other fields on this request. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub available_decisions: Option>, pub parsed_cmd: Vec, } @@ -116,6 +124,52 @@ impl ExecApprovalRequestEvent { .clone() .unwrap_or_else(|| self.call_id.clone()) } + + pub fn default_available_decisions( + network_approval_context: Option<&NetworkApprovalContext>, + proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>, + proposed_network_policy_amendments: Option<&[NetworkPolicyAmendment]>, + additional_permissions: Option<&PermissionProfile>, + ) -> Vec { + if network_approval_context.is_some() { + let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession]; + if let Some(amendment) = proposed_network_policy_amendments.and_then(|amendments| { + amendments + .iter() + .find(|amendment| amendment.action == NetworkPolicyRuleAction::Allow) + }) { + decisions.push(ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: amendment.clone(), + }); + } + decisions.push(ReviewDecision::Abort); + return decisions; + } + + if additional_permissions.is_some() { + return vec![ReviewDecision::Approved, ReviewDecision::Abort]; + } + + let mut decisions = vec![ReviewDecision::Approved]; + if let Some(prefix) = proposed_execpolicy_amendment { + decisions.push(ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment: prefix.clone(), + }); + } + decisions.push(ReviewDecision::Abort); + decisions + } + + pub fn effective_available_decisions(&self) -> Vec { + self.available_decisions.clone().unwrap_or_else(|| { + Self::default_available_decisions( + self.network_approval_context.as_ref(), + self.proposed_execpolicy_amendment.as_ref(), + self.proposed_network_policy_amendments.as_deref(), + self.additional_permissions.as_ref(), + ) + }) + } } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index a8770cc90a..214899fb97 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -20,6 +20,7 @@ use crate::config_types::Personality; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::config_types::WindowsSandboxLevel; use crate::custom_prompts::CustomPrompt; +use crate::dynamic_tools::DynamicToolCallOutputContentItem; use crate::dynamic_tools::DynamicToolCallRequest; use crate::dynamic_tools::DynamicToolResponse; use crate::dynamic_tools::DynamicToolSpec; @@ -1053,6 +1054,8 @@ pub enum EventMsg { DynamicToolCallRequest(DynamicToolCallRequest), + DynamicToolCallResponse(DynamicToolCallResponseEvent), + SkillRequestApproval(SkillRequestApprovalEvent), ElicitationRequest(ElicitationRequestEvent), @@ -1781,6 +1784,27 @@ pub struct McpToolCallEndEvent { pub result: Result, } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)] +pub struct DynamicToolCallResponseEvent { + /// Identifier for the corresponding DynamicToolCallRequest. + pub call_id: String, + /// Turn ID that this dynamic tool call belongs to. + pub turn_id: String, + /// Dynamic tool name. + pub tool: String, + /// Dynamic tool call arguments. + pub arguments: serde_json::Value, + /// Dynamic tool response content items. + pub content_items: Vec, + /// Whether the tool call succeeded. + pub success: bool, + /// Optional error text when the tool call failed before producing a response. + pub error: Option, + /// The duration of the dynamic tool call. + #[ts(type = "string")] + pub duration: Duration, +} + impl McpToolCallEndEvent { pub fn is_success(&self) -> bool { match &self.result { @@ -2765,8 +2789,8 @@ pub enum ReviewDecision { proposed_execpolicy_amendment: ExecPolicyAmendment, }, - /// User has approved this command and wants to automatically approve any - /// future identical instances (`command` and `cwd` match exactly) for the + /// User has approved this request and wants future prompts in the same + /// session-scoped approval cache to be automatically approved for the /// remainder of the session. ApprovedForSession, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index dd746ad24a..bd742997d7 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -3563,6 +3563,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: 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 3d30914781..d82d014827 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, + available_decisions: None, parsed_cmd: Vec::new(), }, ), @@ -544,6 +545,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: Vec::new(), }, ), @@ -622,6 +624,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: Vec::new(), }, ), diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index fd58baef46..7466cdb2fc 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -46,6 +46,7 @@ pub(crate) enum ApprovalRequest { id: String, command: Vec, reason: Option, + available_decisions: Vec, network_approval_context: Option, proposed_execpolicy_amendment: Option, proposed_network_policy_amendments: Option>, @@ -115,15 +116,13 @@ impl ApprovalOverlay { ) -> (Vec, SelectionViewParams) { let (options, title) = match &variant { ApprovalVariant::Exec { + available_decisions, network_approval_context, - proposed_execpolicy_amendment, - proposed_network_policy_amendments, additional_permissions, .. } => ( exec_options( - proposed_execpolicy_amendment.clone(), - proposed_network_policy_amendments.clone(), + available_decisions, network_approval_context.as_ref(), additional_permissions.as_ref(), ), @@ -365,6 +364,7 @@ impl From for ApprovalRequestState { id, command, reason, + available_decisions, network_approval_context, proposed_execpolicy_amendment, proposed_network_policy_amendments, @@ -397,6 +397,7 @@ impl From for ApprovalRequestState { variant: ApprovalVariant::Exec { id, command, + available_decisions, network_approval_context, proposed_execpolicy_amendment, proposed_network_policy_amendments, @@ -451,11 +452,16 @@ impl From for ApprovalRequestState { } #[derive(Clone)] +#[allow(clippy::large_enum_variant)] +#[allow(dead_code)] enum ApprovalVariant { Exec { id: String, command: Vec, + available_decisions: Vec, + #[allow(dead_code)] network_approval_context: Option, + #[allow(dead_code)] proposed_execpolicy_amendment: Option, proposed_network_policy_amendments: Option>, additional_permissions: Option, @@ -492,100 +498,93 @@ impl ApprovalOption { } fn exec_options( - proposed_execpolicy_amendment: Option, - proposed_network_policy_amendments: Option>, + available_decisions: &[ReviewDecision], network_approval_context: Option<&NetworkApprovalContext>, additional_permissions: Option<&PermissionProfile>, ) -> Vec { - if network_approval_context.is_some() { - let mut options = vec![ - ApprovalOption { - label: "Yes, just this once".to_string(), + available_decisions + .iter() + .filter_map(|decision| match decision { + ReviewDecision::Approved => Some(ApprovalOption { + label: if network_approval_context.is_some() { + "Yes, just this once".to_string() + } else { + "Yes, proceed".to_string() + }, decision: ApprovalDecision::Review(ReviewDecision::Approved), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], - }, - ApprovalOption { - label: "Yes, and allow this host for this conversation".to_string(), + }), + ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment, + } => { + let rendered_prefix = + strip_bash_lc_and_escape(proposed_execpolicy_amendment.command()); + if rendered_prefix.contains('\n') || rendered_prefix.contains('\r') { + return None; + } + + Some(ApprovalOption { + label: format!( + "Yes, and don't ask again for commands that start with `{rendered_prefix}`" + ), + decision: ApprovalDecision::Review( + ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment: proposed_execpolicy_amendment.clone(), + }, + ), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))], + }) + } + ReviewDecision::ApprovedForSession => Some(ApprovalOption { + label: if network_approval_context.is_some() { + "Yes, and allow this host for this conversation".to_string() + } else if additional_permissions.is_some() { + "Yes, and allow these permissions for this session".to_string() + } else { + "Yes, and don't ask again for this command in this session".to_string() + }, decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('a'))], - }, - ]; - for amendment in proposed_network_policy_amendments.unwrap_or_default() { - let (label, shortcut) = match amendment.action { - NetworkPolicyRuleAction::Allow => ( - "Yes, and allow this host in the future".to_string(), - KeyCode::Char('p'), - ), - NetworkPolicyRuleAction::Deny => continue, - }; - options.push(ApprovalOption { - label, - decision: ApprovalDecision::Review(ReviewDecision::NetworkPolicyAmendment { - network_policy_amendment: amendment, - }), + }), + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment, + } => { + let (label, shortcut) = match network_policy_amendment.action { + NetworkPolicyRuleAction::Allow => ( + "Yes, and allow this host in the future".to_string(), + KeyCode::Char('p'), + ), + NetworkPolicyRuleAction::Deny => ( + "No, and block this host in the future".to_string(), + KeyCode::Char('d'), + ), + }; + Some(ApprovalOption { + label, + decision: ApprovalDecision::Review(ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: network_policy_amendment.clone(), + }), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(shortcut)], + }) + } + ReviewDecision::Denied => Some(ApprovalOption { + label: "No, continue without running it".to_string(), + decision: ApprovalDecision::Review(ReviewDecision::Denied), display_shortcut: None, - additional_shortcuts: vec![key_hint::plain(shortcut)], - }); - } - options.push(ApprovalOption { - label: "No, and tell Codex what to do differently".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Abort), - display_shortcut: Some(key_hint::plain(KeyCode::Esc)), - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], - }); - return options; - } - - if additional_permissions.is_some() { - return vec![ - ApprovalOption { - label: "Yes, proceed".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Approved), - display_shortcut: None, - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], - }, - ApprovalOption { + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('d'))], + }), + ReviewDecision::Abort => Some(ApprovalOption { label: "No, and tell Codex what to do differently".to_string(), decision: ApprovalDecision::Review(ReviewDecision::Abort), display_shortcut: Some(key_hint::plain(KeyCode::Esc)), additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], - }, - ]; - } - - vec![ApprovalOption { - label: "Yes, proceed".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Approved), - display_shortcut: None, - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], - }] - .into_iter() - .chain(proposed_execpolicy_amendment.and_then(|prefix| { - let rendered_prefix = strip_bash_lc_and_escape(prefix.command()); - if rendered_prefix.contains('\n') || rendered_prefix.contains('\r') { - return None; - } - - Some(ApprovalOption { - label: format!( - "Yes, and don't ask again for commands that start with `{rendered_prefix}`" - ), - decision: ApprovalDecision::Review(ReviewDecision::ApprovedExecpolicyAmendment { - proposed_execpolicy_amendment: prefix, }), - display_shortcut: None, - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))], }) - })) - .chain([ApprovalOption { - label: "No, and tell Codex what to do differently".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Abort), - display_shortcut: Some(key_hint::plain(KeyCode::Esc)), - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], - }]) - .collect() + .collect() } fn format_additional_permissions_rule( @@ -695,6 +694,7 @@ mod tests { id: "test".to_string(), command: vec!["echo".to_string(), "hi".to_string()], reason: Some("reason".to_string()), + available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, @@ -740,6 +740,15 @@ mod tests { id: "test".to_string(), command: vec!["echo".to_string()], reason: None, + available_decisions: vec![ + ReviewDecision::Approved, + ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment: ExecPolicyAmendment::new(vec![ + "echo".to_string(), + ]), + }, + ReviewDecision::Abort, + ], network_approval_context: None, proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ "echo".to_string(), @@ -781,6 +790,17 @@ mod tests { id: "test".to_string(), command: vec!["curl".to_string(), "https://example.com".to_string()], reason: None, + available_decisions: vec![ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: NetworkPolicyAmendment { + host: "example.com".to_string(), + action: NetworkPolicyRuleAction::Allow, + }, + }, + ReviewDecision::Abort, + ], network_approval_context: Some(NetworkApprovalContext { host: "example.com".to_string(), protocol: NetworkApprovalProtocol::Https, @@ -818,6 +838,7 @@ mod tests { id: "test".into(), command, reason: None, + available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, @@ -850,17 +871,17 @@ mod tests { protocol: NetworkApprovalProtocol::Https, }; let options = exec_options( - Some(ExecPolicyAmendment::new(vec!["curl".to_string()])), - Some(vec![ - NetworkPolicyAmendment { - host: "example.com".to_string(), - action: NetworkPolicyRuleAction::Allow, + &[ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: NetworkPolicyAmendment { + host: "example.com".to_string(), + action: NetworkPolicyRuleAction::Allow, + }, }, - NetworkPolicyAmendment { - host: "example.com".to_string(), - action: NetworkPolicyRuleAction::Deny, - }, - ]), + ReviewDecision::Abort, + ], Some(&network_context), None, ); @@ -877,6 +898,29 @@ mod tests { ); } + #[test] + fn generic_exec_options_can_offer_allow_for_session() { + let options = exec_options( + &[ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::Abort, + ], + None, + None, + ); + + let labels: Vec = options.into_iter().map(|option| option.label).collect(); + assert_eq!( + labels, + vec![ + "Yes, proceed".to_string(), + "Yes, and don't ask again for this command in this session".to_string(), + "No, and tell Codex what to do differently".to_string(), + ] + ); + } + #[test] fn additional_permissions_exec_options_hide_execpolicy_amendment() { let additional_permissions = PermissionProfile { @@ -886,7 +930,11 @@ mod tests { }), ..Default::default() }; - let options = exec_options(None, None, None, Some(&additional_permissions)); + let options = exec_options( + &[ReviewDecision::Approved, ReviewDecision::Abort], + None, + Some(&additional_permissions), + ); let labels: Vec = options.into_iter().map(|option| option.label).collect(); assert_eq!( @@ -906,6 +954,7 @@ mod tests { id: "test".into(), command: vec!["cat".into(), "/tmp/readme.txt".into()], reason: None, + available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, @@ -946,6 +995,7 @@ mod tests { id: "test".into(), command: vec!["cat".into(), "/tmp/readme.txt".into()], reason: Some("need filesystem access".into()), + available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, @@ -973,6 +1023,17 @@ mod tests { id: "test".into(), command: vec!["curl".into(), "https://example.com".into()], reason: Some("network request blocked".into()), + available_decisions: vec![ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: NetworkPolicyAmendment { + host: "example.com".to_string(), + action: NetworkPolicyRuleAction::Allow, + }, + }, + ReviewDecision::Abort, + ], network_approval_context: Some(NetworkApprovalContext { host: "example.com".to_string(), protocol: NetworkApprovalProtocol::Https, diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 3202a61621..6b04c495ca 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -1117,6 +1117,10 @@ mod tests { id: "1".to_string(), command: vec!["echo".into(), "ok".into()], reason: None, + available_decisions: vec![ + codex_protocol::protocol::ReviewDecision::Approved, + codex_protocol::protocol::ReviewDecision::Abort, + ], network_approval_context: None, proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 533aa10561..4062712a78 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2568,10 +2568,12 @@ impl ChatWidget { .unwrap_or_else(|_| ev.command.join(" ")); self.notify(Notification::ExecApprovalRequested { command }); + let available_decisions = ev.effective_available_decisions(); let request = ApprovalRequest::Exec { id: ev.effective_approval_id(), command: ev.command, reason: ev.reason, + available_decisions, network_approval_context: ev.network_approval_context, proposed_execpolicy_amendment: ev.proposed_execpolicy_amendment, proposed_network_policy_amendments: ev.proposed_network_policy_amendments, @@ -4464,6 +4466,7 @@ impl ChatWidget { | EventMsg::ReasoningContentDelta(_) | EventMsg::ReasoningRawContentDelta(_) | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) | EventMsg::SkillRequestApproval(_) => {} EventMsg::RealtimeConversationStarted(ev) => { if !from_replay { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 22ba19af63..3e90f0c5d2 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -2784,6 +2784,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -2833,6 +2834,7 @@ async fn exec_approval_uses_approval_id_when_present() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }), }); @@ -2869,6 +2871,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -2923,6 +2926,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -6742,6 +6746,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> { ])), proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -6802,6 +6807,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> { ])), proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -6849,6 +6855,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, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -7215,6 +7222,7 @@ async fn status_widget_and_approval_modal_snapshot() { ])), proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 23b1ff4aad..a0556fc318 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -447,7 +447,7 @@ impl AuthModeWidget { " Decide how much autonomy you want to grant Codex".into(), Line::from(vec![ " For more details see the ".into(), - "\u{1b}]8;;https://github.com/openai/codex\u{7}Codex docs\u{1b}]8;;\u{7}".underlined(), + "\u{1b}]8;;https://developers.openai.com/codex/security\u{7}Codex docs\u{1b}]8;;\u{7}".underlined(), ]) .dim(), "".into(),