diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index e74a763656..661ae6f53c 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -228,6 +228,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." + }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -244,7 +255,7 @@ "type": "null" } ], - "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted." + "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted. Cannot be combined with `permissionProfile`." }, "size": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 8cb8b67c20..10c3f02030 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -6317,6 +6317,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." + }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -6333,7 +6344,7 @@ "type": "null" } ], - "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted." + "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted. Cannot be combined with `permissionProfile`." }, "size": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index ea6a74f98c..3fb63f76ee 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -2868,6 +2868,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." + }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -2884,7 +2895,7 @@ "type": "null" } ], - "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted." + "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted. Cannot be combined with `permissionProfile`." }, "size": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json index 986ec4cc1e..53c93d4f1e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -5,6 +5,17 @@ "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" }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "CommandExecTerminalSize": { "description": "PTY size in character cells for `command/exec` PTY sessions.", "properties": { @@ -27,6 +38,217 @@ ], "type": "object" }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "none" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "current_working_directory" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "CurrentWorkingDirectoryFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, "NetworkAccess": { "enum": [ "restricted", @@ -34,6 +256,53 @@ ], "type": "string" }, + "PermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfileFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "PermissionProfileFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "ReadOnlyAccess": { "oneOf": [ { @@ -247,6 +516,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." + }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -263,7 +543,7 @@ "type": "null" } ], - "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted." + "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted. Cannot be combined with `permissionProfile`." }, "size": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts index 097cfdfecc..659974feaf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts @@ -2,6 +2,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; /** @@ -92,6 +93,14 @@ size?: CommandExecTerminalSize | null, * Optional sandbox policy for this command. * * Uses the same shape as thread/turn execution sandbox configuration and - * defaults to the user's configured policy when omitted. + * defaults to the user's configured policy when omitted. Cannot be + * combined with `permissionProfile`. */ -sandboxPolicy?: SandboxPolicy | null, }; +sandboxPolicy?: SandboxPolicy | null, +/** + * Optional full permissions profile for this command. + * + * Defaults to the user's configured permissions when omitted. Cannot be + * combined with `sandboxPolicy`. + */ +permissionProfile?: PermissionProfile | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/mappers.rs b/codex-rs/app-server-protocol/src/protocol/mappers.rs index 93f12691be..dae91e650f 100644 --- a/codex-rs/app-server-protocol/src/protocol/mappers.rs +++ b/codex-rs/app-server-protocol/src/protocol/mappers.rs @@ -18,6 +18,7 @@ impl From for v2::CommandExecParams { env: None, size: None, sandbox_policy: value.sandbox_policy.map(std::convert::Into::into), + permission_profile: None, } } } diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index feb13cfd80..27b49e75cb 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2838,9 +2838,16 @@ pub struct CommandExecParams { /// Optional sandbox policy for this command. /// /// Uses the same shape as thread/turn execution sandbox configuration and - /// defaults to the user's configured policy when omitted. + /// defaults to the user's configured policy when omitted. Cannot be + /// combined with `permissionProfile`. #[ts(optional = nullable)] pub sandbox_policy: Option, + /// Optional full permissions profile for this command. + /// + /// Defaults to the user's configured permissions when omitted. Cannot be + /// combined with `sandboxPolicy`. + #[ts(optional = nullable)] + pub permission_profile: Option, } /// Final buffered result for `command/exec`. @@ -7603,6 +7610,7 @@ mod tests { env: None, size: None, sandbox_policy: None, + permission_profile: None, } ); } @@ -7623,6 +7631,7 @@ mod tests { env: None, size: None, sandbox_policy: None, + permission_profile: None, }; let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); @@ -7637,6 +7646,7 @@ mod tests { "env": null, "size": null, "sandboxPolicy": null, + "permissionProfile": null, "outputBytesCap": null, }) ); @@ -7662,6 +7672,7 @@ mod tests { env: None, size: None, sandbox_policy: None, + permission_profile: None, }; let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); @@ -7678,6 +7689,7 @@ mod tests { "env": null, "size": null, "sandboxPolicy": null, + "permissionProfile": null, }) ); @@ -7706,6 +7718,7 @@ mod tests { ])), size: None, sandbox_policy: None, + permission_profile: None, }; let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); @@ -7724,6 +7737,7 @@ mod tests { }, "size": null, "sandboxPolicy": null, + "permissionProfile": null, }) ); @@ -7793,6 +7807,7 @@ mod tests { cols: 120, }), sandbox_policy: None, + permission_profile: None, }; let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); @@ -7811,6 +7826,7 @@ mod tests { "cols": 120, }, "sandboxPolicy": null, + "permissionProfile": null, }) ); diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index ca489eaad7..183c389852 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -810,7 +810,13 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin "cwd": "/Users/me/project", // optional; defaults to server cwd "env": { "FOO": "override" }, // optional; merges into the server env and overrides matching names "size": { "rows": 40, "cols": 120 }, // optional; PTY size in character cells, only valid with tty=true - "sandboxPolicy": { "type": "workspaceWrite" }, // optional; defaults to user config + "permissionProfile": { // optional; defaults to user config + "fileSystem": { "entries": [ + { "path": { "type": "special", "value": { "kind": "root" } }, "access": "read" }, + { "path": { "type": "special", "value": { "kind": "current_working_directory" } }, "access": "write" } + ] }, + "network": { "enabled": false } + }, "outputBytesCap": 1048576, // optional; per-stream capture cap "disableOutputCap": false, // optional; cannot be combined with outputBytesCap "timeoutMs": 10000, // optional; ms timeout; defaults to server timeout @@ -823,12 +829,12 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin } } ``` -- For clients that are already sandboxed externally, set `sandboxPolicy` to `{"type":"externalSandbox","networkAccess":"enabled"}` (or omit `networkAccess` to keep it restricted). Codex will not enforce its own sandbox in this mode; it tells the model it has full file-system access and passes the `networkAccess` state through `environment_context`. +- For clients that are already sandboxed externally, set the legacy `sandboxPolicy` to `{"type":"externalSandbox","networkAccess":"enabled"}` (or omit `networkAccess` to keep it restricted). Codex will not enforce its own sandbox in this mode; it tells the model it has full file-system access and passes the `networkAccess` state through `environment_context`. Notes: - Empty `command` arrays are rejected. -- `sandboxPolicy` accepts the same shape used by `turn/start` (e.g., `dangerFullAccess`, `readOnly`, `workspaceWrite` with flags, `externalSandbox` with `networkAccess` `restricted|enabled`). +- Prefer `permissionProfile` for command permission overrides. The legacy `sandboxPolicy` field accepts the same shape used by `turn/start` (e.g., `dangerFullAccess`, `readOnly`, `workspaceWrite` with flags, `externalSandbox` with `networkAccess` `restricted|enabled`), but cannot be combined with `permissionProfile`. - `env` merges into the environment produced by the server's shell environment policy. Matching names are overridden; unspecified variables are left intact. - When omitted, `timeoutMs` falls back to the server default. - When omitted, `outputBytesCap` falls back to the server default of 1 MiB per stream. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 646acf45bd..cece3afa58 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2106,7 +2106,16 @@ impl CodexMessageProcessor { env: env_overrides, size, sandbox_policy, + permission_profile, } = params; + if sandbox_policy.is_some() && permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandboxPolicy`".to_string(), + ) + .await; + return; + } if size.is_some() && !tty { let error = JSONRPCErrorError { @@ -2238,13 +2247,52 @@ impl CodexMessageProcessor { arg0: None, }; - let requested_policy = sandbox_policy.map(|policy| policy.to_core()); let ( effective_policy, effective_file_system_sandbox_policy, effective_network_sandbox_policy, - ) = match requested_policy { - Some(policy) => match self.config.permissions.sandbox_policy.can_set(&policy) { + ) = if let Some(permission_profile) = permission_profile { + let permission_profile = + codex_protocol::models::PermissionProfile::from(permission_profile); + let sandbox_policy = match permission_profile.to_legacy_sandbox_policy(&sandbox_cwd) { + Ok(sandbox_policy) => sandbox_policy, + Err(err) => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("invalid permission profile: {err}"), + data: None, + }; + self.outgoing.send_error(request, error).await; + return; + } + }; + match self + .config + .permissions + .sandbox_policy + .can_set(&sandbox_policy) + { + Ok(()) => { + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + ( + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + ) + } + Err(err) => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("invalid permission profile: {err}"), + data: None, + }; + self.outgoing.send_error(request, error).await; + return; + } + } + } else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) { + match self.config.permissions.sandbox_policy.can_set(&policy) { Ok(()) => { let file_system_sandbox_policy = codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, &sandbox_cwd); @@ -2261,12 +2309,13 @@ impl CodexMessageProcessor { self.outgoing.send_error(request, error).await; return; } - }, - None => ( + } + } else { + ( self.config.permissions.sandbox_policy.get().clone(), self.config.permissions.file_system_sandbox_policy.clone(), self.config.permissions.network_sandbox_policy, - ), + ) }; let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone(); diff --git a/codex-rs/app-server/tests/suite/v2/command_exec.rs b/codex-rs/app-server/tests/suite/v2/command_exec.rs index d7645f13e3..f93e5fc0b8 100644 --- a/codex-rs/app-server/tests/suite/v2/command_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/command_exec.rs @@ -5,6 +5,7 @@ use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use base64::Engine; use base64::engine::general_purpose::STANDARD; +use codex_app_server_protocol::AdditionalNetworkPermissions; use codex_app_server_protocol::CommandExecOutputDeltaNotification; use codex_app_server_protocol::CommandExecOutputStream; use codex_app_server_protocol::CommandExecParams; @@ -13,9 +14,16 @@ use codex_app_server_protocol::CommandExecResponse; use codex_app_server_protocol::CommandExecTerminalSize; use codex_app_server_protocol::CommandExecTerminateParams; use codex_app_server_protocol::CommandExecWriteParams; +use codex_app_server_protocol::FileSystemAccessMode; +use codex_app_server_protocol::FileSystemPath; +use codex_app_server_protocol::FileSystemSandboxEntry; +use codex_app_server_protocol::FileSystemSpecialPath; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCNotification; +use codex_app_server_protocol::PermissionProfile; +use codex_app_server_protocol::PermissionProfileFileSystemPermissions; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::SandboxPolicy; use pretty_assertions::assert_eq; use std::collections::HashMap; use tempfile::TempDir; @@ -57,6 +65,7 @@ async fn command_exec_without_streams_can_be_terminated() -> Result<()> { env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; let terminate_request_id = mcp @@ -109,6 +118,7 @@ async fn command_exec_without_process_id_keeps_buffered_compatibility() -> Resul env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -167,6 +177,7 @@ async fn command_exec_env_overrides_merge_with_server_environment_and_support_un ])), size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -186,6 +197,91 @@ async fn command_exec_env_overrides_merge_with_server_environment_and_support_un Ok(()) } +#[tokio::test] +async fn command_exec_accepts_permission_profile() -> Result<()> { + let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri(), "never")?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let command_request_id = mcp + .send_command_exec_request(CommandExecParams { + command: vec![ + "sh".to_string(), + "-lc".to_string(), + "printf 'profile'".to_string(), + ], + process_id: None, + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + disable_output_cap: false, + disable_timeout: false, + timeout_ms: None, + cwd: None, + env: None, + size: None, + sandbox_policy: None, + permission_profile: Some(root_read_only_permission_profile()), + }) + .await?; + + let response = mcp + .read_stream_until_response_message(RequestId::Integer(command_request_id)) + .await?; + let response: CommandExecResponse = to_response(response)?; + assert_eq!( + response, + CommandExecResponse { + exit_code: 0, + stdout: "profile".to_string(), + stderr: String::new(), + } + ); + + Ok(()) +} + +#[tokio::test] +async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result<()> { + let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri(), "never")?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let command_request_id = mcp + .send_command_exec_request(CommandExecParams { + command: vec!["sh".to_string(), "-lc".to_string(), "true".to_string()], + process_id: None, + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + disable_output_cap: false, + disable_timeout: false, + timeout_ms: None, + cwd: None, + env: None, + size: None, + sandbox_policy: Some(SandboxPolicy::DangerFullAccess), + permission_profile: Some(root_read_only_permission_profile()), + }) + .await?; + + let error = mcp + .read_stream_until_error_message(RequestId::Integer(command_request_id)) + .await?; + assert_eq!( + error.error.message, + "`permissionProfile` cannot be combined with `sandboxPolicy`" + ); + + Ok(()) +} + #[tokio::test] async fn command_exec_rejects_disable_timeout_with_timeout_ms() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; @@ -209,6 +305,7 @@ async fn command_exec_rejects_disable_timeout_with_timeout_ms() -> Result<()> { env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -246,6 +343,7 @@ async fn command_exec_rejects_disable_output_cap_with_output_bytes_cap() -> Resu env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -283,6 +381,7 @@ async fn command_exec_rejects_negative_timeout_ms() -> Result<()> { env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -320,6 +419,7 @@ async fn command_exec_without_process_id_rejects_streaming() -> Result<()> { env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -361,6 +461,7 @@ async fn command_exec_non_streaming_respects_output_cap() -> Result<()> { env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -408,6 +509,7 @@ async fn command_exec_streaming_does_not_buffer_output() -> Result<()> { env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -471,6 +573,7 @@ async fn command_exec_pipe_streams_output_and_accepts_write() -> Result<()> { env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -546,6 +649,7 @@ async fn command_exec_tty_implies_streaming_and_reports_pty_output() -> Result<( env: None, size: None, sandbox_policy: None, + permission_profile: None, }) .await?; @@ -619,6 +723,7 @@ async fn command_exec_tty_supports_initial_size_and_resize() -> Result<()> { cols: 101, }), sandbox_policy: None, + permission_profile: None, }) .await?; @@ -888,6 +993,23 @@ fn decode_delta_notification( serde_json::from_value(params).context("deserialize command/exec/outputDelta notification") } +fn root_read_only_permission_profile() -> PermissionProfile { + PermissionProfile { + network: Some(AdditionalNetworkPermissions { + enabled: Some(false), + }), + file_system: Some(PermissionProfileFileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }], + glob_scan_max_depth: None, + }), + } +} + async fn read_initialize_response( stream: &mut super::connection_handling_websocket::WsClient, request_id: i64,