diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 905d6af657..893c126ac9 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -2775,6 +2775,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, @@ -7399,6 +7488,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ], "title": "EventMsg" diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 46a47ac762..c82db73cf3 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -617,6 +617,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], @@ -3353,6 +3354,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, 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 5e73227a7f..a4f998df8b 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 @@ -4782,6 +4782,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/v2/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/v2/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/v2/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/v2/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ], "title": "EventMsg" @@ -10234,6 +10323,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], @@ -12348,6 +12438,84 @@ ], "type": "string" }, + "NetworkRequirements": { + "properties": { + "allowLocalBinding": { + "type": [ + "boolean", + "null" + ] + }, + "allowUnixSockets": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "allowUpstreamProxy": { + "type": [ + "boolean", + "null" + ] + }, + "allowedDomains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "dangerouslyAllowNonLoopbackAdmin": { + "type": [ + "boolean", + "null" + ] + }, + "dangerouslyAllowNonLoopbackProxy": { + "type": [ + "boolean", + "null" + ] + }, + "deniedDomains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "httpPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "socksPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, "OverriddenMetadata": { "properties": { "effectiveValue": true, diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index 3b5ed750b7..497c2df1af 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -2775,6 +2775,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index 9c5c3653e8..3f644e0ba9 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -2775,6 +2775,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index a008e02538..7ea8c2731d 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -2775,6 +2775,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index d6ddd65172..3ba96a852a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -52,6 +52,84 @@ }, "type": "object" }, + "NetworkRequirements": { + "properties": { + "allowLocalBinding": { + "type": [ + "boolean", + "null" + ] + }, + "allowUnixSockets": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "allowUpstreamProxy": { + "type": [ + "boolean", + "null" + ] + }, + "allowedDomains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "dangerouslyAllowNonLoopbackAdmin": { + "type": [ + "boolean", + "null" + ] + }, + "dangerouslyAllowNonLoopbackProxy": { + "type": [ + "boolean", + "null" + ] + }, + "deniedDomains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "httpPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "socksPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, "ResidencyRequirement": { "enum": [ "us" 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 56ef813231..7fc1dab9b6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -52,6 +52,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 625c99af93..5ed713e580 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -52,6 +52,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 dfbd76a20b..3187f75f77 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 61b12ceff3..094db44d73 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -207,6 +207,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 50c3d60e37..dcac91e0dd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 c4fa3b2028..5760be15cb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 0534f6e16e..901fc829c3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -207,6 +207,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 148bbe7d7d..6c6f2d1eff 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 c85d7ce97a..9759946487 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -207,6 +207,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 44aca775c1..86d1e2940b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 055c3fd4a9..86f9f65925 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 798caa5995..99d1f569c1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 65b2a66be0..19bf47db87 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], 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 1a63c0d7d0..c70f6f7cdf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/typescript/CollabResumeBeginEvent.ts b/codex-rs/app-server-protocol/schema/typescript/CollabResumeBeginEvent.ts new file mode 100644 index 0000000000..b703666723 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/CollabResumeBeginEvent.ts @@ -0,0 +1,18 @@ +// 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 { ThreadId } from "./ThreadId"; + +export type CollabResumeBeginEvent = { +/** + * Identifier for the collab tool call. + */ +call_id: string, +/** + * Thread ID of the sender. + */ +sender_thread_id: ThreadId, +/** + * Thread ID of the receiver. + */ +receiver_thread_id: ThreadId, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/CollabResumeEndEvent.ts b/codex-rs/app-server-protocol/schema/typescript/CollabResumeEndEvent.ts new file mode 100644 index 0000000000..9eed310360 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/CollabResumeEndEvent.ts @@ -0,0 +1,24 @@ +// 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 { AgentStatus } from "./AgentStatus"; +import type { ThreadId } from "./ThreadId"; + +export type CollabResumeEndEvent = { +/** + * Identifier for the collab tool call. + */ +call_id: string, +/** + * Thread ID of the sender. + */ +sender_thread_id: ThreadId, +/** + * Thread ID of the receiver. + */ +receiver_thread_id: ThreadId, +/** + * Last known status of the receiver agent reported to the sender agent after + * resume. + */ +status: AgentStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts index c18088eaaf..2c0514870f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts +++ b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts @@ -17,6 +17,8 @@ import type { CollabAgentSpawnBeginEvent } from "./CollabAgentSpawnBeginEvent"; import type { CollabAgentSpawnEndEvent } from "./CollabAgentSpawnEndEvent"; import type { CollabCloseBeginEvent } from "./CollabCloseBeginEvent"; import type { CollabCloseEndEvent } from "./CollabCloseEndEvent"; +import type { CollabResumeBeginEvent } from "./CollabResumeBeginEvent"; +import type { CollabResumeEndEvent } from "./CollabResumeEndEvent"; import type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent"; import type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent"; import type { ContextCompactedEvent } from "./ContextCompactedEvent"; @@ -72,4 +74,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": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent; +export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index a6ff6fbaf1..c0c00a0c81 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -37,6 +37,8 @@ export type { CollabAgentSpawnBeginEvent } from "./CollabAgentSpawnBeginEvent"; export type { CollabAgentSpawnEndEvent } from "./CollabAgentSpawnEndEvent"; export type { CollabCloseBeginEvent } from "./CollabCloseBeginEvent"; export type { CollabCloseEndEvent } from "./CollabCloseEndEvent"; +export type { CollabResumeBeginEvent } from "./CollabResumeBeginEvent"; +export type { CollabResumeEndEvent } from "./CollabResumeEndEvent"; export type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent"; export type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent"; export type { CollaborationMode } from "./CollaborationMode"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentTool.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentTool.ts index 11db4dbf9a..3637853a38 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentTool.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentTool.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CollabAgentTool = "spawnAgent" | "sendInput" | "wait" | "closeAgent"; +export type CollabAgentTool = "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index 89cecfd189..f99c880697 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -6,4 +6,4 @@ import type { AskForApproval } from "./AskForApproval"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; -export type ConfigRequirements = { allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, enforceResidency: ResidencyRequirement | null, }; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, enforceResidency: ResidencyRequirement | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts new file mode 100644 index 0000000000..b7ac9d2f7a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.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 NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, allowedDomains: Array | null, deniedDomains: Array | null, allowUnixSockets: Array | null, allowLocalBinding: boolean | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index ec39048641..d595035dda 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -37,5 +37,8 @@ personality?: Personality | null, /** outputSchema?: JsonValue | null, /** * EXPERIMENTAL - Set a pre-set collaboration mode. * Takes precedence over model, reasoning_effort, and developer instructions if set. + * + * For `collaboration_mode.settings.developer_instructions`, `null` means + * "use the built-in instructions for the selected mode". */ collaborationMode?: CollaborationMode | null}; 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 9e7547a9c3..3c6caf034d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -91,6 +91,7 @@ export type { Model } from "./Model"; export type { ModelListParams } from "./ModelListParams"; export type { ModelListResponse } from "./ModelListResponse"; export type { NetworkAccess } from "./NetworkAccess"; +export type { NetworkRequirements } from "./NetworkRequirements"; export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; export type { PatchChangeKind } from "./PatchChangeKind"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 4f5b3df5b6..fd0c33e8fc 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -527,7 +527,7 @@ pub struct ConfigReadResponse { pub layers: Option>, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ConfigRequirements { @@ -535,6 +535,24 @@ pub struct ConfigRequirements { pub allowed_sandbox_modes: Option>, pub allowed_web_search_modes: Option>, pub enforce_residency: Option, + #[experimental("configRequirements/read.network")] + pub network: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct NetworkRequirements { + pub enabled: Option, + pub http_port: Option, + pub socks_port: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_non_loopback_admin: Option, + pub allowed_domains: Option>, + pub denied_domains: Option>, + pub allow_unix_sockets: Option>, + pub allow_local_binding: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] @@ -2071,6 +2089,9 @@ pub struct TurnStartParams { /// EXPERIMENTAL - Set a pre-set collaboration mode. /// Takes precedence over model, reasoning_effort, and developer instructions if set. + /// + /// For `collaboration_mode.settings.developer_instructions`, `null` means + /// "use the built-in instructions for the selected mode". #[experimental("turn/start.collaborationMode")] #[ts(optional = nullable)] pub collaboration_mode: Option, @@ -2500,6 +2521,7 @@ pub enum CommandExecutionStatus { pub enum CollabAgentTool { SpawnAgent, SendInput, + ResumeAgent, Wait, CloseAgent, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 6065e404f9..920761a17f 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -94,7 +94,7 @@ Example (from OpenAI's official VSCode extension): - `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success. - `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. - `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. -- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. +- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". - `turn/steer` — add user input to an already in-flight turn without starting a new turn; returns the active `turnId` that accepted the input. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. - `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review. @@ -116,7 +116,7 @@ Example (from OpenAI's official VSCode extension): - `config/read` — fetch the effective config on disk after resolving config layering. - `config/value/write` — write a single config key/value to the user's config.toml on disk. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk. -- `configRequirements/read` — fetch the loaded requirements allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`) and `enforceResidency` from `requirements.toml` and/or MDM (or `null` if none are configured). +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), `enforceResidency`, and `network` constraints. ### Example: Start or resume a thread @@ -497,7 +497,7 @@ Today both notifications carry an empty `items` array even when item events were - `commandExecution` — `{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`. - `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`. - `mcpToolCall` — `{id, server, tool, status, arguments, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`. -- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`. +- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`. - `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion. - `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool. - `enteredReviewMode` — `{id, review}` sent when the reviewer starts; `review` is a short user-facing label such as `"current changes"` or the requested target description. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 1d3d7ff059..bdf0a58baa 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -596,6 +596,28 @@ pub(crate) async fn apply_bespoke_event_handling( .send_server_notification(ServerNotification::ItemCompleted(notification)) .await; } + EventMsg::CollabResumeBegin(begin_event) => { + let item = collab_resume_begin_item(begin_event); + let notification = ItemStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemStarted(notification)) + .await; + } + EventMsg::CollabResumeEnd(end_event) => { + let item = collab_resume_end_item(end_event); + let notification = ItemCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemCompleted(notification)) + .await; + } EventMsg::AgentMessageContentDelta(event) => { let codex_protocol::protocol::AgentMessageContentDeltaEvent { item_id, delta, .. } = event; @@ -1758,6 +1780,44 @@ async fn on_command_execution_request_approval_response( } } +fn collab_resume_begin_item( + begin_event: codex_core::protocol::CollabResumeBeginEvent, +) -> ThreadItem { + ThreadItem::CollabAgentToolCall { + id: begin_event.call_id, + tool: CollabAgentTool::ResumeAgent, + status: V2CollabToolCallStatus::InProgress, + sender_thread_id: begin_event.sender_thread_id.to_string(), + receiver_thread_ids: vec![begin_event.receiver_thread_id.to_string()], + prompt: None, + agents_states: HashMap::new(), + } +} + +fn collab_resume_end_item(end_event: codex_core::protocol::CollabResumeEndEvent) -> ThreadItem { + let status = match &end_event.status { + codex_protocol::protocol::AgentStatus::Errored(_) + | codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed, + _ => V2CollabToolCallStatus::Completed, + }; + let receiver_id = end_event.receiver_thread_id.to_string(); + let agents_states = [( + receiver_id.clone(), + V2CollabAgentStatus::from(end_event.status), + )] + .into_iter() + .collect(); + ThreadItem::CollabAgentToolCall { + id: end_event.call_id, + tool: CollabAgentTool::ResumeAgent, + status, + sender_thread_id: end_event.sender_thread_id.to_string(), + receiver_thread_ids: vec![receiver_id], + prompt: None, + agents_states, + } +} + /// similar to handle_mcp_tool_call_begin in exec async fn construct_mcp_tool_call_notification( begin_event: McpToolCallBeginEvent, @@ -1838,6 +1898,8 @@ mod tests { use anyhow::anyhow; use anyhow::bail; use codex_app_server_protocol::TurnPlanStepStatus; + use codex_core::protocol::CollabResumeBeginEvent; + use codex_core::protocol::CollabResumeEndEvent; use codex_core::protocol::CreditsSnapshot; use codex_core::protocol::McpInvocation; use codex_core::protocol::RateLimitSnapshot; @@ -1882,6 +1944,55 @@ mod tests { assert_eq!(completion_status, None); } + #[test] + fn collab_resume_begin_maps_to_item_started_resume_agent() { + let event = CollabResumeBeginEvent { + call_id: "call-1".to_string(), + sender_thread_id: ThreadId::new(), + receiver_thread_id: ThreadId::new(), + }; + + let item = collab_resume_begin_item(event.clone()); + let expected = ThreadItem::CollabAgentToolCall { + id: event.call_id, + tool: CollabAgentTool::ResumeAgent, + status: V2CollabToolCallStatus::InProgress, + sender_thread_id: event.sender_thread_id.to_string(), + receiver_thread_ids: vec![event.receiver_thread_id.to_string()], + prompt: None, + agents_states: HashMap::new(), + }; + assert_eq!(item, expected); + } + + #[test] + fn collab_resume_end_maps_to_item_completed_resume_agent() { + let event = CollabResumeEndEvent { + call_id: "call-2".to_string(), + sender_thread_id: ThreadId::new(), + receiver_thread_id: ThreadId::new(), + status: codex_protocol::protocol::AgentStatus::NotFound, + }; + + let item = collab_resume_end_item(event.clone()); + let receiver_id = event.receiver_thread_id.to_string(); + let expected = ThreadItem::CollabAgentToolCall { + id: event.call_id, + tool: CollabAgentTool::ResumeAgent, + status: V2CollabToolCallStatus::Failed, + sender_thread_id: event.sender_thread_id.to_string(), + receiver_thread_ids: vec![receiver_id.clone()], + prompt: None, + agents_states: [( + receiver_id, + V2CollabAgentStatus::from(codex_protocol::protocol::AgentStatus::NotFound), + )] + .into_iter() + .collect(), + }; + assert_eq!(item, expected); + } + #[tokio::test] async fn test_handle_error_records_message() -> Result<()> { let conversation_id = ThreadId::new(); diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index f96daebc19..29fbd3d74d 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -206,6 +206,7 @@ use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::run_login_server; use codex_protocol::ThreadId; +use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; use codex_protocol::config_types::WindowsSandboxLevel; @@ -397,6 +398,27 @@ impl CodexMessageProcessor { .unwrap_or_default() } + /// If a client sends `developer_instructions: null` during a mode switch, + /// use the built-in instructions for that mode. + fn normalize_turn_start_collaboration_mode( + &self, + mut collaboration_mode: CollaborationMode, + ) -> CollaborationMode { + if collaboration_mode.settings.developer_instructions.is_none() + && let Some(instructions) = self + .thread_manager + .list_collaboration_modes() + .into_iter() + .find(|preset| preset.mode == Some(collaboration_mode.mode)) + .and_then(|preset| preset.developer_instructions.flatten()) + .filter(|instructions| !instructions.is_empty()) + { + collaboration_mode.settings.developer_instructions = Some(instructions); + } + + collaboration_mode + } + fn review_request_from_target( target: ApiReviewTarget, ) -> Result<(ReviewRequest, String), JSONRPCErrorError> { @@ -4554,6 +4576,10 @@ impl CodexMessageProcessor { } }; + let collaboration_mode = params + .collaboration_mode + .map(|mode| self.normalize_turn_start_collaboration_mode(mode)); + // Map v2 input items to core input items. let mapped_items: Vec = params .input @@ -4567,7 +4593,7 @@ impl CodexMessageProcessor { || params.model.is_some() || params.effort.is_some() || params.summary.is_some() - || params.collaboration_mode.is_some() + || collaboration_mode.is_some() || params.personality.is_some(); // If any overrides are provided, update the session turn context first. @@ -4581,7 +4607,7 @@ impl CodexMessageProcessor { model: params.model, effort: params.effort.map(Some), summary: params.summary, - collaboration_mode: params.collaboration_mode, + collaboration_mode, personality: params.personality, }) .await; diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index 14c4e44173..e1531dce2f 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -9,6 +9,7 @@ use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::ConfigWriteErrorCode; use codex_app_server_protocol::ConfigWriteResponse; use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::NetworkRequirements; use codex_app_server_protocol::SandboxMode; use codex_core::config::ConfigService; use codex_core::config::ConfigServiceError; @@ -129,6 +130,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR enforce_residency: requirements .enforce_residency .map(map_residency_requirement_to_api), + network: requirements.network.map(map_network_requirements_to_api), } } @@ -149,6 +151,23 @@ fn map_residency_requirement_to_api( } } +fn map_network_requirements_to_api( + network: codex_core::config_loader::NetworkRequirementsToml, +) -> NetworkRequirements { + NetworkRequirements { + enabled: network.enabled, + http_port: network.http_port, + socks_port: network.socks_port, + allow_upstream_proxy: network.allow_upstream_proxy, + dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy, + dangerously_allow_non_loopback_admin: network.dangerously_allow_non_loopback_admin, + allowed_domains: network.allowed_domains, + denied_domains: network.denied_domains, + allow_unix_sockets: network.allow_unix_sockets, + allow_local_binding: network.allow_local_binding, + } +} + fn map_error(err: ConfigServiceError) -> JSONRPCErrorError { if let Some(code) = err.write_error_code() { return config_write_error(code, err.to_string()); @@ -174,6 +193,7 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> #[cfg(test)] mod tests { use super::*; + use codex_core::config_loader::NetworkRequirementsToml as CoreNetworkRequirementsToml; use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use pretty_assertions::assert_eq; @@ -194,6 +214,18 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: Some(CoreResidencyRequirement::Us), + network: Some(CoreNetworkRequirementsToml { + enabled: Some(true), + http_port: Some(8080), + socks_port: Some(1080), + allow_upstream_proxy: Some(false), + dangerously_allow_non_loopback_proxy: Some(false), + dangerously_allow_non_loopback_admin: Some(false), + allowed_domains: Some(vec!["api.openai.com".to_string()]), + denied_domains: Some(vec!["example.com".to_string()]), + allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), + allow_local_binding: Some(true), + }), }; let mapped = map_requirements_toml_to_api(requirements); @@ -217,6 +249,21 @@ mod tests { mapped.enforce_residency, Some(codex_app_server_protocol::ResidencyRequirement::Us), ); + assert_eq!( + mapped.network, + Some(NetworkRequirements { + enabled: Some(true), + http_port: Some(8080), + socks_port: Some(1080), + allow_upstream_proxy: Some(false), + dangerously_allow_non_loopback_proxy: Some(false), + dangerously_allow_non_loopback_admin: Some(false), + allowed_domains: Some(vec!["api.openai.com".to_string()]), + denied_domains: Some(vec!["example.com".to_string()]), + allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), + allow_local_binding: Some(true), + }), + ); } #[test] @@ -228,6 +275,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let mapped = map_requirements_toml_to_api(requirements); diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 395561f69a..27fc335cb4 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -408,6 +408,8 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { let request = response_mock.single_request(); let payload = request.body_json(); assert_eq!(payload["model"].as_str(), Some("mock-model-collab")); + let payload_text = payload.to_string(); + assert!(payload_text.contains("The `request_user_input` tool is unavailable in Default mode.")); Ok(()) } diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 30d49bd406..36787e98f9 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -385,6 +385,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }) ); } @@ -426,6 +427,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }) ); } @@ -470,6 +472,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }) ); assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 2); diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index a600a0d8b6..bee0dd1b27 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -5,7 +5,9 @@ use crate::error::Result as CodexResult; use crate::thread_manager::ThreadManagerState; use codex_protocol::ThreadId; use codex_protocol::protocol::Op; +use codex_protocol::protocol::SessionSource; use codex_protocol::user_input::UserInput; +use std::path::PathBuf; use std::sync::Arc; use std::sync::Weak; use tokio::sync::watch; @@ -39,7 +41,7 @@ impl AgentControl { &self, config: crate::config::Config, prompt: String, - session_source: Option, + session_source: Option, ) -> CodexResult { let state = self.upgrade()?; let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; @@ -65,6 +67,32 @@ impl AgentControl { Ok(new_thread.thread_id) } + /// Resume an existing agent thread from a recorded rollout file. + pub(crate) async fn resume_agent_from_rollout( + &self, + config: crate::config::Config, + rollout_path: PathBuf, + session_source: SessionSource, + ) -> CodexResult { + let state = self.upgrade()?; + let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; + + let resumed_thread = state + .resume_thread_from_rollout_with_source( + config, + rollout_path, + self.clone(), + session_source, + ) + .await?; + reservation.commit(resumed_thread.thread_id); + // Resumed threads are re-registered in-memory and need the same listener + // attachment path as freshly spawned threads. + state.notify_thread_created(resumed_thread.thread_id); + + Ok(resumed_thread.thread_id) + } + /// Send a `user` prompt to an existing agent thread. pub(crate) async fn send_prompt( &self, @@ -287,6 +315,24 @@ mod tests { ); } + #[tokio::test] + async fn resume_agent_errors_when_manager_dropped() { + let control = AgentControl::default(); + let (_home, config) = test_config().await; + let err = control + .resume_agent_from_rollout( + config, + PathBuf::from("/tmp/missing-rollout.jsonl"), + SessionSource::Exec, + ) + .await + .expect_err("resume_agent should fail without a manager"); + assert_eq!( + err.to_string(), + "unsupported operation: thread manager dropped" + ); + } + #[tokio::test] async fn send_prompt_errors_when_thread_missing() { let harness = AgentControlHarness::new().await; @@ -518,4 +564,88 @@ mod tests { .await .expect("shutdown agent"); } + + #[tokio::test] + async fn resume_agent_respects_max_threads_limit() { + let max_threads = 1usize; + let (_home, config) = test_config_with_cli_overrides(vec![( + "agents.max_threads".to_string(), + TomlValue::Integer(max_threads as i64), + )]) + .await; + let manager = ThreadManager::with_models_provider_and_home( + CodexAuth::from_api_key("dummy"), + config.model_provider.clone(), + config.codex_home.clone(), + ); + let control = manager.agent_control(); + + let resumable_id = control + .spawn_agent(config.clone(), "hello".to_string(), None) + .await + .expect("spawn_agent should succeed"); + let rollout_path = manager + .get_thread(resumable_id) + .await + .expect("thread should exist") + .rollout_path() + .expect("rollout path should exist"); + let _ = control + .shutdown_agent(resumable_id) + .await + .expect("shutdown resumable thread"); + + let active_id = control + .spawn_agent(config.clone(), "occupy".to_string(), None) + .await + .expect("spawn_agent should succeed for active slot"); + + let err = control + .resume_agent_from_rollout(config, rollout_path, SessionSource::Exec) + .await + .expect_err("resume should respect max threads"); + let CodexErr::AgentLimitReached { + max_threads: seen_max_threads, + } = err + else { + panic!("expected CodexErr::AgentLimitReached"); + }; + assert_eq!(seen_max_threads, max_threads); + + let _ = control + .shutdown_agent(active_id) + .await + .expect("shutdown active thread"); + } + + #[tokio::test] + async fn resume_agent_releases_slot_after_resume_failure() { + let max_threads = 1usize; + let (_home, config) = test_config_with_cli_overrides(vec![( + "agents.max_threads".to_string(), + TomlValue::Integer(max_threads as i64), + )]) + .await; + let manager = ThreadManager::with_models_provider_and_home( + CodexAuth::from_api_key("dummy"), + config.model_provider.clone(), + config.codex_home.clone(), + ); + let control = manager.agent_control(); + + let missing_rollout = config.codex_home.join("sessions/missing-rollout.jsonl"); + let _ = control + .resume_agent_from_rollout(config.clone(), missing_rollout, SessionSource::Exec) + .await + .expect_err("resume should fail for missing rollout path"); + + let resumed_id = control + .spawn_agent(config, "hello".to_string(), None) + .await + .expect("spawn should succeed after failed resume"); + let _ = control + .shutdown_agent(resumed_id) + .await + .expect("shutdown resumed thread"); + } } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 9d7d9195cf..dee21376cb 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1653,6 +1653,7 @@ impl Config { mcp_servers, exec_policy: _, enforce_residency, + network: _network_requirements, } = requirements; apply_requirement_constrained_value( @@ -4379,6 +4380,7 @@ model_verbosity = "high" mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let requirement_source = crate::config_loader::RequirementSource::Unknown; let requirement_source_for_error = requirement_source.clone(); @@ -4936,6 +4938,7 @@ mcp_oauth_callback_port = 5678 mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let config = ConfigBuilder::default() diff --git a/codex-rs/core/src/config_loader/config_requirements.rs b/codex-rs/core/src/config_loader/config_requirements.rs index da475bd4ed..21dbdb3d7f 100644 --- a/codex-rs/core/src/config_loader/config_requirements.rs +++ b/codex-rs/core/src/config_loader/config_requirements.rs @@ -81,6 +81,8 @@ pub struct ConfigRequirements { pub mcp_servers: Option>>, pub(crate) exec_policy: Option>, pub enforce_residency: ConstrainedWithSource>, + /// Managed network constraints derived from requirements. + pub network: Option>, } impl Default for ConfigRequirements { @@ -101,6 +103,7 @@ impl Default for ConfigRequirements { mcp_servers: None, exec_policy: None, enforce_residency: ConstrainedWithSource::new(Constrained::allow_any(None), None), + network: None, } } } @@ -123,6 +126,64 @@ pub struct McpServerRequirement { pub identity: McpServerIdentity, } +#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct NetworkRequirementsToml { + pub enabled: Option, + pub http_port: Option, + pub socks_port: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_non_loopback_admin: Option, + pub allowed_domains: Option>, + pub denied_domains: Option>, + pub allow_unix_sockets: Option>, + pub allow_local_binding: Option, +} + +/// Normalized network constraints derived from requirements TOML. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct NetworkConstraints { + pub enabled: Option, + pub http_port: Option, + pub socks_port: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_non_loopback_admin: Option, + pub allowed_domains: Option>, + pub denied_domains: Option>, + pub allow_unix_sockets: Option>, + pub allow_local_binding: Option, +} + +impl From for NetworkConstraints { + fn from(value: NetworkRequirementsToml) -> Self { + let NetworkRequirementsToml { + enabled, + http_port, + socks_port, + allow_upstream_proxy, + dangerously_allow_non_loopback_proxy, + dangerously_allow_non_loopback_admin, + allowed_domains, + denied_domains, + allow_unix_sockets, + allow_local_binding, + } = value; + Self { + enabled, + http_port, + socks_port, + allow_upstream_proxy, + dangerously_allow_non_loopback_proxy, + dangerously_allow_non_loopback_admin, + allowed_domains, + denied_domains, + allow_unix_sockets, + allow_local_binding, + } + } +} + #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "lowercase")] pub enum WebSearchModeRequirement { @@ -170,6 +231,8 @@ pub struct ConfigRequirementsToml { pub mcp_servers: Option>, pub rules: Option, pub enforce_residency: Option, + #[serde(rename = "experimental_network")] + pub network: Option, } /// Value paired with the requirement source it came from, for better error @@ -202,6 +265,7 @@ pub struct ConfigRequirementsWithSources { pub mcp_servers: Option>>, pub rules: Option>, pub enforce_residency: Option>, + pub network: Option>, } impl ConfigRequirementsWithSources { @@ -236,6 +300,7 @@ impl ConfigRequirementsWithSources { mcp_servers, rules, enforce_residency, + network, } ); } @@ -248,6 +313,7 @@ impl ConfigRequirementsWithSources { mcp_servers, rules, enforce_residency, + network, } = self; ConfigRequirementsToml { allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), @@ -256,6 +322,7 @@ impl ConfigRequirementsWithSources { mcp_servers: mcp_servers.map(|sourced| sourced.value), rules: rules.map(|sourced| sourced.value), enforce_residency: enforce_residency.map(|sourced| sourced.value), + network: network.map(|sourced| sourced.value), } } } @@ -301,6 +368,7 @@ impl ConfigRequirementsToml { && self.mcp_servers.is_none() && self.rules.is_none() && self.enforce_residency.is_none() + && self.network.is_none() } } @@ -315,6 +383,7 @@ impl TryFrom for ConfigRequirements { mcp_servers, rules, enforce_residency, + network, } = toml; let approval_policy = match allowed_approval_policies { @@ -471,6 +540,10 @@ impl TryFrom for ConfigRequirements { } None => ConstrainedWithSource::new(Constrained::allow_any(None), None), }; + let network = network.map(|sourced_network| { + let Sourced { value, source } = sourced_network; + Sourced::new(NetworkConstraints::from(value), source) + }); Ok(ConfigRequirements { approval_policy, sandbox_policy, @@ -478,6 +551,7 @@ impl TryFrom for ConfigRequirements { mcp_servers, exec_policy, enforce_residency, + network, }) } } @@ -506,6 +580,7 @@ mod tests { mcp_servers, rules, enforce_residency, + network, } = toml; ConfigRequirementsWithSources { allowed_approval_policies: allowed_approval_policies @@ -518,6 +593,7 @@ mod tests { rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)), enforce_residency: enforce_residency .map(|value| Sourced::new(value, RequirementSource::Unknown)), + network: network.map(|value| Sourced::new(value, RequirementSource::Unknown)), } } @@ -547,6 +623,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: Some(enforce_residency), + network: None, }; target.merge_unset_fields(source.clone(), other); @@ -566,6 +643,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)), + network: None, } ); } @@ -597,6 +675,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, } ); Ok(()) @@ -636,6 +715,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, } ); Ok(()) @@ -951,6 +1031,50 @@ mod tests { Ok(()) } + #[test] + fn network_requirements_are_preserved_as_constraints_with_source() -> Result<()> { + let toml_str = r#" + [experimental_network] + enabled = true + allow_upstream_proxy = false + allowed_domains = ["api.example.com", "*.openai.com"] + denied_domains = ["blocked.example.com"] + allow_unix_sockets = ["/tmp/example.sock"] + allow_local_binding = false + "#; + + let source = RequirementSource::CloudRequirements; + let mut requirements_with_sources = ConfigRequirementsWithSources::default(); + requirements_with_sources.merge_unset_fields(source.clone(), from_str(toml_str)?); + + let requirements = ConfigRequirements::try_from(requirements_with_sources)?; + let sourced_network = requirements + .network + .expect("network requirements should be preserved as constraints"); + + assert_eq!(sourced_network.source, source); + assert_eq!(sourced_network.value.enabled, Some(true)); + assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false)); + assert_eq!( + sourced_network.value.allowed_domains.as_ref(), + Some(&vec![ + "api.example.com".to_string(), + "*.openai.com".to_string() + ]) + ); + assert_eq!( + sourced_network.value.denied_domains.as_ref(), + Some(&vec!["blocked.example.com".to_string()]) + ); + assert_eq!( + sourced_network.value.allow_unix_sockets.as_ref(), + Some(&vec!["/tmp/example.sock".to_string()]) + ); + assert_eq!(sourced_network.value.allow_local_binding, Some(false)); + + Ok(()) + } + #[test] fn deserialize_mcp_server_requirements() -> Result<()> { let toml_str = r#" diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index c79388a71e..0966ac8747 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -37,6 +37,8 @@ pub use config_requirements::ConfigRequirementsToml; pub use config_requirements::ConstrainedWithSource; pub use config_requirements::McpServerIdentity; pub use config_requirements::McpServerRequirement; +pub use config_requirements::NetworkConstraints; +pub use config_requirements::NetworkRequirementsToml; pub use config_requirements::RequirementSource; pub use config_requirements::ResidencyRequirement; pub use config_requirements::SandboxModeRequirement; diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 7f9c2a9a8c..1b5125840f 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -568,6 +568,7 @@ allowed_approval_policies = ["on-request"] mcp_servers: None, rules: None, enforce_residency: None, + network: None, }) }), ) @@ -615,6 +616,7 @@ allowed_approval_policies = ["on-request"] mcp_servers: None, rules: None, enforce_residency: None, + network: None, }, ); load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?; @@ -651,6 +653,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let expected = requirements.clone(); let cloud_requirements = CloudRequirementsLoader::new(async move { Some(requirements) }); diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 587b68a913..5043c9ddf5 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -109,6 +109,8 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::CollabWaitingBegin(_) | EventMsg::CollabWaitingEnd(_) | EventMsg::CollabCloseBegin(_) - | EventMsg::CollabCloseEnd(_) => false, + | EventMsg::CollabCloseEnd(_) + | EventMsg::CollabResumeBegin(_) + | EventMsg::CollabResumeEnd(_) => false, } } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index e702d9a004..13f9efafb0 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -405,6 +405,25 @@ impl ThreadManagerState { .await } + pub(crate) async fn resume_thread_from_rollout_with_source( + &self, + config: Config, + rollout_path: PathBuf, + agent_control: AgentControl, + session_source: SessionSource, + ) -> CodexResult { + let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?; + self.spawn_thread_with_source( + config, + initial_history, + Arc::clone(&self.auth_manager), + agent_control, + session_source, + Vec::new(), + ) + .await + } + /// Spawn a new thread with optional history and register it with the manager. pub(crate) async fn spawn_thread( &self, diff --git a/codex-rs/core/src/tools/handlers/collab.rs b/codex-rs/core/src/tools/handlers/collab.rs index e6f0c3c959..bfe213d11f 100644 --- a/codex-rs/core/src/tools/handlers/collab.rs +++ b/codex-rs/core/src/tools/handlers/collab.rs @@ -22,8 +22,12 @@ use codex_protocol::protocol::CollabAgentSpawnBeginEvent; use codex_protocol::protocol::CollabAgentSpawnEndEvent; use codex_protocol::protocol::CollabCloseBeginEvent; use codex_protocol::protocol::CollabCloseEndEvent; +use codex_protocol::protocol::CollabResumeBeginEvent; +use codex_protocol::protocol::CollabResumeEndEvent; use codex_protocol::protocol::CollabWaitingBeginEvent; use codex_protocol::protocol::CollabWaitingEndEvent; +use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::SubAgentSource; use serde::Deserialize; use serde::Serialize; @@ -71,6 +75,7 @@ impl ToolHandler for CollabHandler { match tool_name.as_str() { "spawn_agent" => spawn::handle(session, turn, call_id, arguments).await, "send_input" => send_input::handle(session, turn, call_id, arguments).await, + "resume_agent" => resume_agent::handle(session, turn, call_id, arguments).await, "wait" => wait::handle(session, turn, call_id, arguments).await, "close_agent" => close_agent::handle(session, turn, call_id, arguments).await, other => Err(FunctionCallError::RespondToModel(format!( @@ -86,8 +91,6 @@ mod spawn { use crate::agent::exceeds_thread_spawn_depth_limit; use crate::agent::next_thread_spawn_depth; - use codex_protocol::protocol::SessionSource; - use codex_protocol::protocol::SubAgentSource; use std::sync::Arc; #[derive(Debug, Deserialize)] @@ -148,10 +151,7 @@ mod spawn { .spawn_agent( config, prompt.clone(), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { - parent_thread_id: session.conversation_id, - depth: child_depth, - })), + Some(thread_spawn_source(session.conversation_id, child_depth)), ) .await .map_err(collab_spawn_error); @@ -279,6 +279,152 @@ mod send_input { } } +mod resume_agent { + use super::*; + use crate::agent::next_thread_spawn_depth; + use crate::rollout::find_thread_path_by_id_str; + use std::sync::Arc; + + #[derive(Debug, Deserialize)] + struct ResumeAgentArgs { + id: String, + } + + #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] + pub(super) struct ResumeAgentResult { + pub(super) status: AgentStatus, + } + + pub async fn handle( + session: Arc, + turn: Arc, + call_id: String, + arguments: String, + ) -> Result { + let args: ResumeAgentArgs = parse_arguments(&arguments)?; + let receiver_thread_id = agent_id(&args.id)?; + let child_depth = next_thread_spawn_depth(&turn.session_source); + if exceeds_thread_spawn_depth_limit(child_depth) { + return Err(FunctionCallError::RespondToModel( + "Agent depth limit reached. Solve the task yourself.".to_string(), + )); + } + + session + .send_event( + &turn, + CollabResumeBeginEvent { + call_id: call_id.clone(), + sender_thread_id: session.conversation_id, + receiver_thread_id, + } + .into(), + ) + .await; + + let mut status = session + .services + .agent_control + .get_status(receiver_thread_id) + .await; + let error = if matches!(status, AgentStatus::NotFound) { + // If the thread is no longer active, attempt to restore it from rollout. + match try_resume_closed_agent( + &session, + &turn, + receiver_thread_id, + &args.id, + child_depth, + ) + .await + { + Ok(resumed_status) => { + status = resumed_status; + None + } + Err(err) => { + status = session + .services + .agent_control + .get_status(receiver_thread_id) + .await; + Some(err) + } + } + } else { + None + }; + + session + .send_event( + &turn, + CollabResumeEndEvent { + call_id, + sender_thread_id: session.conversation_id, + receiver_thread_id, + status: status.clone(), + } + .into(), + ) + .await; + + if let Some(err) = error { + return Err(err); + } + + let content = serde_json::to_string(&ResumeAgentResult { status }).map_err(|err| { + FunctionCallError::Fatal(format!("failed to serialize resume_agent result: {err}")) + })?; + + Ok(ToolOutput::Function { + body: FunctionCallOutputBody::Text(content), + success: Some(true), + }) + } + + async fn try_resume_closed_agent( + session: &Arc, + turn: &Arc, + receiver_thread_id: ThreadId, + receiver_id: &str, + child_depth: i32, + ) -> Result { + let rollout_path = find_thread_path_by_id_str( + turn.config.codex_home.as_path(), + receiver_id, + ) + .await + .map_err(|err| { + FunctionCallError::RespondToModel(format!( + "tool failed: failed to locate rollout for agent {receiver_thread_id}: {err}" + )) + })? + .ok_or_else(|| { + FunctionCallError::RespondToModel(format!( + "agent with id {receiver_thread_id} not found" + )) + })?; + + let config = build_agent_resume_config(turn.as_ref(), child_depth)?; + let resumed_thread_id = session + .services + .agent_control + .resume_agent_from_rollout( + config, + rollout_path, + thread_spawn_source(session.conversation_id, child_depth), + ) + .await + .map_err(|err| collab_agent_error(receiver_thread_id, err))?; + + Ok(session + .services + .agent_control + .get_status(resumed_thread_id) + .await) + } +} + mod wait { use super::*; use crate::agent::status::is_final; @@ -585,14 +731,39 @@ fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError { } } +fn thread_spawn_source(parent_thread_id: ThreadId, depth: i32) -> SessionSource { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth, + }) +} + fn build_agent_spawn_config( base_instructions: &BaseInstructions, turn: &TurnContext, child_depth: i32, +) -> Result { + let mut config = build_agent_shared_config(turn, child_depth)?; + config.base_instructions = Some(base_instructions.text.clone()); + Ok(config) +} + +fn build_agent_resume_config( + turn: &TurnContext, + child_depth: i32, +) -> Result { + let mut config = build_agent_shared_config(turn, child_depth)?; + // For resume, keep base instructions sourced from rollout/session metadata. + config.base_instructions = None; + Ok(config) +} + +fn build_agent_shared_config( + turn: &TurnContext, + child_depth: i32, ) -> Result { let base_config = turn.config.clone(); let mut config = (*base_config).clone(); - config.base_instructions = Some(base_instructions.text.clone()); config.model = Some(turn.model_info.slug.clone()); config.model_provider = turn.provider.clone(); config.model_reasoning_effort = turn.reasoning_effort; @@ -883,6 +1054,193 @@ mod tests { .expect("shutdown should submit"); } + #[tokio::test] + async fn resume_agent_rejects_invalid_id() { + let (session, turn) = make_session_and_context().await; + let invocation = invocation( + Arc::new(session), + Arc::new(turn), + "resume_agent", + function_payload(json!({"id": "not-a-uuid"})), + ); + let Err(err) = CollabHandler.handle(invocation).await else { + panic!("invalid id should be rejected"); + }; + let FunctionCallError::RespondToModel(msg) = err else { + panic!("expected respond-to-model error"); + }; + assert!(msg.starts_with("invalid agent id not-a-uuid:")); + } + + #[tokio::test] + async fn resume_agent_reports_missing_agent() { + let (mut session, turn) = make_session_and_context().await; + let manager = thread_manager(); + session.services.agent_control = manager.agent_control(); + let agent_id = ThreadId::new(); + let invocation = invocation( + Arc::new(session), + Arc::new(turn), + "resume_agent", + function_payload(json!({"id": agent_id.to_string()})), + ); + let Err(err) = CollabHandler.handle(invocation).await else { + panic!("missing agent should be reported"); + }; + assert_eq!( + err, + FunctionCallError::RespondToModel(format!("agent with id {agent_id} not found")) + ); + } + + #[tokio::test] + async fn resume_agent_noops_for_active_agent() { + let (mut session, turn) = make_session_and_context().await; + let manager = thread_manager(); + session.services.agent_control = manager.agent_control(); + let config = turn.config.as_ref().clone(); + let thread = manager.start_thread(config).await.expect("start thread"); + let agent_id = thread.thread_id; + let status_before = manager.agent_control().get_status(agent_id).await; + let invocation = invocation( + Arc::new(session), + Arc::new(turn), + "resume_agent", + function_payload(json!({"id": agent_id.to_string()})), + ); + + let output = CollabHandler + .handle(invocation) + .await + .expect("resume_agent should succeed"); + let ToolOutput::Function { + body: FunctionCallOutputBody::Text(content), + success, + .. + } = output + else { + panic!("expected function output"); + }; + let result: resume_agent::ResumeAgentResult = + serde_json::from_str(&content).expect("resume_agent result should be json"); + assert_eq!(result.status, status_before); + assert_eq!(success, Some(true)); + + let thread_ids = manager.list_thread_ids().await; + assert_eq!(thread_ids, vec![agent_id]); + + let _ = thread + .thread + .submit(Op::Shutdown {}) + .await + .expect("shutdown should submit"); + } + + #[tokio::test] + async fn resume_agent_restores_closed_agent_and_accepts_send_input() { + let (mut session, turn) = make_session_and_context().await; + let manager = thread_manager(); + session.services.agent_control = manager.agent_control(); + let config = turn.config.as_ref().clone(); + let thread = manager.start_thread(config).await.expect("start thread"); + let agent_id = thread.thread_id; + let _ = manager + .agent_control() + .shutdown_agent(agent_id) + .await + .expect("shutdown agent"); + assert_eq!( + manager.agent_control().get_status(agent_id).await, + AgentStatus::NotFound + ); + let session = Arc::new(session); + let turn = Arc::new(turn); + + let resume_invocation = invocation( + session.clone(), + turn.clone(), + "resume_agent", + function_payload(json!({"id": agent_id.to_string()})), + ); + let output = CollabHandler + .handle(resume_invocation) + .await + .expect("resume_agent should succeed"); + let ToolOutput::Function { + body: FunctionCallOutputBody::Text(content), + success, + .. + } = output + else { + panic!("expected function output"); + }; + let result: resume_agent::ResumeAgentResult = + serde_json::from_str(&content).expect("resume_agent result should be json"); + assert_ne!(result.status, AgentStatus::NotFound); + assert_eq!(success, Some(true)); + + let send_invocation = invocation( + session, + turn, + "send_input", + function_payload(json!({"id": agent_id.to_string(), "message": "hello"})), + ); + let output = CollabHandler + .handle(send_invocation) + .await + .expect("send_input should succeed after resume"); + let ToolOutput::Function { + body: FunctionCallOutputBody::Text(content), + success, + .. + } = output + else { + panic!("expected function output"); + }; + let result: serde_json::Value = + serde_json::from_str(&content).expect("send_input result should be json"); + let submission_id = result + .get("submission_id") + .and_then(|value| value.as_str()) + .unwrap_or_default(); + assert!(!submission_id.is_empty()); + assert_eq!(success, Some(true)); + + let _ = manager + .agent_control() + .shutdown_agent(agent_id) + .await + .expect("shutdown resumed agent"); + } + + #[tokio::test] + async fn resume_agent_rejects_when_depth_limit_exceeded() { + let (mut session, mut turn) = make_session_and_context().await; + let manager = thread_manager(); + session.services.agent_control = manager.agent_control(); + + turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: session.conversation_id, + depth: MAX_THREAD_SPAWN_DEPTH, + }); + + let invocation = invocation( + Arc::new(session), + Arc::new(turn), + "resume_agent", + function_payload(json!({"id": ThreadId::new().to_string()})), + ); + let Err(err) = CollabHandler.handle(invocation).await else { + panic!("resume should fail when depth limit exceeded"); + }; + assert_eq!( + err, + FunctionCallError::RespondToModel( + "Agent depth limit reached. Solve the task yourself.".to_string() + ) + ); + } + #[derive(Debug, Deserialize, PartialEq, Eq)] struct WaitResult { status: HashMap, @@ -1259,4 +1617,35 @@ mod tests { assert_eq!(config.user_instructions, base_config.user_instructions); } + + #[tokio::test] + async fn build_agent_resume_config_clears_base_instructions() { + let (_session, mut turn) = make_session_and_context().await; + let mut base_config = (*turn.config).clone(); + base_config.base_instructions = Some("caller-base".to_string()); + turn.config = Arc::new(base_config); + + let config = build_agent_resume_config(&turn, 0).expect("resume config"); + + let mut expected = (*turn.config).clone(); + expected.base_instructions = None; + expected.model = Some(turn.model_info.slug.clone()); + expected.model_provider = turn.provider.clone(); + expected.model_reasoning_effort = turn.reasoning_effort; + expected.model_reasoning_summary = turn.reasoning_summary; + expected.developer_instructions = turn.developer_instructions.clone(); + expected.compact_prompt = turn.compact_prompt.clone(); + expected.shell_environment_policy = turn.shell_environment_policy.clone(); + expected.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone(); + expected.cwd = turn.cwd.clone(); + expected + .approval_policy + .set(turn.approval_policy) + .expect("approval policy set"); + expected + .sandbox_policy + .set(turn.sandbox_policy) + .expect("sandbox policy set"); + assert_eq!(config, expected); + } } diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index ddf3671785..8e4c47722d 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -47,9 +47,10 @@ pub(crate) fn build_command_spec( /// the original script: /// /// shell -lc "