diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index d8ab1eae68..b235e2eeeb 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -500,7 +500,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize }, capabilities: Some(InitializeCapabilities { experimental_api: false, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), }, @@ -592,7 +592,7 @@ async fn compaction_event_ingests_custom_fact() { }, capabilities: Some(InitializeCapabilities { experimental_api: false, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), }, diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index f836a253fc..860faf471c 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -39,7 +39,6 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::Result as JsonRpcResult; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::SupportedServerRequestMethod; use codex_arg0::Arg0DispatchPaths; use codex_core::config::Config; use codex_core::config_loader::CloudRequirementsLoader; @@ -368,8 +367,8 @@ pub struct InProcessClientStartArgs { pub client_version: String, /// Whether experimental APIs are requested at initialize time. pub experimental_api: bool, - /// Server-initiated requests this client can render and answer. - pub supported_server_requests: Vec, + /// Whether the client can render and answer permission confirmations. + pub permission_confirmations: bool, /// Notification methods this client opts out of receiving. pub opt_out_notification_methods: Vec, /// Queue capacity for command/event channels (clamped to at least 1). @@ -381,11 +380,7 @@ impl InProcessClientStartArgs { pub fn initialize_params(&self) -> InitializeParams { let capabilities = InitializeCapabilities { experimental_api: self.experimental_api, - supported_server_requests: if self.supported_server_requests.is_empty() { - None - } else { - Some(self.supported_server_requests.clone()) - }, + permission_confirmations: self.permission_confirmations, opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() { None } else { @@ -996,7 +991,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, - supported_server_requests: Vec::new(), + permission_confirmations: false, opt_out_notification_methods: Vec::new(), channel_capacity, }) @@ -1009,11 +1004,7 @@ mod tests { } #[tokio::test] - async fn in_process_initialize_params_include_supported_server_requests() { - let expected_supported = vec![ - SupportedServerRequestMethod::PermissionsRequestApproval, - SupportedServerRequestMethod::PermissionPresetRequestApproval, - ]; + async fn in_process_initialize_params_include_permission_confirmations() { let args = InProcessClientStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(build_test_config().await), @@ -1028,7 +1019,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, - supported_server_requests: expected_supported.clone(), + permission_confirmations: true, opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }; @@ -1037,10 +1028,7 @@ mod tests { .initialize_params() .capabilities .expect("capabilities should be populated"); - assert_eq!( - capabilities.supported_server_requests, - Some(expected_supported) - ); + assert!(capabilities.permission_confirmations); } async fn start_test_remote_server(handler: F) -> String @@ -1207,7 +1195,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, - supported_server_requests: Vec::new(), + permission_confirmations: false, opt_out_notification_methods: Vec::new(), channel_capacity: 8, } @@ -1652,7 +1640,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, - supported_server_requests: Vec::new(), + permission_confirmations: false, opt_out_notification_methods: Vec::new(), channel_capacity: 1, }) @@ -2046,7 +2034,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, - supported_server_requests: Vec::new(), + permission_confirmations: false, opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, } diff --git a/codex-rs/app-server-client/src/remote.rs b/codex-rs/app-server-client/src/remote.rs index fe1e85274f..f15012d875 100644 --- a/codex-rs/app-server-client/src/remote.rs +++ b/codex-rs/app-server-client/src/remote.rs @@ -36,7 +36,6 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::Result as JsonRpcResult; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::SupportedServerRequestMethod; use codex_utils_rustls_provider::ensure_rustls_crypto_provider; use futures::SinkExt; use futures::StreamExt; @@ -65,7 +64,7 @@ pub struct RemoteAppServerConnectArgs { pub client_name: String, pub client_version: String, pub experimental_api: bool, - pub supported_server_requests: Vec, + pub permission_confirmations: bool, pub opt_out_notification_methods: Vec, pub channel_capacity: usize, } @@ -74,11 +73,7 @@ impl RemoteAppServerConnectArgs { fn initialize_params(&self) -> InitializeParams { let capabilities = InitializeCapabilities { experimental_api: self.experimental_api, - supported_server_requests: if self.supported_server_requests.is_empty() { - None - } else { - Some(self.supported_server_requests.clone()) - }, + permission_confirmations: self.permission_confirmations, opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() { None } else { diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 004da7c2e0..af1ac6a117 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1023,15 +1023,10 @@ "null" ] }, - "supportedServerRequests": { - "description": "Server request methods this connection knows how to render and answer.", - "items": { - "$ref": "#/definitions/SupportedServerRequestMethod" - }, - "type": [ - "array", - "null" - ] + "permissionConfirmations": { + "default": false, + "description": "Opt into model-initiated permission confirmation requests.", + "type": "boolean" } }, "type": "object" @@ -2579,14 +2574,6 @@ }, "type": "object" }, - "SupportedServerRequestMethod": { - "description": "Server-initiated request methods a client can opt into during initialize.", - "enum": [ - "item/permissions/requestApproval", - "item/permissionPreset/requestApproval" - ], - "type": "string" - }, "TextElement": { "properties": { "byteRange": { diff --git a/codex-rs/app-server-protocol/schema/json/PermissionPresetRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/PermissionPresetRequestApprovalParams.json index 3844893701..b1d3fc3dc1 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionPresetRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionPresetRequestApprovalParams.json @@ -1,75 +1,6 @@ { "$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`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", - "enum": [ - "user", - "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" - } - ] - }, - "NetworkAccess": { - "enum": [ - "restricted", - "enabled" - ], - "type": "string" - }, "PermissionPresetId": { "description": "A built-in permission-mode preset that app clients can confirm.", "enum": [ @@ -79,191 +10,13 @@ "guardian-approvals" ], "type": "string" - }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, - "SandboxPolicy": { - "oneOf": [ - { - "properties": { - "type": { - "enum": [ - "dangerFullAccess" - ], - "title": "DangerFullAccessSandboxPolicyType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "DangerFullAccessSandboxPolicy", - "type": "object" - }, - { - "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, - "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" - }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, - "type": { - "enum": [ - "workspaceWrite" - ], - "title": "WorkspaceWriteSandboxPolicyType", - "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - } - }, - "required": [ - "type" - ], - "title": "WorkspaceWriteSandboxPolicy", - "type": "object" - } - ] } }, - "description": "Request sent to app clients when a model asks to switch permission modes.\n\nCore resolves the requested preset before emitting this payload, so clients should render the provided settings and label rather than recomputing policy choices from the preset id.", + "description": "Request sent to app clients when a model asks to switch permission modes.\n\nClients should open their local permission picker with the requested preset preselected, allowing the user to accept it or choose a different preset.", "properties": { - "approvalPolicy": { - "$ref": "#/definitions/AskForApproval" - }, - "approvalsReviewer": { - "$ref": "#/definitions/ApprovalsReviewer" - }, - "description": { - "type": "string" - }, "itemId": { "type": "string" }, - "label": { - "type": "string" - }, "preset": { "$ref": "#/definitions/PermissionPresetId" }, @@ -273,9 +26,6 @@ "null" ] }, - "sandboxPolicy": { - "$ref": "#/definitions/SandboxPolicy" - }, "threadId": { "type": "string" }, @@ -284,13 +34,8 @@ } }, "required": [ - "approvalPolicy", - "approvalsReviewer", - "description", "itemId", - "label", "preset", - "sandboxPolicy", "threadId", "turnId" ], diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 356ec351ef..6e78686754 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -101,64 +101,6 @@ ], "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`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", - "enum": [ - "user", - "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" - } - ] - }, "ChatgptAuthTokensRefreshParams": { "properties": { "previousAccountId": { @@ -1245,13 +1187,6 @@ ], "type": "object" }, - "NetworkAccess": { - "enum": [ - "restricted", - "enabled" - ], - "type": "string" - }, "NetworkApprovalContext": { "properties": { "host": { @@ -1427,23 +1362,11 @@ "type": "string" }, "PermissionPresetRequestApprovalParams": { - "description": "Request sent to app clients when a model asks to switch permission modes.\n\nCore resolves the requested preset before emitting this payload, so clients should render the provided settings and label rather than recomputing policy choices from the preset id.", + "description": "Request sent to app clients when a model asks to switch permission modes.\n\nClients should open their local permission picker with the requested preset preselected, allowing the user to accept it or choose a different preset.", "properties": { - "approvalPolicy": { - "$ref": "#/definitions/AskForApproval" - }, - "approvalsReviewer": { - "$ref": "#/definitions/ApprovalsReviewer" - }, - "description": { - "type": "string" - }, "itemId": { "type": "string" }, - "label": { - "type": "string" - }, "preset": { "$ref": "#/definitions/PermissionPresetId" }, @@ -1453,9 +1376,6 @@ "null" ] }, - "sandboxPolicy": { - "$ref": "#/definitions/SandboxPolicy" - }, "threadId": { "type": "string" }, @@ -1464,13 +1384,8 @@ } }, "required": [ - "approvalPolicy", - "approvalsReviewer", - "description", "itemId", - "label", "preset", - "sandboxPolicy", "threadId", "turnId" ], @@ -1514,53 +1429,6 @@ ], "type": "object" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "RequestId": { "anyOf": [ { @@ -1598,125 +1466,6 @@ }, "type": "object" }, - "SandboxPolicy": { - "oneOf": [ - { - "properties": { - "type": { - "enum": [ - "dangerFullAccess" - ], - "title": "DangerFullAccessSandboxPolicyType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "DangerFullAccessSandboxPolicy", - "type": "object" - }, - { - "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, - "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" - }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, - "type": { - "enum": [ - "workspaceWrite" - ], - "title": "WorkspaceWriteSandboxPolicyType", - "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - } - }, - "required": [ - "type" - ], - "title": "WorkspaceWriteSandboxPolicy", - "type": "object" - } - ] - }, "ThreadId": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index d0b385096a..723d09293c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -2296,15 +2296,10 @@ "null" ] }, - "supportedServerRequests": { - "description": "Server request methods this connection knows how to render and answer.", - "items": { - "$ref": "#/definitions/SupportedServerRequestMethod" - }, - "type": [ - "array", - "null" - ] + "permissionConfirmations": { + "default": false, + "description": "Opt into model-initiated permission confirmation requests.", + "type": "boolean" } }, "type": "object" @@ -3289,23 +3284,11 @@ }, "PermissionPresetRequestApprovalParams": { "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Request sent to app clients when a model asks to switch permission modes.\n\nCore resolves the requested preset before emitting this payload, so clients should render the provided settings and label rather than recomputing policy choices from the preset id.", + "description": "Request sent to app clients when a model asks to switch permission modes.\n\nClients should open their local permission picker with the requested preset preselected, allowing the user to accept it or choose a different preset.", "properties": { - "approvalPolicy": { - "$ref": "#/definitions/v2/AskForApproval" - }, - "approvalsReviewer": { - "$ref": "#/definitions/v2/ApprovalsReviewer" - }, - "description": { - "type": "string" - }, "itemId": { "type": "string" }, - "label": { - "type": "string" - }, "preset": { "$ref": "#/definitions/PermissionPresetId" }, @@ -3315,9 +3298,6 @@ "null" ] }, - "sandboxPolicy": { - "$ref": "#/definitions/v2/SandboxPolicy" - }, "threadId": { "type": "string" }, @@ -3326,13 +3306,8 @@ } }, "required": [ - "approvalPolicy", - "approvalsReviewer", - "description", "itemId", - "label", "preset", - "sandboxPolicy", "threadId", "turnId" ], @@ -4853,14 +4828,6 @@ ], "title": "ServerRequest" }, - "SupportedServerRequestMethod": { - "description": "Server-initiated request methods a client can opt into during initialize.", - "enum": [ - "item/permissions/requestApproval", - "item/permissionPreset/requestApproval" - ], - "type": "string" - }, "ToolRequestUserInputAnswer": { "description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index a94e319772..529a444170 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -5348,15 +5348,10 @@ "null" ] }, - "supportedServerRequests": { - "description": "Server request methods this connection knows how to render and answer.", - "items": { - "$ref": "#/definitions/SupportedServerRequestMethod" - }, - "type": [ - "array", - "null" - ] + "permissionConfirmations": { + "default": false, + "description": "Opt into model-initiated permission confirmation requests.", + "type": "boolean" } }, "type": "object" @@ -10267,14 +10262,6 @@ } ] }, - "SupportedServerRequestMethod": { - "description": "Server-initiated request methods a client can opt into during initialize.", - "enum": [ - "item/permissions/requestApproval", - "item/permissionPreset/requestApproval" - ], - "type": "string" - }, "TerminalInteractionNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json index 7d0da1e7a8..f1e40e8fe5 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json @@ -40,26 +40,13 @@ "null" ] }, - "supportedServerRequests": { - "description": "Server request methods this connection knows how to render and answer.", - "items": { - "$ref": "#/definitions/SupportedServerRequestMethod" - }, - "type": [ - "array", - "null" - ] + "permissionConfirmations": { + "default": false, + "description": "Opt into model-initiated permission confirmation requests.", + "type": "boolean" } }, "type": "object" - }, - "SupportedServerRequestMethod": { - "description": "Server-initiated request methods a client can opt into during initialize.", - "enum": [ - "item/permissions/requestApproval", - "item/permissionPreset/requestApproval" - ], - "type": "string" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts index 8cf667561e..35ff5cd44b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -1,7 +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 { SupportedServerRequestMethod } from "./SupportedServerRequestMethod"; /** * Client-declared capabilities negotiated during initialize. @@ -12,9 +11,9 @@ export type InitializeCapabilities = { */ experimentalApi: boolean, /** - * Server request methods this connection knows how to render and answer. + * Opt into model-initiated permission confirmation requests. */ -supportedServerRequests?: Array | null, +permissionConfirmations: boolean, /** * Exact notification method names that should be suppressed for this * connection (for example `thread/started`). diff --git a/codex-rs/app-server-protocol/schema/typescript/SupportedServerRequestMethod.ts b/codex-rs/app-server-protocol/schema/typescript/SupportedServerRequestMethod.ts deleted file mode 100644 index 6225ebd119..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/SupportedServerRequestMethod.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -/** - * Server-initiated request methods a client can opt into during initialize. - */ -export type SupportedServerRequestMethod = "item/permissions/requestApproval" | "item/permissionPreset/requestApproval"; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 94b2a14526..2a35207896 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -67,7 +67,6 @@ export type { ServiceTier } from "./ServiceTier"; export type { SessionSource } from "./SessionSource"; export type { Settings } from "./Settings"; export type { SubAgentSource } from "./SubAgentSource"; -export type { SupportedServerRequestMethod } from "./SupportedServerRequestMethod"; export type { ThreadId } from "./ThreadId"; export type { Tool } from "./Tool"; export type { Verbosity } from "./Verbosity"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionPresetRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionPresetRequestApprovalParams.ts index a41ade23f2..52f91b77ff 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionPresetRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionPresetRequestApprovalParams.ts @@ -1,16 +1,12 @@ // 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 { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AskForApproval } from "./AskForApproval"; import type { PermissionPresetId } from "./PermissionPresetId"; -import type { SandboxPolicy } from "./SandboxPolicy"; /** * Request sent to app clients when a model asks to switch permission modes. * - * Core resolves the requested preset before emitting this payload, so clients - * should render the provided settings and label rather than recomputing policy - * choices from the preset id. + * Clients should open their local permission picker with the requested preset + * preselected, allowing the user to accept it or choose a different preset. */ -export type PermissionPresetRequestApprovalParams = { threadId: string, turnId: string, itemId: string, preset: PermissionPresetId, label: string, description: string, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, reason: string | null, }; +export type PermissionPresetRequestApprovalParams = { threadId: string, turnId: string, itemId: string, preset: PermissionPresetId, reason: string | null, }; diff --git a/codex-rs/app-server-protocol/src/lib.rs b/codex-rs/app-server-protocol/src/lib.rs index ee48ff3a80..d5c2f4b243 100644 --- a/codex-rs/app-server-protocol/src/lib.rs +++ b/codex-rs/app-server-protocol/src/lib.rs @@ -37,7 +37,6 @@ pub use protocol::v1::InterruptConversationResponse; pub use protocol::v1::LoginApiKeyParams; pub use protocol::v1::Profile; pub use protocol::v1::SandboxSettings; -pub use protocol::v1::SupportedServerRequestMethod; pub use protocol::v1::Tools; pub use protocol::v1::UserSavedConfig; pub use protocol::v2::*; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 72407192c9..6926a77e3d 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1103,10 +1103,7 @@ mod tests { }, capabilities: Some(v1::InitializeCapabilities { experimental_api: true, - supported_server_requests: Some(vec![ - v1::SupportedServerRequestMethod::PermissionsRequestApproval, - v1::SupportedServerRequestMethod::PermissionPresetRequestApproval, - ]), + permission_confirmations: true, opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), @@ -1127,10 +1124,7 @@ mod tests { }, "capabilities": { "experimentalApi": true, - "supportedServerRequests": [ - "item/permissions/requestApproval", - "item/permissionPreset/requestApproval" - ], + "permissionConfirmations": true, "optOutNotificationMethods": [ "thread/started", "item/agentMessage/delta" @@ -1156,10 +1150,7 @@ mod tests { }, "capabilities": { "experimentalApi": true, - "supportedServerRequests": [ - "item/permissions/requestApproval", - "item/permissionPreset/requestApproval" - ], + "permissionConfirmations": true, "optOutNotificationMethods": [ "thread/started", "item/agentMessage/delta" @@ -1180,10 +1171,7 @@ mod tests { }, capabilities: Some(v1::InitializeCapabilities { experimental_api: true, - supported_server_requests: Some(vec![ - v1::SupportedServerRequestMethod::PermissionsRequestApproval, - v1::SupportedServerRequestMethod::PermissionPresetRequestApproval, - ]), + permission_confirmations: true, opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), @@ -1196,7 +1184,7 @@ mod tests { } #[test] - fn deserialize_initialize_without_supported_server_requests() -> Result<()> { + fn deserialize_initialize_without_permission_confirmations() -> Result<()> { let request: ClientRequest = serde_json::from_value(json!({ "method": "initialize", "id": 42, @@ -1216,7 +1204,7 @@ mod tests { panic!("expected initialize request"); }; let capabilities = params.capabilities.expect("capabilities"); - assert_eq!(capabilities.supported_server_requests, None); + assert!(!capabilities.permission_confirmations); Ok(()) } diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index f7d5c313bd..5a54385c14 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -46,26 +46,15 @@ pub struct InitializeCapabilities { /// Opt into receiving experimental API methods and fields. #[serde(default)] pub experimental_api: bool, - /// Server request methods this connection knows how to render and answer. - #[ts(optional = nullable)] - pub supported_server_requests: Option>, + /// Opt into model-initiated permission confirmation requests. + #[serde(default)] + pub permission_confirmations: bool, /// Exact notification method names that should be suppressed for this /// connection (for example `thread/started`). #[ts(optional = nullable)] pub opt_out_notification_methods: Option>, } -/// Server-initiated request methods a client can opt into during initialize. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, TS)] -pub enum SupportedServerRequestMethod { - #[serde(rename = "item/permissions/requestApproval")] - #[ts(rename = "item/permissions/requestApproval")] - PermissionsRequestApproval, - #[serde(rename = "item/permissionPreset/requestApproval")] - #[ts(rename = "item/permissionPreset/requestApproval")] - PermissionPresetRequestApproval, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct InitializeResponse { diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 2dd10592e0..56e3b5e7d4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -6289,9 +6289,8 @@ impl From for PermissionPresetId { /// Request sent to app clients when a model asks to switch permission modes. /// -/// Core resolves the requested preset before emitting this payload, so clients -/// should render the provided settings and label rather than recomputing policy -/// choices from the preset id. +/// Clients should open their local permission picker with the requested preset +/// preselected, allowing the user to accept it or choose a different preset. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -6300,11 +6299,6 @@ pub struct PermissionPresetRequestApprovalParams { pub turn_id: String, pub item_id: String, pub preset: PermissionPresetId, - pub label: String, - pub description: String, - pub approval_policy: AskForApproval, - pub approvals_reviewer: ApprovalsReviewer, - pub sandbox_policy: SandboxPolicy, pub reason: Option, } diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 17dec3fb15..22781427dd 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1553,7 +1553,7 @@ impl CodexClient { }, capabilities: Some(InitializeCapabilities { experimental_api, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: Some( NOTIFICATIONS_TO_OPT_OUT .iter() diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index df53131aac..951001527f 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -89,7 +89,7 @@ Clients must send a single `initialize` request per transport connection before `initialize.params.capabilities` also supports per-connection notification opt-out via `optOutNotificationMethods`, which is a list of exact method names to suppress for that connection. Matching is exact (no wildcards/prefixes). Unknown method names are accepted and ignored. -Clients can also opt into specific server-initiated request methods through `supportedServerRequests`. This is required for conversational permission confirmations such as `item/permissions/requestApproval` and `item/permissionPreset/requestApproval`: if a connection omits the matching method, app-server does not send that request to the client and the permission change is rejected without changing the session. +Clients can also opt into model-initiated permission confirmations through `permissionConfirmations: true`. This covers `item/permissions/requestApproval` and `item/permissionPreset/requestApproval`: if a connection omits the capability, app-server does not send those requests to the client and permission changes are rejected without changing the session. Applications building on top of `codex app-server` should identify themselves via the `clientInfo` parameter. @@ -127,10 +127,7 @@ Example with notification opt-out and permission request support: }, "capabilities": { "experimentalApi": true, - "supportedServerRequests": [ - "item/permissions/requestApproval", - "item/permissionPreset/requestApproval" - ], + "permissionConfirmations": true, "optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"] } } @@ -1077,7 +1074,7 @@ the client can offer session-scoped and/or persistent approval choices. ### Permission requests -Clients must opt into each conversational permission request method by listing it in `initialize.params.capabilities.supportedServerRequests`. The opt-in is per connection and per method. App-server uses this capability before starting a thread so unsupported clients do not expose the model-facing tools that trigger these request methods. Existing clients that do not advertise support keep their current behavior: app-server does not ask the model to use an unsupported approval UI, no permissions are changed, and delivery-time denials are only a fallback if a request is still produced. +Clients must opt into conversational permission requests with `initialize.params.capabilities.permissionConfirmations: true`. The opt-in is per connection and enables both permission request methods. App-server uses this capability before starting a thread so unsupported clients do not expose the model-facing tools that trigger these requests. Existing clients that do not advertise support keep their current behavior: app-server does not ask the model to use an unsupported approval UI, no permissions are changed, and delivery-time denials are only a fallback if a request is still produced. The built-in `request_permissions` tool sends an `item/permissions/requestApproval` JSON-RPC request to the client with the requested permission profile. This v2 payload mirrors the command-execution `additionalPermissions` shape: it can request network access and additional filesystem access. @@ -1121,7 +1118,7 @@ Within the same turn, granted permissions are sticky: later shell-like tool call If the session approval policy uses `Granular` with `request_permissions: false`, standalone `request_permissions` tool calls are auto-denied and no `item/permissions/requestApproval` prompt is sent. Inline `with_additional_permissions` command requests remain controlled by `sandbox_approval`, and any previously granted permissions remain sticky for later shell-like calls in the same turn. -The built-in `request_permission_preset` tool sends an `item/permissionPreset/requestApproval` JSON-RPC request when a model asks the client to open the permission preset picker. The request includes the resolved preset id, label, description, approval policy, approvals reviewer, sandbox policy, and optional reason so clients can show their native picker with the requested option already selected. Clients respond with `{ "decision": "accepted", "preset": "full-access" }` or `{ "decision": "declined", "preset": "full-access" }`. When the client omits `item/permissionPreset/requestApproval` from `supportedServerRequests`, app-server hides the preset tool for new threads; if a request is still produced, app-server declines it and leaves the active permissions unchanged. +The built-in `request_permission_preset` tool sends an `item/permissionPreset/requestApproval` JSON-RPC request when a model asks the client to open the permission preset picker. The request includes the requested preset id and optional reason; clients should open their native picker with that preset already selected and may let the user choose a different preset before responding. Clients respond with `{ "decision": "accepted", "preset": "full-access" }` or `{ "decision": "declined", "preset": "full-access" }`. When the client omits `permissionConfirmations`, app-server hides the preset tool for new threads; if a request is still produced, app-server declines it and leaves the active permissions unchanged. ### Dynamic tool calls (experimental) diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 94339b230e..bbc1f7c383 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -920,11 +920,6 @@ pub(crate) async fn apply_bespoke_event_handling( turn_id: request.turn_id.clone(), item_id: request.call_id.clone(), preset: request.preset.into(), - label: request.label.clone(), - description: request.description, - approval_policy: request.approval_policy.into(), - approvals_reviewer: request.approvals_reviewer.into(), - sandbox_policy: request.sandbox_policy.into(), reason: request.reason, }; let (pending_request_id, rx) = request_permission_preset_outgoing diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 1a65781382..fbd46c2a22 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -117,7 +117,6 @@ use codex_app_server_protocol::SkillsConfigWriteParams; use codex_app_server_protocol::SkillsConfigWriteResponse; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; -use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadArchiveParams; use codex_app_server_protocol::ThreadArchiveResponse; @@ -3749,10 +3748,10 @@ impl CodexMessageProcessor { pub(crate) async fn connection_initialized( &self, connection_id: ConnectionId, - supported_server_requests: HashSet, + permission_confirmations: bool, ) { self.thread_state_manager - .connection_initialized(connection_id, supported_server_requests) + .connection_initialized(connection_id, permission_confirmations) .await; } @@ -7558,16 +7557,9 @@ impl CodexMessageProcessor { let subscribed_connection_ids = thread_state_manager .subscribed_connection_ids(conversation_id) .await; - let request_permissions_connection_ids = thread_state_manager - .subscribed_connection_ids_supporting( + let permission_confirmation_connection_ids = thread_state_manager + .subscribed_connection_ids_with_permission_confirmations( conversation_id, - SupportedServerRequestMethod::PermissionsRequestApproval, - ) - .await; - let request_permission_preset_connection_ids = thread_state_manager - .subscribed_connection_ids_supporting( - conversation_id, - SupportedServerRequestMethod::PermissionPresetRequestApproval, ) .await; let thread_outgoing = ThreadScopedOutgoingMessageSender::new( @@ -7576,9 +7568,9 @@ impl CodexMessageProcessor { conversation_id, ); let request_permissions_outgoing = thread_outgoing - .with_connection_ids(request_permissions_connection_ids); + .with_connection_ids(permission_confirmation_connection_ids.clone()); let request_permission_preset_outgoing = thread_outgoing - .with_connection_ids(request_permission_preset_connection_ids); + .with_connection_ids(permission_confirmation_connection_ids); if let EventMsg::RawResponseItem(raw_response_item_event) = &event.msg && !raw_events_enabled @@ -8728,20 +8720,13 @@ async fn apply_surface_capability_feature_gates( thread_state_manager: &ThreadStateManager, request_id: &ConnectionRequestId, ) { - let supported_server_requests = thread_state_manager - .supported_server_requests_for_connection(request_id.connection_id) + let permission_confirmations = thread_state_manager + .permission_confirmations_for_connection(request_id.connection_id) .await; gate_permission_tool_feature( config, Feature::RequestPermissionsTool, - supported_server_requests - .contains(&SupportedServerRequestMethod::PermissionsRequestApproval), - ); - gate_permission_tool_feature( - config, - Feature::RequestPermissionPresetTool, - supported_server_requests - .contains(&SupportedServerRequestMethod::PermissionPresetRequestApproval), + permission_confirmations, ); } @@ -10000,7 +9985,7 @@ mod tests { let (cancel_tx, cancel_rx) = oneshot::channel(); manager - .connection_initialized(connection, HashSet::new()) + .connection_initialized(connection, /*permission_confirmations*/ false) .await; manager .try_ensure_connection_subscribed( @@ -10048,10 +10033,10 @@ mod tests { let (cancel_tx, mut cancel_rx) = oneshot::channel(); manager - .connection_initialized(connection_a, HashSet::new()) + .connection_initialized(connection_a, /*permission_confirmations*/ false) .await; manager - .connection_initialized(connection_b, HashSet::new()) + .connection_initialized(connection_b, /*permission_confirmations*/ false) .await; manager .try_ensure_connection_subscribed( @@ -10096,7 +10081,7 @@ mod tests { let connection = ConnectionId(1); manager - .connection_initialized(connection, HashSet::new()) + .connection_initialized(connection, /*permission_confirmations*/ false) .await; let threads_to_unload = manager.remove_connection(connection).await; assert_eq!(threads_to_unload, Vec::::new()); diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 2b4f95bc57..0a916c22e7 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -802,8 +802,7 @@ pub async fn run_main_with_transport( connection_id, connection_state .session - .supported_server_requests - .clone(), + .permission_confirmations, ) .await; connection_state diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 027a959a33..a0982dd0fd 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -56,7 +56,6 @@ use codex_app_server_protocol::JSONRPCRequest; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestPayload; -use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::experimental_required_message; use codex_arg0::Arg0DispatchPaths; use codex_chatgpt::connectors; @@ -180,7 +179,7 @@ pub(crate) struct ConnectionSessionState { pub(crate) initialized: bool, pub(crate) experimental_api_enabled: bool, pub(crate) opted_out_notification_methods: HashSet, - pub(crate) supported_server_requests: HashSet, + pub(crate) permission_confirmations: bool, pub(crate) app_server_client_name: Option, pub(crate) client_version: Option, } @@ -461,10 +460,10 @@ impl MessageProcessor { pub(crate) async fn connection_initialized( &self, connection_id: ConnectionId, - supported_server_requests: HashSet, + permission_confirmations: bool, ) { self.codex_message_processor - .connection_initialized(connection_id, supported_server_requests) + .connection_initialized(connection_id, permission_confirmations) .await; } @@ -570,25 +569,21 @@ impl MessageProcessor { let ( experimental_api_enabled, opt_out_notification_methods, - supported_server_requests, + permission_confirmations, ) = match params.capabilities { Some(capabilities) => ( capabilities.experimental_api, capabilities .opt_out_notification_methods .unwrap_or_default(), - capabilities - .supported_server_requests - .unwrap_or_default() - .into_iter() - .collect(), + capabilities.permission_confirmations, ), - None => (false, Vec::new(), HashSet::new()), + None => (false, Vec::new(), false), }; session.experimental_api_enabled = experimental_api_enabled; session.opted_out_notification_methods = opt_out_notification_methods.into_iter().collect(); - session.supported_server_requests = supported_server_requests; + session.permission_confirmations = permission_confirmations; let ClientInfo { name, title: _title, @@ -664,10 +659,7 @@ impl MessageProcessor { // initialize handling for the specific connection. outbound_initialized.store(true, Ordering::Release); self.codex_message_processor - .connection_initialized( - connection_id, - session.supported_server_requests.clone(), - ) + .connection_initialized(connection_id, session.permission_confirmations) .await; } return; diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index 8a7f6e303c..5aeaa005de 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -1,7 +1,6 @@ use crate::outgoing_message::ConnectionId; use crate::outgoing_message::ConnectionRequestId; use codex_app_server_protocol::RequestId; -use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::ThreadHistoryBuilder; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError; @@ -174,8 +173,7 @@ impl Default for ThreadEntry { #[derive(Default)] struct ThreadStateManagerInner { live_connections: HashSet, - supported_server_requests_by_connection: - HashMap>, + permission_confirmations_by_connection: HashSet, threads: HashMap, thread_ids_by_connection: HashMap>, } @@ -193,25 +191,29 @@ impl ThreadStateManager { pub(crate) async fn connection_initialized( &self, connection_id: ConnectionId, - supported_server_requests: HashSet, + permission_confirmations: bool, ) { let mut state = self.state.lock().await; state.live_connections.insert(connection_id); - state - .supported_server_requests_by_connection - .insert(connection_id, supported_server_requests); + if permission_confirmations { + state + .permission_confirmations_by_connection + .insert(connection_id); + } else { + state + .permission_confirmations_by_connection + .remove(&connection_id); + } } - pub(crate) async fn supported_server_requests_for_connection( + pub(crate) async fn permission_confirmations_for_connection( &self, connection_id: ConnectionId, - ) -> HashSet { + ) -> bool { let state = self.state.lock().await; state - .supported_server_requests_by_connection - .get(&connection_id) - .cloned() - .unwrap_or_default() + .permission_confirmations_by_connection + .contains(&connection_id) } pub(crate) async fn subscribed_connection_ids(&self, thread_id: ThreadId) -> Vec { @@ -231,10 +233,9 @@ impl ThreadStateManager { connection_ids } - pub(crate) async fn subscribed_connection_ids_supporting( + pub(crate) async fn subscribed_connection_ids_with_permission_confirmations( &self, thread_id: ThreadId, - method: SupportedServerRequestMethod, ) -> Vec { let state = self.state.lock().await; let Some(thread_entry) = state.threads.get(&thread_id) else { @@ -245,9 +246,8 @@ impl ThreadStateManager { .iter() .filter(|connection_id| { state - .supported_server_requests_by_connection - .get(connection_id) - .is_some_and(|supported| supported.contains(&method)) + .permission_confirmations_by_connection + .contains(connection_id) }) .copied() .collect::>(); @@ -409,7 +409,7 @@ impl ThreadStateManager { let mut state = self.state.lock().await; state.live_connections.remove(&connection_id); state - .supported_server_requests_by_connection + .permission_confirmations_by_connection .remove(&connection_id); let thread_ids = state .thread_ids_by_connection diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index c1458adee0..83056b61ca 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -33,7 +33,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu default_client_info(), Some(InitializeCapabilities { experimental_api: false, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), ) @@ -64,7 +64,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R default_client_info(), Some(InitializeCapabilities { experimental_api: false, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), ) @@ -101,7 +101,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< default_client_info(), Some(InitializeCapabilities { experimental_api: false, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), ) @@ -142,7 +142,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul default_client_info(), Some(InitializeCapabilities { experimental_api: false, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), ) @@ -180,7 +180,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa default_client_info(), Some(InitializeCapabilities { experimental_api: false, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), ) @@ -217,7 +217,7 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil default_client_info(), Some(InitializeCapabilities { experimental_api: false, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), ) diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 2aad489548..077fca03cf 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -158,7 +158,7 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu }, Some(InitializeCapabilities { experimental_api: true, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: Some(vec!["thread/started".to_string()]), }), ), diff --git a/codex-rs/app-server/tests/suite/v2/request_permissions.rs b/codex-rs/app-server/tests/suite/v2/request_permissions.rs index 9834dd611b..65b29dde8a 100644 --- a/codex-rs/app-server/tests/suite/v2/request_permissions.rs +++ b/codex-rs/app-server/tests/suite/v2/request_permissions.rs @@ -10,10 +10,10 @@ use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::PermissionPresetId as ApiPermissionPresetId; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerRequestResolvedNotification; -use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -27,8 +27,8 @@ use wiremock::MockServer; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); #[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn permission_tools_are_hidden_without_supported_server_requests() -> Result<()> { - let body = first_responses_request_body_for_supported_server_requests(Vec::new()).await?; +async fn permission_tools_are_hidden_without_permission_confirmations() -> Result<()> { + let body = first_responses_request_body_for_permission_confirmations(false).await?; assert_permission_tool_exposure( &body, /*expect_permissions*/ false, /*expect_preset*/ false, ); @@ -36,36 +36,8 @@ async fn permission_tools_are_hidden_without_supported_server_requests() -> Resu } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn permission_tools_expose_only_narrow_permission_capability() -> Result<()> { - let body = first_responses_request_body_for_supported_server_requests(vec![ - SupportedServerRequestMethod::PermissionsRequestApproval, - ]) - .await?; - assert_permission_tool_exposure( - &body, /*expect_permissions*/ true, /*expect_preset*/ false, - ); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn permission_tools_expose_only_preset_capability() -> Result<()> { - let body = first_responses_request_body_for_supported_server_requests(vec![ - SupportedServerRequestMethod::PermissionPresetRequestApproval, - ]) - .await?; - assert_permission_tool_exposure( - &body, /*expect_permissions*/ false, /*expect_preset*/ true, - ); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn permission_tools_expose_both_supported_capabilities() -> Result<()> { - let body = first_responses_request_body_for_supported_server_requests(vec![ - SupportedServerRequestMethod::PermissionsRequestApproval, - SupportedServerRequestMethod::PermissionPresetRequestApproval, - ]) - .await?; +async fn permission_tools_expose_both_with_permission_confirmations() -> Result<()> { + let body = first_responses_request_body_for_permission_confirmations(true).await?; assert_permission_tool_exposure( &body, /*expect_permissions*/ true, /*expect_preset*/ true, ); @@ -83,11 +55,7 @@ async fn request_permissions_round_trips_when_client_supports_app_request() -> R create_config_toml(codex_home.path(), &server.uri())?; let mut mcp = McpProcess::new(codex_home.path()).await?; - initialize_with_supported_server_requests( - &mut mcp, - vec![SupportedServerRequestMethod::PermissionsRequestApproval], - ) - .await?; + initialize_with_permission_confirmations(&mut mcp, true).await?; let thread_id = start_thread_and_turn(&mut mcp, "pick a directory").await?; @@ -192,11 +160,7 @@ async fn request_permission_preset_round_trips_when_client_supports_app_request( create_config_toml(codex_home.path(), &server.uri())?; let mut mcp = McpProcess::new(codex_home.path()).await?; - initialize_with_supported_server_requests( - &mut mcp, - vec![SupportedServerRequestMethod::PermissionPresetRequestApproval], - ) - .await?; + initialize_with_permission_confirmations(&mut mcp, true).await?; let thread_id = start_thread_and_turn(&mut mcp, "make this session full access").await?; @@ -211,7 +175,7 @@ async fn request_permission_preset_round_trips_when_client_supports_app_request( }; assert_eq!(params.thread_id, thread_id); assert_eq!(params.item_id, "call1"); - assert_eq!(params.label, "Full Access"); + assert_eq!(params.preset, ApiPermissionPresetId::FullAccess); let resolved_request_id = request_id.clone(); mcp.send_response( @@ -291,31 +255,9 @@ async fn request_permission_preset_auto_declines_without_app_request() -> Result Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn request_permission_preset_auto_declines_with_only_permissions_capability() -> Result<()> { - let codex_home = tempfile::TempDir::new()?; - let responses = vec![ - create_request_permission_preset_sse_response("call1")?, - create_final_assistant_message_sse_response("done")?, - ]; - let server = create_mock_responses_server_sequence(responses).await; - create_config_toml(codex_home.path(), &server.uri())?; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - initialize_with_supported_server_requests( - &mut mcp, - vec![SupportedServerRequestMethod::PermissionsRequestApproval], - ) - .await?; - - start_thread_and_turn(&mut mcp, "make this session full access").await?; - wait_for_turn_completed_without_server_request(&mut mcp).await?; - Ok(()) -} - -async fn initialize_with_supported_server_requests( +async fn initialize_with_permission_confirmations( mcp: &mut McpProcess, - supported_server_requests: Vec, + permission_confirmations: bool, ) -> Result<()> { let initialized = timeout( DEFAULT_READ_TIMEOUT, @@ -327,7 +269,7 @@ async fn initialize_with_supported_server_requests( }, Some(InitializeCapabilities { experimental_api: true, - supported_server_requests: Some(supported_server_requests), + permission_confirmations, opt_out_notification_methods: None, }), ), @@ -339,8 +281,8 @@ async fn initialize_with_supported_server_requests( Ok(()) } -async fn first_responses_request_body_for_supported_server_requests( - supported_server_requests: Vec, +async fn first_responses_request_body_for_permission_confirmations( + permission_confirmations: bool, ) -> Result { let codex_home = tempfile::TempDir::new()?; let responses = vec![create_final_assistant_message_sse_response("done")?]; @@ -348,10 +290,10 @@ async fn first_responses_request_body_for_supported_server_requests( create_config_toml(codex_home.path(), &server.uri())?; let mut mcp = McpProcess::new(codex_home.path()).await?; - if supported_server_requests.is_empty() { - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + if permission_confirmations { + initialize_with_permission_confirmations(&mut mcp, true).await?; } else { - initialize_with_supported_server_requests(&mut mcp, supported_server_requests).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; } start_thread_and_turn(&mut mcp, "Hello").await?; @@ -506,7 +448,6 @@ stream_max_retries = 0 [features] request_permissions_tool = true -request_permission_preset_tool = true "# ), ) diff --git a/codex-rs/app-server/tests/suite/v2/thread_status.rs b/codex-rs/app-server/tests/suite/v2/thread_status.rs index 7dabce9db5..1f73c8f4a0 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_status.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_status.rs @@ -145,7 +145,7 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> { }, Some(InitializeCapabilities { experimental_api: true, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]), }), ), diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index cff4f60db0..bd74c46595 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -443,9 +443,6 @@ "remote_models": { "type": "boolean" }, - "request_permission_preset_tool": { - "type": "boolean" - }, "request_permissions": { "type": "boolean" }, @@ -2295,9 +2292,6 @@ "remote_models": { "type": "boolean" }, - "request_permission_preset_tool": { - "type": "boolean" - }, "request_permissions": { "type": "boolean" }, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 14d4a90f6e..a5a07b6d29 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3344,9 +3344,8 @@ impl Session { /// Sends a permission preset request to the active client and waits for the user's decision. /// - /// Core resolves and validates the preset before emitting the event so the - /// client confirms concrete settings rather than interpreting policy names. - /// The settings are not applied by this function; they become active only + /// Core validates the preset before emitting the event. The settings are + /// not applied by this function; they become active only /// when `notify_request_permission_preset_response` receives an accepted /// response for the pending call id. pub async fn request_permission_preset( @@ -3416,11 +3415,6 @@ impl Session { call_id, turn_id: turn_context.sub_id.clone(), preset: preset_id, - label: preset.label.to_string(), - description: preset.description.to_string(), - approval_policy: preset.approval, - approvals_reviewer: preset.approvals_reviewer, - sandbox_policy: preset.sandbox, reason: args.reason, }); self.send_event(turn_context, event).await; @@ -3445,33 +3439,47 @@ impl Session { call_id: &str, response: RequestPermissionPresetResponse, ) { - if matches!(response.decision, RequestPermissionPresetDecision::Accepted) - && let Some(preset) = self + let mut response = response; + if matches!(response.decision, RequestPermissionPresetDecision::Accepted) { + if let Some(preset) = self .resolve_permission_preset_for_current_session(response.preset) .await - { - let preset_override = TurnPermissionPresetOverride { - approval_policy: preset.approval, - approvals_reviewer: preset.approvals_reviewer, - sandbox_policy: preset.sandbox.clone(), - }; { - let mut active = self.active_turn.lock().await; - if let Some(at) = active.as_mut() { - let mut ts = at.turn_state.lock().await; - ts.record_permission_preset_override(preset_override); - } - } - if let Err(err) = self - .update_settings(SessionSettingsUpdate { + let update = SessionSettingsUpdate { approval_policy: Some(preset.approval), approvals_reviewer: Some(preset.approvals_reviewer), - sandbox_policy: Some(preset.sandbox), + sandbox_policy: Some(preset.sandbox.clone()), ..Default::default() - }) - .await - { - warn!("failed to apply approved permission preset: {err}"); + }; + match self.update_settings(update).await { + Ok(()) => { + let preset_override = TurnPermissionPresetOverride { + approval_policy: preset.approval, + approvals_reviewer: preset.approvals_reviewer, + sandbox_policy: preset.sandbox, + }; + let mut active = self.active_turn.lock().await; + if let Some(at) = active.as_mut() { + let mut ts = at.turn_state.lock().await; + ts.record_permission_preset_override(preset_override); + } + } + Err(err) => { + warn!("failed to apply approved permission preset: {err}"); + response = RequestPermissionPresetResponse { + decision: RequestPermissionPresetDecision::Declined, + preset: response.preset, + message: format!("failed to apply approved permission preset: {err}"), + }; + } + } + } else { + response = RequestPermissionPresetResponse { + decision: RequestPermissionPresetDecision::Declined, + preset: response.preset, + message: "requested permission preset is no longer available in this session" + .to_string(), + }; } } @@ -3996,9 +4004,6 @@ impl Session { turn_context .features .enabled(Feature::RequestPermissionsTool), - turn_context - .features - .enabled(Feature::RequestPermissionPresetTool), ) .into_text(), ); diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index ed0c3e8df3..e92aad3d77 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -56,7 +56,6 @@ fn build_permissions_update_item( &next.cwd, next.features.enabled(Feature::ExecPermissionApprovals), next.features.enabled(Feature::RequestPermissionsTool), - next.features.enabled(Feature::RequestPermissionPresetTool), )) } diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 81b273ae67..ec6b7d67d2 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -556,7 +556,6 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { test.config.cwd.as_path(), /*exec_permission_approvals_enabled*/ false, /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ false, ) .into_text(); // Normalize line endings to handle Windows vs Unix differences diff --git a/codex-rs/core/tests/suite/request_permission_preset_tool.rs b/codex-rs/core/tests/suite/request_permission_preset_tool.rs index 8a8dcae19a..14ddb9ddfd 100644 --- a/codex-rs/core/tests/suite/request_permission_preset_tool.rs +++ b/codex-rs/core/tests/suite/request_permission_preset_tool.rs @@ -55,7 +55,7 @@ async fn accepted_permission_preset_request_returns_model_output() -> Result<()> }); config .features - .enable(Feature::RequestPermissionPresetTool) + .enable(Feature::RequestPermissionsTool) .expect("test config should allow feature update"); }); let test = builder.build(&server).await?; diff --git a/codex-rs/debug-client/src/client.rs b/codex-rs/debug-client/src/client.rs index 0f1c113b27..56f8dd7d62 100644 --- a/codex-rs/debug-client/src/client.rs +++ b/codex-rs/debug-client/src/client.rs @@ -103,7 +103,7 @@ impl AppServerClient { }, capabilities: Some(InitializeCapabilities { experimental_api: true, - supported_server_requests: None, + permission_confirmations: false, opt_out_notification_methods: None, }), }, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 5c0797b14d..359378b831 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -483,7 +483,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result client_name: "codex_exec".to_string(), client_version: env!("CARGO_PKG_VERSION").to_string(), experimental_api: true, - supported_server_requests: Vec::new(), + permission_confirmations: false, opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }; diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index b28b21103b..2de7e88414 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -95,10 +95,8 @@ pub enum Feature { ExecPermissionApprovals, /// Enable Claude-style lifecycle hooks loaded from hooks.json files. CodexHooks, - /// Expose the built-in request_permissions tool. + /// Expose built-in conversational permission tools. RequestPermissionsTool, - /// Expose the built-in request_permission_preset tool. - RequestPermissionPresetTool, /// Allow the model to request web searches that fetch live content. WebSearchRequest, /// Allow the model to request web searches that fetch cached content. @@ -711,12 +709,6 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, - FeatureSpec { - id: Feature::RequestPermissionPresetTool, - key: "request_permission_preset_tool", - stage: Stage::Stable, - default_enabled: true, - }, FeatureSpec { id: Feature::UseLinuxSandboxBwrap, key: "use_linux_sandbox_bwrap", diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index ac167cfc41..60ba735e11 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -112,12 +112,6 @@ fn request_permissions_tool_is_stable_and_enabled_by_default() { assert_eq!(Feature::RequestPermissionsTool.default_enabled(), true); } -#[test] -fn request_permission_preset_tool_is_stable_and_enabled_by_default() { - assert_eq!(Feature::RequestPermissionPresetTool.stage(), Stage::Stable); - assert_eq!(Feature::RequestPermissionPresetTool.default_enabled(), true); -} - #[test] fn tool_suggest_is_stable_and_enabled_by_default() { assert_eq!(Feature::ToolSuggest.stage(), Stage::Stable); diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 1b8edc7cc7..48d01ed8d4 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -391,7 +391,6 @@ struct PermissionsPromptConfig<'a> { exec_policy: &'a Policy, exec_permission_approvals_enabled: bool, request_permissions_tool_enabled: bool, - request_permission_preset_tool_enabled: bool, } impl DeveloperInstructions { @@ -405,14 +404,11 @@ impl DeveloperInstructions { exec_policy: &Policy, exec_permission_approvals_enabled: bool, request_permissions_tool_enabled: bool, - request_permission_preset_tool_enabled: bool, ) -> DeveloperInstructions { let append_permission_tool_sections = |text: &str| { let mut sections = vec![text.to_string()]; if request_permissions_tool_enabled { sections.push(request_permissions_tool_prompt_section().to_string()); - } - if request_permission_preset_tool_enabled { sections.push(request_permission_preset_tool_prompt_section().to_string()); } sections.join("\n\n") @@ -426,8 +422,6 @@ impl DeveloperInstructions { let mut sections = vec![on_request_rule]; if request_permissions_tool_enabled { sections.push(request_permissions_tool_prompt_section().to_string()); - } - if request_permission_preset_tool_enabled { sections.push(request_permission_preset_tool_prompt_section().to_string()); } if let Some(prefixes) = approved_command_prefixes_text(exec_policy) { @@ -451,7 +445,6 @@ impl DeveloperInstructions { exec_policy, exec_permission_approvals_enabled, request_permissions_tool_enabled, - request_permission_preset_tool_enabled, ), }; @@ -518,7 +511,6 @@ impl DeveloperInstructions { cwd: &Path, exec_permission_approvals_enabled: bool, request_permissions_tool_enabled: bool, - request_permission_preset_tool_enabled: bool, ) -> Self { let network_access = if sandbox_policy.has_full_network_access() { NetworkAccess::Enabled @@ -545,7 +537,6 @@ impl DeveloperInstructions { exec_policy, exec_permission_approvals_enabled, request_permissions_tool_enabled, - request_permission_preset_tool_enabled, }, writable_roots, ) @@ -584,7 +575,6 @@ impl DeveloperInstructions { config.exec_policy, config.exec_permission_approvals_enabled, config.request_permissions_tool_enabled, - config.request_permission_preset_tool_enabled, )) .concat(DeveloperInstructions::from_writable_roots(writable_roots)) .concat(end_tag) @@ -648,15 +638,12 @@ fn granular_instructions( exec_policy: &Policy, exec_permission_approvals_enabled: bool, request_permissions_tool_enabled: bool, - request_permission_preset_tool_enabled: bool, ) -> String { let sandbox_approval_prompts_allowed = granular_config.allows_sandbox_approval(); let shell_permission_requests_available = exec_permission_approvals_enabled && sandbox_approval_prompts_allowed; let request_permissions_tool_prompts_allowed = request_permissions_tool_enabled && granular_config.allows_request_permissions(); - let request_permission_preset_tool_prompts_allowed = - request_permission_preset_tool_enabled && granular_config.allows_request_permissions(); let categories = [ Some(( granular_config.allows_sandbox_approval(), @@ -668,7 +655,7 @@ fn granular_instructions( granular_config.allows_request_permissions(), "`request_permissions`", )), - request_permission_preset_tool_enabled.then_some(( + request_permissions_tool_enabled.then_some(( granular_config.allows_request_permissions(), "`request_permission_preset`", )), @@ -711,8 +698,6 @@ fn granular_instructions( if request_permissions_tool_prompts_allowed { sections.push(request_permissions_tool_prompt_section().to_string()); - } - if request_permission_preset_tool_prompts_allowed { sections.push(request_permission_preset_tool_prompt_section().to_string()); } @@ -1722,7 +1707,6 @@ mod tests { exec_policy: &Policy::empty(), exec_permission_approvals_enabled: false, request_permissions_tool_enabled: false, - request_permission_preset_tool_enabled: false, }, /*writable_roots*/ None, ); @@ -1756,7 +1740,6 @@ mod tests { &PathBuf::from("/tmp"), /*exec_permission_approvals_enabled*/ false, /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ false, ); let text = instructions.into_text(); assert!(text.contains("Network access is enabled.")); @@ -1781,7 +1764,6 @@ mod tests { exec_policy: &exec_policy, exec_permission_approvals_enabled: false, request_permissions_tool_enabled: false, - request_permission_preset_tool_enabled: false, }, /*writable_roots*/ None, ); @@ -1803,7 +1785,6 @@ mod tests { exec_policy: &Policy::empty(), exec_permission_approvals_enabled: false, request_permissions_tool_enabled: true, - request_permission_preset_tool_enabled: false, }, /*writable_roots*/ None, ); @@ -1811,6 +1792,7 @@ mod tests { let text = instructions.into_text(); assert!(text.contains("`approval_policy` is `unless-trusted`")); assert!(text.contains("# request_permissions Tool")); + assert!(text.contains("# request_permission_preset Tool")); assert!(text.contains("After this tool returns, the user has already approved or denied")); assert!(text.contains("do not say \"once you approve\"")); } @@ -1826,7 +1808,6 @@ mod tests { exec_policy: &Policy::empty(), exec_permission_approvals_enabled: false, request_permissions_tool_enabled: true, - request_permission_preset_tool_enabled: false, }, /*writable_roots*/ None, ); @@ -1834,6 +1815,7 @@ mod tests { let text = instructions.into_text(); assert!(text.contains("`approval_policy` is `on-failure`")); assert!(text.contains("# request_permissions Tool")); + assert!(text.contains("# request_permission_preset Tool")); } #[test] @@ -1847,7 +1829,6 @@ mod tests { exec_policy: &Policy::empty(), exec_permission_approvals_enabled: true, request_permissions_tool_enabled: false, - request_permission_preset_tool_enabled: false, }, /*writable_roots*/ None, ); @@ -1868,7 +1849,6 @@ mod tests { exec_policy: &Policy::empty(), exec_permission_approvals_enabled: false, request_permissions_tool_enabled: true, - request_permission_preset_tool_enabled: false, }, /*writable_roots*/ None, ); @@ -1880,10 +1860,16 @@ mod tests { ); assert!(text.contains("allow writing to ~/Downloads in this session")); assert!(text.contains("Set `scope` to `session`")); + assert!(text.contains("# request_permission_preset Tool")); + assert!(text.contains( + "asks conversationally to change the broad sandboxing, approval, or permission mode" + )); + assert!(text.contains("Use `request_permissions` instead for named paths")); + assert!(text.contains("never use the preset picker")); } #[test] - fn includes_request_permission_preset_tool_instructions_when_enabled() { + fn omits_permission_tool_instructions_when_disabled() { let instructions = DeveloperInstructions::from_permissions_with_network( SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, @@ -1893,18 +1879,13 @@ mod tests { exec_policy: &Policy::empty(), exec_permission_approvals_enabled: false, request_permissions_tool_enabled: false, - request_permission_preset_tool_enabled: true, }, /*writable_roots*/ None, ); let text = instructions.into_text(); - assert!(text.contains("# request_permission_preset Tool")); - assert!(text.contains( - "asks conversationally to change the broad sandboxing, approval, or permission mode" - )); - assert!(text.contains("Use `request_permissions` instead for named paths")); - assert!(text.contains("never use the preset picker")); + assert!(!text.contains("# request_permissions Tool")); + assert!(!text.contains("# request_permission_preset Tool")); } #[test] @@ -1918,7 +1899,6 @@ mod tests { exec_policy: &Policy::empty(), exec_permission_approvals_enabled: true, request_permissions_tool_enabled: true, - request_permission_preset_tool_enabled: false, }, /*writable_roots*/ None, ); @@ -1926,6 +1906,7 @@ mod tests { let text = instructions.into_text(); assert!(text.contains("with_additional_permissions")); assert!(text.contains("# request_permissions Tool")); + assert!(text.contains("# request_permission_preset Tool")); } #[test] @@ -1936,7 +1917,6 @@ mod tests { &Policy::empty(), /*exec_permission_approvals_enabled*/ false, /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ false, ) .into_text(); @@ -1952,7 +1932,6 @@ mod tests { &Policy::empty(), /*exec_permission_approvals_enabled*/ false, /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ false, ) .into_text(); @@ -1968,7 +1947,6 @@ mod tests { rejected_categories: &[&str], include_shell_permission_request_instructions: bool, include_request_permissions_tool_section: bool, - include_request_permission_preset_tool_section: bool, ) -> String { let mut sections = vec![granular_prompt_intro_text().to_string()]; if !prompted_categories.is_empty() { @@ -1988,8 +1966,6 @@ mod tests { } if include_request_permissions_tool_section { sections.push(request_permissions_tool_prompt_section().to_string()); - } - if include_request_permission_preset_tool_section { sections.push(request_permission_preset_tool_prompt_section().to_string()); } sections.join("\n\n") @@ -2009,7 +1985,6 @@ mod tests { &Policy::empty(), /*exec_permission_approvals_enabled*/ true, /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ false, ) .into_text(); @@ -2044,7 +2019,6 @@ mod tests { &Policy::empty(), /*exec_permission_approvals_enabled*/ true, /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ false, ) .into_text(); @@ -2060,7 +2034,6 @@ mod tests { &[], /*include_shell_permission_request_instructions*/ true, /*include_request_permissions_tool_section*/ false, - /*include_request_permission_preset_tool_section*/ false, ) ); } @@ -2079,7 +2052,6 @@ mod tests { &Policy::empty(), /*exec_permission_approvals_enabled*/ false, /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ false, ) .into_text(); @@ -2095,7 +2067,6 @@ mod tests { &[], /*include_shell_permission_request_instructions*/ false, /*include_request_permissions_tool_section*/ false, - /*include_request_permission_preset_tool_section*/ false, ) ); } @@ -2114,47 +2085,9 @@ mod tests { &Policy::empty(), /*exec_permission_approvals_enabled*/ true, /*request_permissions_tool_enabled*/ true, - /*request_permission_preset_tool_enabled*/ false, ) .into_text(); assert!(allowed.contains("# request_permissions Tool")); - - let rejected = DeveloperInstructions::from( - AskForApproval::Granular(GranularApprovalConfig { - sandbox_approval: true, - rules: true, - skill_approval: true, - request_permissions: false, - mcp_elicitations: true, - }), - ApprovalsReviewer::User, - &Policy::empty(), - /*exec_permission_approvals_enabled*/ true, - /*request_permissions_tool_enabled*/ true, - /*request_permission_preset_tool_enabled*/ false, - ) - .into_text(); - assert!(!rejected.contains("# request_permissions Tool")); - } - - #[test] - fn granular_policy_includes_request_permission_preset_tool_only_when_that_prompt_can_still_fire() - { - let allowed = DeveloperInstructions::from( - AskForApproval::Granular(GranularApprovalConfig { - sandbox_approval: true, - rules: true, - skill_approval: true, - request_permissions: true, - mcp_elicitations: true, - }), - ApprovalsReviewer::User, - &Policy::empty(), - /*exec_permission_approvals_enabled*/ true, - /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ true, - ) - .into_text(); assert!(allowed.contains("# request_permission_preset Tool")); let rejected = DeveloperInstructions::from( @@ -2168,10 +2101,10 @@ mod tests { ApprovalsReviewer::User, &Policy::empty(), /*exec_permission_approvals_enabled*/ true, - /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ true, + /*request_permissions_tool_enabled*/ true, ) .into_text(); + assert!(!rejected.contains("# request_permissions Tool")); assert!(!rejected.contains("# request_permission_preset Tool")); assert!(rejected.contains("- `request_permission_preset`")); } @@ -2191,7 +2124,6 @@ mod tests { &Policy::empty(), /*exec_permission_approvals_enabled*/ true, /*request_permissions_tool_enabled*/ false, - /*request_permission_preset_tool_enabled*/ false, ) .into_text(); diff --git a/codex-rs/protocol/src/request_permission_preset.rs b/codex-rs/protocol/src/request_permission_preset.rs index dd2f24a1e2..57c1a7dca8 100644 --- a/codex-rs/protocol/src/request_permission_preset.rs +++ b/codex-rs/protocol/src/request_permission_preset.rs @@ -1,13 +1,10 @@ //! Protocol types for conversational permission-mode preset requests. //! //! These messages represent the narrow bridge between a model tool call and a -//! client-owned confirmation UI. Core resolves a requested preset into concrete -//! sandbox, approval, and reviewer settings before emitting the event, and the -//! settings only become active after the client returns an accepted response. +//! client-owned confirmation UI. Core validates the requested preset before +//! emitting the event, and the settings only become active after the client +//! returns an accepted response. -use crate::config_types::ApprovalsReviewer; -use crate::protocol::AskForApproval; -use crate::protocol::SandboxPolicy; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -81,11 +78,6 @@ pub struct RequestPermissionPresetEvent { #[serde(default)] pub turn_id: String, pub preset: PermissionPresetId, - pub label: String, - pub description: String, - pub approval_policy: AskForApproval, - pub approvals_reviewer: ApprovalsReviewer, - pub sandbox_policy: SandboxPolicy, #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, } diff --git a/codex-rs/tools/src/tool_config.rs b/codex-rs/tools/src/tool_config.rs index b17b9628ac..47adb8b866 100644 --- a/codex-rs/tools/src/tool_config.rs +++ b/codex-rs/tools/src/tool_config.rs @@ -97,7 +97,6 @@ pub struct ToolsConfig { pub tool_suggest: bool, pub exec_permission_approvals_enabled: bool, pub request_permissions_tool_enabled: bool, - pub request_permission_preset_tool_enabled: bool, pub guardian_approval_enabled: bool, pub code_mode_enabled: bool, pub code_mode_only_enabled: bool, @@ -163,8 +162,6 @@ impl ToolsConfig { && supports_image_generation(model_info); let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals); let request_permissions_tool_enabled = features.enabled(Feature::RequestPermissionsTool); - let request_permission_preset_tool_enabled = - features.enabled(Feature::RequestPermissionPresetTool); let guardian_approval_enabled = features.enabled(Feature::GuardianApproval); let shell_command_backend = if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) { @@ -223,7 +220,6 @@ impl ToolsConfig { tool_suggest: include_tool_suggest, exec_permission_approvals_enabled, request_permissions_tool_enabled, - request_permission_preset_tool_enabled, guardian_approval_enabled, code_mode_enabled: include_code_mode, code_mode_only_enabled: include_code_mode_only, diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index efa93405e2..f6538f1665 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -247,9 +247,7 @@ pub fn build_tool_registry_plan( config.code_mode_enabled, ); plan.register_handler("request_permissions", ToolHandlerKind::RequestPermissions); - } - if config.request_permission_preset_tool_enabled { let available_preset_ids = builtin_permission_presets( cfg!(target_os = "windows"), config.guardian_approval_enabled, diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 5ef00cfad9..33d1c34ca3 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -126,8 +126,6 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { if config.request_permissions_tool_enabled { let spec = create_request_permissions_tool(request_permissions_tool_description()); expected.insert(spec.name().to_string(), spec); - } - if config.request_permission_preset_tool_enabled { let available_preset_ids = if cfg!(target_os = "windows") { vec!["auto", "full-access", "read-only"] } else { @@ -594,6 +592,16 @@ fn request_permissions_is_enabled_by_default_and_can_be_disabled() { request_permissions_tool.spec, create_request_permissions_tool(request_permissions_tool_description()) ); + let request_permission_preset_tool = find_tool(&tools, "request_permission_preset"); + let available_preset_ids = if cfg!(target_os = "windows") { + vec!["auto", "full-access", "read-only"] + } else { + vec!["auto", "full-access"] + }; + assert_eq!( + request_permission_preset_tool.spec, + create_request_permission_preset_tool(available_preset_ids) + ); let mut features = Features::with_defaults(); features.disable(Feature::RequestPermissionsTool); @@ -614,60 +622,7 @@ fn request_permissions_is_enabled_by_default_and_can_be_disabled() { &[], ); assert_lacks_tool_name(&tools, "request_permissions"); -} - -#[test] -fn request_permission_preset_requires_feature_flag() { - let model_info = model_info(); - let mut features = Features::with_defaults(); - features.disable(Feature::RequestPermissionPresetTool); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - image_generation_tool_auth_allowed: true, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); assert_lacks_tool_name(&tools, "request_permission_preset"); - - let mut features = Features::with_defaults(); - features.enable(Feature::RequestPermissionPresetTool); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - image_generation_tool_auth_allowed: true, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); - let request_permission_preset_tool = find_tool(&tools, "request_permission_preset"); - let available_preset_ids = if cfg!(target_os = "windows") { - vec!["auto", "full-access", "read-only"] - } else { - vec!["auto", "full-access"] - }; - assert_eq!( - request_permission_preset_tool.spec, - create_request_permission_preset_tool(available_preset_ids) - ); } #[test] diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index a703d6af28..386b35381f 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1851,11 +1851,6 @@ impl App { call_id: params.item_id.clone(), reason: params.reason.clone(), preset: params.preset.to_core(), - label: params.label.clone(), - description: params.description.clone(), - approval_policy: params.approval_policy.to_core(), - approvals_reviewer: params.approvals_reviewer.to_core(), - sandbox_policy: params.sandbox_policy.to_core(), }), ), _ => None, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a5e5ec2de1..7db1c735d4 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1589,11 +1589,6 @@ fn request_permission_preset_from_params( call_id: params.item_id, reason: params.reason, preset: params.preset.to_core(), - label: params.label, - description: params.description, - approval_policy: params.approval_policy.to_core(), - approvals_reviewer: params.approvals_reviewer.to_core(), - sandbox_policy: params.sandbox_policy.to_core(), } } @@ -8617,22 +8612,24 @@ impl ChatWidget { vec![Box::new(move |tx| { let sandbox_clone = sandbox.clone(); let message = format!("Permissions updated to {label}"); - tx.send(AppEvent::CodexOp( - AppCommand::override_turn_context( - /*cwd*/ None, - Some(approval), - Some(approvals_reviewer), - Some(sandbox_clone.clone()), - /*windows_sandbox_level*/ None, - /*model*/ None, - /*effort*/ None, - /*summary*/ None, - /*service_tier*/ None, - /*collaboration_mode*/ None, - /*personality*/ None, - ) - .into_core(), - )); + if response.is_none() { + tx.send(AppEvent::CodexOp( + AppCommand::override_turn_context( + /*cwd*/ None, + Some(approval), + Some(approvals_reviewer), + Some(sandbox_clone.clone()), + /*windows_sandbox_level*/ None, + /*model*/ None, + /*effort*/ None, + /*summary*/ None, + /*service_tier*/ None, + /*collaboration_mode*/ None, + /*personality*/ None, + ) + .into_core(), + )); + } tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone)); tx.send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer)); diff --git a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs index 41e8899b2d..3fe224a29b 100644 --- a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs +++ b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs @@ -2,8 +2,6 @@ //! to keep the primary module under blob-size policy limits. use super::*; -use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::request_permission_preset::PermissionPresetId; use codex_protocol::request_permission_preset::RequestPermissionPresetEvent; use pretty_assertions::assert_eq; @@ -69,11 +67,6 @@ async fn permission_preset_request_opens_permissions_picker_with_requested_prese call_id: "preset-call".into(), turn_id: "preset-turn".into(), preset: PermissionPresetId::FullAccess, - label: "Full access".into(), - description: "Run commands without filesystem sandboxing; network access enabled.".into(), - approval_policy: AskForApproval::Never, - approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::DangerFullAccess, reason: Some("the user asked to switch into full access mode".into()), }; chat.handle_codex_event(Event { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index b546f5392e..0f920fdb2b 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -33,7 +33,6 @@ use codex_app_server_client::RemoteAppServerConnectArgs; use codex_app_server_protocol::Account as AppServerAccount; use codex_app_server_protocol::AuthMode as AppServerAuthMode; use codex_app_server_protocol::ConfigWarningNotification; -use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::Thread as AppServerThread; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey; @@ -352,7 +351,7 @@ async fn connect_remote_app_server( client_name: "codex-tui".to_string(), client_version: env!("CARGO_PKG_VERSION").to_string(), experimental_api: true, - supported_server_requests: tui_supported_server_requests(), + permission_confirmations: true, opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }) @@ -461,7 +460,7 @@ where client_name: "codex-tui".to_string(), client_version: env!("CARGO_PKG_VERSION").to_string(), experimental_api: true, - supported_server_requests: tui_supported_server_requests(), + permission_confirmations: true, opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }) @@ -470,13 +469,6 @@ where Ok(client) } -fn tui_supported_server_requests() -> Vec { - vec![ - SupportedServerRequestMethod::PermissionsRequestApproval, - SupportedServerRequestMethod::PermissionPresetRequestApproval, - ] -} - async fn shutdown_app_server_if_present(app_server: Option) { if let Some(app_server) = app_server && let Err(err) = app_server.shutdown().await @@ -1824,18 +1816,6 @@ mod tests { } } - #[test] - fn tui_advertises_permission_confirmation_server_requests() { - let supported = tui_supported_server_requests(); - assert_eq!( - supported, - vec![ - SupportedServerRequestMethod::PermissionsRequestApproval, - SupportedServerRequestMethod::PermissionPresetRequestApproval, - ] - ); - } - #[test] fn normalize_remote_addr_rejects_invalid_input() { let err = normalize_remote_addr("https://127.0.0.1:4500") diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index a108e9b539..da6a2c354e 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -997,7 +997,7 @@ mod tests { client_name: "test".to_string(), client_version: "test".to_string(), experimental_api: true, - supported_server_requests: Vec::new(), + permission_confirmations: false, opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, })