Add app-server next-turn state API

This commit is contained in:
Eric Traut
2026-05-13 09:10:18 -07:00
parent 63a3a26013
commit 7c16071325
29 changed files with 4259 additions and 112 deletions
+1
View File
@@ -176,6 +176,7 @@ pub(crate) fn server_notification_requires_delivery(notification: &ServerNotific
matches!(
notification,
ServerNotification::TurnCompleted(_)
| ServerNotification::ThreadTurnContextUpdated(_)
| ServerNotification::ItemCompleted(_)
| ServerNotification::AgentMessageDelta(_)
| ServerNotification::PlanDelta(_)
@@ -4053,6 +4053,133 @@
],
"type": "string"
},
"ThreadTurnContextUpdateParams": {
"properties": {
"approvalPolicy": {
"anyOf": [
{
"$ref": "#/definitions/AskForApproval"
},
{
"type": "null"
}
],
"description": "Override the approval policy for subsequent turns."
},
"approvalsReviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "Override where approval requests are routed for review on subsequent turns."
},
"collaborationMode": {
"anyOf": [
{
"$ref": "#/definitions/CollaborationMode"
},
{
"type": "null"
}
],
"description": "Set a pre-set collaboration mode for subsequent turns."
},
"cwd": {
"description": "Override the working directory for subsequent turns.",
"type": [
"string",
"null"
]
},
"effort": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the reasoning effort for subsequent turns."
},
"model": {
"description": "Override the model for subsequent turns.",
"type": [
"string",
"null"
]
},
"permissions": {
"anyOf": [
{
"$ref": "#/definitions/PermissionProfileSelectionParams"
},
{
"type": "null"
}
],
"description": "Select a named permissions profile for subsequent turns. Cannot be combined with `sandboxPolicy`."
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
],
"description": "Override the personality for subsequent turns."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Override the sandbox policy for subsequent turns."
},
"serviceTier": {
"description": "Override the service tier for subsequent turns.",
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
],
"description": "Override the reasoning summary for subsequent turns."
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadUnarchiveParams": {
"properties": {
"threadId": {
@@ -5470,6 +5597,30 @@
"title": "Turn/startRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/turnContext/update"
],
"title": "Thread/turnContext/updateRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadTurnContextUpdateParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/turnContext/updateRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -64,6 +64,59 @@
},
"type": "object"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
},
"modifications": {
"default": [],
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
"items": {
"$ref": "#/definitions/ActivePermissionProfileModification"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"ActivePermissionProfileModification": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootActivePermissionProfileModification",
"type": "object"
}
]
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
@@ -415,6 +468,65 @@
],
"type": "object"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
"user",
"auto_review",
"guardian_subagent"
],
"type": "string"
},
"AskForApproval": {
"oneOf": [
{
"enum": [
"untrusted",
"on-failure",
"on-request",
"never"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"granular": {
"properties": {
"mcp_elicitations": {
"type": "boolean"
},
"request_permissions": {
"default": false,
"type": "boolean"
},
"rules": {
"type": "boolean"
},
"sandbox_approval": {
"type": "boolean"
},
"skill_approval": {
"default": false,
"type": "boolean"
}
},
"required": [
"mcp_elicitations",
"rules",
"sandbox_approval"
],
"type": "object"
}
},
"required": [
"granular"
],
"title": "GranularAskForApproval",
"type": "object"
}
]
},
"AuthMode": {
"description": "Authentication mode for OpenAI-backed providers.",
"oneOf": [
@@ -658,6 +770,22 @@
],
"type": "string"
},
"CollaborationMode": {
"description": "Collaboration mode for a Codex session.",
"properties": {
"mode": {
"$ref": "#/definitions/ModeKind"
},
"settings": {
"$ref": "#/definitions/Settings"
}
},
"required": [
"mode",
"settings"
],
"type": "object"
},
"CommandAction": {
"oneOf": [
{
@@ -2250,6 +2378,14 @@
}
]
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"default"
],
"type": "string"
},
"ModelRerouteReason": {
"enum": [
"highRiskCyberActivity"
@@ -2311,6 +2447,13 @@
],
"type": "object"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
@@ -2394,6 +2537,143 @@
}
]
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"managed"
],
"title": "ManagedPermissionProfileType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"type"
],
"title": "ManagedPermissionProfile",
"type": "object"
},
{
"description": "Do not apply an outer sandbox.",
"properties": {
"type": {
"enum": [
"disabled"
],
"title": "DisabledPermissionProfileType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DisabledPermissionProfile",
"type": "object"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"oneOf": [
{
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"Personality": {
"enum": [
"none",
"friendly",
"pragmatic"
],
"type": "string"
},
"PlanDeltaNotification": {
"description": "EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should not assume concatenated deltas match the completed plan item content.",
"properties": {
@@ -2647,6 +2927,26 @@
],
"type": "string"
},
"ReasoningSummary": {
"description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries",
"oneOf": [
{
"enum": [
"auto",
"concise",
"detailed"
],
"type": "string"
},
{
"description": "Option to disable reasoning summaries.",
"enum": [
"none"
],
"type": "string"
}
]
},
"ReasoningSummaryPartAddedNotification": {
"properties": {
"itemId": {
@@ -2795,6 +3095,105 @@
},
"type": "object"
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
},
"ServerRequestResolvedNotification": {
"properties": {
"requestId": {
@@ -2850,6 +3249,34 @@
}
]
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
"developer_instructions": {
"type": [
"string",
"null"
]
},
"model": {
"type": "string"
},
"reasoning_effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
}
},
"required": [
"model"
],
"type": "object"
},
"SkillsChangedNotification": {
"description": "Notification emitted when watched local skill files change.\n\nTreat this as an invalidation signal and re-run `skills/list` with the client's current parameters when refreshed skill metadata is needed.",
"type": "object"
@@ -4284,6 +4711,106 @@
],
"type": "object"
},
"ThreadTurnContext": {
"properties": {
"activePermissionProfile": {
"anyOf": [
{
"$ref": "#/definitions/ActivePermissionProfile"
},
{
"type": "null"
}
]
},
"approvalPolicy": {
"$ref": "#/definitions/AskForApproval"
},
"approvalsReviewer": {
"$ref": "#/definitions/ApprovalsReviewer"
},
"collaborationMode": {
"$ref": "#/definitions/CollaborationMode"
},
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"model": {
"type": "string"
},
"modelProvider": {
"type": "string"
},
"permissionProfile": {
"$ref": "#/definitions/PermissionProfile"
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
]
},
"sandboxPolicy": {
"$ref": "#/definitions/SandboxPolicy"
},
"serviceTier": {
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
]
}
},
"required": [
"approvalPolicy",
"approvalsReviewer",
"collaborationMode",
"cwd",
"model",
"modelProvider",
"permissionProfile",
"sandboxPolicy"
],
"type": "object"
},
"ThreadTurnContextUpdatedNotification": {
"properties": {
"threadId": {
"type": "string"
},
"turnContext": {
"$ref": "#/definitions/ThreadTurnContext"
}
},
"required": [
"threadId",
"turnContext"
],
"type": "object"
},
"ThreadUnarchivedNotification": {
"properties": {
"threadId": {
@@ -4913,6 +5440,26 @@
"title": "Thread/status/changedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/turnContext/updated"
],
"title": "Thread/turnContext/updatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadTurnContextUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/turnContext/updatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -1261,6 +1261,30 @@
"title": "Turn/startRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"thread/turnContext/update"
],
"title": "Thread/turnContext/updateRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadTurnContextUpdateParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/turnContext/updateRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -3867,6 +3891,26 @@
"title": "Thread/status/changedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/turnContext/updated"
],
"title": "Thread/turnContext/updatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadTurnContextUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/turnContext/updatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -17550,6 +17594,250 @@
"title": "ThreadTokenUsageUpdatedNotification",
"type": "object"
},
"ThreadTurnContext": {
"properties": {
"activePermissionProfile": {
"anyOf": [
{
"$ref": "#/definitions/v2/ActivePermissionProfile"
},
{
"type": "null"
}
]
},
"approvalPolicy": {
"$ref": "#/definitions/v2/AskForApproval"
},
"approvalsReviewer": {
"$ref": "#/definitions/v2/ApprovalsReviewer"
},
"collaborationMode": {
"$ref": "#/definitions/v2/CollaborationMode"
},
"cwd": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"effort": {
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningEffort"
},
{
"type": "null"
}
]
},
"model": {
"type": "string"
},
"modelProvider": {
"type": "string"
},
"permissionProfile": {
"$ref": "#/definitions/v2/PermissionProfile"
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/v2/Personality"
},
{
"type": "null"
}
]
},
"sandboxPolicy": {
"$ref": "#/definitions/v2/SandboxPolicy"
},
"serviceTier": {
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningSummary"
},
{
"type": "null"
}
]
}
},
"required": [
"approvalPolicy",
"approvalsReviewer",
"collaborationMode",
"cwd",
"model",
"modelProvider",
"permissionProfile",
"sandboxPolicy"
],
"type": "object"
},
"ThreadTurnContextUpdateParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"approvalPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/AskForApproval"
},
{
"type": "null"
}
],
"description": "Override the approval policy for subsequent turns."
},
"approvalsReviewer": {
"anyOf": [
{
"$ref": "#/definitions/v2/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "Override where approval requests are routed for review on subsequent turns."
},
"collaborationMode": {
"anyOf": [
{
"$ref": "#/definitions/v2/CollaborationMode"
},
{
"type": "null"
}
],
"description": "Set a pre-set collaboration mode for subsequent turns."
},
"cwd": {
"description": "Override the working directory for subsequent turns.",
"type": [
"string",
"null"
]
},
"effort": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningEffort"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the reasoning effort for subsequent turns."
},
"model": {
"description": "Override the model for subsequent turns.",
"type": [
"string",
"null"
]
},
"permissions": {
"anyOf": [
{
"$ref": "#/definitions/v2/PermissionProfileSelectionParams"
},
{
"type": "null"
}
],
"description": "Select a named permissions profile for subsequent turns. Cannot be combined with `sandboxPolicy`."
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/v2/Personality"
},
{
"type": "null"
}
],
"description": "Override the personality for subsequent turns."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Override the sandbox policy for subsequent turns."
},
"serviceTier": {
"description": "Override the service tier for subsequent turns.",
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningSummary"
},
{
"type": "null"
}
],
"description": "Override the reasoning summary for subsequent turns."
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadTurnContextUpdateParams",
"type": "object"
},
"ThreadTurnContextUpdateResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"turnContext": {
"$ref": "#/definitions/v2/ThreadTurnContext"
}
},
"required": [
"turnContext"
],
"title": "ThreadTurnContextUpdateResponse",
"type": "object"
},
"ThreadTurnContextUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
},
"turnContext": {
"$ref": "#/definitions/v2/ThreadTurnContext"
}
},
"required": [
"threadId",
"turnContext"
],
"title": "ThreadTurnContextUpdatedNotification",
"type": "object"
},
"ThreadUnarchiveParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -2001,6 +2001,30 @@
"title": "Turn/startRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/turnContext/update"
],
"title": "Thread/turnContext/updateRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadTurnContextUpdateParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/turnContext/updateRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -11182,6 +11206,26 @@
"title": "Thread/status/changedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/turnContext/updated"
],
"title": "Thread/turnContext/updatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadTurnContextUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/turnContext/updatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -15374,6 +15418,250 @@
"title": "ThreadTokenUsageUpdatedNotification",
"type": "object"
},
"ThreadTurnContext": {
"properties": {
"activePermissionProfile": {
"anyOf": [
{
"$ref": "#/definitions/ActivePermissionProfile"
},
{
"type": "null"
}
]
},
"approvalPolicy": {
"$ref": "#/definitions/AskForApproval"
},
"approvalsReviewer": {
"$ref": "#/definitions/ApprovalsReviewer"
},
"collaborationMode": {
"$ref": "#/definitions/CollaborationMode"
},
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"model": {
"type": "string"
},
"modelProvider": {
"type": "string"
},
"permissionProfile": {
"$ref": "#/definitions/PermissionProfile"
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
]
},
"sandboxPolicy": {
"$ref": "#/definitions/SandboxPolicy"
},
"serviceTier": {
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
]
}
},
"required": [
"approvalPolicy",
"approvalsReviewer",
"collaborationMode",
"cwd",
"model",
"modelProvider",
"permissionProfile",
"sandboxPolicy"
],
"type": "object"
},
"ThreadTurnContextUpdateParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"approvalPolicy": {
"anyOf": [
{
"$ref": "#/definitions/AskForApproval"
},
{
"type": "null"
}
],
"description": "Override the approval policy for subsequent turns."
},
"approvalsReviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "Override where approval requests are routed for review on subsequent turns."
},
"collaborationMode": {
"anyOf": [
{
"$ref": "#/definitions/CollaborationMode"
},
{
"type": "null"
}
],
"description": "Set a pre-set collaboration mode for subsequent turns."
},
"cwd": {
"description": "Override the working directory for subsequent turns.",
"type": [
"string",
"null"
]
},
"effort": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the reasoning effort for subsequent turns."
},
"model": {
"description": "Override the model for subsequent turns.",
"type": [
"string",
"null"
]
},
"permissions": {
"anyOf": [
{
"$ref": "#/definitions/PermissionProfileSelectionParams"
},
{
"type": "null"
}
],
"description": "Select a named permissions profile for subsequent turns. Cannot be combined with `sandboxPolicy`."
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
],
"description": "Override the personality for subsequent turns."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Override the sandbox policy for subsequent turns."
},
"serviceTier": {
"description": "Override the service tier for subsequent turns.",
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
],
"description": "Override the reasoning summary for subsequent turns."
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadTurnContextUpdateParams",
"type": "object"
},
"ThreadTurnContextUpdateResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"turnContext": {
"$ref": "#/definitions/ThreadTurnContext"
}
},
"required": [
"turnContext"
],
"title": "ThreadTurnContextUpdateResponse",
"type": "object"
},
"ThreadTurnContextUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
},
"turnContext": {
"$ref": "#/definitions/ThreadTurnContext"
}
},
"required": [
"threadId",
"turnContext"
],
"title": "ThreadTurnContextUpdatedNotification",
"type": "object"
},
"ThreadUnarchiveParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -0,0 +1,451 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
"user",
"auto_review",
"guardian_subagent"
],
"type": "string"
},
"AskForApproval": {
"oneOf": [
{
"enum": [
"untrusted",
"on-failure",
"on-request",
"never"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"granular": {
"properties": {
"mcp_elicitations": {
"type": "boolean"
},
"request_permissions": {
"default": false,
"type": "boolean"
},
"rules": {
"type": "boolean"
},
"sandbox_approval": {
"type": "boolean"
},
"skill_approval": {
"default": false,
"type": "boolean"
}
},
"required": [
"mcp_elicitations",
"rules",
"sandbox_approval"
],
"type": "object"
}
},
"required": [
"granular"
],
"title": "GranularAskForApproval",
"type": "object"
}
]
},
"CollaborationMode": {
"description": "Collaboration mode for a Codex session.",
"properties": {
"mode": {
"$ref": "#/definitions/ModeKind"
},
"settings": {
"$ref": "#/definitions/Settings"
}
},
"required": [
"mode",
"settings"
],
"type": "object"
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"default"
],
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"PermissionProfileModificationParams": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootPermissionProfileModificationParams",
"type": "object"
}
]
},
"PermissionProfileSelectionParams": {
"oneOf": [
{
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
"properties": {
"id": {
"type": "string"
},
"modifications": {
"items": {
"$ref": "#/definitions/PermissionProfileModificationParams"
},
"type": [
"array",
"null"
]
},
"type": {
"enum": [
"profile"
],
"title": "ProfilePermissionProfileSelectionParamsType",
"type": "string"
}
},
"required": [
"id",
"type"
],
"title": "ProfilePermissionProfileSelectionParams",
"type": "object"
}
]
},
"Personality": {
"enum": [
"none",
"friendly",
"pragmatic"
],
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string"
},
"ReasoningSummary": {
"description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries",
"oneOf": [
{
"enum": [
"auto",
"concise",
"detailed"
],
"type": "string"
},
{
"description": "Option to disable reasoning summaries.",
"enum": [
"none"
],
"type": "string"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
"developer_instructions": {
"type": [
"string",
"null"
]
},
"model": {
"type": "string"
},
"reasoning_effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
}
},
"required": [
"model"
],
"type": "object"
}
},
"properties": {
"approvalPolicy": {
"anyOf": [
{
"$ref": "#/definitions/AskForApproval"
},
{
"type": "null"
}
],
"description": "Override the approval policy for subsequent turns."
},
"approvalsReviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "Override where approval requests are routed for review on subsequent turns."
},
"collaborationMode": {
"anyOf": [
{
"$ref": "#/definitions/CollaborationMode"
},
{
"type": "null"
}
],
"description": "Set a pre-set collaboration mode for subsequent turns."
},
"cwd": {
"description": "Override the working directory for subsequent turns.",
"type": [
"string",
"null"
]
},
"effort": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
],
"description": "Override the reasoning effort for subsequent turns."
},
"model": {
"description": "Override the model for subsequent turns.",
"type": [
"string",
"null"
]
},
"permissions": {
"anyOf": [
{
"$ref": "#/definitions/PermissionProfileSelectionParams"
},
{
"type": "null"
}
],
"description": "Select a named permissions profile for subsequent turns. Cannot be combined with `sandboxPolicy`."
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
],
"description": "Override the personality for subsequent turns."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Override the sandbox policy for subsequent turns."
},
"serviceTier": {
"description": "Override the service tier for subsequent turns.",
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
],
"description": "Override the reasoning summary for subsequent turns."
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadTurnContextUpdateParams",
"type": "object"
}
@@ -0,0 +1,739 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
},
"modifications": {
"default": [],
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
"items": {
"$ref": "#/definitions/ActivePermissionProfileModification"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"ActivePermissionProfileModification": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootActivePermissionProfileModification",
"type": "object"
}
]
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
"user",
"auto_review",
"guardian_subagent"
],
"type": "string"
},
"AskForApproval": {
"oneOf": [
{
"enum": [
"untrusted",
"on-failure",
"on-request",
"never"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"granular": {
"properties": {
"mcp_elicitations": {
"type": "boolean"
},
"request_permissions": {
"default": false,
"type": "boolean"
},
"rules": {
"type": "boolean"
},
"sandbox_approval": {
"type": "boolean"
},
"skill_approval": {
"default": false,
"type": "boolean"
}
},
"required": [
"mcp_elicitations",
"rules",
"sandbox_approval"
],
"type": "object"
}
},
"required": [
"granular"
],
"title": "GranularAskForApproval",
"type": "object"
}
]
},
"CollaborationMode": {
"description": "Collaboration mode for a Codex session.",
"properties": {
"mode": {
"$ref": "#/definitions/ModeKind"
},
"settings": {
"$ref": "#/definitions/Settings"
}
},
"required": [
"mode",
"settings"
],
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"default"
],
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"managed"
],
"title": "ManagedPermissionProfileType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"type"
],
"title": "ManagedPermissionProfile",
"type": "object"
},
{
"description": "Do not apply an outer sandbox.",
"properties": {
"type": {
"enum": [
"disabled"
],
"title": "DisabledPermissionProfileType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DisabledPermissionProfile",
"type": "object"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"oneOf": [
{
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"Personality": {
"enum": [
"none",
"friendly",
"pragmatic"
],
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string"
},
"ReasoningSummary": {
"description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries",
"oneOf": [
{
"enum": [
"auto",
"concise",
"detailed"
],
"type": "string"
},
{
"description": "Option to disable reasoning summaries.",
"enum": [
"none"
],
"type": "string"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
"developer_instructions": {
"type": [
"string",
"null"
]
},
"model": {
"type": "string"
},
"reasoning_effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
}
},
"required": [
"model"
],
"type": "object"
},
"ThreadTurnContext": {
"properties": {
"activePermissionProfile": {
"anyOf": [
{
"$ref": "#/definitions/ActivePermissionProfile"
},
{
"type": "null"
}
]
},
"approvalPolicy": {
"$ref": "#/definitions/AskForApproval"
},
"approvalsReviewer": {
"$ref": "#/definitions/ApprovalsReviewer"
},
"collaborationMode": {
"$ref": "#/definitions/CollaborationMode"
},
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"model": {
"type": "string"
},
"modelProvider": {
"type": "string"
},
"permissionProfile": {
"$ref": "#/definitions/PermissionProfile"
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
]
},
"sandboxPolicy": {
"$ref": "#/definitions/SandboxPolicy"
},
"serviceTier": {
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
]
}
},
"required": [
"approvalPolicy",
"approvalsReviewer",
"collaborationMode",
"cwd",
"model",
"modelProvider",
"permissionProfile",
"sandboxPolicy"
],
"type": "object"
}
},
"properties": {
"turnContext": {
"$ref": "#/definitions/ThreadTurnContext"
}
},
"required": [
"turnContext"
],
"title": "ThreadTurnContextUpdateResponse",
"type": "object"
}
@@ -0,0 +1,743 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
},
"modifications": {
"default": [],
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
"items": {
"$ref": "#/definitions/ActivePermissionProfileModification"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"ActivePermissionProfileModification": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootActivePermissionProfileModification",
"type": "object"
}
]
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
"user",
"auto_review",
"guardian_subagent"
],
"type": "string"
},
"AskForApproval": {
"oneOf": [
{
"enum": [
"untrusted",
"on-failure",
"on-request",
"never"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"granular": {
"properties": {
"mcp_elicitations": {
"type": "boolean"
},
"request_permissions": {
"default": false,
"type": "boolean"
},
"rules": {
"type": "boolean"
},
"sandbox_approval": {
"type": "boolean"
},
"skill_approval": {
"default": false,
"type": "boolean"
}
},
"required": [
"mcp_elicitations",
"rules",
"sandbox_approval"
],
"type": "object"
}
},
"required": [
"granular"
],
"title": "GranularAskForApproval",
"type": "object"
}
]
},
"CollaborationMode": {
"description": "Collaboration mode for a Codex session.",
"properties": {
"mode": {
"$ref": "#/definitions/ModeKind"
},
"settings": {
"$ref": "#/definitions/Settings"
}
},
"required": [
"mode",
"settings"
],
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"default"
],
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"managed"
],
"title": "ManagedPermissionProfileType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"type"
],
"title": "ManagedPermissionProfile",
"type": "object"
},
{
"description": "Do not apply an outer sandbox.",
"properties": {
"type": {
"enum": [
"disabled"
],
"title": "DisabledPermissionProfileType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DisabledPermissionProfile",
"type": "object"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"oneOf": [
{
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"Personality": {
"enum": [
"none",
"friendly",
"pragmatic"
],
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string"
},
"ReasoningSummary": {
"description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries",
"oneOf": [
{
"enum": [
"auto",
"concise",
"detailed"
],
"type": "string"
},
{
"description": "Option to disable reasoning summaries.",
"enum": [
"none"
],
"type": "string"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
"developer_instructions": {
"type": [
"string",
"null"
]
},
"model": {
"type": "string"
},
"reasoning_effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
}
},
"required": [
"model"
],
"type": "object"
},
"ThreadTurnContext": {
"properties": {
"activePermissionProfile": {
"anyOf": [
{
"$ref": "#/definitions/ActivePermissionProfile"
},
{
"type": "null"
}
]
},
"approvalPolicy": {
"$ref": "#/definitions/AskForApproval"
},
"approvalsReviewer": {
"$ref": "#/definitions/ApprovalsReviewer"
},
"collaborationMode": {
"$ref": "#/definitions/CollaborationMode"
},
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"model": {
"type": "string"
},
"modelProvider": {
"type": "string"
},
"permissionProfile": {
"$ref": "#/definitions/PermissionProfile"
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
]
},
"sandboxPolicy": {
"$ref": "#/definitions/SandboxPolicy"
},
"serviceTier": {
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
]
}
},
"required": [
"approvalPolicy",
"approvalsReviewer",
"collaborationMode",
"cwd",
"model",
"modelProvider",
"permissionProfile",
"sandboxPolicy"
],
"type": "object"
}
},
"properties": {
"threadId": {
"type": "string"
},
"turnContext": {
"$ref": "#/definitions/ThreadTurnContext"
}
},
"required": [
"threadId",
"turnContext"
],
"title": "ThreadTurnContextUpdatedNotification",
"type": "object"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
// 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 { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { CollaborationMode } from "../CollaborationMode";
import type { Personality } from "../Personality";
import type { ReasoningEffort } from "../ReasoningEffort";
import type { ReasoningSummary } from "../ReasoningSummary";
import type { ActivePermissionProfile } from "./ActivePermissionProfile";
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
import type { AskForApproval } from "./AskForApproval";
import type { PermissionProfile } from "./PermissionProfile";
import type { SandboxPolicy } from "./SandboxPolicy";
export type ThreadTurnContext = { model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, permissionProfile: PermissionProfile, activePermissionProfile: ActivePermissionProfile | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, personality: Personality | null, collaborationMode: CollaborationMode, };
@@ -0,0 +1,58 @@
// 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 { CollaborationMode } from "../CollaborationMode";
import type { Personality } from "../Personality";
import type { ReasoningEffort } from "../ReasoningEffort";
import type { ReasoningSummary } from "../ReasoningSummary";
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
import type { AskForApproval } from "./AskForApproval";
import type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams";
import type { SandboxPolicy } from "./SandboxPolicy";
export type ThreadTurnContextUpdateParams = { threadId: string,
/**
* Override the working directory for subsequent turns.
*/
cwd?: string | null,
/**
* Override the approval policy for subsequent turns.
*/
approvalPolicy?: AskForApproval | null,
/**
* Override where approval requests are routed for review on subsequent turns.
*/
approvalsReviewer?: ApprovalsReviewer | null,
/**
* Override the sandbox policy for subsequent turns.
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Select a named permissions profile for subsequent turns. Cannot be
* combined with `sandboxPolicy`.
*/
permissions?: PermissionProfileSelectionParams | null,
/**
* Override the model for subsequent turns.
*/
model?: string | null,
/**
* Override the service tier for subsequent turns.
*/
serviceTier?: string | null | null,
/**
* Override the reasoning effort for subsequent turns.
*/
effort?: ReasoningEffort | null | null,
/**
* Override the reasoning summary for subsequent turns.
*/
summary?: ReasoningSummary | null,
/**
* Override the personality for subsequent turns.
*/
personality?: Personality | null,
/**
* Set a pre-set collaboration mode for subsequent turns.
*/
collaborationMode?: CollaborationMode | null, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadTurnContext } from "./ThreadTurnContext";
export type ThreadTurnContextUpdateResponse = { turnContext: ThreadTurnContext, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadTurnContext } from "./ThreadTurnContext";
export type ThreadTurnContextUpdatedNotification = { threadId: string, turnContext: ThreadTurnContext, };
@@ -409,6 +409,10 @@ export type { ThreadStatus } from "./ThreadStatus";
export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification";
export type { ThreadTokenUsage } from "./ThreadTokenUsage";
export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification";
export type { ThreadTurnContext } from "./ThreadTurnContext";
export type { ThreadTurnContextUpdateParams } from "./ThreadTurnContextUpdateParams";
export type { ThreadTurnContextUpdateResponse } from "./ThreadTurnContextUpdateResponse";
export type { ThreadTurnContextUpdatedNotification } from "./ThreadTurnContextUpdatedNotification";
export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams";
export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse";
export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification";
@@ -732,6 +732,12 @@ client_request_definitions! {
serialization: thread_id(params.thread_id),
response: v2::TurnStartResponse,
},
ThreadTurnContextUpdate => "thread/turnContext/update" {
params: v2::ThreadTurnContextUpdateParams,
inspect_params: true,
serialization: thread_id(params.thread_id),
response: v2::ThreadTurnContextUpdateResponse,
},
TurnSteer => "turn/steer" {
params: v2::TurnSteerParams,
inspect_params: true,
@@ -1432,6 +1438,7 @@ server_notification_definitions! {
Error => "error" (v2::ErrorNotification),
ThreadStarted => "thread/started" (v2::ThreadStartedNotification),
ThreadStatusChanged => "thread/status/changed" (v2::ThreadStatusChangedNotification),
ThreadTurnContextUpdated => "thread/turnContext/updated" (v2::ThreadTurnContextUpdatedNotification),
ThreadArchived => "thread/archived" (v2::ThreadArchivedNotification),
ThreadUnarchived => "thread/unarchived" (v2::ThreadUnarchivedNotification),
ThreadClosed => "thread/closed" (v2::ThreadClosedNotification),
@@ -1531,6 +1538,9 @@ mod tests {
use anyhow::Result;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::RealtimeConversationVersion;
use codex_protocol::protocol::RealtimeOutputModality;
@@ -1552,6 +1562,31 @@ mod tests {
test_path_buf(&path).abs()
}
fn sample_thread_turn_context(cwd: AbsolutePathBuf) -> v2::ThreadTurnContext {
v2::ThreadTurnContext {
model: "gpt-5".to_string(),
model_provider: "openai".to_string(),
service_tier: None,
cwd,
approval_policy: v2::AskForApproval::OnFailure,
approvals_reviewer: v2::ApprovalsReviewer::User,
sandbox_policy: v2::SandboxPolicy::DangerFullAccess,
permission_profile: v2::PermissionProfile::Disabled,
active_permission_profile: None,
effort: None,
summary: None,
personality: None,
collaboration_mode: CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5".to_string(),
reasoning_effort: None,
developer_instructions: None,
},
},
}
}
fn request_id() -> RequestId {
const REQUEST_ID: i64 = 1;
RequestId::Integer(REQUEST_ID)
@@ -1589,6 +1624,20 @@ mod tests {
})
);
let thread_turn_context_update = ClientRequest::ThreadTurnContextUpdate {
request_id: request_id(),
params: v2::ThreadTurnContextUpdateParams {
thread_id: thread_id.clone(),
..Default::default()
},
};
assert_eq!(
thread_turn_context_update.serialization_scope(),
Some(ClientRequestSerializationScope::Thread {
thread_id: thread_id.clone()
})
);
let thread_fork = ClientRequest::ThreadFork {
request_id: request_id(),
params: v2::ThreadForkParams {
@@ -2263,6 +2312,40 @@ mod tests {
Ok(())
}
#[test]
fn serialize_thread_turn_context_update_request() -> Result<()> {
let request = ClientRequest::ThreadTurnContextUpdate {
request_id: RequestId::Integer(5),
params: v2::ThreadTurnContextUpdateParams {
thread_id: "thread-1".to_string(),
model: Some("gpt-5.2".to_string()),
service_tier: Some(None),
..Default::default()
},
};
assert_eq!(
json!({
"method": "thread/turnContext/update",
"id": 5,
"params": {
"threadId": "thread-1",
"cwd": null,
"approvalPolicy": null,
"approvalsReviewer": null,
"sandboxPolicy": null,
"permissions": null,
"model": "gpt-5.2",
"serviceTier": null,
"summary": null,
"personality": null,
"collaborationMode": null
}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_client_response() -> Result<()> {
let cwd = absolute_path("/tmp");
@@ -2354,6 +2437,72 @@ mod tests {
Ok(())
}
#[test]
fn serialize_thread_turn_context_response_and_notification() -> Result<()> {
let cwd = absolute_path("/tmp");
let turn_context = sample_thread_turn_context(cwd);
let response = ClientResponse::ThreadTurnContextUpdate {
request_id: RequestId::Integer(11),
response: v2::ThreadTurnContextUpdateResponse {
turn_context: turn_context.clone(),
},
};
let notification = ServerNotification::ThreadTurnContextUpdated(
v2::ThreadTurnContextUpdatedNotification {
thread_id: "thread-1".to_string(),
turn_context,
},
);
let turn_context_json = json!({
"model": "gpt-5",
"modelProvider": "openai",
"serviceTier": null,
"cwd": absolute_path_string("tmp"),
"approvalPolicy": "on-failure",
"approvalsReviewer": "user",
"sandboxPolicy": {
"type": "dangerFullAccess"
},
"permissionProfile": {
"type": "disabled"
},
"activePermissionProfile": null,
"effort": null,
"summary": null,
"personality": null,
"collaborationMode": {
"mode": "default",
"settings": {
"model": "gpt-5",
"reasoning_effort": null,
"developer_instructions": null
}
}
});
assert_eq!(
json!({
"method": "thread/turnContext/update",
"id": 11,
"response": {
"turnContext": turn_context_json
}
}),
serde_json::to_value(&response)?,
);
assert_eq!(
json!({
"method": "thread/turnContext/updated",
"params": {
"threadId": "thread-1",
"turnContext": turn_context_json
}
}),
serde_json::to_value(&notification)?,
);
Ok(())
}
#[test]
fn serialize_config_requirements_read() -> Result<()> {
let request = ClientRequest::ConfigRequirementsRead {
@@ -3486,6 +3486,39 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
assert_eq!(serialized_without_override.get("serviceTier"), None);
}
#[test]
fn thread_turn_context_update_params_support_partial_updates_and_explicit_nulls() {
let params: ThreadTurnContextUpdateParams = serde_json::from_value(json!({
"threadId": "thread_123",
"model": "gpt-5.2",
"serviceTier": null,
"effort": null
}))
.expect("params should deserialize");
assert_eq!(params.thread_id, "thread_123");
assert_eq!(params.model.as_deref(), Some("gpt-5.2"));
assert_eq!(params.service_tier, Some(None));
assert_eq!(params.effort, Some(None));
assert_eq!(params.cwd, None);
let serialized = serde_json::to_value(&params).expect("params should serialize");
assert_eq!(
serialized.get("serviceTier"),
Some(&serde_json::Value::Null)
);
assert_eq!(serialized.get("effort"), Some(&serde_json::Value::Null));
assert_eq!(serialized.get("cwd"), Some(&serde_json::Value::Null));
let without_overrides = ThreadTurnContextUpdateParams {
thread_id: "thread_123".to_string(),
..Default::default()
};
let serialized_without_overrides =
serde_json::to_value(&without_overrides).expect("params should serialize");
assert_eq!(serialized_without_overrides.get("serviceTier"), None);
assert_eq!(serialized_without_overrides.get("effort"), None);
}
#[test]
fn turn_start_params_round_trip_environments() {
let cwd = test_absolute_path();
@@ -13,7 +13,9 @@ use super::TurnEnvironmentParams;
use super::TurnItemsView;
use super::shared::v2_enum_from_core;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus;
@@ -169,6 +171,88 @@ pub struct ThreadStartParams {
pub persist_extended_history: bool,
}
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnContextUpdateParams {
pub thread_id: String,
/// Override the working directory for subsequent turns.
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
/// Override the approval policy for subsequent turns.
#[experimental(nested)]
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
/// Override where approval requests are routed for review on subsequent turns.
#[ts(optional = nullable)]
pub approvals_reviewer: Option<ApprovalsReviewer>,
/// Override the sandbox policy for subsequent turns.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Select a named permissions profile for subsequent turns. Cannot be
/// combined with `sandboxPolicy`.
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
/// Override the model for subsequent turns.
#[ts(optional = nullable)]
pub model: Option<String>,
/// Override the service tier for subsequent turns.
#[serde(
default,
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub service_tier: Option<Option<String>>,
/// Override the reasoning effort for subsequent turns.
#[serde(
default,
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub effort: Option<Option<ReasoningEffort>>,
/// Override the reasoning summary for subsequent turns.
#[ts(optional = nullable)]
pub summary: Option<ReasoningSummary>,
/// Override the personality for subsequent turns.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
/// Set a pre-set collaboration mode for subsequent turns.
#[ts(optional = nullable)]
pub collaboration_mode: Option<CollaborationMode>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnContext {
pub model: String,
pub model_provider: String,
pub service_tier: Option<String>,
pub cwd: AbsolutePathBuf,
pub approval_policy: AskForApproval,
pub approvals_reviewer: ApprovalsReviewer,
pub sandbox_policy: SandboxPolicy,
pub permission_profile: PermissionProfile,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub effort: Option<ReasoningEffort>,
pub summary: Option<ReasoningSummary>,
pub personality: Option<Personality>,
pub collaboration_mode: CollaborationMode,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnContextUpdateResponse {
pub turn_context: ThreadTurnContext,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -1131,6 +1215,14 @@ pub struct ThreadStatusChangedNotification {
pub status: ThreadStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnContextUpdatedNotification {
pub thread_id: String,
pub turn_context: ThreadTurnContext,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
+2
View File
@@ -157,6 +157,7 @@ Example with notification opt-out:
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
- `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. Prefer experimental `permissions` profile selection for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
- `thread/turnContext/update` — update the stored defaults used by subsequent turns without starting a turn. Omitted fields leave the current value unchanged; fields with explicit clearing support, such as `serviceTier`, accept `null` to clear the value. The response is `{ "turnContext": ... }` with the full effective state, and `thread/turnContext/updated` is emitted only when that state changes. `turn/start` emits the same notification when its turn-context overrides change the stored defaults.
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`.
- `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"`.
@@ -1210,6 +1211,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
- `turn/started` — `{ turn }` with the turn id, empty `items`, and `status: "inProgress"`.
- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`.
- `thread/turnContext/updated` — `{ threadId, turnContext }` whenever the effective next-turn state changes. `turnContext` includes the full effective state: model/provider, service tier, cwd, approval policy, approvals reviewer, sandbox compatibility projection, permission profile, active permission profile, reasoning effort/summary, personality, and collaboration mode.
- `turn/diff/updated` — `{ threadId, turnId, diff }` represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item. `diff` is the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individual `fileChange` items.
- `turn/plan/updated` — `{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`.
- `model/rerouted` — `{ threadId, turnId, fromModel, toModel, reason }` when the backend reroutes a request to a different model (for example, due to high-risk cyber safety checks).
+39 -1
View File
@@ -102,7 +102,10 @@ pub const DEFAULT_IN_PROCESS_CHANNEL_CAPACITY: usize = CHANNEL_CAPACITY;
type PendingClientRequestResponse = std::result::Result<Result, JSONRPCErrorError>;
fn server_notification_requires_delivery(notification: &ServerNotification) -> bool {
matches!(notification, ServerNotification::TurnCompleted(_))
matches!(
notification,
ServerNotification::TurnCompleted(_) | ServerNotification::ThreadTurnContextUpdated(_)
)
}
/// Input needed to start an in-process app-server runtime.
@@ -725,11 +728,19 @@ mod tests {
use codex_app_server_protocol::SessionSource as ApiSessionSource;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadTurnContext;
use codex_app_server_protocol::ThreadTurnContextUpdatedNotification;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnItemsView;
use codex_app_server_protocol::TurnStatus;
use codex_core::config::ConfigBuilder;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use pretty_assertions::assert_eq;
use std::path::Path;
use tempfile::TempDir;
@@ -885,5 +896,32 @@ mod tests {
},
})
));
assert!(server_notification_requires_delivery(
&ServerNotification::ThreadTurnContextUpdated(ThreadTurnContextUpdatedNotification {
thread_id: "thread-1".to_string(),
turn_context: ThreadTurnContext {
model: "gpt-5".to_string(),
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp/project").abs(),
approval_policy: codex_app_server_protocol::AskForApproval::Never,
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,
sandbox_policy: codex_app_server_protocol::SandboxPolicy::DangerFullAccess,
permission_profile: PermissionProfile::read_only().into(),
active_permission_profile: None,
effort: None,
summary: None,
personality: None,
collaboration_mode: CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5".to_string(),
reasoning_effort: None,
developer_instructions: None,
},
},
},
})
));
}
}
@@ -1143,6 +1143,11 @@ impl MessageProcessor {
)
.await
}
ClientRequest::ThreadTurnContextUpdate { params, .. } => {
self.turn_processor
.thread_turn_context_update(&request_id, params)
.await
}
ClientRequest::ThreadInjectItems { params, .. } => {
self.turn_processor.thread_inject_items(params).await
}
@@ -221,6 +221,10 @@ use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadStartedNotification;
use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::ThreadTurnContext;
use codex_app_server_protocol::ThreadTurnContextUpdateParams;
use codex_app_server_protocol::ThreadTurnContextUpdateResponse;
use codex_app_server_protocol::ThreadTurnContextUpdatedNotification;
use codex_app_server_protocol::ThreadTurnsItemsListParams;
use codex_app_server_protocol::ThreadTurnsListParams;
use codex_app_server_protocol::ThreadTurnsListResponse;
@@ -1,4 +1,5 @@
use super::*;
use codex_protocol::openai_models::ReasoningEffort;
#[derive(Clone)]
pub(crate) struct TurnRequestProcessor {
@@ -16,6 +17,75 @@ pub(crate) struct TurnRequestProcessor {
skills_watcher: Arc<SkillsWatcher>,
}
#[derive(Clone)]
struct TurnContextOverrideRequest {
cwd: Option<PathBuf>,
approval_policy: Option<AskForApproval>,
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
sandbox_policy: Option<codex_app_server_protocol::SandboxPolicy>,
permissions: Option<PermissionProfileSelectionParams>,
model: Option<String>,
service_tier: Option<Option<String>>,
effort: Option<Option<ReasoningEffort>>,
summary: Option<codex_protocol::config_types::ReasoningSummary>,
collaboration_mode: Option<CollaborationMode>,
personality: Option<Personality>,
}
impl TurnContextOverrideRequest {
fn has_any_overrides(&self) -> bool {
self.cwd.is_some()
|| self.approval_policy.is_some()
|| self.approvals_reviewer.is_some()
|| self.sandbox_policy.is_some()
|| self.permissions.is_some()
|| self.model.is_some()
|| self.service_tier.is_some()
|| self.effort.is_some()
|| self.summary.is_some()
|| self.collaboration_mode.is_some()
|| self.personality.is_some()
}
}
#[derive(Clone)]
struct ResolvedTurnContextOverrides {
has_any_overrides: bool,
cwd: Option<PathBuf>,
approval_policy: Option<codex_protocol::protocol::AskForApproval>,
approvals_reviewer: Option<codex_protocol::config_types::ApprovalsReviewer>,
sandbox_policy: Option<codex_protocol::protocol::SandboxPolicy>,
permission_profile: Option<codex_protocol::models::PermissionProfile>,
active_permission_profile: Option<codex_protocol::models::ActivePermissionProfile>,
windows_sandbox_level: Option<WindowsSandboxLevel>,
model: Option<String>,
effort: Option<Option<ReasoningEffort>>,
summary: Option<codex_protocol::config_types::ReasoningSummary>,
service_tier: Option<Option<String>>,
collaboration_mode: Option<CollaborationMode>,
personality: Option<Personality>,
}
impl ResolvedTurnContextOverrides {
fn to_core_overrides(&self) -> CodexThreadTurnContextOverrides {
CodexThreadTurnContextOverrides {
cwd: self.cwd.clone(),
approval_policy: self.approval_policy,
approvals_reviewer: self.approvals_reviewer,
sandbox_policy: self.sandbox_policy.clone(),
permission_profile: self.permission_profile.clone(),
active_permission_profile: self.active_permission_profile.clone(),
windows_sandbox_level: self.windows_sandbox_level,
model: self.model.clone(),
effort: self.effort,
summary: self.summary,
service_tier: self.service_tier.clone(),
collaboration_mode: self.collaboration_mode.clone(),
personality: self.personality,
}
}
}
impl TurnRequestProcessor {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
@@ -74,6 +144,16 @@ impl TurnRequestProcessor {
.map(|response| Some(response.into()))
}
pub(crate) async fn thread_turn_context_update(
&self,
request_id: &ConnectionRequestId,
params: ThreadTurnContextUpdateParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_turn_context_update_inner(request_id, params)
.await
.map(|response| Some(response.into()))
}
pub(crate) async fn turn_steer(
&self,
request_id: &ConnectionRequestId,
@@ -312,6 +392,105 @@ impl TurnRequestProcessor {
Ok(())
}
async fn resolve_turn_context_overrides(
&self,
base_snapshot: &ThreadConfigSnapshot,
request: TurnContextOverrideRequest,
) -> Result<ResolvedTurnContextOverrides, JSONRPCErrorError> {
if request.sandbox_policy.is_some() && request.permissions.is_some() {
return Err(invalid_request(
"`permissions` cannot be combined with `sandboxPolicy`",
));
}
let has_any_overrides = request.has_any_overrides();
let cwd = request.cwd;
let approval_policy = request.approval_policy.map(AskForApproval::to_core);
let approvals_reviewer = request
.approvals_reviewer
.map(codex_app_server_protocol::ApprovalsReviewer::to_core);
let sandbox_policy = request.sandbox_policy.map(|p| p.to_core());
let (permission_profile, active_permission_profile) =
if let Some(permissions) = request.permissions {
let mut overrides = ConfigOverrides {
cwd: cwd.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(
&mut overrides,
Some(permissions),
);
let config = self
.config_manager
.load_for_cwd(
/*request_overrides*/ None,
overrides,
Some(base_snapshot.cwd.to_path_buf()),
)
.await
.map_err(|err| config_load_error(&err))?;
// Startup config is allowed to fall back when requirements
// disallow a configured profile. An explicit turn context
// update is different: reject it before accepting the request.
if let Some(warning) = config.startup_warnings.iter().find(|warning| {
warning.contains("Configured value for `permission_profile` is disallowed")
}) {
return Err(invalid_request(format!(
"invalid turn context override: {warning}"
)));
}
(
Some(config.permissions.permission_profile()),
config.permissions.active_permission_profile(),
)
} else {
(None, None)
};
let resolved = ResolvedTurnContextOverrides {
has_any_overrides,
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level: None,
model: request.model,
effort: request.effort,
summary: request.summary,
service_tier: request.service_tier,
collaboration_mode: request
.collaboration_mode
.map(|mode| self.normalize_turn_start_collaboration_mode(mode)),
personality: request.personality,
};
Ok(resolved)
}
async fn maybe_emit_turn_context_updated(
&self,
thread_id: &str,
before: &ThreadTurnContext,
after: ThreadTurnContext,
) {
if before == &after {
return;
}
self.outgoing
.send_server_notification(ServerNotification::ThreadTurnContextUpdated(
ThreadTurnContextUpdatedNotification {
thread_id: thread_id.to_string(),
turn_context: after,
},
))
.await;
}
async fn turn_start_inner(
&self,
request_id: ConnectionRequestId,
@@ -343,9 +522,8 @@ impl TurnRequestProcessor {
self.track_error_response(&request_id, error, /*error_type*/ None);
})?;
let collaboration_mode = params
.collaboration_mode
.map(|mode| self.normalize_turn_start_collaboration_mode(mode));
let before_snapshot = thread.config_snapshot().await;
let before_turn_context = thread_turn_context_from_snapshot(&before_snapshot);
let environment_selections = self.parse_environment_selections(params.environments)?;
// Map v2 input items to core input items.
@@ -355,120 +533,57 @@ impl TurnRequestProcessor {
.map(V2UserInput::into_core)
.collect();
let turn_has_input = !mapped_items.is_empty();
let has_any_overrides = params.cwd.is_some()
|| params.approval_policy.is_some()
|| params.approvals_reviewer.is_some()
|| params.sandbox_policy.is_some()
|| params.permissions.is_some()
|| params.model.is_some()
|| params.service_tier.is_some()
|| params.effort.is_some()
|| params.summary.is_some()
|| collaboration_mode.is_some()
|| params.personality.is_some();
if params.sandbox_policy.is_some() && params.permissions.is_some() {
return Err(invalid_request(
"`permissions` cannot be combined with `sandboxPolicy`",
));
}
let cwd = params.cwd;
let approval_policy = params.approval_policy.map(AskForApproval::to_core);
let approvals_reviewer = params
.approvals_reviewer
.map(codex_app_server_protocol::ApprovalsReviewer::to_core);
let sandbox_policy = params.sandbox_policy.map(|p| p.to_core());
let (permission_profile, active_permission_profile) =
if let Some(permissions) = params.permissions {
let snapshot = thread.config_snapshot().await;
let mut overrides = ConfigOverrides {
cwd: cwd.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(
&mut overrides,
Some(permissions),
);
let config = self
.config_manager
.load_for_cwd(
/*request_overrides*/ None,
overrides,
Some(snapshot.cwd.to_path_buf()),
)
let resolved_overrides = self
.resolve_turn_context_overrides(
&before_snapshot,
TurnContextOverrideRequest {
cwd: params.cwd,
approval_policy: params.approval_policy,
approvals_reviewer: params.approvals_reviewer,
sandbox_policy: params.sandbox_policy,
permissions: params.permissions,
model: params.model,
service_tier: params.service_tier,
effort: params.effort.map(Some),
summary: params.summary,
collaboration_mode: params.collaboration_mode,
personality: params.personality,
},
)
.await?;
let after_turn_context = if resolved_overrides.has_any_overrides {
Some(thread_turn_context_from_snapshot(
&thread
.preview_turn_context_overrides(resolved_overrides.to_core_overrides())
.await
.map_err(|err| config_load_error(&err))?;
// Startup config is allowed to fall back when requirements
// disallow a configured profile. An explicit turn request
// is different: reject it before accepting user input.
if let Some(warning) = config.startup_warnings.iter().find(|warning| {
warning.contains("Configured value for `permission_profile` is disallowed")
}) {
return Err(invalid_request(format!(
"invalid turn context override: {warning}"
)));
}
(
Some(config.permissions.permission_profile()),
config.permissions.active_permission_profile(),
)
} else {
(None, None)
};
let model = params.model;
let effort = params.effort.map(Some);
let summary = params.summary;
let service_tier = params.service_tier;
let personality = params.personality;
// If any overrides are provided, validate them synchronously so the
// request can fail before accepting user input. The actual update is
// still queued together with the input below to preserve submission order.
if has_any_overrides {
thread
.validate_turn_context_overrides(CodexThreadTurnContextOverrides {
cwd: cwd.clone(),
approval_policy,
approvals_reviewer,
sandbox_policy: sandbox_policy.clone(),
permission_profile: permission_profile.clone(),
active_permission_profile: active_permission_profile.clone(),
windows_sandbox_level: None,
model: model.clone(),
effort,
summary,
service_tier: service_tier.clone(),
collaboration_mode: collaboration_mode.clone(),
personality,
})
.await
.map_err(|err| invalid_request(format!("invalid turn context override: {err}")))?;
}
.map_err(|err| {
invalid_request(format!("invalid turn context override: {err}"))
})?,
))
} else {
None
};
// Start the turn by submitting the user input. Return its submission id as turn_id.
let turn_op = if has_any_overrides {
let turn_op = if resolved_overrides.has_any_overrides {
Op::UserInputWithTurnContext {
items: mapped_items,
environments: environment_selections,
final_output_json_schema: params.output_schema,
responsesapi_client_metadata: params.responsesapi_client_metadata,
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level: None,
model,
effort,
summary,
service_tier,
collaboration_mode,
personality,
cwd: resolved_overrides.cwd,
approval_policy: resolved_overrides.approval_policy,
approvals_reviewer: resolved_overrides.approvals_reviewer,
sandbox_policy: resolved_overrides.sandbox_policy,
permission_profile: resolved_overrides.permission_profile,
active_permission_profile: resolved_overrides.active_permission_profile,
windows_sandbox_level: resolved_overrides.windows_sandbox_level,
model: resolved_overrides.model,
effort: resolved_overrides.effort,
summary: resolved_overrides.summary,
service_tier: resolved_overrides.service_tier,
collaboration_mode: resolved_overrides.collaboration_mode,
personality: resolved_overrides.personality,
}
} else {
Op::UserInput {
@@ -498,6 +613,14 @@ impl TurnRequestProcessor {
&config_snapshot.session_source,
);
}
if let Some(after_turn_context) = after_turn_context {
self.maybe_emit_turn_context_updated(
&params.thread_id,
&before_turn_context,
after_turn_context,
)
.await;
}
self.outgoing
.record_request_turn_id(&request_id, &turn_id)
@@ -516,6 +639,59 @@ impl TurnRequestProcessor {
Ok(TurnStartResponse { turn })
}
async fn thread_turn_context_update_inner(
&self,
request_id: &ConnectionRequestId,
params: ThreadTurnContextUpdateParams,
) -> Result<ThreadTurnContextUpdateResponse, JSONRPCErrorError> {
let (_, thread) = self
.load_thread(&params.thread_id)
.await
.inspect_err(|error| {
self.track_error_response(request_id, error, /*error_type*/ None);
})?;
let before_snapshot = thread.config_snapshot().await;
let before_turn_context = thread_turn_context_from_snapshot(&before_snapshot);
let resolved_overrides = self
.resolve_turn_context_overrides(
&before_snapshot,
TurnContextOverrideRequest {
cwd: params.cwd,
approval_policy: params.approval_policy,
approvals_reviewer: params.approvals_reviewer,
sandbox_policy: params.sandbox_policy,
permissions: params.permissions,
model: params.model,
service_tier: params.service_tier,
effort: params.effort,
summary: params.summary,
collaboration_mode: params.collaboration_mode,
personality: params.personality,
},
)
.await?;
let after_snapshot = if resolved_overrides.has_any_overrides {
thread
.update_turn_context_overrides(resolved_overrides.to_core_overrides())
.await
.map_err(|err| invalid_request(format!("invalid turn context override: {err}")))?
} else {
before_snapshot
};
let after_turn_context = thread_turn_context_from_snapshot(&after_snapshot);
self.maybe_emit_turn_context_updated(
&params.thread_id,
&before_turn_context,
after_turn_context.clone(),
)
.await;
Ok(ThreadTurnContextUpdateResponse {
turn_context: after_turn_context,
})
}
async fn thread_inject_items_response_inner(
&self,
params: ThreadInjectItemsParams,
@@ -1110,6 +1286,29 @@ impl TurnRequestProcessor {
}
}
fn thread_turn_context_from_snapshot(config_snapshot: &ThreadConfigSnapshot) -> ThreadTurnContext {
ThreadTurnContext {
model: config_snapshot.model.clone(),
model_provider: config_snapshot.model_provider_id.clone(),
service_tier: config_snapshot.service_tier.clone(),
cwd: config_snapshot.cwd.clone(),
approval_policy: config_snapshot.approval_policy.into(),
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
sandbox_policy: thread_response_sandbox_policy(
&config_snapshot.permission_profile,
config_snapshot.cwd.as_path(),
),
permission_profile: config_snapshot.permission_profile.clone().into(),
active_permission_profile: thread_response_active_permission_profile(
config_snapshot.active_permission_profile.clone(),
),
effort: config_snapshot.reasoning_effort,
summary: config_snapshot.reasoning_summary,
personality: config_snapshot.personality,
collaboration_mode: config_snapshot.collaboration_mode.clone(),
}
}
fn xcode_26_4_mcp_elicitations_auto_deny(
client_name: Option<&str>,
client_version: Option<&str>,
@@ -89,6 +89,7 @@ use codex_app_server_protocol::ThreadRollbackParams;
use codex_app_server_protocol::ThreadSetNameParams;
use codex_app_server_protocol::ThreadShellCommandParams;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadTurnContextUpdateParams;
use codex_app_server_protocol::ThreadTurnsItemsListParams;
use codex_app_server_protocol::ThreadTurnsListParams;
use codex_app_server_protocol::ThreadUnarchiveParams;
@@ -487,6 +488,15 @@ impl McpProcess {
self.send_request("thread/rollback", params).await
}
/// Send a `thread/turnContext/update` JSON-RPC request.
pub async fn send_thread_turn_context_update_request(
&mut self,
params: ThreadTurnContextUpdateParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/turnContext/update", params).await
}
/// Send a `thread/list` JSON-RPC request.
pub async fn send_thread_list_request(
&mut self,
@@ -14,10 +14,12 @@ use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ThreadLoadedListParams;
use codex_app_server_protocol::ThreadLoadedListResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadTurnContextUpdateParams;
use futures::SinkExt;
use futures::StreamExt;
use hmac::Hmac;
@@ -104,6 +106,64 @@ async fn websocket_transport_routes_per_connection_handshake_and_responses() ->
Ok(())
}
#[tokio::test]
async fn websocket_turn_context_updates_broadcast_to_other_connections() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri(), "never")?;
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
let mut ws1 = connect_websocket(bind_addr).await?;
let mut ws2 = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws1, /*id*/ 1, "ws_context_owner").await?;
read_response_for_id(&mut ws1, /*id*/ 1).await?;
send_initialize_request(&mut ws2, /*id*/ 2, "ws_context_peer").await?;
read_response_for_id(&mut ws2, /*id*/ 2).await?;
let thread_id = start_thread(&mut ws1, /*id*/ 3).await?;
send_request(
&mut ws1,
"thread/turnContext/update",
/*id*/ 4,
Some(serde_json::to_value(ThreadTurnContextUpdateParams {
thread_id: thread_id.clone(),
model: Some("mock-model-updated".to_string()),
..Default::default()
})?),
)
.await?;
let (_response, caller_notification) = read_response_and_notification_for_method(
&mut ws1,
/*id*/ 4,
"thread/turnContext/updated",
)
.await?;
let peer_notification =
read_notification_for_method(&mut ws2, "thread/turnContext/updated").await?;
let ServerNotification::ThreadTurnContextUpdated(caller) =
ServerNotification::try_from(caller_notification)?
else {
bail!("expected caller thread/turnContext/updated notification");
};
let ServerNotification::ThreadTurnContextUpdated(peer) =
ServerNotification::try_from(peer_notification)?
else {
bail!("expected peer thread/turnContext/updated notification");
};
assert_eq!(caller.thread_id, thread_id);
assert_eq!(peer, caller);
process
.kill()
.await
.context("failed to stop websocket app-server process")?;
Ok(())
}
#[tokio::test]
async fn websocket_transport_serves_health_endpoints_on_same_listener() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
@@ -59,6 +59,7 @@ mod thread_rollback;
mod thread_shell_command;
mod thread_start;
mod thread_status;
mod thread_turn_context;
mod thread_unarchive;
mod thread_unsubscribe;
mod turn_interrupt;
@@ -0,0 +1,253 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::create_mock_responses_server_sequence_unchecked;
use app_test_support::to_response;
use app_test_support::write_mock_responses_config_toml;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxPolicy;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ThreadSource;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadTurnContextUpdateParams;
use codex_app_server_protocol::ThreadTurnContextUpdateResponse;
use codex_app_server_protocol::ThreadTurnContextUpdatedNotification;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::UserInput as V2UserInput;
use codex_features::Feature;
use codex_protocol::openai_models::ReasoningEffort;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
fn write_config(codex_home: &TempDir, server_uri: &str) -> Result<()> {
write_mock_responses_config_toml(
codex_home.path(),
server_uri,
&BTreeMap::<Feature, bool>::new(),
/*auto_compact_limit*/ 1_000_000,
/*requires_openai_auth*/ None,
"mock_provider",
"compact",
)?;
Ok(())
}
async fn start_thread(mcp: &mut McpProcess) -> Result<ThreadStartResponse> {
let request_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
service_tier: Some(Some("flex".to_string())),
thread_source: Some(ThreadSource::User),
..Default::default()
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
to_response::<ThreadStartResponse>(response)
}
async fn read_turn_context_updated(
mcp: &mut McpProcess,
) -> Result<ThreadTurnContextUpdatedNotification> {
let notification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("thread/turnContext/updated"),
)
.await??;
let notification: ServerNotification = notification.try_into()?;
let ServerNotification::ThreadTurnContextUpdated(notification) = notification else {
anyhow::bail!("expected thread/turnContext/updated notification");
};
Ok(notification)
}
#[tokio::test]
async fn thread_turn_context_update_applies_partial_patch_and_emits_full_state() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
write_config(&codex_home, &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let ThreadStartResponse { thread, .. } = start_thread(&mut mcp).await?;
let request_id = mcp
.send_thread_turn_context_update_request(ThreadTurnContextUpdateParams {
thread_id: thread.id.clone(),
model: Some("gpt-5.2".to_string()),
effort: Some(Some(ReasoningEffort::High)),
..Default::default()
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response = to_response::<ThreadTurnContextUpdateResponse>(response)?;
assert_eq!(response.turn_context.model, "gpt-5.2");
assert_eq!(response.turn_context.service_tier.as_deref(), Some("flex"));
assert_eq!(response.turn_context.effort, Some(ReasoningEffort::High));
assert_eq!(response.turn_context.cwd, thread.cwd);
let notification = read_turn_context_updated(&mut mcp).await?;
assert_eq!(notification.thread_id, thread.id);
assert_eq!(notification.turn_context, response.turn_context);
mcp.clear_message_buffer();
let no_op_request = mcp
.send_thread_turn_context_update_request(ThreadTurnContextUpdateParams {
thread_id: thread.id,
model: Some("gpt-5.2".to_string()),
effort: Some(Some(ReasoningEffort::High)),
..Default::default()
})
.await?;
let no_op_response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(no_op_request)),
)
.await??;
let no_op_response = to_response::<ThreadTurnContextUpdateResponse>(no_op_response)?;
assert_eq!(no_op_response.turn_context, response.turn_context);
assert!(
!mcp.pending_notification_methods()
.iter()
.any(|method| method == "thread/turnContext/updated")
);
Ok(())
}
#[tokio::test]
async fn thread_turn_context_update_clears_service_tier_with_explicit_null() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
write_config(&codex_home, &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let ThreadStartResponse { thread, .. } = start_thread(&mut mcp).await?;
let request_id = mcp
.send_thread_turn_context_update_request(ThreadTurnContextUpdateParams {
thread_id: thread.id,
service_tier: Some(None),
..Default::default()
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response = to_response::<ThreadTurnContextUpdateResponse>(response)?;
assert_eq!(response.turn_context.service_tier, None);
let notification = read_turn_context_updated(&mut mcp).await?;
assert_eq!(notification.turn_context.service_tier, None);
Ok(())
}
#[tokio::test]
async fn thread_turn_context_update_rejects_sandbox_policy_with_permissions() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
write_config(&codex_home, &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let ThreadStartResponse { thread, .. } = start_thread(&mut mcp).await?;
let request_id = mcp
.send_thread_turn_context_update_request(ThreadTurnContextUpdateParams {
thread_id: thread.id,
sandbox_policy: Some(SandboxPolicy::DangerFullAccess),
permissions: Some(PermissionProfileSelectionParams::Profile {
id: ":read-only".to_string(),
modifications: None,
}),
..Default::default()
})
.await?;
let err: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(err.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(
err.error
.message
.contains("`permissions` cannot be combined with `sandboxPolicy`"),
"unexpected error message: {}",
err.error.message
);
Ok(())
}
#[tokio::test]
async fn turn_start_emits_turn_context_updated_when_overrides_change_defaults() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(vec![
create_final_assistant_message_sse_response("Done")?,
])
.await;
let codex_home = TempDir::new()?;
write_config(&codex_home, &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let ThreadStartResponse { thread, .. } = start_thread(&mut mcp).await?;
let request_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
model: Some("gpt-5.2".to_string()),
effort: Some(ReasoningEffort::Low),
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let notification = read_turn_context_updated(&mut mcp).await?;
assert_eq!(notification.thread_id, thread.id);
assert_eq!(notification.turn_context.model, "gpt-5.2");
assert_eq!(notification.turn_context.effort, Some(ReasoningEffort::Low));
assert_eq!(
notification.turn_context.service_tier.as_deref(),
Some("flex")
);
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
Ok(())
}
@@ -47,6 +47,7 @@ pub(super) fn server_notification_thread_target(
ServerNotification::ThreadStatusChanged(notification) => {
Some(notification.thread_id.as_str())
}
ServerNotification::ThreadTurnContextUpdated(_) => None,
ServerNotification::ThreadArchived(notification) => Some(notification.thread_id.as_str()),
ServerNotification::ThreadUnarchived(notification) => Some(notification.thread_id.as_str()),
ServerNotification::ThreadClosed(notification) => Some(notification.thread_id.as_str()),
+1
View File
@@ -217,6 +217,7 @@ impl ChatWidget {
| ServerNotification::AccountRateLimitsUpdated(_)
| ServerNotification::ThreadStarted(_)
| ServerNotification::ThreadStatusChanged(_)
| ServerNotification::ThreadTurnContextUpdated(_)
| ServerNotification::ThreadArchived(_)
| ServerNotification::ThreadUnarchived(_)
| ServerNotification::RawResponseItemCompleted(_)