mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
refactor(app-server): simplify permission confirmation flow
Replace the per-method v1 app-server capability with a single `permissionConfirmations` opt-in. Route both permission request flows through that capability. Collapse the separate preset tool feature into `request_permissions_tool`, and make core apply accepted preset responses so clients only confirm the user decision.
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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<SupportedServerRequestMethod>,
|
||||
/// 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<String>,
|
||||
/// 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<F, Fut>(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,
|
||||
}
|
||||
|
||||
@@ -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<SupportedServerRequestMethod>,
|
||||
pub permission_confirmations: bool,
|
||||
pub opt_out_notification_methods: Vec<String>,
|
||||
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 {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+1
-256
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<SupportedServerRequestMethod> | null,
|
||||
permissionConfirmations: boolean,
|
||||
/**
|
||||
* Exact notification method names that should be suppressed for this
|
||||
* connection (for example `thread/started`).
|
||||
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
+3
-7
@@ -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, };
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Vec<SupportedServerRequestMethod>>,
|
||||
/// 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<Vec<String>>,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -6289,9 +6289,8 @@ impl From<CorePermissionPresetId> 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<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<SupportedServerRequestMethod>,
|
||||
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::<ThreadId>::new());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>,
|
||||
pub(crate) supported_server_requests: HashSet<SupportedServerRequestMethod>,
|
||||
pub(crate) permission_confirmations: bool,
|
||||
pub(crate) app_server_client_name: Option<String>,
|
||||
pub(crate) client_version: Option<String>,
|
||||
}
|
||||
@@ -461,10 +460,10 @@ impl MessageProcessor {
|
||||
pub(crate) async fn connection_initialized(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
supported_server_requests: HashSet<SupportedServerRequestMethod>,
|
||||
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;
|
||||
|
||||
@@ -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<ConnectionId>,
|
||||
supported_server_requests_by_connection:
|
||||
HashMap<ConnectionId, HashSet<SupportedServerRequestMethod>>,
|
||||
permission_confirmations_by_connection: HashSet<ConnectionId>,
|
||||
threads: HashMap<ThreadId, ThreadEntry>,
|
||||
thread_ids_by_connection: HashMap<ConnectionId, HashSet<ThreadId>>,
|
||||
}
|
||||
@@ -193,25 +191,29 @@ impl ThreadStateManager {
|
||||
pub(crate) async fn connection_initialized(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
supported_server_requests: HashSet<SupportedServerRequestMethod>,
|
||||
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<SupportedServerRequestMethod> {
|
||||
) -> 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<ConnectionId> {
|
||||
@@ -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<ConnectionId> {
|
||||
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::<Vec<_>>();
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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()]),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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<SupportedServerRequestMethod>,
|
||||
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<SupportedServerRequestMethod>,
|
||||
async fn first_responses_request_body_for_permission_confirmations(
|
||||
permission_confirmations: bool,
|
||||
) -> Result<Value> {
|
||||
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
|
||||
"#
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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()]),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+37
-32
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -103,7 +103,7 @@ impl AppServerClient {
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
supported_server_requests: None,
|
||||
permission_confirmations: false,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+2
-22
@@ -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<SupportedServerRequestMethod> {
|
||||
vec![
|
||||
SupportedServerRequestMethod::PermissionsRequestApproval,
|
||||
SupportedServerRequestMethod::PermissionPresetRequestApproval,
|
||||
]
|
||||
}
|
||||
|
||||
async fn shutdown_app_server_if_present(app_server: Option<AppServerSession>) {
|
||||
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")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user