From 6e9e2c2eef2f4a1d282bb024323633a138a17aa8 Mon Sep 17 00:00:00 2001 From: xl-openai Date: Mon, 20 Apr 2026 19:42:01 -0700 Subject: [PATCH 01/16] feat: Support more plugin MCP file shapes. (#18780) Update core-plugins MCP loading to accept either an mcpServers object or a top-level server map in .mcp.json --- codex-rs/core-plugins/src/loader.rs | 97 ++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/codex-rs/core-plugins/src/loader.rs b/codex-rs/core-plugins/src/loader.rs index f3e5f8d4f1..21c5aa6b43 100644 --- a/codex-rs/core-plugins/src/loader.rs +++ b/codex-rs/core-plugins/src/loader.rs @@ -67,11 +67,26 @@ pub fn log_plugin_load_errors(outcome: &PluginLoadOutcome) { #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] -struct PluginMcpFile { - #[serde(default)] +struct PluginMcpServersFile { mcp_servers: HashMap, } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum PluginMcpFile { + McpServersObject(PluginMcpServersFile), + ServerMap(HashMap), +} + +impl PluginMcpFile { + fn into_mcp_servers(self) -> HashMap { + match self { + Self::McpServersObject(file) => file.mcp_servers, + Self::ServerMap(mcp_servers) => mcp_servers, + } + } +} + #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] struct PluginAppFile { @@ -775,7 +790,7 @@ async fn load_mcp_servers_from_file( }; normalize_plugin_mcp_servers( plugin_root, - parsed.mcp_servers, + parsed.into_mcp_servers(), mcp_config_path.to_string_lossy().as_ref(), ) } @@ -988,6 +1003,82 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn plugin_mcp_file_supports_mcp_servers_object_format() { + let parsed = serde_json::from_str::( + r#"{ + "mcpServers": { + "sample": { + "command": "sample-mcp" + } + } +}"#, + ) + .expect("parse wrapped plugin mcp config") + .into_mcp_servers(); + + assert_eq!( + parsed, + HashMap::from([( + "sample".to_string(), + serde_json::json!({ + "command": "sample-mcp" + }), + )]) + ); + } + + #[test] + fn plugin_mcp_file_supports_mcp_servers_object_format_with_metadata() { + let parsed = serde_json::from_str::( + r#"{ + "$schema": "https://example.com/plugin-mcp.schema.json", + "mcpServers": { + "sample": { + "command": "sample-mcp" + } + } +}"#, + ) + .expect("parse plugin mcp config with metadata") + .into_mcp_servers(); + + assert_eq!( + parsed, + HashMap::from([( + "sample".to_string(), + serde_json::json!({ + "command": "sample-mcp" + }), + )]) + ); + } + + #[test] + fn plugin_mcp_file_supports_top_level_server_map_format() { + let parsed = serde_json::from_str::( + r#"{ + "linear": { + "type": "http", + "url": "https://mcp.linear.app/mcp" + } +}"#, + ) + .expect("parse flat plugin mcp config") + .into_mcp_servers(); + + assert_eq!( + parsed, + HashMap::from([( + "linear".to_string(), + serde_json::json!({ + "type": "http", + "url": "https://mcp.linear.app/mcp" + }), + )]) + ); + } + #[test] fn materialize_git_subdir_uses_sparse_checkout() { let codex_home = tempfile::tempdir().expect("create codex home"); From 3d2f12389511529e6fa3422ba965cdd42131f12a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:45 -0700 Subject: [PATCH 02/16] protocol: preserve glob scan depth in permission profiles (#18713) ## Why #18274 made `PermissionProfile` the canonical file-system permissions shape, but the round-trip from `FileSystemSandboxPolicy` to `PermissionProfile` still dropped one piece of policy metadata: `glob_scan_max_depth`. That field is security-relevant for deny-read globs such as `**/*.env`. On Linux, bubblewrap sandbox construction uses it to bound unreadable glob expansion. If a profile copied from active runtime permissions loses this value and is submitted back as an override, the resulting `FileSystemSandboxPolicy` can behave differently even though the visible permission entries look equivalent. ## What changed - Add `glob_scan_max_depth` to protocol `FileSystemPermissions` and preserve it when converting to/from `FileSystemSandboxPolicy`. - Keep legacy `read`/`write` JSON for simple path-only permissions, but force canonical JSON when glob scan depth is present so the metadata is not silently dropped. - Carry `globScanMaxDepth` through app-server `AdditionalFileSystemPermissions`, generated JSON/TypeScript schemas, and app-server/TUI conversion call sites. - Preserve the metadata through sandboxing permission normalization, merging, and intersection. - Carry the merged scan depth into the effective `FileSystemSandboxPolicy` used for command execution, so bounded deny-read globs reach Linux bubblewrap materialization. ## Verification - `cargo test -p codex-sandboxing glob_scan -- --nocapture` - `cargo test -p codex-sandboxing policy_transforms -- --nocapture` - `just fix -p codex-sandboxing` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18713). * #18288 * #18287 * #18286 * #18285 * #18284 * #18283 * #18282 * #18281 * #18280 * #18279 * #18278 * #18277 * #18276 * #18275 * __->__ #18713 --- ...CommandExecutionRequestApprovalParams.json | 8 ++ .../PermissionsRequestApprovalParams.json | 8 ++ .../PermissionsRequestApprovalResponse.json | 8 ++ .../schema/json/ServerRequest.json | 8 ++ .../codex_app_server_protocol.schemas.json | 8 ++ .../v2/AdditionalFileSystemPermissions.ts | 2 +- .../src/protocol/common.rs | 1 + .../app-server-protocol/src/protocol/v2.rs | 29 ++++- codex-rs/app-server/src/transport/mod.rs | 2 + .../tests/suite/v2/request_permissions.rs | 1 + codex-rs/protocol/src/models.rs | 96 ++++++++++++++- codex-rs/sandboxing/src/policy_transforms.rs | 75 +++++++++++- .../sandboxing/src/policy_transforms_tests.rs | 109 ++++++++++++++++++ codex-rs/tui/src/app/app_server_requests.rs | 1 + codex-rs/tui/src/app/tests.rs | 2 + .../src/app_server_approval_conversions.rs | 3 + .../tui/src/bottom_pane/approval_overlay.rs | 1 + .../src/chatwidget/tests/approval_requests.rs | 2 + 18 files changed, 352 insertions(+), 12 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index b55e14f4ad..3968da2a39 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json index 2c8bfba83c..e389983bfa 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json index 4564138527..11cce8c88c 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index b778ae0205..298b97b4bf 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/AbsolutePathBuf" 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 7f645dfd3d..42ca071963 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 @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/v2/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalFileSystemPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalFileSystemPermissions.ts index 21af5dda71..2be8f7f0e7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalFileSystemPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalFileSystemPermissions.ts @@ -4,4 +4,4 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; -export type AdditionalFileSystemPermissions = { read: Array | null, write: Array | null, entries?: Array, }; +export type AdditionalFileSystemPermissions = { read: Array | null, write: Array | null, globScanMaxDepth?: number, entries?: Array, }; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 6253315610..aa3ff8331c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2056,6 +2056,7 @@ mod tests { file_system: Some(v2::AdditionalFileSystemPermissions { read: Some(vec![absolute_path("/tmp/allowed")]), write: None, + glob_scan_max_depth: None, entries: None, }), }), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 77f339db53..a029860f2a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::collections::HashMap; +use std::num::NonZeroUsize; use std::path::PathBuf; use crate::RequestId; @@ -1162,6 +1163,9 @@ pub struct AdditionalFileSystemPermissions { pub write: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub glob_scan_max_depth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub entries: Option>, } @@ -1171,12 +1175,14 @@ impl From for AdditionalFileSystemPermissions { Self { read, write, + glob_scan_max_depth: None, entries: None, } } else { Self { read: None, write: None, + glob_scan_max_depth: value.glob_scan_max_depth, entries: Some( value .entries @@ -1191,16 +1197,19 @@ impl From for AdditionalFileSystemPermissions { impl From for CoreFileSystemPermissions { fn from(value: AdditionalFileSystemPermissions) -> Self { - if let Some(entries) = value.entries { + let mut permissions = if let Some(entries) = value.entries { Self { entries: entries .into_iter() .map(CoreFileSystemSandboxEntry::from) .collect(), + glob_scan_max_depth: None, } } else { CoreFileSystemPermissions::from_read_write_roots(value.read, value.write) - } + }; + permissions.glob_scan_max_depth = value.glob_scan_max_depth; + permissions } } @@ -6950,6 +6959,7 @@ mod tests { use codex_utils_absolute_path::test_support::test_path_buf; use pretty_assertions::assert_eq; use serde_json::json; + use std::num::NonZeroUsize; use std::path::PathBuf; fn absolute_path_string(path: &str) -> String { @@ -7084,6 +7094,7 @@ mod tests { AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) .expect("path must be absolute"), ]), + glob_scan_max_depth: None, entries: None, }), } @@ -7155,6 +7166,7 @@ mod tests { access: CoreFileSystemAccessMode::None, }, ], + glob_scan_max_depth: NonZeroUsize::new(2), }; let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone()); @@ -7163,6 +7175,7 @@ mod tests { AdditionalFileSystemPermissions { read: None, write: None, + glob_scan_max_depth: NonZeroUsize::new(2), entries: Some(vec![ FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -7185,6 +7198,17 @@ mod tests { ); } + #[test] + fn additional_file_system_permissions_rejects_zero_glob_scan_depth() { + serde_json::from_value::(json!({ + "read": null, + "write": null, + "globScanMaxDepth": 0, + "entries": [], + })) + .expect_err("zero glob scan depth should fail deserialization"); + } + #[test] fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() { let read_only_path = if cfg!(windows) { @@ -7225,6 +7249,7 @@ mod tests { AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) .expect("path must be absolute"), ]), + glob_scan_max_depth: None, entries: None, }), } diff --git a/codex-rs/app-server/src/transport/mod.rs b/codex-rs/app-server/src/transport/mod.rs index d12107b1d8..a07bd44188 100644 --- a/codex-rs/app-server/src/transport/mod.rs +++ b/codex-rs/app-server/src/transport/mod.rs @@ -781,6 +781,7 @@ mod tests { codex_app_server_protocol::AdditionalFileSystemPermissions { read: Some(vec![absolute_path("/tmp/allowed")]), write: None, + glob_scan_max_depth: None, entries: None, }, ), @@ -844,6 +845,7 @@ mod tests { codex_app_server_protocol::AdditionalFileSystemPermissions { read: Some(vec![absolute_path("/tmp/allowed")]), write: None, + glob_scan_max_depth: None, entries: None, }, ), diff --git a/codex-rs/app-server/tests/suite/v2/request_permissions.rs b/codex-rs/app-server/tests/suite/v2/request_permissions.rs index 05a45011de..295ac96a3b 100644 --- a/codex-rs/app-server/tests/suite/v2/request_permissions.rs +++ b/codex-rs/app-server/tests/suite/v2/request_permissions.rs @@ -93,6 +93,7 @@ async fn request_permissions_round_trip() -> Result<()> { file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions { read: None, write: Some(vec![requested_writes[0].clone()]), + glob_scan_max_depth: None, entries: None, }), }, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 25c44fae09..b0134630f3 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::fmt; use std::io; +use std::num::NonZeroUsize; use std::path::Path; use std::path::PathBuf; use std::sync::LazyLock; @@ -146,6 +147,7 @@ impl SandboxPermissions { #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, JsonSchema, TS)] pub struct FileSystemPermissions { pub entries: Vec, + pub glob_scan_max_depth: Option, } pub type LegacyReadWriteRoots = (Option>, Option>); @@ -172,7 +174,10 @@ impl FileSystemPermissions { access: FileSystemAccessMode::Write, })); } - Self { entries } + Self { + entries, + glob_scan_max_depth: None, + } } pub fn explicit_path_entries( @@ -190,6 +195,10 @@ impl FileSystemPermissions { } fn as_legacy_permissions(&self) -> Option { + if self.glob_scan_max_depth.is_some() { + return None; + } + let mut read = Vec::new(); let mut write = Vec::new(); @@ -225,6 +234,8 @@ struct LegacyFileSystemPermissions { struct CanonicalFileSystemPermissions { #[serde(default, skip_serializing_if = "Vec::is_empty")] entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + glob_scan_max_depth: Option, } #[derive(Debug, Clone, Deserialize)] @@ -244,6 +255,7 @@ impl Serialize for FileSystemPermissions { } else { CanonicalFileSystemPermissions { entries: self.entries.clone(), + glob_scan_max_depth: self.glob_scan_max_depth, } .serialize(serializer) } @@ -256,9 +268,13 @@ impl<'de> Deserialize<'de> for FileSystemPermissions { D: Deserializer<'de>, { match FileSystemPermissionsDe::deserialize(deserializer)? { - FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions { entries }) => { - Ok(Self { entries }) - } + FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions { + entries, + glob_scan_max_depth, + }) => Ok(Self { + entries, + glob_scan_max_depth, + }), FileSystemPermissionsDe::Legacy(LegacyFileSystemPermissions { read, write }) => { Ok(Self::from_read_write_roots(read, write)) } @@ -352,13 +368,18 @@ impl From<&FileSystemSandboxPolicy> for FileSystemPermissions { }] } }; - Self { entries } + Self { + entries, + glob_scan_max_depth: value.glob_scan_max_depth.and_then(NonZeroUsize::new), + } } } impl From<&FileSystemPermissions> for FileSystemSandboxPolicy { fn from(value: &FileSystemPermissions) -> Self { - FileSystemSandboxPolicy::restricted(value.entries.clone()) + let mut policy = FileSystemSandboxPolicy::restricted(value.entries.clone()); + policy.glob_scan_max_depth = value.glob_scan_max_depth.map(usize::from); + policy } } @@ -1828,6 +1849,69 @@ mod tests { assert_eq!(permission_profile.is_empty(), false); } + #[test] + fn permission_profile_round_trip_preserves_glob_scan_max_depth() { + let mut file_system_sandbox_policy = + FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }]); + file_system_sandbox_policy.glob_scan_max_depth = Some(2); + + let permission_profile = PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!( + permission_profile.file_system_sandbox_policy(), + file_system_sandbox_policy + ); + } + + #[test] + fn file_system_permissions_with_glob_scan_depth_uses_canonical_json() -> Result<()> { + let path = AbsolutePathBuf::try_from(PathBuf::from(if cfg!(windows) { + r"C:\tmp\allowed" + } else { + "/tmp/allowed" + })) + .expect("absolute path"); + let file_system_permissions = FileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { path }, + access: FileSystemAccessMode::Read, + }], + glob_scan_max_depth: NonZeroUsize::new(2), + }; + + let serialized = serde_json::to_value(&file_system_permissions)?; + + assert_eq!(serialized.get("read"), None); + assert_eq!(serialized.get("write"), None); + assert_eq!( + serialized.get("glob_scan_max_depth"), + Some(&serde_json::json!(2)) + ); + assert!(serialized.get("entries").is_some()); + assert_eq!( + serde_json::from_value::(serialized)?, + file_system_permissions + ); + Ok(()) + } + + #[test] + fn file_system_permissions_rejects_zero_glob_scan_depth() { + serde_json::from_value::(serde_json::json!({ + "entries": [], + "glob_scan_max_depth": 0, + })) + .expect_err("zero glob scan depth should fail deserialization"); + } + #[test] fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() { let contents = vec![serde_json::json!({ diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index aa0ccc267b..2b8c01eaae 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -13,6 +13,7 @@ use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; use std::collections::HashSet; +use std::num::NonZeroUsize; #[derive(Debug, Clone, PartialEq, Eq)] pub struct EffectiveSandboxPermissions { @@ -45,6 +46,7 @@ pub fn normalize_additional_permissions( let file_system = match additional_permissions.file_system { Some(file_system) => { let mut entries = Vec::with_capacity(file_system.entries.len()); + let glob_scan_max_depth = file_system.glob_scan_max_depth; for entry in file_system.entries { if matches!(&entry.path, FileSystemPath::GlobPattern { .. }) && entry.access != FileSystemAccessMode::None @@ -73,7 +75,10 @@ pub fn normalize_additional_permissions( entries.push(normalized_entry); } } - let file_system = FileSystemPermissions { entries }; + let file_system = FileSystemPermissions { + entries, + glob_scan_max_depth, + }; (!file_system.is_empty()).then_some(file_system) } None => None, @@ -114,6 +119,13 @@ pub fn merge_permission_profiles( let file_system = match (base.file_system.as_ref(), permissions.file_system.as_ref()) { (Some(base), Some(permissions)) => Some(FileSystemPermissions { entries: merge_permission_entries(&base.entries, &permissions.entries), + glob_scan_max_depth: merge_glob_scan_max_depth( + &base.entries, + base.glob_scan_max_depth.map(usize::from), + &permissions.entries, + permissions.glob_scan_max_depth.map(usize::from), + ) + .and_then(NonZeroUsize::new), }) .filter(|file_system| !file_system.is_empty()), (Some(base), None) => Some(base.clone()), @@ -139,12 +151,21 @@ pub fn intersect_permission_profiles( .file_system .map(|requested_file_system| { let granted_file_system = granted.file_system.unwrap_or_default(); - let entries = requested_file_system + let entries: Vec<_> = requested_file_system .entries .into_iter() .filter(|entry| granted_file_system.entries.contains(entry)) .collect(); - FileSystemPermissions { entries } + FileSystemPermissions { + glob_scan_max_depth: merge_glob_scan_max_depth( + &entries, + requested_file_system.glob_scan_max_depth.map(usize::from), + &entries, + granted_file_system.glob_scan_max_depth.map(usize::from), + ) + .and_then(NonZeroUsize::new), + entries, + } }) .filter(|file_system| !file_system.is_empty()); let network = match (requested.network, granted.network) { @@ -167,6 +188,48 @@ pub fn intersect_permission_profiles( } } +fn merge_glob_scan_max_depth( + left_entries: &[FileSystemSandboxEntry], + left_depth: Option, + right_entries: &[FileSystemSandboxEntry], + right_depth: Option, +) -> Option { + let left_depth = effective_glob_scan_depth(left_entries, left_depth); + let right_depth = effective_glob_scan_depth(right_entries, right_depth); + + match (left_depth, right_depth) { + (Some(GlobScanDepth::Unbounded), _) | (_, Some(GlobScanDepth::Unbounded)) => None, + (Some(GlobScanDepth::Bounded(left)), Some(GlobScanDepth::Bounded(right))) => { + Some(left.max(right)) + } + (Some(GlobScanDepth::Bounded(depth)), None) + | (None, Some(GlobScanDepth::Bounded(depth))) => Some(depth), + (None, None) => None, + } +} + +fn effective_glob_scan_depth( + entries: &[FileSystemSandboxEntry], + depth: Option, +) -> Option { + entries + .iter() + .any(|entry| { + entry.access == FileSystemAccessMode::None + && matches!(&entry.path, FileSystemPath::GlobPattern { .. }) + }) + .then_some(match depth { + Some(depth) => GlobScanDepth::Bounded(depth), + None => GlobScanDepth::Unbounded, + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GlobScanDepth { + Bounded(usize), + Unbounded, +} + fn merge_permission_entries( base: &[FileSystemSandboxEntry], permissions: &[FileSystemSandboxEntry], @@ -234,6 +297,12 @@ fn merge_file_system_policy_with_additional_permissions( merged_policy.entries.push(entry.clone()); } } + merged_policy.glob_scan_max_depth = merge_glob_scan_max_depth( + &file_system_policy.entries, + file_system_policy.glob_scan_max_depth, + &additional_permissions.entries, + additional_permissions.glob_scan_max_depth.map(usize::from), + ); merged_policy } FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index 5de8a5cd5a..8fcff965fe 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -171,6 +171,7 @@ fn normalize_additional_permissions_rejects_glob_read_grants() { }, access: FileSystemAccessMode::Read, }], + glob_scan_max_depth: None, }), ..Default::default() }) @@ -192,6 +193,7 @@ fn normalize_additional_permissions_preserves_deny_globs() { }, access: FileSystemAccessMode::None, }], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), }), ..Default::default() }) @@ -207,6 +209,7 @@ fn normalize_additional_permissions_preserves_deny_globs() { }, access: FileSystemAccessMode::None, }], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), }), ..Default::default() } @@ -288,6 +291,76 @@ fn intersect_permission_profiles_drops_explicit_empty_reads_without_grant() { ); } +#[test] +fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() { + let deny_env_files = FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }; + let requested = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), + }), + ..Default::default() + }; + let granted = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: std::num::NonZeroUsize::new(4), + }), + ..Default::default() + }; + + assert_eq!( + intersect_permission_profiles(requested, granted), + PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files], + glob_scan_max_depth: std::num::NonZeroUsize::new(4), + }), + ..Default::default() + } + ); +} + +#[test] +fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() { + let deny_env_files = FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }; + let requested = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), + }), + ..Default::default() + }; + let granted = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: None, + }), + ..Default::default() + }; + + assert_eq!( + intersect_permission_profiles(requested, granted), + PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files], + glob_scan_max_depth: None, + }), + ..Default::default() + } + ); +} + #[test] fn read_only_additional_permissions_can_enable_network_without_writes() { let temp_dir = TempDir::new().expect("create temp dir"); @@ -402,6 +475,42 @@ fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roo ); } +#[test] +fn merge_file_system_policy_with_additional_permissions_carries_bounded_glob_scan_depth() { + let deny_env_files = FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }; + let merged_policy = merge_file_system_policy_with_additional_permissions( + &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }]), + &FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), + }, + ); + + assert_eq!(merged_policy, { + let mut policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }, + deny_env_files, + ]); + policy.glob_scan_max_depth = Some(2); + policy + }); +} + #[test] fn effective_file_system_sandbox_policy_returns_base_policy_without_additional_permissions() { let temp_dir = TempDir::new().expect("create temp dir"); diff --git a/codex-rs/tui/src/app/app_server_requests.rs b/codex-rs/tui/src/app/app_server_requests.rs index c54aae826f..58ee962fdb 100644 --- a/codex-rs/tui/src/app/app_server_requests.rs +++ b/codex-rs/tui/src/app/app_server_requests.rs @@ -558,6 +558,7 @@ mod tests { file_system: Some(AdditionalFileSystemPermissions { read: Some(vec![absolute_path(read_path)]), write: Some(vec![absolute_path(write_path)]), + glob_scan_max_depth: None, entries: None, }), }, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 2fad117cc0..5ed786ef05 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2601,6 +2601,7 @@ async fn inactive_thread_exec_approval_preserves_context() { file_system: Some(AdditionalFileSystemPermissions { read: Some(vec![test_absolute_path("/tmp/read-only")]), write: Some(vec![test_absolute_path("/tmp/write")]), + glob_scan_max_depth: None, entries: None, }), }); @@ -2708,6 +2709,7 @@ async fn inactive_thread_permissions_approval_preserves_file_system_permissions( file_system: Some(AdditionalFileSystemPermissions { read: Some(vec![test_absolute_path("/tmp/read-only")]), write: Some(vec![test_absolute_path("/tmp/write")]), + glob_scan_max_depth: None, entries: None, }), }, diff --git a/codex-rs/tui/src/app_server_approval_conversions.rs b/codex-rs/tui/src/app_server_approval_conversions.rs index 7b8e777d5b..a26f9b2a90 100644 --- a/codex-rs/tui/src/app_server_approval_conversions.rs +++ b/codex-rs/tui/src/app_server_approval_conversions.rs @@ -92,6 +92,7 @@ mod tests { file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions { read: Some(vec![absolute_path("/tmp/read-only")]), write: Some(vec![absolute_path("/tmp/write")]), + glob_scan_max_depth: None, entries: None, }), } @@ -109,6 +110,7 @@ mod tests { }, access: FileSystemAccessMode::Write, }], + glob_scan_max_depth: None, }), ..Default::default() }), @@ -117,6 +119,7 @@ mod tests { file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions { read: None, write: None, + glob_scan_max_depth: None, entries: Some(vec![codex_app_server_protocol::FileSystemSandboxEntry { path: codex_app_server_protocol::FileSystemPath::Special { value: codex_app_server_protocol::FileSystemSpecialPath::Root, diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 1c148cd02a..5212d81953 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -1372,6 +1372,7 @@ mod tests { access: FileSystemAccessMode::None, }, ], + glob_scan_max_depth: None, }), ..Default::default() }; diff --git a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs index 472a13c118..4e5bc7556e 100644 --- a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs +++ b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs @@ -113,6 +113,7 @@ fn app_server_exec_approval_request_preserves_permissions_context() { file_system: Some(AppServerAdditionalFileSystemPermissions { read: Some(vec![read_path.clone()]), write: Some(vec![write_path.clone()]), + glob_scan_max_depth: None, entries: None, }), }), @@ -163,6 +164,7 @@ fn app_server_request_permissions_preserves_file_system_permissions() { file_system: Some(AppServerAdditionalFileSystemPermissions { read: Some(vec![read_path.clone()]), write: Some(vec![write_path.clone()]), + glob_scan_max_depth: None, entries: None, }), }, From 8aa3a11adaa0630ebe5ddcdb802ada6a112db415 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 03/16] sandboxing: intersect permission profiles semantically --- .../app-server/src/bespoke_event_handling.rs | 102 +++++++++++++++++- .../core/src/tools/handlers/apply_patch.rs | 1 + codex-rs/core/src/tools/handlers/mod.rs | 3 +- codex-rs/core/src/tools/handlers/shell.rs | 1 + .../core/src/tools/handlers/unified_exec.rs | 1 + .../core/tests/suite/request_permissions.rs | 6 +- codex-rs/sandboxing/src/policy_transforms.rs | 102 +++++++++++++++++- .../sandboxing/src/policy_transforms_tests.rs | 80 +++++++++++++- 8 files changed, 279 insertions(+), 17 deletions(-) diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index c001a77bfa..0b96727ace 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -2597,9 +2597,12 @@ async fn on_request_permissions_response( let response = receiver.await; resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await; drop(request_permissions_guard); - let Some(response) = - request_permissions_response_from_client_result(requested_permissions, response) - else { + let cwd = conversation.config_snapshot().await.cwd; + let Some(response) = request_permissions_response_from_client_result( + requested_permissions, + response, + cwd.as_path(), + ) else { return; }; @@ -2617,6 +2620,7 @@ async fn on_request_permissions_response( fn request_permissions_response_from_client_result( requested_permissions: CoreRequestPermissionProfile, response: std::result::Result, + cwd: &std::path::Path, ) -> Option { let value = match response { Ok(Ok(value)) => value, @@ -2649,6 +2653,7 @@ fn request_permissions_response_from_client_result( permissions: intersect_permission_profiles( requested_permissions.into(), response.permissions.into(), + cwd, ) .into(), scope: response.scope.to_core(), @@ -3023,6 +3028,10 @@ mod tests { use codex_protocol::mcp::CallToolResult; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::plan_tool::PlanItemArg; use codex_protocol::plan_tool::StepStatus; use codex_protocol::protocol::CollabResumeBeginEvent; @@ -3707,6 +3716,7 @@ mod tests { let response = request_permissions_response_from_client_result( CoreRequestPermissionProfile::default(), Ok(Err(error)), + std::env::current_dir().expect("current dir").as_path(), ); assert_eq!(response, None); @@ -3793,12 +3803,14 @@ mod tests { ), ]; + let cwd = std::env::current_dir().expect("current dir"); for (granted_permissions, expected_permissions) in cases { let response = request_permissions_response_from_client_result( requested_permissions.clone(), Ok(Ok(serde_json::json!({ "permissions": granted_permissions, }))), + cwd.as_path(), ) .expect("response should be accepted"); @@ -3820,6 +3832,7 @@ mod tests { "scope": "session", "permissions": {}, }))), + std::env::current_dir().expect("current dir").as_path(), ) .expect("response should be accepted"); @@ -3832,6 +3845,89 @@ mod tests { ); } + #[test] + fn request_permissions_response_accepts_explicit_child_grant_for_requested_cwd_scope() { + let temp_dir = TempDir::new().expect("temp dir"); + let cwd = AbsolutePathBuf::from_absolute_path(temp_dir.path()).expect("absolute cwd"); + let child = cwd.join("child"); + let requested_permissions = CoreRequestPermissionProfile { + file_system: Some(CoreFileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: None, + }), + ..Default::default() + }; + + let response = request_permissions_response_from_client_result( + requested_permissions, + Ok(Ok(serde_json::json!({ + "permissions": { + "fileSystem": { + "write": [child], + }, + }, + }))), + cwd.as_path(), + ) + .expect("response should be accepted"); + + assert_eq!( + response.permissions, + CoreRequestPermissionProfile { + file_system: Some(CoreFileSystemPermissions::from_read_write_roots( + None, + Some(vec![child]), + )), + ..Default::default() + } + ); + } + + #[test] + fn request_permissions_response_ignores_broader_cwd_grant_for_requested_child_path() { + let temp_dir = TempDir::new().expect("temp dir"); + let cwd = AbsolutePathBuf::from_absolute_path(temp_dir.path()).expect("absolute cwd"); + let child = cwd.join("child"); + let requested_permissions = CoreRequestPermissionProfile { + file_system: Some(CoreFileSystemPermissions::from_read_write_roots( + None, + Some(vec![child]), + )), + ..Default::default() + }; + + let response = request_permissions_response_from_client_result( + requested_permissions, + Ok(Ok(serde_json::json!({ + "permissions": { + "fileSystem": { + "entries": [{ + "path": { + "type": "special", + "value": { + "kind": "current_working_directory" + } + }, + "access": "write" + }], + }, + }, + }))), + cwd.as_path(), + ) + .expect("response should be accepted"); + + assert_eq!( + response.permissions, + CoreRequestPermissionProfile::default() + ); + } + #[test] fn collab_resume_begin_maps_to_item_started_resume_agent() { let event = CollabResumeBeginEvent { diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 9d40bbad0c..033315a694 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -259,6 +259,7 @@ async fn effective_patch_permissions( ); let effective_additional_permissions = apply_granted_turn_permissions( session, + turn.cwd.as_path(), crate::sandboxing::SandboxPermissions::UseDefault, write_permissions_for_paths(&file_paths, &file_system_sandbox_policy, &turn.cwd), ) diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index 17c7c09be9..0217cab33a 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -169,6 +169,7 @@ pub(super) fn implicit_granted_permissions( pub(super) async fn apply_granted_turn_permissions( session: &Session, + cwd: &std::path::Path, sandbox_permissions: SandboxPermissions, additional_permissions: Option, ) -> EffectiveAdditionalPermissions { @@ -192,7 +193,7 @@ pub(super) async fn apply_granted_turn_permissions( ); let permissions_preapproved = match (effective_permissions.as_ref(), granted_permissions) { (Some(effective_permissions), Some(granted_permissions)) => { - intersect_permission_profiles(effective_permissions.clone(), granted_permissions) + intersect_permission_profiles(effective_permissions.clone(), granted_permissions, cwd) == *effective_permissions } _ => false, diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index b6bf4bc7ad..8770238942 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -424,6 +424,7 @@ impl ShellHandler { let requested_additional_permissions = additional_permissions.clone(); let effective_additional_permissions = apply_granted_turn_permissions( session.as_ref(), + turn.cwd.as_path(), exec_params.sandbox_permissions, additional_permissions, ) diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index ab5279d1b8..fd04d42f2e 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -230,6 +230,7 @@ impl ToolHandler for UnifiedExecHandler { let requested_additional_permissions = additional_permissions.clone(); let effective_additional_permissions = apply_granted_turn_permissions( context.session.as_ref(), + context.turn.cwd.as_path(), sandbox_permissions, additional_permissions, ) diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index 39b0cfb44b..614cd10b9f 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -1586,17 +1586,17 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions() .unwrap_or_else(|| panic!("expected filesystem permissions")); let (approval_reads, approval_writes) = approval_file_system .legacy_read_write_roots() - .unwrap_or_default(); + .unwrap_or_else(|| panic!("expected legacy-compatible permissions")); assert!(approval_reads.as_ref().is_none_or(Vec::is_empty)); let mut approval_writes = approval_writes.unwrap_or_default(); approval_writes.sort_by_key(|path| path.display().to_string()); - let (_expected_reads, expected_writes) = merged_permissions + let (_, expected_writes) = merged_permissions .file_system .unwrap_or_else(|| panic!("expected merged filesystem permissions")) .legacy_read_write_roots() - .unwrap_or_default(); + .unwrap_or_else(|| panic!("expected legacy-compatible permissions")); let mut expected_writes = expected_writes.unwrap_or_default(); expected_writes.sort_by_key(|path| path.display().to_string()); diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 2b8c01eaae..8ec505cf82 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -6,6 +6,7 @@ use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::ReadOnlyAccess; @@ -14,6 +15,8 @@ use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; use std::collections::HashSet; use std::num::NonZeroUsize; +use std::path::Path; +use std::path::PathBuf; #[derive(Debug, Clone, PartialEq, Eq)] pub struct EffectiveSandboxPermissions { @@ -146,21 +149,42 @@ pub fn merge_permission_profiles( pub fn intersect_permission_profiles( requested: PermissionProfile, granted: PermissionProfile, + cwd: &Path, ) -> PermissionProfile { let file_system = requested .file_system .map(|requested_file_system| { let granted_file_system = granted.file_system.unwrap_or_default(); - let entries: Vec<_> = requested_file_system + let requested_policy = + FileSystemSandboxPolicy::restricted(requested_file_system.entries.clone()); + let mut entries: Vec<_> = granted_file_system .entries - .into_iter() - .filter(|entry| granted_file_system.entries.contains(entry)) + .iter() + .filter(|entry| { + granted_file_system_entry_within_request( + &requested_file_system, + &requested_policy, + entry, + cwd, + ) + }) + .cloned() .collect(); + for entry in requested_file_system + .entries + .iter() + .chain(granted_file_system.entries.iter()) + .filter(|entry| entry.access == FileSystemAccessMode::None) + { + if !entries.contains(entry) { + entries.push(entry.clone()); + } + } FileSystemPermissions { glob_scan_max_depth: merge_glob_scan_max_depth( - &entries, + &requested_file_system.entries, requested_file_system.glob_scan_max_depth.map(usize::from), - &entries, + &granted_file_system.entries, granted_file_system.glob_scan_max_depth.map(usize::from), ) .and_then(NonZeroUsize::new), @@ -230,6 +254,74 @@ enum GlobScanDepth { Unbounded, } +fn granted_file_system_entry_within_request( + requested: &FileSystemPermissions, + requested_policy: &FileSystemSandboxPolicy, + granted_entry: &FileSystemSandboxEntry, + cwd: &Path, +) -> bool { + if !granted_entry.access.can_read() { + return false; + } + + if let Some(path) = resolve_permission_path(&granted_entry.path, cwd) { + return access_covers( + requested_policy.resolve_access_with_cwd(path.as_path(), cwd), + granted_entry.access, + ); + } + + requested.entries.iter().any(|requested_entry| { + access_covers(requested_entry.access, granted_entry.access) + && requested_entry.path == granted_entry.path + }) +} + +fn access_covers(requested: FileSystemAccessMode, granted: FileSystemAccessMode) -> bool { + match granted { + FileSystemAccessMode::Read => requested.can_read(), + FileSystemAccessMode::Write => requested.can_write(), + FileSystemAccessMode::None => false, + } +} + +fn resolve_permission_path(path: &FileSystemPath, cwd: &Path) -> Option { + match path { + FileSystemPath::Path { path } => Some(path.clone()), + FileSystemPath::GlobPattern { .. } => None, + FileSystemPath::Special { value } => match value { + FileSystemSpecialPath::Root => { + let root = cwd.ancestors().last()?; + AbsolutePathBuf::from_absolute_path(root).ok() + } + FileSystemSpecialPath::CurrentWorkingDirectory => { + AbsolutePathBuf::from_absolute_path(cwd).ok() + } + FileSystemSpecialPath::ProjectRoots { subpath } => { + let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok()?; + Some(match subpath { + Some(subpath) => { + AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path()) + } + None => cwd, + }) + } + FileSystemSpecialPath::Tmpdir => { + let tmpdir = std::env::var_os("TMPDIR")?; + if tmpdir.is_empty() { + None + } else { + AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok() + } + } + FileSystemSpecialPath::SlashTmp => AbsolutePathBuf::from_absolute_path("/tmp") + .ok() + .filter(|path| path.as_path().is_dir()), + FileSystemSpecialPath::Minimal | FileSystemSpecialPath::Unknown { .. } => None, + }, + } +} + fn merge_permission_entries( base: &[FileSystemSandboxEntry], permissions: &[FileSystemSandboxEntry], diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index 8fcff965fe..4423a7cf13 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -244,7 +244,7 @@ fn intersect_permission_profiles_preserves_explicit_empty_requested_reads() { let granted = requested.clone(); assert_eq!( - intersect_permission_profiles(requested.clone(), granted), + intersect_permission_profiles(requested.clone(), granted, temp_dir.path()), requested ); } @@ -265,7 +265,7 @@ fn intersect_permission_profiles_drops_ungranted_nonempty_path_requests() { }; assert_eq!( - intersect_permission_profiles(requested, PermissionProfile::default()), + intersect_permission_profiles(requested, PermissionProfile::default(), temp_dir.path()), PermissionProfile::default() ); } @@ -286,13 +286,82 @@ fn intersect_permission_profiles_drops_explicit_empty_reads_without_grant() { }; assert_eq!( - intersect_permission_profiles(requested, PermissionProfile::default()), + intersect_permission_profiles(requested, PermissionProfile::default(), temp_dir.path()), + PermissionProfile::default() + ); +} + +#[test] +fn intersect_permission_profiles_accepts_child_path_granted_for_requested_cwd() { + let temp_dir = TempDir::new().expect("create temp dir"); + let cwd = AbsolutePathBuf::from_absolute_path( + canonicalize(temp_dir.path()).expect("canonicalize temp dir"), + ) + .expect("absolute temp dir"); + let child = cwd.join("child"); + let requested = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: None, + }), + ..Default::default() + }; + let granted = PermissionProfile { + file_system: Some(FileSystemPermissions::from_read_write_roots( + None, + Some(vec![child.clone()]), + )), + ..Default::default() + }; + + assert_eq!( + intersect_permission_profiles(requested, granted.clone(), cwd.as_path()), + granted + ); +} + +#[test] +fn intersect_permission_profiles_drops_broader_cwd_grant_for_requested_child_path() { + let temp_dir = TempDir::new().expect("create temp dir"); + let cwd = AbsolutePathBuf::from_absolute_path( + canonicalize(temp_dir.path()).expect("canonicalize temp dir"), + ) + .expect("absolute temp dir"); + let child = cwd.join("child"); + let requested = PermissionProfile { + file_system: Some(FileSystemPermissions::from_read_write_roots( + None, + Some(vec![child]), + )), + ..Default::default() + }; + let granted = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: None, + }), + ..Default::default() + }; + + assert_eq!( + intersect_permission_profiles(requested, granted, cwd.as_path()), PermissionProfile::default() ); } #[test] fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() { + let cwd = std::env::current_dir().expect("current dir"); let deny_env_files = FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), @@ -315,7 +384,7 @@ fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() { }; assert_eq!( - intersect_permission_profiles(requested, granted), + intersect_permission_profiles(requested, granted, cwd.as_path()), PermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![deny_env_files], @@ -328,6 +397,7 @@ fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() { #[test] fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() { + let cwd = std::env::current_dir().expect("current dir"); let deny_env_files = FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), @@ -350,7 +420,7 @@ fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() { }; assert_eq!( - intersect_permission_profiles(requested, granted), + intersect_permission_profiles(requested, granted, cwd.as_path()), PermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![deny_env_files], From 26a7cc7ea67cc3e707e4220fffcad6d9c287ffbc Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 04/16] exec-server: carry filesystem sandbox profiles --- codex-rs/core/src/session/mod.rs | 1 - codex-rs/core/src/session/turn_context.rs | 17 +- .../core/src/tools/runtimes/apply_patch.rs | 20 +- .../src/tools/runtimes/apply_patch_tests.rs | 67 +-- codex-rs/exec-server/src/file_system.rs | 29 +- codex-rs/exec-server/src/fs_sandbox.rs | 455 +++++++----------- codex-rs/exec-server/tests/file_system.rs | 11 +- 7 files changed, 228 insertions(+), 372 deletions(-) diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index f159546575..9c0d4c69a0 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -791,7 +791,6 @@ pub(crate) fn session_loop_termination_from_handle( .boxed() .shared() } - async fn thread_title_from_state_db( state_db: Option<&state_db::StateDbHandle>, codex_home: &AbsolutePathBuf, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index dd86804ee5..dbbe955211 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -1,6 +1,7 @@ use super::*; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; +use codex_sandboxing::policy_transforms::merge_permission_profiles; pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool { matches!( @@ -73,6 +74,13 @@ pub(crate) struct TurnContext { pub(crate) turn_timing_state: Arc, } impl TurnContext { + pub(crate) fn permission_profile(&self) -> PermissionProfile { + PermissionProfile::from_runtime_permissions( + &self.file_system_sandbox_policy, + self.network_sandbox_policy, + ) + } + pub(crate) fn model_context_window(&self) -> Option { let effective_context_window_percent = self.model_info.effective_context_window_percent; self.model_info @@ -209,17 +217,18 @@ impl TurnContext { &self, additional_permissions: Option, ) -> FileSystemSandboxContext { + let base_permissions = self.permission_profile(); + let permissions = + merge_permission_profiles(Some(&base_permissions), additional_permissions.as_ref()) + .unwrap_or(base_permissions); FileSystemSandboxContext { - sandbox_policy: self.sandbox_policy.get().clone(), - sandbox_policy_cwd: Some(self.cwd.clone()), - file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(), + permissions, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self .config .permissions .windows_sandbox_private_desktop, use_legacy_landlock: self.features.use_legacy_landlock(), - additional_permissions, } } diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index aac8e6bc13..756562264b 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -22,7 +22,6 @@ use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; use codex_protocol::models::PermissionProfile; -use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -32,6 +31,7 @@ use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; +use codex_sandboxing::policy_transforms::merge_permission_profiles; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::path::PathBuf; @@ -75,22 +75,18 @@ impl ApplyPatchRuntime { return None; } - let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( - attempt.policy, - attempt.sandbox_cwd, + let base_permissions = PermissionProfile::from_runtime_permissions( + attempt.file_system_policy, + attempt.network_policy, ); - let file_system_sandbox_policy = (attempt.file_system_policy - != &legacy_file_system_sandbox_policy) - .then(|| attempt.file_system_policy.clone()); - + let permissions = + merge_permission_profiles(Some(&base_permissions), req.additional_permissions.as_ref()) + .unwrap_or(base_permissions); Some(FileSystemSandboxContext { - sandbox_policy: attempt.policy.clone(), - sandbox_policy_cwd: Some(attempt.sandbox_cwd.clone()), - file_system_sandbox_policy, + permissions, windows_sandbox_level: attempt.windows_sandbox_level, windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop, use_legacy_landlock: attempt.use_legacy_landlock, - additional_permissions: req.additional_permissions.clone(), }) } diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 7f3641c03c..58adca2038 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -3,15 +3,13 @@ use crate::tools::sandboxing::SandboxAttempt; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; -use codex_protocol::permissions::FileSystemAccessMode; -use codex_protocol::permissions::FileSystemPath; -use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxType; +use codex_sandboxing::policy_transforms::merge_permission_profiles; use core_test_support::PathBufExt; use pretty_assertions::assert_eq; use std::collections::HashMap; @@ -102,12 +100,7 @@ fn file_system_sandbox_context_uses_active_attempt() { permissions_preapproved: false, }; let sandbox_policy = SandboxPolicy::new_read_only_policy(); - let mut file_system_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, path.as_path()); - file_system_policy.entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Path { path: path.clone() }, - access: FileSystemAccessMode::None, - }); + let file_system_policy = FileSystemSandboxPolicy::from(&sandbox_policy); let manager = SandboxManager::new(); let attempt = SandboxAttempt { sandbox: SandboxType::MacosSeatbelt, @@ -126,13 +119,16 @@ fn file_system_sandbox_context_uses_active_attempt() { let sandbox = ApplyPatchRuntime::file_system_sandbox_context_for_attempt(&req, &attempt) .expect("sandbox context"); - assert_eq!(sandbox.sandbox_policy, sandbox_policy); - assert_eq!(sandbox.sandbox_policy_cwd, Some(path.clone())); - assert_eq!( - sandbox.file_system_sandbox_policy, - Some(file_system_policy.clone()) + let base_permissions = PermissionProfile::from_runtime_permissions( + &file_system_policy, + NetworkSandboxPolicy::Restricted, ); - assert_eq!(sandbox.additional_permissions, Some(additional_permissions)); + let Some(expected_permissions) = + merge_permission_profiles(Some(&base_permissions), Some(&additional_permissions)) + else { + panic!("merged permissions should not be empty"); + }; + assert_eq!(sandbox.permissions, expected_permissions); assert_eq!( sandbox.windows_sandbox_level, WindowsSandboxLevel::RestrictedToken @@ -141,47 +137,6 @@ fn file_system_sandbox_context_uses_active_attempt() { assert_eq!(sandbox.use_legacy_landlock, true); } -#[test] -fn file_system_sandbox_context_omits_legacy_equivalent_policy() { - let path = std::env::temp_dir() - .join("apply-patch-runtime-legacy-equivalent.txt") - .abs(); - let req = ApplyPatchRequest { - action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()), - file_paths: vec![path.clone()], - changes: HashMap::new(), - exec_approval_requirement: ExecApprovalRequirement::Skip { - bypass_sandbox: false, - proposed_execpolicy_amendment: None, - }, - additional_permissions: None, - permissions_preapproved: false, - }; - let sandbox_policy = SandboxPolicy::new_read_only_policy(); - let file_system_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, path.as_path()); - let manager = SandboxManager::new(); - let attempt = SandboxAttempt { - sandbox: SandboxType::MacosSeatbelt, - policy: &sandbox_policy, - file_system_policy: &file_system_policy, - network_policy: NetworkSandboxPolicy::Restricted, - enforce_managed_network: false, - manager: &manager, - sandbox_cwd: &path, - codex_linux_sandbox_exe: None, - use_legacy_landlock: true, - windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, - windows_sandbox_private_desktop: true, - }; - - let sandbox = ApplyPatchRuntime::file_system_sandbox_context_for_attempt(&req, &attempt) - .expect("sandbox context"); - - assert_eq!(sandbox.sandbox_policy_cwd, Some(path)); - assert_eq!(sandbox.file_system_sandbox_policy, None); -} - #[test] fn no_sandbox_attempt_has_no_file_system_context() { let path = std::env::temp_dir() diff --git a/codex-rs/exec-server/src/file_system.rs b/codex-rs/exec-server/src/file_system.rs index 64ebc891db..6367ae95e8 100644 --- a/codex-rs/exec-server/src/file_system.rs +++ b/codex-rs/exec-server/src/file_system.rs @@ -1,7 +1,9 @@ use async_trait::async_trait; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use tokio::io; @@ -41,37 +43,36 @@ pub struct ReadDirectoryEntry { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct FileSystemSandboxContext { - pub sandbox_policy: SandboxPolicy, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox_policy_cwd: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub file_system_sandbox_policy: Option, + pub permissions: PermissionProfile, pub windows_sandbox_level: WindowsSandboxLevel, #[serde(default)] pub windows_sandbox_private_desktop: bool, #[serde(default)] pub use_legacy_landlock: bool, - pub additional_permissions: Option, } impl FileSystemSandboxContext { pub fn new(sandbox_policy: SandboxPolicy) -> Self { + let permissions = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::from(&sandbox_policy), + ); + Self::from_permission_profile(permissions) + } + + pub fn from_permission_profile(permissions: PermissionProfile) -> Self { Self { - sandbox_policy, - sandbox_policy_cwd: None, - file_system_sandbox_policy: None, + permissions, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, use_legacy_landlock: false, - additional_permissions: None, } } pub fn should_run_in_sandbox(&self) -> bool { - matches!( - self.sandbox_policy, - SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } - ) + let file_system_policy = self.permissions.file_system_sandbox_policy(); + matches!(file_system_policy.kind, FileSystemSandboxKind::Restricted) + && !file_system_policy.has_full_disk_write_access() } } diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index c29d6b7653..2fd2239f27 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; use codex_app_server_protocol::JSONRPCErrorError; -use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; @@ -12,7 +14,6 @@ use codex_sandboxing::SandboxExecRequest; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; use codex_sandboxing::SandboxablePreference; -use codex_sandboxing::policy_transforms::merge_permission_profiles; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; use tokio::io::AsyncWriteExt; @@ -36,13 +37,6 @@ pub(crate) struct FileSystemSandboxRunner { helper_env: HashMap, } -struct HelperSandboxInputs { - sandbox_policy: SandboxPolicy, - file_system_policy: FileSystemSandboxPolicy, - network_policy: NetworkSandboxPolicy, - cwd: AbsolutePathBuf, -} - impl FileSystemSandboxRunner { pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self { Self { @@ -56,12 +50,21 @@ impl FileSystemSandboxRunner { sandbox: &FileSystemSandboxContext, request: FsHelperRequest, ) -> Result { - let HelperSandboxInputs { - sandbox_policy, - file_system_policy, - network_policy, - cwd, - } = helper_sandbox_inputs(sandbox)?; + let cwd = current_sandbox_cwd().map_err(io_error)?; + let cwd = AbsolutePathBuf::from_absolute_path(cwd.as_path()) + .map_err(|err| invalid_request(format!("current directory is not absolute: {err}")))?; + let mut file_system_policy = sandbox.permissions.file_system_sandbox_policy(); + if !sandbox.use_legacy_landlock { + add_helper_runtime_permissions( + &mut file_system_policy, + helper_read_root(&self.runtime_paths), + cwd.as_path(), + ); + } + normalize_file_system_policy_root_aliases(&mut file_system_policy); + let network_policy = NetworkSandboxPolicy::Restricted; + let sandbox_policy = + compatibility_sandbox_policy(&file_system_policy, network_policy, cwd.as_path()); let command = self.sandbox_exec_request( &sandbox_policy, &file_system_policy, @@ -95,10 +98,7 @@ impl FileSystemSandboxRunner { args: vec![CODEX_FS_HELPER_ARG1.to_string()], cwd: cwd.clone(), env: self.helper_env.clone(), - additional_permissions: self.helper_permissions( - sandbox_context.additional_permissions.as_ref(), - /*include_helper_read_root*/ !sandbox_context.use_legacy_landlock, - ), + additional_permissions: None, }; sandbox_manager .transform(SandboxTransformRequest { @@ -117,99 +117,91 @@ impl FileSystemSandboxRunner { }) .map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}"))) } - - fn helper_permissions( - &self, - additional_permissions: Option<&PermissionProfile>, - include_helper_read_root: bool, - ) -> Option { - let inherited_permissions = additional_permissions - .map(|permissions| PermissionProfile { - network: None, - file_system: permissions.file_system.clone(), - }) - .filter(|permissions| !permissions.is_empty()); - let helper_permissions = include_helper_read_root - .then(|| { - self.runtime_paths - .codex_self_exe - .parent() - .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()) - }) - .flatten() - .map(|helper_read_root| PermissionProfile { - network: None, - file_system: Some(FileSystemPermissions::from_read_write_roots( - Some(vec![helper_read_root]), - /*write*/ None, - )), - }); - - merge_permission_profiles(inherited_permissions.as_ref(), helper_permissions.as_ref()) - } } -fn helper_sandbox_inputs( - sandbox: &FileSystemSandboxContext, -) -> Result { - let sandbox_policy = normalize_sandbox_policy_root_aliases( - sandbox_policy_with_helper_runtime_defaults(&sandbox.sandbox_policy), - ); - let cwd = match &sandbox.sandbox_policy_cwd { - Some(cwd) => cwd.clone(), - None if sandbox.file_system_sandbox_policy.is_some() => { - return Err(invalid_request( - "fileSystemSandboxPolicy requires sandboxPolicyCwd".to_string(), - )); +fn helper_read_root(runtime_paths: &ExecServerRuntimePaths) -> Option { + runtime_paths + .codex_self_exe + .parent() + .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()) +} + +fn add_helper_runtime_permissions( + file_system_policy: &mut FileSystemSandboxPolicy, + helper_read_root: Option, + cwd: &std::path::Path, +) { + if !file_system_policy.has_full_disk_read_access() { + let minimal_read_entry = FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Minimal, + }, + access: FileSystemAccessMode::Read, + }; + if !file_system_policy.entries.contains(&minimal_read_entry) { + file_system_policy.entries.push(minimal_read_entry); } - None => { - let cwd = current_sandbox_cwd().map_err(io_error)?; - AbsolutePathBuf::from_absolute_path(cwd.as_path()).map_err(|err| { - invalid_request(format!("current directory is not absolute: {err}")) - })? + } + + let Some(helper_read_root) = helper_read_root else { + return; + }; + if file_system_policy.can_read_path_with_cwd(helper_read_root.as_path(), cwd) { + return; + } + + file_system_policy.entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: helper_read_root, + }, + access: FileSystemAccessMode::Read, + }); +} + +fn compatibility_sandbox_policy( + file_system_policy: &FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, + cwd: &std::path::Path, +) -> SandboxPolicy { + file_system_policy + .to_legacy_sandbox_policy(network_policy, cwd) + .unwrap_or_else(|_| compatibility_workspace_write_policy(file_system_policy, cwd)) +} + +fn compatibility_workspace_write_policy( + file_system_policy: &FileSystemSandboxPolicy, + cwd: &std::path::Path, +) -> SandboxPolicy { + let read_only_access = if file_system_policy.has_full_disk_read_access() { + ReadOnlyAccess::FullAccess + } else { + ReadOnlyAccess::Restricted { + include_platform_defaults: file_system_policy.include_platform_defaults(), + readable_roots: file_system_policy.get_readable_roots_with_cwd(cwd), } }; - let file_system_policy = sandbox - .file_system_sandbox_policy - .clone() - .unwrap_or_else(|| { - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()) - }); - Ok(HelperSandboxInputs { - sandbox_policy, - file_system_policy, - network_policy: NetworkSandboxPolicy::Restricted, - cwd, - }) -} + let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok(); + let writable_roots = file_system_policy + .get_writable_roots_with_cwd(cwd) + .into_iter() + .map(|root| root.root) + .filter(|root| cwd_abs.as_ref() != Some(root)) + .collect(); -fn normalize_sandbox_policy_root_aliases(sandbox_policy: SandboxPolicy) -> SandboxPolicy { - let mut sandbox_policy = sandbox_policy; - match &mut sandbox_policy { - SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { readable_roots, .. }, - .. - } => { - normalize_root_aliases(readable_roots); - } - SandboxPolicy::WorkspaceWrite { - writable_roots, - read_only_access, - .. - } => { - normalize_root_aliases(writable_roots); - if let ReadOnlyAccess::Restricted { readable_roots, .. } = read_only_access { - normalize_root_aliases(readable_roots); - } - } - _ => {} + SandboxPolicy::WorkspaceWrite { + writable_roots, + read_only_access, + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, } - sandbox_policy } -fn normalize_root_aliases(paths: &mut Vec) { - for path in paths { - *path = normalize_top_level_alias(path.clone()); +fn normalize_file_system_policy_root_aliases(file_system_policy: &mut FileSystemSandboxPolicy) { + for entry in &mut file_system_policy.entries { + if let FileSystemPath::Path { path } = &mut entry.path { + *path = normalize_top_level_alias(path.clone()); + } } } @@ -314,39 +306,6 @@ fn spawn_command( command.spawn().map_err(io_error) } -fn sandbox_policy_with_helper_runtime_defaults(sandbox_policy: &SandboxPolicy) -> SandboxPolicy { - let mut sandbox_policy = sandbox_policy.clone(); - match &mut sandbox_policy { - SandboxPolicy::ReadOnly { - access, - network_access, - } => { - enable_platform_defaults(access); - *network_access = false; - } - SandboxPolicy::WorkspaceWrite { - read_only_access, - network_access, - .. - } => { - enable_platform_defaults(read_only_access); - *network_access = false; - } - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {} - } - sandbox_policy -} - -fn enable_platform_defaults(access: &mut ReadOnlyAccess) { - if let ReadOnlyAccess::Restricted { - include_platform_defaults, - .. - } = access - { - *include_platform_defaults = true; - } -} - fn io_error(err: std::io::Error) -> JSONRPCErrorError { internal_error(err.to_string()) } @@ -362,9 +321,9 @@ mod tests { use std::collections::HashMap; use std::ffi::OsString; - use codex_protocol::models::FileSystemPermissions; - use codex_protocol::models::NetworkPermissions; - use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::ReadOnlyAccess; @@ -373,17 +332,18 @@ mod tests { use pretty_assertions::assert_eq; use crate::ExecServerRuntimePaths; - use crate::FileSystemSandboxContext; use super::FileSystemSandboxRunner; + use super::add_helper_runtime_permissions; use super::helper_env; use super::helper_env_from_vars; use super::helper_env_key_is_allowed; - use super::helper_sandbox_inputs; - use super::sandbox_policy_with_helper_runtime_defaults; + use super::helper_read_root; #[test] - fn helper_sandbox_policy_enables_platform_defaults_for_read_only_access() { + fn helper_permissions_enable_minimal_reads_for_read_only_access() { + let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) + .expect("absolute cwd"); let sandbox_policy = SandboxPolicy::ReadOnly { access: ReadOnlyAccess::Restricted { include_platform_defaults: false, @@ -391,136 +351,76 @@ mod tests { }, network_access: false, }; + let mut policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()); - let updated = sandbox_policy_with_helper_runtime_defaults(&sandbox_policy); + add_helper_runtime_permissions(&mut policy, /*helper_read_root*/ None, cwd.as_path()); - assert_eq!( - updated, - SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: Vec::new(), - }, - network_access: false, - } - ); + assert!(policy.include_platform_defaults()); } #[test] - fn helper_sandbox_policy_enables_platform_defaults_for_workspace_read_access() { + fn helper_permissions_enable_minimal_reads_for_workspace_read_access() { + let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) + .expect("absolute cwd"); let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), read_only_access: ReadOnlyAccess::Restricted { include_platform_defaults: false, readable_roots: Vec::new(), }, - network_access: true, + network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; + let mut policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()); - let updated = sandbox_policy_with_helper_runtime_defaults(&sandbox_policy); + add_helper_runtime_permissions(&mut policy, /*helper_read_root*/ None, cwd.as_path()); - assert_eq!( - updated, - SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: Vec::new(), - }, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - } - ); + assert!(policy.include_platform_defaults()); } #[test] - fn helper_sandbox_inputs_use_context_cwd_and_file_system_policy() { - let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) - .expect("absolute temp dir"); - let sandbox_policy = SandboxPolicy::new_workspace_write_policy(); - let file_system_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( - &sandbox_policy, - cwd.as_path(), - ); - let mut sandbox_context = FileSystemSandboxContext::new(sandbox_policy.clone()); - sandbox_context.sandbox_policy_cwd = Some(cwd.clone()); - sandbox_context.file_system_sandbox_policy = Some(file_system_policy.clone()); - - let inputs = helper_sandbox_inputs(&sandbox_context).expect("helper sandbox inputs"); - - assert_eq!(inputs.cwd, cwd); - assert_eq!(inputs.sandbox_policy, sandbox_policy); - assert_eq!(inputs.file_system_policy, file_system_policy); - assert_eq!(inputs.network_policy, NetworkSandboxPolicy::Restricted); - } - - #[test] - fn helper_sandbox_inputs_rejects_file_system_policy_without_cwd() { - let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) - .expect("absolute temp dir"); - let sandbox_policy = SandboxPolicy::new_workspace_write_policy(); - let file_system_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( - &sandbox_policy, - cwd.as_path(), - ); - let mut sandbox_context = FileSystemSandboxContext::new(sandbox_policy); - sandbox_context.file_system_sandbox_policy = Some(file_system_policy); - - let err = match helper_sandbox_inputs(&sandbox_context) { - Ok(_) => panic!("expected invalid sandbox inputs"), - Err(err) => err, - }; - - assert_eq!( - err.message, - "fileSystemSandboxPolicy requires sandboxPolicyCwd" - ); - } - - #[test] - fn helper_permissions_strip_network_grants() { + fn helper_permissions_preserve_existing_writes() { let codex_self_exe = std::env::current_exe().expect("current exe"); - let runtime_paths = ExecServerRuntimePaths::new( - codex_self_exe.clone(), - /*codex_linux_sandbox_exe*/ None, - ) - .expect("runtime paths"); - let runner = FileSystemSandboxRunner::new(runtime_paths); + let runtime_paths = + ExecServerRuntimePaths::new(codex_self_exe, /*codex_linux_sandbox_exe*/ None) + .expect("runtime paths"); + let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) + .expect("absolute cwd"); + let writable = cwd.join("writable"); + let sandbox_policy = SandboxPolicy::ReadOnly { + access: ReadOnlyAccess::Restricted { + include_platform_defaults: false, + readable_roots: Vec::new(), + }, + network_access: true, + }; + let mut policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()); + policy.entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: writable.clone(), + }, + access: FileSystemAccessMode::Write, + }); let readable = AbsolutePathBuf::from_absolute_path( - codex_self_exe.parent().expect("current exe parent"), + runtime_paths + .codex_self_exe + .parent() + .expect("current exe parent"), ) .expect("absolute readable path"); - let writable = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) - .expect("absolute writable path"); - let permissions = runner - .helper_permissions( - Some(&PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - file_system: Some(FileSystemPermissions::from_read_write_roots( - Some(vec![]), - Some(vec![writable.clone()]), - )), - }), - /*include_helper_read_root*/ true, - ) - .expect("helper permissions"); - let (read, write) = permissions - .file_system - .as_ref() - .and_then(FileSystemPermissions::legacy_read_write_roots) - .expect("helper permissions should stay lossless as legacy read/write roots"); + add_helper_runtime_permissions( + &mut policy, + helper_read_root(&runtime_paths), + cwd.as_path(), + ); - assert_eq!(permissions.network, None); - assert_eq!(write, Some(vec![writable])); - assert_eq!(read, Some(vec![readable])); + assert!(policy.can_read_path_with_cwd(readable.as_path(), cwd.as_path())); + assert!(policy.can_write_path_with_cwd(writable.as_path(), cwd.as_path())); } #[test] @@ -618,46 +518,35 @@ mod tests { #[test] fn helper_permissions_include_helper_read_root_without_additional_permissions() { - let codex_self_exe = std::env::current_exe().expect("current exe"); - let runtime_paths = ExecServerRuntimePaths::new( - codex_self_exe.clone(), - /*codex_linux_sandbox_exe*/ None, - ) - .expect("runtime paths"); - let runner = FileSystemSandboxRunner::new(runtime_paths); - let readable = AbsolutePathBuf::from_absolute_path( - codex_self_exe.parent().expect("current exe parent"), - ) - .expect("absolute readable path"); - - let permissions = runner - .helper_permissions( - /*additional_permissions*/ None, /*include_helper_read_root*/ true, - ) - .expect("helper permissions"); - - assert_eq!(permissions.network, None); - assert_eq!( - permissions.file_system, - Some(FileSystemPermissions::from_read_write_roots( - Some(vec![readable]), - /*write*/ None, - )) - ); - } - - #[test] - fn legacy_landlock_helper_permissions_do_not_add_helper_read_root() { let codex_self_exe = std::env::current_exe().expect("current exe"); let runtime_paths = ExecServerRuntimePaths::new(codex_self_exe, /*codex_linux_sandbox_exe*/ None) .expect("runtime paths"); - let runner = FileSystemSandboxRunner::new(runtime_paths); + let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) + .expect("absolute cwd"); + let sandbox_policy = SandboxPolicy::ReadOnly { + access: ReadOnlyAccess::Restricted { + include_platform_defaults: false, + readable_roots: Vec::new(), + }, + network_access: false, + }; + let mut policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()); + let readable = AbsolutePathBuf::from_absolute_path( + runtime_paths + .codex_self_exe + .parent() + .expect("current exe parent"), + ) + .expect("absolute readable path"); - let permissions = runner.helper_permissions( - /*additional_permissions*/ None, /*include_helper_read_root*/ false, + add_helper_runtime_permissions( + &mut policy, + helper_read_root(&runtime_paths), + cwd.as_path(), ); - assert_eq!(permissions, None); + assert!(policy.can_read_path_with_cwd(readable.as_path(), cwd.as_path())); } } diff --git a/codex-rs/exec-server/tests/file_system.rs b/codex-rs/exec-server/tests/file_system.rs index d4f94c7e44..a63c2d60ff 100644 --- a/codex-rs/exec-server/tests/file_system.rs +++ b/codex-rs/exec-server/tests/file_system.rs @@ -25,6 +25,7 @@ use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::policy_transforms::merge_permission_profiles; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -567,13 +568,19 @@ async fn file_system_sandboxed_write_allows_additional_write_root(use_remote: bo std::fs::create_dir_all(&writable_dir)?; let mut sandbox = read_only_sandbox(readable_dir); - sandbox.additional_permissions = Some(PermissionProfile { + let additional_permissions = PermissionProfile { network: None, file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![absolute_path(writable_dir)]), )), - }); + }; + let Some(permissions) = + merge_permission_profiles(Some(&sandbox.permissions), Some(&additional_permissions)) + else { + panic!("merged permissions should not be empty"); + }; + sandbox.permissions = permissions; file_system .write_file( From 31d1b49a15f96717814dbd45dffbaace59dad909 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 05/16] core: derive active permission profiles --- codex-rs/app-server/src/codex_message_processor.rs | 5 +++++ codex-rs/core/src/codex_thread.rs | 2 ++ codex-rs/core/src/config/mod.rs | 12 ++++++++++++ codex-rs/core/src/session/session.rs | 8 ++++++++ 4 files changed, 27 insertions(+) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 52a3d2fcb3..92d07ded06 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -10670,6 +10670,11 @@ mod tests { approval_policy: codex_protocol::protocol::AskForApproval::OnRequest, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + permission_profile: + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + std::path::Path::new("/tmp"), + ), cwd: test_path_buf("/tmp").abs(), ephemeral: false, reasoning_effort: None, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 6513f09d07..7c8e9fa68a 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -11,6 +11,7 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::mcp::CallToolResult; use codex_protocol::models::ContentItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; @@ -42,6 +43,7 @@ pub struct ThreadConfigSnapshot { pub approval_policy: AskForApproval, pub approvals_reviewer: ApprovalsReviewer, pub sandbox_policy: SandboxPolicy, + pub permission_profile: PermissionProfile, pub cwd: AbsolutePathBuf, pub ephemeral: bool, pub reasoning_effort: Option, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 2a4171acf2..20e21b0b17 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -78,6 +78,7 @@ use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemSandboxPolicy; @@ -219,6 +220,17 @@ pub struct Permissions { pub windows_sandbox_private_desktop: bool, } +impl Permissions { + /// Effective runtime permissions after config requirements and runtime + /// readable-root additions have been applied. + pub fn permission_profile(&self) -> PermissionProfile { + PermissionProfile::from_runtime_permissions( + &self.file_system_sandbox_policy, + self.network_sandbox_policy, + ) + } +} + /// Application configuration loaded from disk and merged with overrides. #[derive(Debug, Clone, PartialEq)] pub struct Config { diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index a2080bbeab..30efec5303 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -91,6 +91,13 @@ impl SessionConfiguration { &self.codex_home } + pub(super) fn permission_profile(&self) -> PermissionProfile { + PermissionProfile::from_runtime_permissions( + &self.file_system_sandbox_policy, + self.network_sandbox_policy, + ) + } + pub(super) fn thread_config_snapshot(&self) -> ThreadConfigSnapshot { ThreadConfigSnapshot { model: self.collaboration_mode.model().to_string(), @@ -99,6 +106,7 @@ impl SessionConfiguration { approval_policy: self.approval_policy.value(), approvals_reviewer: self.approvals_reviewer, sandbox_policy: self.sandbox_policy.get().clone(), + permission_profile: self.permission_profile(), cwd: self.cwd.clone(), ephemeral: self.original_config_do_not_use.ephemeral, reasoning_effort: self.collaboration_mode.reasoning_effort(), From 47010046fd71289896f9d70f816530acf6ba8c88 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 06/16] app-server: expose thread permission profiles --- .../analytics/src/analytics_client_tests.rs | 12 + .../codex_app_server_protocol.schemas.json | 511 ++++++++++-------- .../codex_app_server_protocol.v2.schemas.json | 281 ++++++++++ .../schema/json/v2/ThreadForkResponse.json | 273 ++++++++++ .../schema/json/v2/ThreadResumeResponse.json | 273 ++++++++++ .../schema/json/v2/ThreadStartResponse.json | 273 ++++++++++ .../schema/typescript/v2/PermissionProfile.ts | 7 + .../PermissionProfileFileSystemPermissions.ts | 6 + .../typescript/v2/ThreadForkResponse.ts | 3 +- .../typescript/v2/ThreadResumeResponse.ts | 3 +- .../typescript/v2/ThreadStartResponse.ts | 3 +- .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 24 + .../app-server-protocol/src/protocol/v2.rs | 108 ++++ .../app-server/src/codex_message_processor.rs | 7 + 15 files changed, 1557 insertions(+), 229 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index bf97da4284..15f456ef3f 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -65,6 +65,7 @@ use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::NonSteerableTurnKind; +use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy; use codex_app_server_protocol::ServerNotification; @@ -91,6 +92,7 @@ use codex_plugin::PluginTelemetryMetadata; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ModeKind; +use codex_protocol::models::PermissionProfile as CorePermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::HookEventName; use codex_protocol::protocol::HookRunStatus; @@ -152,11 +154,20 @@ fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) - approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, + permission_profile: sample_permission_profile(), reasoning_effort: None, }, } } +fn sample_permission_profile() -> AppServerPermissionProfile { + CorePermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + &test_path_buf("/tmp"), + ) + .into() +} + fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata { CodexAppServerClientMetadata { product_client_id: DEFAULT_ORIGINATOR.to_string(), @@ -203,6 +214,7 @@ fn sample_thread_resume_response_with_source( approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, + permission_profile: sample_permission_profile(), reasoning_effort: None, }, } 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 42ca071963..27b1d93d1f 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 @@ -9,7 +9,7 @@ "properties": { "entries": { "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "$ref": "#/definitions/v2/FileSystemSandboxEntry" }, "type": [ "array", @@ -45,17 +45,6 @@ }, "type": "object" }, - "AdditionalNetworkPermissions": { - "properties": { - "enabled": { - "type": [ - "boolean", - "null" - ] - } - }, - "type": "object" - }, "AdditionalPermissionProfile": { "properties": { "fileSystem": { @@ -71,7 +60,7 @@ "network": { "anyOf": [ { - "$ref": "#/definitions/AdditionalNetworkPermissions" + "$ref": "#/definitions/v2/AdditionalNetworkPermissions" }, { "type": "null" @@ -2268,217 +2257,6 @@ "title": "FileChangeRequestApprovalResponse", "type": "object" }, - "FileSystemAccessMode": { - "enum": [ - "read", - "write", - "none" - ], - "type": "string" - }, - "FileSystemPath": { - "oneOf": [ - { - "properties": { - "path": { - "$ref": "#/definitions/v2/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" - } - ] - }, "FuzzyFileSearchMatchType": { "enum": [ "file", @@ -2621,7 +2399,7 @@ "network": { "anyOf": [ { - "$ref": "#/definitions/AdditionalNetworkPermissions" + "$ref": "#/definitions/v2/AdditionalNetworkPermissions" }, { "type": "null" @@ -3694,7 +3472,7 @@ "network": { "anyOf": [ { - "$ref": "#/definitions/AdditionalNetworkPermissions" + "$ref": "#/definitions/v2/AdditionalNetworkPermissions" }, { "type": "null" @@ -5413,6 +5191,17 @@ ], "type": "string" }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "AgentMessageDeltaNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -8041,6 +7830,217 @@ "title": "FileChangePatchUpdatedNotification", "type": "object" }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "none" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/v2/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/v2/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/v2/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/v2/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" + } + ] + }, "FileUpdateChange": { "properties": { "diff": { @@ -10505,6 +10505,53 @@ } ] }, + "PermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfileFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "PermissionProfileFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/v2/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "Personality": { "enum": [ "none", @@ -13624,6 +13671,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/v2/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -13657,6 +13707,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], @@ -15008,6 +15059,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/v2/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -15041,6 +15095,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], @@ -15307,6 +15362,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/v2/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -15340,6 +15398,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], 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 d00378de51..d9503317f5 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 @@ -128,6 +128,17 @@ ], "type": "string" }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "AgentMessageDeltaNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -4370,6 +4381,217 @@ "title": "FileChangePatchUpdatedNotification", "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" + } + ] + }, "FileUpdateChange": { "properties": { "diff": { @@ -6989,6 +7211,53 @@ } ] }, + "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": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "Personality": { "enum": [ "none", @@ -11240,6 +11509,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -11273,6 +11545,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], @@ -12624,6 +12897,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -12657,6 +12933,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], @@ -12923,6 +13200,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -12956,6 +13236,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 909c2af4d8..fa97949712 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.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" + }, "AgentPath": { "type": "string" }, @@ -448,6 +459,217 @@ ], "type": "string" }, + "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" + } + ] + }, "FileUpdateChange": { "properties": { "diff": { @@ -686,6 +908,53 @@ } ] }, + "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": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "ReadOnlyAccess": { "oneOf": [ { @@ -2219,6 +2488,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -2252,6 +2524,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index aef0cd737c..0003171d0f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.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" + }, "AgentPath": { "type": "string" }, @@ -448,6 +459,217 @@ ], "type": "string" }, + "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" + } + ] + }, "FileUpdateChange": { "properties": { "diff": { @@ -686,6 +908,53 @@ } ] }, + "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": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "ReadOnlyAccess": { "oneOf": [ { @@ -2219,6 +2488,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -2252,6 +2524,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index a3d20555f1..114fd30772 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.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" + }, "AgentPath": { "type": "string" }, @@ -448,6 +459,217 @@ ], "type": "string" }, + "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" + } + ] + }, "FileUpdateChange": { "properties": { "diff": { @@ -686,6 +908,53 @@ } ] }, + "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": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "ReadOnlyAccess": { "oneOf": [ { @@ -2219,6 +2488,9 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "$ref": "#/definitions/PermissionProfile" + }, "reasoningEffort": { "anyOf": [ { @@ -2252,6 +2524,7 @@ "cwd", "model", "modelProvider", + "permissionProfile", "sandbox", "thread" ], diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts new file mode 100644 index 0000000000..603f607720 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts @@ -0,0 +1,7 @@ +// 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 { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; +import type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions"; + +export type PermissionProfile = { network: AdditionalNetworkPermissions | null, fileSystem: PermissionProfileFileSystemPermissions | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts new file mode 100644 index 0000000000..204a42764c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; + +export type PermissionProfileFileSystemPermissions = { entries: Array, globScanMaxDepth?: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts index 470e98c9b8..add13b6f28 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -6,6 +6,7 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; @@ -17,4 +18,4 @@ instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, }; +approvalsReviewer: ApprovalsReviewer, sandbox: SandboxPolicy, permissionProfile: PermissionProfile, reasoningEffort: ReasoningEffort | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index 177add8350..cf73f3997c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -6,6 +6,7 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; @@ -17,4 +18,4 @@ instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, }; +approvalsReviewer: ApprovalsReviewer, sandbox: SandboxPolicy, permissionProfile: PermissionProfile, reasoningEffort: ReasoningEffort | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index fd84a41ae8..3885f61a34 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -6,6 +6,7 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; @@ -17,4 +18,4 @@ instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, }; +approvalsReviewer: ApprovalsReviewer, sandbox: SandboxPolicy, permissionProfile: PermissionProfile, reasoningEffort: ReasoningEffort | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index e37759112a..d6ff55c4dc 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -230,6 +230,8 @@ export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; export type { PatchChangeKind } from "./PatchChangeKind"; export type { PermissionGrantScope } from "./PermissionGrantScope"; +export type { PermissionProfile } from "./PermissionProfile"; +export type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions"; export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams"; export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse"; export type { PlanDeltaNotification } from "./PlanDeltaNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index aa3ff8331c..f119a65c60 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1448,6 +1448,12 @@ mod tests { approval_policy: v2::AskForApproval::OnFailure, approvals_reviewer: v2::ApprovalsReviewer::User, sandbox: v2::SandboxPolicy::DangerFullAccess, + permission_profile: + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + std::path::Path::new("/tmp"), + ) + .into(), reasoning_effort: None, }, }; @@ -1490,6 +1496,24 @@ mod tests { "sandbox": { "type": "dangerFullAccess" }, + "permissionProfile": { + "network": { + "enabled": true, + }, + "fileSystem": { + "entries": [ + { + "path": { + "type": "special", + "value": { + "kind": "root", + }, + }, + "access": "write", + }, + ], + }, + }, "reasoningEffort": null } }), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a029860f2a..cad88e9553 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1378,6 +1378,70 @@ impl From for CoreFileSystemSandboxEntry { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PermissionProfileFileSystemPermissions { + pub entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub glob_scan_max_depth: Option, +} + +impl From for PermissionProfileFileSystemPermissions { + fn from(value: CoreFileSystemPermissions) -> Self { + Self { + entries: value + .entries + .into_iter() + .map(FileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth: value.glob_scan_max_depth, + } + } +} + +impl From for CoreFileSystemPermissions { + fn from(value: PermissionProfileFileSystemPermissions) -> Self { + Self { + entries: value + .entries + .into_iter() + .map(CoreFileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth: value.glob_scan_max_depth, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PermissionProfile { + pub network: Option, + pub file_system: Option, +} + +impl From for PermissionProfile { + fn from(value: CorePermissionProfile) -> Self { + Self { + network: value.network.map(AdditionalNetworkPermissions::from), + file_system: value + .file_system + .map(PermissionProfileFileSystemPermissions::from), + } + } +} + +impl From for CorePermissionProfile { + fn from(value: PermissionProfile) -> Self { + Self { + network: value.network.map(CoreNetworkPermissions::from), + file_system: value.file_system.map(CoreFileSystemPermissions::from), + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -2964,6 +3028,7 @@ pub struct ThreadStartResponse { /// Reviewer currently used for approval requests on this thread. pub approvals_reviewer: ApprovalsReviewer, pub sandbox: SandboxPolicy, + pub permission_profile: PermissionProfile, pub reasoning_effort: Option, } @@ -3053,6 +3118,7 @@ pub struct ThreadResumeResponse { /// Reviewer currently used for approval requests on this thread. pub approvals_reviewer: ApprovalsReviewer, pub sandbox: SandboxPolicy, + pub permission_profile: PermissionProfile, pub reasoning_effort: Option, } @@ -3133,6 +3199,7 @@ pub struct ThreadForkResponse { /// Reviewer currently used for approval requests on this thread. pub approvals_reviewer: ApprovalsReviewer, pub sandbox: SandboxPolicy, + pub permission_profile: PermissionProfile, pub reasoning_effort: Option, } @@ -7209,6 +7276,47 @@ mod tests { .expect_err("zero glob scan depth should fail deserialization"); } + #[test] + fn permission_profile_file_system_permissions_preserves_glob_scan_depth() { + let core_permissions = CoreFileSystemPermissions { + entries: vec![CoreFileSystemSandboxEntry { + path: CoreFileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: CoreFileSystemAccessMode::None, + }], + glob_scan_max_depth: NonZeroUsize::new(2), + }; + + let permissions = PermissionProfileFileSystemPermissions::from(core_permissions.clone()); + + assert_eq!( + permissions, + PermissionProfileFileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }], + glob_scan_max_depth: NonZeroUsize::new(2), + } + ); + assert_eq!( + CoreFileSystemPermissions::from(permissions), + core_permissions + ); + } + + #[test] + fn permission_profile_file_system_permissions_rejects_zero_glob_scan_depth() { + serde_json::from_value::(json!({ + "entries": [], + "globScanMaxDepth": 0, + })) + .expect_err("zero glob scan depth should fail deserialization"); + } + #[test] fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() { let read_only_path = if cfg!(windows) { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 92d07ded06..a097efc33b 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2777,6 +2777,7 @@ impl CodexMessageProcessor { approval_policy: config_snapshot.approval_policy.into(), approvals_reviewer: config_snapshot.approvals_reviewer.into(), sandbox: config_snapshot.sandbox_policy.into(), + permission_profile: config_snapshot.permission_profile.into(), reasoning_effort: config_snapshot.reasoning_effort, }; if listener_task_context.general_analytics_enabled { @@ -4508,6 +4509,7 @@ impl CodexMessageProcessor { thread_status, /*has_live_in_progress_turn*/ false, ); + let permission_profile = codex_thread.config_snapshot().await.permission_profile; let response = ThreadResumeResponse { thread, @@ -4519,6 +4521,7 @@ impl CodexMessageProcessor { approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), sandbox: session_configured.sandbox_policy.into(), + permission_profile: permission_profile.into(), reasoning_effort: session_configured.reasoning_effort, }; if self.config.features.enabled(Feature::GeneralAnalytics) { @@ -5162,6 +5165,7 @@ impl CodexMessageProcessor { .await, /*has_in_progress_turn*/ false, ); + let permission_profile = forked_thread.config_snapshot().await.permission_profile; let response = ThreadForkResponse { thread: thread.clone(), @@ -5173,6 +5177,7 @@ impl CodexMessageProcessor { approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), sandbox: session_configured.sandbox_policy.into(), + permission_profile: permission_profile.into(), reasoning_effort: session_configured.reasoning_effort, }; if self.config.features.enabled(Feature::GeneralAnalytics) { @@ -8965,6 +8970,7 @@ async fn handle_pending_thread_resume_request( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, cwd, reasoning_effort, .. @@ -8980,6 +8986,7 @@ async fn handle_pending_thread_resume_request( approval_policy: approval_policy.into(), approvals_reviewer: approvals_reviewer.into(), sandbox: sandbox_policy.into(), + permission_profile: permission_profile.into(), reasoning_effort, }; let token_usage_thread = response.thread.clone(); From 68b5f9d41f1c644dc4ccda6ecd198219a0329974 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 07/16] app-server: accept permission profile overrides --- .../schema/json/ClientRequest.json | 313 ++++++++++++++++++ .../codex_app_server_protocol.schemas.json | 44 +++ .../codex_app_server_protocol.v2.schemas.json | 44 +++ .../schema/json/v2/ThreadForkParams.json | 284 ++++++++++++++++ .../schema/json/v2/ThreadResumeParams.json | 284 ++++++++++++++++ .../schema/json/v2/ThreadStartParams.json | 284 ++++++++++++++++ .../schema/json/v2/TurnStartParams.json | 280 ++++++++++++++++ .../schema/typescript/v2/ThreadForkParams.ts | 7 +- .../typescript/v2/ThreadResumeParams.ts | 7 +- .../schema/typescript/v2/ThreadStartParams.ts | 7 +- .../schema/typescript/v2/TurnStartParams.ts | 5 + .../app-server-protocol/src/protocol/v2.rs | 31 ++ codex-rs/app-server/README.md | 14 +- .../app-server/src/codex_message_processor.rs | 55 +++ .../src/message_processor/tracing_tests.rs | 1 + .../app-server/tests/suite/v2/skills_list.rs | 1 + .../app-server/tests/suite/v2/turn_start.rs | 2 + codex-rs/core/src/config/config_tests.rs | 42 +++ codex-rs/core/src/config/mod.rs | 38 ++- codex-rs/core/src/session/handlers.rs | 3 + codex-rs/core/src/session/session.rs | 31 +- codex-rs/core/src/session/tests.rs | 2 + .../tests/suite/collaboration_instructions.rs | 13 + codex-rs/core/tests/suite/compact.rs | 1 + codex-rs/core/tests/suite/compact_remote.rs | 2 + .../core/tests/suite/compact_resume_fork.rs | 1 + codex-rs/core/tests/suite/model_overrides.rs | 2 + codex-rs/core/tests/suite/model_switching.rs | 3 + .../core/tests/suite/model_visible_layout.rs | 1 + codex-rs/core/tests/suite/override_updates.rs | 3 + .../core/tests/suite/permissions_messages.rs | 4 + codex-rs/core/tests/suite/personality.rs | 4 + codex-rs/core/tests/suite/prompt_caching.rs | 2 + codex-rs/core/tests/suite/remote_models.rs | 2 + codex-rs/core/tests/suite/resume.rs | 1 + codex-rs/core/tests/suite/review.rs | 1 + codex-rs/exec/src/lib.rs | 2 + codex-rs/exec/src/lib_tests.rs | 12 + codex-rs/protocol/src/models.rs | 7 + codex-rs/protocol/src/protocol.rs | 5 + codex-rs/tui/src/app/tests.rs | 6 + codex-rs/tui/src/app_command.rs | 2 + codex-rs/tui/src/app_server_session.rs | 7 + .../tui/src/chatwidget/tests/permissions.rs | 1 + 44 files changed, 1844 insertions(+), 17 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index f9cdca83e3..e74a763656 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -12,6 +12,17 @@ ], "type": "string" }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "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": [ @@ -697,6 +708,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" + } + ] + }, "FsCopyParams": { "description": "Copy a file or directory tree on the host filesystem.", "properties": { @@ -1455,6 +1677,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" + }, "Personality": { "enum": [ "none", @@ -2813,6 +3082,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." + }, "sandbox": { "anyOf": [ { @@ -3184,6 +3464,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -3367,6 +3658,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -3563,6 +3865,17 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." + }, "personality": { "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 27b1d93d1f..8804c22635 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 @@ -13603,6 +13603,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." + }, "sandbox": { "anyOf": [ { @@ -14981,6 +14992,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -15274,6 +15296,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -16051,6 +16084,17 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." + }, "personality": { "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 d9503317f5..63e7eabdc4 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 @@ -11441,6 +11441,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." + }, "sandbox": { "anyOf": [ { @@ -12819,6 +12830,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -13112,6 +13134,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -13889,6 +13922,17 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." + }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index a8fa95e2e9..a294ab6a16 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -1,6 +1,21 @@ { "$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" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "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": [ @@ -59,6 +74,264 @@ } ] }, + "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" + } + ] + }, + "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" + }, "SandboxMode": { "enum": [ "read-only", @@ -139,6 +412,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." + }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 35a9e5b2a8..2df6c40ca7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -1,6 +1,21 @@ { "$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" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "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": [ @@ -133,6 +148,217 @@ } ] }, + "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" + } + ] + }, "FunctionCallOutputBody": { "anyOf": [ { @@ -325,6 +551,53 @@ } ] }, + "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" + }, "Personality": { "enum": [ "none", @@ -1052,6 +1325,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 21f4d7ef74..67fed5abc1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -1,6 +1,21 @@ { "$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" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "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": [ @@ -79,6 +94,264 @@ ], "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" + } + ] + }, + "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" + }, "Personality": { "enum": [ "none", @@ -175,6 +448,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index cad1d8b5bc..ae751d33c4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.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" + }, "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": [ @@ -98,6 +109,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" + } + ] + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -113,6 +335,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" + }, "Personality": { "enum": [ "none", @@ -555,6 +824,17 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." + }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index a7ba311803..bd73b3c3c3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -5,6 +5,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; /** @@ -27,7 +28,11 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** + * Full permissions override for the forked thread. Cannot be combined + * with `sandbox`. + */ +permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** * If true, persist additional rollout EventMsg variants required to * reconstruct a richer thread history on subsequent resume/fork/read. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index 770344de8e..d245efe4a2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -7,6 +7,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; /** @@ -36,7 +37,11 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** + * Full permissions override for the resumed thread. Cannot be combined + * with `sandbox`. + */ +permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** * If true, persist additional rollout EventMsg variants required to * reconstruct a richer thread history on subsequent resume/fork/read. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index 904487e81c..8b9dafec9f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -6,6 +6,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; import type { ThreadStartSource } from "./ThreadStartSource"; @@ -13,7 +14,11 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string | * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** + * Full permissions override for this thread. Cannot be combined with + * `sandbox`. + */ +permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /** * If true, opt into emitting raw Responses API items on the event stream. * This is for internal use only (e.g. Codex Cloud). */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index 8f57a5e68b..3d12e6001c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -9,6 +9,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { UserInput } from "./UserInput"; @@ -26,6 +27,10 @@ approvalsReviewer?: ApprovalsReviewer | null, /** * Override the sandbox policy for this turn and subsequent turns. */ sandboxPolicy?: SandboxPolicy | null, /** + * Override the full permissions profile for this turn and subsequent + * turns. Cannot be combined with `sandboxPolicy`. + */ +permissionProfile?: PermissionProfile | null, /** * Override the model for this turn and subsequent turns. */ model?: string | null, /** diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index cad88e9553..e398a35a9c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2960,6 +2960,10 @@ pub struct ThreadStartParams { pub approvals_reviewer: Option, #[ts(optional = nullable)] pub sandbox: Option, + /// Full permissions override for this thread. Cannot be combined with + /// `sandbox`. + #[ts(optional = nullable)] + pub permission_profile: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -3086,6 +3090,10 @@ pub struct ThreadResumeParams { pub approvals_reviewer: Option, #[ts(optional = nullable)] pub sandbox: Option, + /// Full permissions override for the resumed thread. Cannot be combined + /// with `sandbox`. + #[ts(optional = nullable)] + pub permission_profile: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -3167,6 +3175,10 @@ pub struct ThreadForkParams { pub approvals_reviewer: Option, #[ts(optional = nullable)] pub sandbox: Option, + /// Full permissions override for the forked thread. Cannot be combined + /// with `sandbox`. + #[ts(optional = nullable)] + pub permission_profile: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -4570,6 +4582,10 @@ pub struct TurnStartParams { /// Override the sandbox policy for this turn and subsequent turns. #[ts(optional = nullable)] pub sandbox_policy: Option, + /// Override the full permissions profile for this turn and subsequent + /// turns. Cannot be combined with `sandboxPolicy`. + #[ts(optional = nullable)] + pub permission_profile: Option, /// Override the model for this turn and subsequent turns. #[ts(optional = nullable)] pub model: Option, @@ -9458,6 +9474,20 @@ mod tests { "approvalPolicy": "on-failure", "approvalsReviewer": "user", "sandbox": { "type": "dangerFullAccess" }, + "permissionProfile": { + "network": { "enabled": true }, + "fileSystem": { + "entries": [ + { + "path": { + "type": "special", + "value": { "kind": "root" } + }, + "access": "write" + } + ] + } + }, "reasoningEffort": null }); @@ -9497,6 +9527,7 @@ mod tests { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, model: None, service_tier: None, effort: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 5b596dea43..e5d2315a14 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -82,7 +82,7 @@ Use the thread APIs to create, list, or archive conversations. Drive a conversat - Initialize once per connection: Immediately after opening a transport connection, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request on that connection before this handshake gets rejected. - Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and you’ll also get a `thread/started` notification. If you’re continuing an existing conversation, call `thread/resume` with its ID instead. If you want to branch from an existing conversation, call `thread/fork` to create a new thread id with copied history. Like `thread/start`, `thread/fork` also accepts `ephemeral: true` for an in-memory temporary thread. The returned `thread.ephemeral` flag tells you whether the session is intentionally in-memory only; when it is `true`, `thread.path` is `null`. -- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, approval policy, approvals reviewer, etc. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running. +- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy or `permissionProfile`, approval policy, approvals reviewer, etc. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running. - Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. You’ll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes). - Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage. @@ -136,9 +136,9 @@ Example with notification opt-out: ## API Overview -- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. -- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. -- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. +- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. For permissions, prefer `permissionProfile`; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissionProfile`. +- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. +- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Accepts the same permission override rules as `thread/start`. - `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. @@ -155,7 +155,7 @@ Example with notification opt-out: - `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. - `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted. - `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. -- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". +- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Prefer `permissionProfile` for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissionProfile`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". - `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success. - `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. @@ -220,6 +220,8 @@ Start a fresh thread when you need a new Codex conversation. "cwd": "/Users/me/project", "approvalPolicy": "never", "sandbox": "workspaceWrite", + // Prefer "permissionProfile" for full permission overrides. Do not send + // both "sandbox" and "permissionProfile". "personality": "friendly", "serviceName": "my_app_server_client", // optional metrics tag (`service_name`) "sessionStartSource": "startup", // optional: "startup" (default) or "clear" @@ -536,6 +538,8 @@ You can optionally specify config overrides on the new turn. If specified, these "writableRoots": ["/Users/me/project"], "networkAccess": true }, + // Prefer "permissionProfile" for full permission overrides. Do not send + // both "sandboxPolicy" and "permissionProfile". "model": "gpt-5.1-codex", "effort": "medium", "summary": "concise", diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index a097efc33b..9c5a6381d9 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -102,6 +102,7 @@ use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::MockExperimentalMethodResponse; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; +use codex_app_server_protocol::PermissionProfile as ApiPermissionProfile; use codex_app_server_protocol::PluginDetail; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallResponse; @@ -2398,6 +2399,7 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, config, service_name, base_instructions, @@ -2410,6 +2412,14 @@ impl CodexMessageProcessor { session_start_source, persist_extended_history, } = params; + if sandbox.is_some() && permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandbox`".to_string(), + ) + .await; + return; + } let mut typesafe_overrides = self.build_thread_config_overrides( model, model_provider, @@ -2418,6 +2428,7 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, base_instructions, developer_instructions, personality, @@ -2835,6 +2846,7 @@ impl CodexMessageProcessor { approval_policy: Option, approvals_reviewer: Option, sandbox: Option, + permission_profile: Option, base_instructions: Option, developer_instructions: Option, personality: Option, @@ -2849,6 +2861,7 @@ impl CodexMessageProcessor { approvals_reviewer: approvals_reviewer .map(codex_app_server_protocol::ApprovalsReviewer::to_core), sandbox_mode: sandbox.map(SandboxMode::to_core), + permission_profile: permission_profile.map(Into::into), codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, @@ -4364,12 +4377,21 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, config: mut request_overrides, base_instructions, developer_instructions, personality, persist_extended_history, } = params; + if sandbox.is_some() && permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandbox`".to_string(), + ) + .await; + return; + } let thread_history = if let Some(history) = history { let Some(thread_history) = self @@ -4398,6 +4420,7 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, base_instructions, developer_instructions, personality, @@ -4905,12 +4928,21 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, config: cli_overrides, base_instructions, developer_instructions, ephemeral, persist_extended_history, } = params; + if sandbox.is_some() && permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandbox`".to_string(), + ) + .await; + return; + } let (rollout_path, source_thread_id) = if let Some(path) = path { (path, None) @@ -4987,6 +5019,7 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, base_instructions, developer_instructions, /*personality*/ None, @@ -7239,6 +7272,7 @@ impl CodexMessageProcessor { || params.approval_policy.is_some() || params.approvals_reviewer.is_some() || params.sandbox_policy.is_some() + || params.permission_profile.is_some() || params.model.is_some() || params.service_tier.is_some() || params.effort.is_some() @@ -7246,6 +7280,15 @@ impl CodexMessageProcessor { || collaboration_mode.is_some() || params.personality.is_some(); + if params.sandbox_policy.is_some() && params.permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandboxPolicy`".to_string(), + ) + .await; + return; + } + // If any overrides are provided, update the session turn context first. if has_any_overrides { let _ = self @@ -7259,6 +7302,7 @@ impl CodexMessageProcessor { .approvals_reviewer .map(codex_app_server_protocol::ApprovalsReviewer::to_core), sandbox_policy: params.sandbox_policy.map(|p| p.to_core()), + permission_profile: params.permission_profile.map(Into::into), windows_sandbox_level: None, model: params.model, effort: params.effort.map(Some), @@ -9171,6 +9215,16 @@ fn collect_resume_override_mismatches( )); } } + if let Some(requested_permission_profile) = request.permission_profile.as_ref() { + let requested_permission_profile = + codex_protocol::models::PermissionProfile::from(requested_permission_profile.clone()); + if requested_permission_profile != config_snapshot.permission_profile { + mismatch_details.push(format!( + "permission_profile requested={requested_permission_profile:?} active={:?}", + config_snapshot.permission_profile + )); + } + } if let Some(requested_personality) = request.personality.as_ref() && config_snapshot.personality.as_ref() != Some(requested_personality) { @@ -10664,6 +10718,7 @@ mod tests { approval_policy: None, approvals_reviewer: None, sandbox: None, + permission_profile: None, config: None, base_instructions: None, developer_instructions: None, diff --git a/codex-rs/app-server/src/message_processor/tracing_tests.rs b/codex-rs/app-server/src/message_processor/tracing_tests.rs index adf583d8f1..ab9717a16e 100644 --- a/codex-rs/app-server/src/message_processor/tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor/tracing_tests.rs @@ -611,6 +611,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + permission_profile: None, approvals_reviewer: None, model: None, service_tier: None, diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 8675b3a429..195c4a86db 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -315,6 +315,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<( approval_policy: None, approvals_reviewer: None, sandbox: None, + permission_profile: None, config: None, service_name: None, base_instructions: None, diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 65f8442b0d..b8e3182fd2 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1749,6 +1749,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }), + permission_profile: None, model: Some("mock-model".to_string()), effort: Some(ReasoningEffort::Medium), summary: Some(ReasoningSummary::Auto), @@ -1783,6 +1784,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess), + permission_profile: None, model: Some("mock-model".to_string()), effort: Some(ReasoningEffort::Medium), summary: Some(ReasoningSummary::Auto), diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index fb48e73163..aad3060ed4 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -53,6 +53,9 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; +use codex_protocol::models::FileSystemPermissions; +use codex_protocol::models::NetworkPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -61,6 +64,7 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::RealtimeVoice; +use codex_protocol::protocol::SandboxPolicy; use serde::Deserialize; use tempfile::tempdir; @@ -782,6 +786,44 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: Ok(()) } +#[tokio::test] +async fn permission_profile_override_populates_runtime_permissions() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let permission_profile = PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + file_system: Some(FileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: None, + }), + }; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + permission_profile: Some(permission_profile.clone()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert_eq!(config.permissions.permission_profile(), permission_profile); + assert_eq!( + config.permissions.sandbox_policy.get(), + &SandboxPolicy::DangerFullAccess + ); + Ok(()) +} + #[tokio::test] async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 20e21b0b17..4dce78b4a7 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1353,6 +1353,7 @@ pub struct ConfigOverrides { pub approval_policy: Option, pub approvals_reviewer: Option, pub sandbox_mode: Option, + pub permission_profile: Option, pub model_provider: Option, pub service_tier: Option>, pub config_profile: Option, @@ -1570,6 +1571,7 @@ impl Config { approval_policy: approval_policy_override, approvals_reviewer: approvals_reviewer_override, sandbox_mode, + permission_profile, model_provider, service_tier: service_tier_override, config_profile: config_profile_key, @@ -1590,6 +1592,13 @@ impl Config { additional_writable_roots, } = overrides; + if sandbox_mode.is_some() && permission_profile.is_some() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "`sandbox_mode` and `permission_profile` overrides cannot both be set", + )); + } + let active_profile_name = config_profile_key .as_ref() .or(cfg.profile.as_ref()) @@ -1710,7 +1719,34 @@ impl Config { sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, - ) = if profiles_are_active { + ) = if let Some(permission_profile) = permission_profile { + let configured_network_proxy_config = NetworkProxyConfig::default(); + let (mut file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + let mut sandbox_policy = permission_profile + .to_legacy_sandbox_policy(resolved_cwd.as_path()) + .map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid permission_profile override: {err}"), + ) + })?; + if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { + file_system_sandbox_policy = file_system_sandbox_policy + .with_additional_writable_roots( + resolved_cwd.as_path(), + &additional_writable_roots, + ); + sandbox_policy = file_system_sandbox_policy + .to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?; + } + ( + configured_network_proxy_config, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + ) + } else if profiles_are_active { let permissions = cfg.permissions.as_ref().ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::InvalidInput, diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 4d69167dab..6ffb5d6550 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -157,6 +157,7 @@ pub(super) async fn user_input_or_turn_inner( approval_policy: Some(approval_policy), approvals_reviewer, sandbox_policy: Some(sandbox_policy), + permission_profile: None, windows_sandbox_level: None, collaboration_mode, reasoning_summary: summary, @@ -1058,6 +1059,7 @@ pub(super) async fn submission_loop( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, windows_sandbox_level, model, effort, @@ -1084,6 +1086,7 @@ pub(super) async fn submission_loop( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, windows_sandbox_level, collaboration_mode: Some(collaboration_mode), reasoning_summary: summary, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 30efec5303..81f3cd3377 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1,4 +1,5 @@ use super::*; +use crate::config::ConstraintError; use tokio::sync::Semaphore; /// Context for an initialized model agent @@ -140,13 +141,6 @@ impl SessionConfiguration { if let Some(approvals_reviewer) = updates.approvals_reviewer { next_configuration.approvals_reviewer = approvals_reviewer; } - let mut sandbox_policy_changed = false; - if let Some(sandbox_policy) = updates.sandbox_policy.clone() { - next_configuration.sandbox_policy.set(sandbox_policy)?; - next_configuration.network_sandbox_policy = - NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get()); - sandbox_policy_changed = true; - } if let Some(windows_sandbox_level) = updates.windows_sandbox_level { next_configuration.windows_sandbox_level = windows_sandbox_level; } @@ -167,13 +161,33 @@ impl SessionConfiguration { let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path(); next_configuration.cwd = absolute_cwd; - if sandbox_policy_changed { + + if let Some(permission_profile) = updates.permission_profile.clone() { + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(&next_configuration.cwd) + .map_err(|err| ConstraintError::InvalidValue { + field_name: "permission_profile", + candidate: format!("{permission_profile:?}"), + allowed: format!( + "permission profiles that can be represented by the active sandbox constraints: {err}" + ), + requirement_source: codex_config::RequirementSource::Unknown, + })?; + next_configuration.sandbox_policy.set(sandbox_policy)?; + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + next_configuration.file_system_sandbox_policy = file_system_sandbox_policy; + next_configuration.network_sandbox_policy = network_sandbox_policy; + } else if let Some(sandbox_policy) = updates.sandbox_policy.clone() { + next_configuration.sandbox_policy.set(sandbox_policy)?; next_configuration.file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( next_configuration.sandbox_policy.get(), &next_configuration.cwd, &self.file_system_sandbox_policy, ); + next_configuration.network_sandbox_policy = + NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get()); } else if cwd_changed && file_system_policy_matches_legacy { // Preserve richer split policies across cwd-only updates; only // rederive when the session is already using the legacy bridge. @@ -199,6 +213,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) approval_policy: Option, pub(crate) approvals_reviewer: Option, pub(crate) sandbox_policy: Option, + pub(crate) permission_profile: Option, pub(crate) windows_sandbox_level: Option, pub(crate) collaboration_mode: Option, pub(crate) reasoning_summary: Option, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index bd41da9f20..07f390ef99 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1677,6 +1677,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result< approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -3711,6 +3712,7 @@ fn op_kind_distinguishes_turn_ops() { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 57ffb35e60..28685895ac 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -127,6 +127,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -226,6 +227,7 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -279,6 +281,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -350,6 +353,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -378,6 +382,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -435,6 +440,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -463,6 +469,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -519,6 +526,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -550,6 +558,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -610,6 +619,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -641,6 +651,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -707,6 +718,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -772,6 +784,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 5468151b47..7a4dc525d9 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -3053,6 +3053,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 30e3c54c94..584c5e9507 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -2070,6 +2070,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -2183,6 +2184,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index efa39aaeeb..d12e0c5e89 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -575,6 +575,7 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs index f7fe7cc322..9f8835f19f 100644 --- a/codex-rs/core/tests/suite/model_overrides.rs +++ b/codex-rs/core/tests/suite/model_overrides.rs @@ -30,6 +30,7 @@ async fn override_turn_context_does_not_persist_when_config_exists() { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), @@ -68,6 +69,7 @@ async fn override_turn_context_does_not_create_config_file() { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::Medium)), diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 139ee7a856..5bea4de2a9 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -146,6 +146,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, @@ -243,6 +244,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, @@ -1025,6 +1027,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(smaller_model_slug.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 0f41637ea2..bee3f3245b 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -451,6 +451,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.2".to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index 13ace1fe6d..63d2b6ed43 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -118,6 +118,7 @@ async fn override_turn_context_without_user_turn_does_not_record_permissions_upd approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -160,6 +161,7 @@ async fn override_turn_context_without_user_turn_does_not_record_environment_upd approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -199,6 +201,7 @@ async fn override_turn_context_without_user_turn_does_not_record_collaboration_u approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index ec6b7d67d2..2f28a28b55 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -102,6 +102,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -230,6 +231,7 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> { approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -316,6 +318,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -418,6 +421,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 251aad76c0..7e729ef493 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -347,6 +347,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -452,6 +453,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -570,6 +572,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -848,6 +851,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 63a21ce1d5..80dec74c3b 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -437,6 +437,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(new_policy.clone()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: Some(Some(ReasoningEffort::High)), @@ -521,6 +522,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.4".to_string()), effort: Some(Some(ReasoningEffort::Low)), diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 9b157bde5f..42b655aaa9 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -590,6 +590,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(REMOTE_MODEL_SLUG.to_string()), effort: None, @@ -834,6 +835,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(model.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index 55d7e45f0a..ce6f2a29f1 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -422,6 +422,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.4".to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index f4da7c549b..2621fdecef 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -838,6 +838,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 781e423fde..7352071a65 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -390,6 +390,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_mode, + permission_profile: None, cwd: resolved_cwd, model_provider: model_provider.clone(), service_tier: None, @@ -748,6 +749,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { approval_policy: Some(default_approval_policy.into()), approvals_reviewer: None, sandbox_policy: Some(default_sandbox_policy.clone().into()), + permission_profile: None, model: None, service_tier: None, effort: default_effort, diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 46f8ca61ff..11f0be82ab 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -422,6 +422,18 @@ fn session_configured_from_thread_response_uses_review_policy_from_response() { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, + permission_profile: codex_app_server_protocol::PermissionProfile::from( + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + std::path::Path::new("/tmp"), + ), + ), reasoning_effort: None, }; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index b0134630f3..5a7bf07852 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -345,6 +345,13 @@ impl PermissionProfile { self.file_system_sandbox_policy() .to_legacy_sandbox_policy(self.network_sandbox_policy(), cwd) } + + pub fn to_runtime_permissions(&self) -> (FileSystemSandboxPolicy, NetworkSandboxPolicy) { + ( + self.file_system_sandbox_policy(), + self.network_sandbox_policy(), + ) + } } impl From for NetworkPermissions { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 418dc841ce..8c542f4df8 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -38,6 +38,7 @@ use crate::message_history::HistoryEntry; use crate::models::BaseInstructions; use crate::models::ContentItem; use crate::models::MessagePhase; +use crate::models::PermissionProfile; use crate::models::ResponseInputItem; use crate::models::ResponseItem; use crate::models::WebSearchAction; @@ -519,6 +520,10 @@ pub enum Op { #[serde(skip_serializing_if = "Option::is_none")] sandbox_policy: Option, + /// Updated permissions profile for tool calls. + #[serde(skip_serializing_if = "Option::is_none")] + permission_profile: Option, + /// Updated Windows sandbox mode for tool execution. #[serde(skip_serializing_if = "Option::is_none")] windows_sandbox_level: Option, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 5ed786ef05..3652f07ffc 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -1820,6 +1820,7 @@ async fn update_feature_flags_enabling_guardian_selects_guardian_approvals() -> approval_policy: Some(guardian_approvals.approval_policy), approvals_reviewer: Some(guardian_approvals.approvals_reviewer), sandbox_policy: Some(guardian_approvals.sandbox_policy.clone()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -1911,6 +1912,7 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -1990,6 +1992,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review approval_policy: Some(guardian_approvals.approval_policy), approvals_reviewer: Some(guardian_approvals.approvals_reviewer), sandbox_policy: Some(guardian_approvals.sandbox_policy.clone()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -2047,6 +2050,7 @@ async fn update_feature_flags_disabling_guardian_clears_manual_review_policy_wit approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -2106,6 +2110,7 @@ async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_rev approval_policy: Some(guardian_approvals.approval_policy), approvals_reviewer: Some(guardian_approvals.approvals_reviewer), sandbox_policy: Some(guardian_approvals.sandbox_policy.clone()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -2193,6 +2198,7 @@ guardian_approval = true approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index e94dced053..c34df5b765 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -183,6 +183,7 @@ impl AppCommand { approval_policy, approvals_reviewer, sandbox_policy, + permission_profile: None, windows_sandbox_level, model, effort, @@ -315,6 +316,7 @@ impl AppCommand { approval_policy, approvals_reviewer, sandbox_policy, + permission_profile: _, windows_sandbox_level, model, effort, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 4d8e213ef7..1dd8fc1bf1 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -536,6 +536,7 @@ impl AppServerSession { approval_policy: Some(approval_policy.into()), approvals_reviewer: Some(approvals_reviewer.into()), sandbox_policy: Some(sandbox_policy.into()), + permission_profile: None, model: Some(model), service_tier, effort, @@ -1503,6 +1504,12 @@ mod tests { approval_policy: codex_protocol::protocol::AskForApproval::Never.into(), approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User, sandbox: codex_protocol::protocol::SandboxPolicy::new_read_only_policy().into(), + permission_profile: + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &codex_protocol::protocol::SandboxPolicy::new_read_only_policy(), + std::path::Path::new("/tmp/project"), + ) + .into(), reasoning_effort: None, }; diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index c1e142c4e1..92ca170605 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -632,6 +632,7 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context approval_policy: Some(AskForApproval::OnRequest), approvals_reviewer: Some(ApprovalsReviewer::GuardianSubagent), sandbox_policy: Some(SandboxPolicy::new_workspace_write_policy()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, From fac89b19c7a0d9dfd9a19d1cfd61fffeb2c0f35a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 08/16] clients: send permission profiles to app-server --- codex-rs/exec/src/lib.rs | 46 ++++++++++-- codex-rs/exec/src/lib_tests.rs | 5 ++ codex-rs/tui/src/app/tests.rs | 24 +++++++ codex-rs/tui/src/app/thread_session_state.rs | 1 + codex-rs/tui/src/app_server_session.rs | 73 ++++++++++++++++++-- 5 files changed, 140 insertions(+), 9 deletions(-) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 7352071a65..68f8567372 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -75,6 +75,7 @@ use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_otel::set_parent_from_context; use codex_otel::traceparent_context_from_env; use codex_protocol::config_types::SandboxMode; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget; @@ -737,6 +738,18 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { items, output_schema, } => { + let (sandbox_policy, permission_profile) = if matches!( + default_sandbox_policy, + SandboxPolicy::ExternalSandbox { .. } + ) { + (Some(default_sandbox_policy.clone().into()), None) + } else { + let permission_profile = PermissionProfile::from_legacy_sandbox_policy( + default_sandbox_policy, + &default_cwd, + ); + (None, Some(permission_profile.into())) + }; let response: TurnStartResponse = send_request_with_response( &client, ClientRequest::TurnStart { @@ -748,8 +761,8 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { cwd: Some(default_cwd), approval_policy: Some(default_approval_policy.into()), approvals_reviewer: None, - sandbox_policy: Some(default_sandbox_policy.clone().into()), - permission_profile: None, + sandbox_policy, + permission_profile, model: None, service_tier: None, effort: default_effort, @@ -923,13 +936,19 @@ fn sandbox_mode_from_policy( } fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { + let permission_profile = permission_profile_override_from_config(config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get())) + .flatten(); ThreadStartParams { model: config.model.clone(), model_provider: Some(config.model_provider_id.clone()), cwd: Some(config.cwd.to_string_lossy().to_string()), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get()), + sandbox, + permission_profile, config: config_request_overrides_from_config(config), ephemeral: Some(config.ephemeral), ..ThreadStartParams::default() @@ -937,6 +956,11 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { } fn thread_resume_params_from_config(config: &Config, thread_id: String) -> ThreadResumeParams { + let permission_profile = permission_profile_override_from_config(config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get())) + .flatten(); ThreadResumeParams { thread_id, model: config.model.clone(), @@ -944,12 +968,26 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa cwd: Some(config.cwd.to_string_lossy().to_string()), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get()), + sandbox, + permission_profile, config: config_request_overrides_from_config(config), ..ThreadResumeParams::default() } } +fn permission_profile_override_from_config( + config: &Config, +) -> Option { + if matches!( + config.permissions.sandbox_policy.get(), + SandboxPolicy::ExternalSandbox { .. } + ) { + None + } else { + Some(config.permissions.permission_profile().into()) + } +} + fn config_request_overrides_from_config(config: &Config) -> Option> { config .active_profile diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 11f0be82ab..ba1ae9b102 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -361,6 +361,11 @@ async fn thread_start_params_include_review_policy_when_review_policy_is_manual_ params.approvals_reviewer, Some(codex_app_server_protocol::ApprovalsReviewer::User) ); + assert_eq!(params.sandbox, None); + assert_eq!( + params.permission_profile, + Some(config.permissions.permission_profile().into()) + ); } #[tokio::test] diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 3652f07ffc..40262efe40 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2384,6 +2384,10 @@ async fn inactive_thread_approval_bubbles_into_active_view() -> Result<()> { ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/agent"), + ), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2543,6 +2547,10 @@ async fn side_defers_subagent_approval_overlay_until_side_exits() -> Result<()> ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/agent"), + ), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2764,6 +2772,10 @@ async fn inactive_thread_approval_badge_clears_after_turn_completion_notificatio ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/agent"), + ), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2817,6 +2829,10 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/main"), + ), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2928,6 +2944,10 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_ let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/main"), + ), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -3916,6 +3936,10 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + cwd.as_path(), + ), cwd: cwd.abs(), instruction_source_paths: Vec::new(), reasoning_effort: None, diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index a8fd34769e..17db50f876 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -24,6 +24,7 @@ impl App { approval_policy: self.config.permissions.approval_policy.value(), approvals_reviewer: self.config.approvals_reviewer, sandbox_policy: self.config.permissions.sandbox_policy.get().clone(), + permission_profile: self.config.permissions.permission_profile(), cwd: thread.cwd.clone(), instruction_source_paths: Vec::new(), reasoning_effort: self.chat_widget.current_reasoning_effort(), diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 1dd8fc1bf1..80c1a7494a 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -83,6 +83,7 @@ use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelAvailabilityNux; use codex_protocol::openai_models::ModelPreset; @@ -146,6 +147,7 @@ pub(crate) struct ThreadSessionState { pub(crate) approval_policy: AskForApproval, pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, pub(crate) sandbox_policy: SandboxPolicy, + pub(crate) permission_profile: PermissionProfile, pub(crate) cwd: AbsolutePathBuf, pub(crate) instruction_source_paths: Vec, pub(crate) reasoning_effort: Option, @@ -525,6 +527,14 @@ impl AppServerSession { output_schema: Option, ) -> Result { let request_id = self.next_request_id(); + let (sandbox_policy, permission_profile) = match sandbox_policy { + policy @ SandboxPolicy::ExternalSandbox { .. } => (Some(policy.into()), None), + policy => { + let permission_profile = + PermissionProfile::from_legacy_sandbox_policy(&policy, &cwd); + (None, Some(permission_profile.into())) + } + }; self.client .request_typed(ClientRequest::TurnStart { request_id, @@ -535,8 +545,8 @@ impl AppServerSession { cwd: Some(cwd), approval_policy: Some(approval_policy.into()), approvals_reviewer: Some(approvals_reviewer.into()), - sandbox_policy: Some(sandbox_policy.into()), - permission_profile: None, + sandbox_policy, + permission_profile, model: Some(model), service_tier, effort, @@ -1004,19 +1014,38 @@ fn sandbox_mode_from_policy( } } +fn permission_profile_override_from_config( + config: &Config, +) -> Option { + if matches!( + config.permissions.sandbox_policy.get(), + SandboxPolicy::ExternalSandbox { .. } + ) { + None + } else { + Some(config.permissions.permission_profile().into()) + } +} + fn thread_start_params_from_config( config: &Config, thread_params_mode: ThreadParamsMode, remote_cwd_override: Option<&std::path::Path>, session_start_source: Option, ) -> ThreadStartParams { + let permission_profile = permission_profile_override_from_config(config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .flatten(); ThreadStartParams { model: config.model.clone(), model_provider: thread_params_mode.model_provider_from_config(config), cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()), + sandbox, + permission_profile, config: config_request_overrides_from_config(config), ephemeral: Some(config.ephemeral), session_start_source, @@ -1031,6 +1060,11 @@ fn thread_resume_params_from_config( thread_params_mode: ThreadParamsMode, remote_cwd_override: Option<&std::path::Path>, ) -> ThreadResumeParams { + let permission_profile = permission_profile_override_from_config(&config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .flatten(); ThreadResumeParams { thread_id: thread_id.to_string(), model: config.model.clone(), @@ -1038,7 +1072,8 @@ fn thread_resume_params_from_config( cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(&config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()), + sandbox, + permission_profile, config: config_request_overrides_from_config(&config), persist_extended_history: true, ..ThreadResumeParams::default() @@ -1051,6 +1086,11 @@ fn thread_fork_params_from_config( thread_params_mode: ThreadParamsMode, remote_cwd_override: Option<&std::path::Path>, ) -> ThreadForkParams { + let permission_profile = permission_profile_override_from_config(&config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .flatten(); ThreadForkParams { thread_id: thread_id.to_string(), model: config.model.clone(), @@ -1058,7 +1098,8 @@ fn thread_fork_params_from_config( cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(&config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()), + sandbox, + permission_profile, config: config_request_overrides_from_config(&config), base_instructions: config.base_instructions.clone(), developer_instructions: config.developer_instructions.clone(), @@ -1135,6 +1176,7 @@ async fn thread_session_state_from_thread_start_response( response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response.sandbox.to_core(), + response.permission_profile.clone().into(), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1158,6 +1200,7 @@ async fn thread_session_state_from_thread_resume_response( response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response.sandbox.to_core(), + response.permission_profile.clone().into(), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1181,6 +1224,7 @@ async fn thread_session_state_from_thread_fork_response( response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response.sandbox.to_core(), + response.permission_profile.clone().into(), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1223,6 +1267,7 @@ async fn thread_session_state_from_thread_response( approval_policy: AskForApproval, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, cwd: AbsolutePathBuf, instruction_source_paths: Vec, reasoning_effort: Option, @@ -1249,6 +1294,7 @@ async fn thread_session_state_from_thread_response( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, cwd, instruction_source_paths, reasoning_effort, @@ -1341,6 +1387,11 @@ mod tests { ); assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string())); + assert_eq!(params.sandbox, None); + assert_eq!( + params.permission_profile, + Some(config.permissions.permission_profile().into()) + ); assert_eq!(params.model_provider, Some(config.model_provider_id)); } @@ -1521,6 +1572,10 @@ mod tests { started.session.instruction_source_paths, response.instruction_sources ); + assert_eq!( + started.session.permission_profile, + response.permission_profile.clone().into() + ); assert_eq!(started.turns.len(), 1); assert_eq!(started.turns[0], response.thread.turns[0]); } @@ -1549,6 +1604,10 @@ mod tests { AskForApproval::Never, codex_protocol::config_types::ApprovalsReviewer::User, SandboxPolicy::new_read_only_policy(), + PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + std::path::Path::new("/tmp/project"), + ), test_path_buf("/tmp/project").abs(), Vec::new(), /*reasoning_effort*/ None, @@ -1579,6 +1638,10 @@ mod tests { AskForApproval::Never, codex_protocol::config_types::ApprovalsReviewer::User, SandboxPolicy::new_read_only_policy(), + PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + std::path::Path::new("/tmp/project"), + ), test_path_buf("/tmp/project").abs(), Vec::new(), /*reasoning_effort*/ None, From 299778060d4db207ea4203e77e17726b2a710799 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 09/16] rollout: persist turn permission profiles --- .../core/src/context_manager/history_tests.rs | 1 + codex-rs/core/src/context_manager/updates.rs | 2 +- .../session/rollout_reconstruction_tests.rs | 8 +++++++ codex-rs/core/src/session/tests.rs | 9 ++++++++ codex-rs/core/src/session/turn_context.rs | 1 + codex-rs/core/tests/suite/resume_warning.rs | 1 + codex-rs/protocol/src/protocol.rs | 21 +++++++++++++++++++ codex-rs/rollout/src/recorder_tests.rs | 1 + codex-rs/state/src/extract.rs | 3 +++ codex-rs/tui/src/app/tests.rs | 1 + codex-rs/tui/src/lib.rs | 1 + 11 files changed, 48 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 1df14ca8bc..bd8e77fd24 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -133,6 +133,7 @@ fn reference_context_item() -> TurnContextItem { timezone: Some("America/Los_Angeles".to_string()), approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: "gpt-test".to_string(), diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 5ced481530..374cbfabc2 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -42,7 +42,7 @@ fn build_permissions_update_item( } let prev = previous?; - if prev.sandbox_policy == *next.sandbox_policy.get() + if prev.permission_profile() == next.permission_profile() && prev.approval_policy == next.approval_policy.value() { return None; diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index 3dca7f0ce6..43ea0a8e85 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -68,6 +68,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), @@ -108,6 +109,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), @@ -903,6 +905,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), @@ -979,6 +982,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), @@ -1009,6 +1013,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), @@ -1122,6 +1127,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: current_model.to_string(), @@ -1233,6 +1239,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), @@ -1383,6 +1390,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 07f390ef99..2aa781c302 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1736,6 +1736,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), @@ -5226,6 +5227,10 @@ async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() let item = turn_context.to_turn_context_item(); assert_eq!(item.file_system_sandbox_policy, None); + assert_eq!( + item.permission_profile, + Some(turn_context.permission_profile()) + ); } #[tokio::test] @@ -5240,6 +5245,10 @@ async fn turn_context_item_stores_split_file_system_sandbox_policy_when_differen item.file_system_sandbox_policy, Some(file_system_sandbox_policy) ); + assert_eq!( + item.permission_profile, + Some(turn_context.permission_profile()) + ); } #[tokio::test] diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index dbbe955211..cbb8d82056 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -260,6 +260,7 @@ impl TurnContext { timezone: self.timezone.clone(), approval_policy: self.approval_policy.value(), sandbox_policy: self.sandbox_policy.get().clone(), + permission_profile: Some(self.permission_profile()), network: self.turn_context_network_item(), file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(), model: self.model_info.slug.clone(), diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index 8ea2be3147..0ccef90231 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -33,6 +33,7 @@ fn resume_history( timezone: None, approval_policy: config.permissions.approval_policy.value(), sandbox_policy: config.permissions.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 8c542f4df8..c87c0cca5d 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2921,6 +2921,8 @@ pub struct TurnContextItem { pub timezone: Option, pub approval_policy: AskForApproval, pub sandbox_policy: SandboxPolicy, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub permission_profile: Option, #[serde(skip_serializing_if = "Option::is_none")] pub network: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -2945,6 +2947,24 @@ pub struct TurnContextItem { pub truncation_policy: Option, } +impl TurnContextItem { + pub fn permission_profile(&self) -> PermissionProfile { + self.permission_profile.clone().unwrap_or_else(|| { + let file_system_sandbox_policy = + self.file_system_sandbox_policy.clone().unwrap_or_else(|| { + FileSystemSandboxPolicy::from_legacy_sandbox_policy( + &self.sandbox_policy, + &self.cwd, + ) + }); + PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + NetworkSandboxPolicy::from(&self.sandbox_policy), + ) + }) + } +} + #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] #[serde(tag = "mode", content = "limit", rename_all = "snake_case")] pub enum TruncationPolicy { @@ -5077,6 +5097,7 @@ mod tests { timezone: None, approval_policy: AskForApproval::Never, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, network: Some(TurnContextNetworkItem { allowed_domains: vec!["api.example.com".to_string()], denied_domains: vec!["blocked.example.com".to_string()], diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index fa789d3a7e..ebad0ebb39 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -547,6 +547,7 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re timezone: None, approval_policy: AskForApproval::Never, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: "test-model".to_string(), diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index 64d7f5bbb7..4d1c3afe31 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -303,6 +303,7 @@ mod tests { timezone: None, approval_policy: AskForApproval::Never, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, network: None, file_system_sandbox_policy: None, model: "gpt-5".to_string(), @@ -342,6 +343,7 @@ mod tests { timezone: None, approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: "gpt-5".to_string(), @@ -375,6 +377,7 @@ mod tests { timezone: None, approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: "gpt-5".to_string(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 40262efe40..f9d9d32a7c 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2857,6 +2857,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re timezone: None, approval_policy: primary_session.approval_policy, sandbox_policy: primary_session.sandbox_policy.clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model: "gpt-agent".to_string(), diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 7c60b2e38a..2cbf32074e 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -2175,6 +2175,7 @@ mod tests { timezone: None, approval_policy: config.permissions.approval_policy.value(), sandbox_policy: config.permissions.sandbox_policy.get().clone(), + permission_profile: None, network: None, file_system_sandbox_policy: None, model, From eb2cbf577fd17fd8071d14fd5516e693d11e9ab8 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 10/16] protocol: report session permission profiles --- codex-rs/core/src/session/session.rs | 1 + codex-rs/exec/src/lib.rs | 4 ++++ codex-rs/exec/tests/event_processor_with_json_output.rs | 1 + codex-rs/mcp-server/src/outgoing_message.rs | 3 +++ codex-rs/protocol/src/protocol.rs | 5 +++++ codex-rs/tui/src/app/tests.rs | 7 +++++++ codex-rs/tui/src/chatwidget.rs | 1 + codex-rs/tui/src/chatwidget/tests/composer_submission.rs | 7 +++++++ codex-rs/tui/src/chatwidget/tests/exec_flow.rs | 1 + codex-rs/tui/src/chatwidget/tests/history_replay.rs | 8 ++++++++ codex-rs/tui/src/chatwidget/tests/permissions.rs | 2 ++ codex-rs/tui/src/chatwidget/tests/plan_mode.rs | 2 ++ codex-rs/tui/src/history_cell.rs | 1 + 13 files changed, 43 insertions(+) diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 81f3cd3377..6fdc5d883e 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -766,6 +766,7 @@ impl Session { approval_policy: session_configuration.approval_policy.value(), approvals_reviewer: session_configuration.approvals_reviewer, sandbox_policy: session_configuration.sandbox_policy.get().clone(), + permission_profile: Some(session_configuration.permission_profile()), cwd: session_configuration.cwd.clone(), reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(), history_log_id, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 68f8567372..508f166fba 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -1031,6 +1031,7 @@ fn session_configured_from_thread_start_response( response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response.sandbox.to_core(), + response.permission_profile.clone().into(), response.cwd.clone(), response.reasoning_effort, ) @@ -1049,6 +1050,7 @@ fn session_configured_from_thread_resume_response( response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response.sandbox.to_core(), + response.permission_profile.clone().into(), response.cwd.clone(), response.reasoning_effort, ) @@ -1077,6 +1079,7 @@ fn session_configured_from_thread_response( approval_policy: AskForApproval, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, cwd: AbsolutePathBuf, reasoning_effort: Option, ) -> Result { @@ -1093,6 +1096,7 @@ fn session_configured_from_thread_response( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile: Some(permission_profile), cwd, reasoning_effort, history_log_id: 0, diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 02e52988cb..e894b8f4eb 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -115,6 +115,7 @@ fn session_configured_produces_thread_started_event() { approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/tmp/project").abs(), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index e94de16587..67ceb91dd8 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -305,6 +305,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, @@ -349,6 +350,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, @@ -418,6 +420,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index c87c0cca5d..5425366d7a 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3594,6 +3594,10 @@ pub struct SessionConfiguredEvent { /// How to sandbox commands executed in the system pub sandbox_policy: SandboxPolicy, + /// Effective permissions for commands executed in the session. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub permission_profile: Option, + /// Working directory that should be treated as the *root* of the /// session. pub cwd: AbsolutePathBuf, @@ -5164,6 +5168,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index f9d9d32a7c..bd75f00ae5 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3704,6 +3704,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/tmp/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::High), history_log_id: 0, @@ -4580,6 +4581,7 @@ async fn refresh_in_memory_config_from_disk_uses_active_chat_widget_cwd() -> Res approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: next_cwd.clone().abs(), reasoning_effort: None, history_log_id: 0, @@ -4696,6 +4698,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -4759,6 +4762,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -4852,6 +4856,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -5235,6 +5240,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -5347,6 +5353,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/tmp/project").abs(), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9c54d322e1..c15f20879a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1394,6 +1394,7 @@ fn thread_session_state_to_legacy_event( approval_policy: session.approval_policy, approvals_reviewer: session.approvals_reviewer, sandbox_policy: session.sandbox_policy, + permission_profile: Some(session.permission_profile), cwd: session.cwd, reasoning_effort: session.reasoning_effort, history_log_id: session.history_log_id, diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index 2f8c6a9b2b..d35c7e9ded 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -17,6 +17,7 @@ async fn submission_preserves_text_elements_and_local_images() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -101,6 +102,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -196,6 +198,7 @@ async fn enter_with_only_remote_images_submits_user_turn() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -261,6 +264,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -301,6 +305,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -341,6 +346,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -384,6 +390,7 @@ async fn submission_prefers_selected_duplicate_skill_path() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 9f181d3d21..b933aef6b2 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -1001,6 +1001,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 084743f98d..3e2f5b4d74 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -17,6 +17,7 @@ async fn resumed_initial_messages_render_history() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -130,6 +131,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -191,6 +193,7 @@ async fn replayed_user_message_preserves_remote_image_urls() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -259,6 +262,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: expected_sandbox.clone(), + permission_profile: None, cwd: expected_cwd.clone(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -302,6 +306,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -355,6 +360,7 @@ async fn replayed_user_message_with_only_local_images_does_not_render_history_ce approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -676,6 +682,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_project_path().abs(), reasoning_effort: None, history_log_id: 0, @@ -723,6 +730,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_project_path().abs(), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 92ca170605..6b7dc2ed8e 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -463,6 +463,7 @@ async fn permissions_selection_marks_guardian_approvals_current_after_session_co approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::GuardianSubagent, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: None, cwd: test_project_path().abs(), reasoning_effort: None, history_log_id: 0, @@ -517,6 +518,7 @@ async fn permissions_selection_marks_guardian_approvals_current_with_custom_work exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, + permission_profile: None, cwd: test_project_path().abs(), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index 8f3de7897f..c4410641ab 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -1010,6 +1010,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -1255,6 +1256,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index a09b313e26..33a05776b9 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -3073,6 +3073,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: test_path_buf("/tmp/project").abs(), reasoning_effort: None, history_log_id: 0, From 4937fa7d2666e8d1ec5afecf4817e048ca98c9e1 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 11/16] app-server: accept command permission profiles --- .../schema/json/ClientRequest.json | 13 +- .../codex_app_server_protocol.schemas.json | 13 +- .../codex_app_server_protocol.v2.schemas.json | 13 +- .../schema/json/v2/CommandExecParams.json | 282 +++++++++++++++++- .../schema/typescript/v2/CommandExecParams.ts | 13 +- .../src/protocol/mappers.rs | 1 + .../app-server-protocol/src/protocol/v2.rs | 18 +- codex-rs/app-server/README.md | 12 +- .../app-server/src/codex_message_processor.rs | 61 +++- .../app-server/tests/suite/v2/command_exec.rs | 122 ++++++++ 10 files changed, 532 insertions(+), 16 deletions(-) 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 8804c22635..7ee9a7d65e 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 63e7eabdc4..2c3cd1b8e9 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 e398a35a9c..4bcfd43dd5 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2839,9 +2839,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`. @@ -7625,6 +7632,7 @@ mod tests { env: None, size: None, sandbox_policy: None, + permission_profile: None, } ); } @@ -7645,6 +7653,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"); @@ -7659,6 +7668,7 @@ mod tests { "env": null, "size": null, "sandboxPolicy": null, + "permissionProfile": null, "outputBytesCap": null, }) ); @@ -7684,6 +7694,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"); @@ -7700,6 +7711,7 @@ mod tests { "env": null, "size": null, "sandboxPolicy": null, + "permissionProfile": null, }) ); @@ -7728,6 +7740,7 @@ mod tests { ])), size: None, sandbox_policy: None, + permission_profile: None, }; let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); @@ -7746,6 +7759,7 @@ mod tests { }, "size": null, "sandboxPolicy": null, + "permissionProfile": null, }) ); @@ -7815,6 +7829,7 @@ mod tests { cols: 120, }), sandbox_policy: None, + permission_profile: None, }; let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); @@ -7833,6 +7848,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 e5d2315a14..90162745d6 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 9c5a6381d9..55c5b2d974 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2121,7 +2121,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 { @@ -2253,13 +2262,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); @@ -2276,12 +2324,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, From 6d8ce0b43025c1b207eb8d65c57b9bcbf8df5adf Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 12/16] tui: sync session permission profiles --- codex-rs/tui/src/chatwidget.rs | 9 +++++++ .../src/chatwidget/tests/history_replay.rs | 26 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c15f20879a..16bc0ba92f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -138,6 +138,7 @@ use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; use codex_protocol::items::UserMessageItem; use codex_protocol::models::MessagePhase; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::local_image_label_text; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg; @@ -2098,6 +2099,12 @@ impl ChatWidget { display: SessionConfiguredDisplay, fork_parent_title: Option, ) { + let permission_profile = event.permission_profile.clone().unwrap_or_else(|| { + PermissionProfile::from_legacy_sandbox_policy(&event.sandbox_policy, &event.cwd) + }); + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + self.last_agent_markdown = None; self.agent_turn_markdowns.clear(); self.visible_user_turn_count = 0; @@ -2134,6 +2141,8 @@ impl ChatWidget { self.config.permissions.sandbox_policy = Constrained::allow_only(event.sandbox_policy.clone()); } + self.config.permissions.file_system_sandbox_policy = file_system_sandbox_policy; + self.config.permissions.network_sandbox_policy = network_sandbox_policy; self.config.approvals_reviewer = event.approvals_reviewer; self.status_line_project_root_name_cache = None; let forked_from_id = event.forked_from_id; diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 3e2f5b4d74..583d35c201 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -252,6 +252,26 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { let expected_sandbox = SandboxPolicy::new_read_only_policy(); let expected_cwd = test_path_buf("/home/user/sub-agent").abs(); + let expected_file_system_policy = + codex_protocol::permissions::FileSystemSandboxPolicy::restricted(vec![ + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::Root, + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::GlobPattern { + pattern: "**/.secret".to_string(), + }, + access: codex_protocol::permissions::FileSystemAccessMode::None, + }, + ]); + let expected_permission_profile = + codex_protocol::models::PermissionProfile::from_runtime_permissions( + &expected_file_system_policy, + codex_protocol::permissions::NetworkSandboxPolicy::Restricted, + ); let configured = codex_protocol::protocol::SessionConfiguredEvent { session_id: ThreadId::new(), forked_from_id: None, @@ -262,7 +282,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: expected_sandbox.clone(), - permission_profile: None, + permission_profile: Some(expected_permission_profile.clone()), cwd: expected_cwd.clone(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -285,6 +305,10 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { chat.config_ref().permissions.sandbox_policy.get(), &expected_sandbox ); + assert_eq!( + chat.config_ref().permissions.permission_profile(), + expected_permission_profile + ); assert_eq!(&chat.config_ref().cwd, &expected_cwd); } From 2f70f509b5fc6ee9b6a9372bf5f765d0d2c109fc Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 13/16] tui: carry permission profiles on user turns --- codex-rs/core/src/guardian/review_session.rs | 1 + codex-rs/core/src/session/handlers.rs | 3 +- codex-rs/core/src/session/tests.rs | 1 + codex-rs/core/tests/common/test_codex.rs | 1 + codex-rs/core/tests/suite/apply_patch_cli.rs | 8 +++ codex-rs/core/tests/suite/approvals.rs | 1 + codex-rs/core/tests/suite/client.rs | 2 + codex-rs/core/tests/suite/code_mode.rs | 1 + .../tests/suite/collaboration_instructions.rs | 2 + codex-rs/core/tests/suite/compact.rs | 7 ++ codex-rs/core/tests/suite/exec_policy.rs | 2 + codex-rs/core/tests/suite/image_rollout.rs | 2 + codex-rs/core/tests/suite/items.rs | 5 ++ codex-rs/core/tests/suite/json_result.rs | 1 + codex-rs/core/tests/suite/live_reload.rs | 1 + codex-rs/core/tests/suite/model_switching.rs | 14 ++++ .../core/tests/suite/model_visible_layout.rs | 5 ++ codex-rs/core/tests/suite/models_cache_ttl.rs | 1 + .../core/tests/suite/models_etag_responses.rs | 1 + codex-rs/core/tests/suite/pending_input.rs | 1 + codex-rs/core/tests/suite/personality.rs | 13 ++++ codex-rs/core/tests/suite/prompt_caching.rs | 5 ++ codex-rs/core/tests/suite/remote_models.rs | 7 ++ .../core/tests/suite/request_permissions.rs | 1 + .../tests/suite/request_permissions_tool.rs | 1 + .../core/tests/suite/request_user_input.rs | 2 + .../suite/responses_api_proxy_headers.rs | 1 + codex-rs/core/tests/suite/rmcp_client.rs | 9 +++ .../tests/suite/safety_check_downgrade.rs | 4 ++ codex-rs/core/tests/suite/shell_snapshot.rs | 4 ++ codex-rs/core/tests/suite/skill_approval.rs | 1 + codex-rs/core/tests/suite/skills.rs | 1 + codex-rs/core/tests/suite/sqlite_state.rs | 1 + codex-rs/core/tests/suite/tool_harness.rs | 5 ++ codex-rs/core/tests/suite/tool_parallelism.rs | 2 + codex-rs/core/tests/suite/truncation.rs | 1 + codex-rs/core/tests/suite/unified_exec.rs | 7 ++ codex-rs/core/tests/suite/user_shell_cmd.rs | 1 + codex-rs/core/tests/suite/view_image.rs | 14 ++++ .../core/tests/suite/websocket_fallback.rs | 1 + codex-rs/protocol/src/protocol.rs | 7 ++ codex-rs/tui/src/app/thread_routing.rs | 2 + codex-rs/tui/src/app_command.rs | 6 ++ codex-rs/tui/src/app_server_session.rs | 20 ++++-- codex-rs/tui/src/chatwidget.rs | 9 +++ .../chatwidget/tests/composer_submission.rs | 69 +++++++++++++++++++ 46 files changed, 246 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 5bb3e9d1b8..384949dd69 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -593,6 +593,7 @@ async fn run_review_on_session( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: params.model.clone(), effort: params.reasoning_effort, summary: Some(params.reasoning_summary), diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 6ffb5d6550..8d0d31f293 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -131,6 +131,7 @@ pub(super) async fn user_input_or_turn_inner( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, model, effort, summary, @@ -157,7 +158,7 @@ pub(super) async fn user_input_or_turn_inner( approval_policy: Some(approval_policy), approvals_reviewer, sandbox_policy: Some(sandbox_policy), - permission_profile: None, + permission_profile, windows_sandbox_level: None, collaboration_mode, reasoning_summary: summary, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 2aa781c302..f5a571d4e8 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3753,6 +3753,7 @@ async fn user_turn_updates_approvals_reviewer() { approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::GuardianSubagent), sandbox_policy: config.permissions.sandbox_policy.get().clone(), + permission_profile: None, model: turn_context.model_info.slug.clone(), effort: config.model_reasoning_effort, summary: config.model_reasoning_summary, diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 5075f91620..b0be472099 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -625,6 +625,7 @@ impl TestCodex { approval_policy, approvals_reviewer: None, sandbox_policy, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index 45c97d3704..4872bef88f 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -366,6 +366,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, @@ -1003,6 +1004,7 @@ async fn apply_patch_custom_tool_streaming_emits_updated_changes() -> Result<()> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: test.session_configured.model.clone(), effort: None, summary: None, @@ -1100,6 +1102,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, @@ -1184,6 +1187,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, @@ -1339,6 +1343,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, @@ -1406,6 +1411,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, @@ -1482,6 +1488,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, @@ -1558,6 +1565,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index f189d5db6c..c393c539a6 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -593,6 +593,7 @@ async fn submit_turn( approval_policy, approvals_reviewer: None, sandbox_policy, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 2086367b21..4433dc9cde 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1646,6 +1646,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: config.permissions.sandbox_policy.get().clone(), + permission_profile: None, model: session_configured.model.clone(), effort: Some(ReasoningEffort::Low), summary: Some( @@ -1765,6 +1766,7 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default() approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: config.permissions.sandbox_policy.get().clone(), + permission_profile: None, model: session_configured.model, effort: None, summary: Some(ReasoningSummary::Concise), diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 40a13467e7..55cc2bba12 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -2617,6 +2617,7 @@ text( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 28685895ac..69a8b587fc 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -183,6 +183,7 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> { approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: test.config.permissions.sandbox_policy.get().clone(), + permission_profile: None, model: test.session_configured.model.clone(), effort: None, summary: Some( @@ -302,6 +303,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: test.config.permissions.sandbox_policy.get().clone(), + permission_profile: None, model: test.session_configured.model.clone(), effort: None, summary: Some( diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 7a4dc525d9..42a7ab9da0 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1674,6 +1674,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: resumed.session_configured.model.clone(), effort: None, summary: None, @@ -1765,6 +1766,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: previous_model.to_string(), effort: None, summary: None, @@ -1790,6 +1792,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: next_model.to_string(), effort: None, summary: None, @@ -1901,6 +1904,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: previous_model.to_string(), effort: None, summary: None, @@ -1950,6 +1954,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: next_model.to_string(), effort: None, summary: None, @@ -3172,6 +3177,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: previous_model.to_string(), effort: None, summary: None, @@ -3197,6 +3203,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: next_model.to_string(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index 3c80fc80d0..d2d3339587 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -53,6 +53,7 @@ async fn submit_user_turn( approval_policy, approvals_reviewer: None, sandbox_policy, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -134,6 +135,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index a7ec8318f1..0376fa600f 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -125,6 +125,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -212,6 +213,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index 25eb21df9e..93e88cbc0f 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -532,6 +532,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result< approval_policy: codex_protocol::protocol::AskForApproval::Never, approvals_reviewer: None, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_configured.model.clone(), effort: None, summary: None, @@ -609,6 +610,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> { approval_policy: codex_protocol::protocol::AskForApproval::Never, approvals_reviewer: None, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_configured.model.clone(), effort: None, summary: None, @@ -718,6 +720,7 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done approval_policy: codex_protocol::protocol::AskForApproval::Never, approvals_reviewer: None, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_configured.model.clone(), effort: None, summary: None, @@ -905,6 +908,7 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p approval_policy: codex_protocol::protocol::AskForApproval::Never, approvals_reviewer: None, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_configured.model.clone(), effort: None, summary: None, @@ -1019,6 +1023,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> { approval_policy: codex_protocol::protocol::AskForApproval::Never, approvals_reviewer: None, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index 755b269472..d09f5ccd24 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -82,6 +82,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/live_reload.rs b/codex-rs/core/tests/suite/live_reload.rs index 6ab001383f..66eee3f795 100644 --- a/codex-rs/core/tests/suite/live_reload.rs +++ b/codex-rs/core/tests/suite/live_reload.rs @@ -63,6 +63,7 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 5bea4de2a9..a883c7f815 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -130,6 +130,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -168,6 +169,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: next_model.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -228,6 +230,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -266,6 +269,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: next_model.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -408,6 +412,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -429,6 +434,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: text_model_slug.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -537,6 +543,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, service_tier: None, @@ -558,6 +565,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, service_tier: None, @@ -669,6 +677,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, service_tier: None, @@ -690,6 +699,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: text_model_slug.to_string(), effort: test.config.model_reasoning_effort, service_tier: None, @@ -803,6 +813,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, service_tier: None, @@ -832,6 +843,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, service_tier: None, @@ -989,6 +1001,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: large_model_slug.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -1049,6 +1062,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: smaller_model_slug.to_string(), effort: test.config.model_reasoning_effort, summary: None, diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index bee3f3245b..84cfaf023a 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -123,6 +123,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -147,6 +148,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { approval_policy: AskForApproval::OnRequest, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -226,6 +228,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -250,6 +253,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -361,6 +365,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: resumed.session_configured.model.clone(), effort: resumed.config.model_reasoning_effort, summary: None, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 2ffce57421..c723055dce 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -99,6 +99,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { approval_policy: codex_protocol::protocol::AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index 34aaf86dee..dabc9af5a9 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -110,6 +110,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 777103534b..a71b6732a6 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -118,6 +118,7 @@ async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 7e729ef493..26c91fccf6 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -105,6 +105,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -156,6 +157,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -214,6 +216,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -278,6 +281,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -330,6 +334,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -369,6 +374,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -436,6 +442,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -475,6 +482,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -555,6 +563,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -594,6 +603,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -713,6 +723,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -834,6 +845,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -873,6 +885,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, summary: None, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 80dec74c3b..8d097d0a51 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -717,6 +717,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: new_policy.clone(), + permission_profile: None, model: "o3".to_string(), effort: Some(ReasoningEffort::High), summary: Some(ReasoningSummary::Detailed), @@ -830,6 +831,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a approval_policy: default_approval_policy, approvals_reviewer: None, sandbox_policy: default_sandbox_policy.clone(), + permission_profile: None, model: default_model.clone(), effort: default_effort, summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), @@ -851,6 +853,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a approval_policy: default_approval_policy, approvals_reviewer: None, sandbox_policy: default_sandbox_policy.clone(), + permission_profile: None, model: default_model.clone(), effort: default_effort, summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), @@ -956,6 +959,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu approval_policy: default_approval_policy, approvals_reviewer: None, sandbox_policy: default_sandbox_policy.clone(), + permission_profile: None, model: default_model, effort: default_effort, summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), @@ -977,6 +981,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: "o3".to_string(), effort: Some(ReasoningEffort::High), summary: Some(ReasoningSummary::Detailed), diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 42b655aaa9..5818331192 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -171,6 +171,7 @@ async fn remote_models_config_context_window_override_clamps_to_max_context_wind summary: None, service_tier: None, collaboration_mode: None, + permission_profile: None, personality: None, }) .await?; @@ -247,6 +248,7 @@ async fn remote_models_config_override_above_max_uses_max_context_window() -> Re summary: None, service_tier: None, collaboration_mode: None, + permission_profile: None, personality: None, }) .await?; @@ -322,6 +324,7 @@ async fn remote_models_use_context_window_when_config_override_is_absent() -> Re summary: None, service_tier: None, collaboration_mode: None, + permission_profile: None, personality: None, }) .await?; @@ -405,6 +408,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: config.permissions.sandbox_policy.get().clone(), + permission_profile: None, model: requested_model.to_string(), effort: None, summary: None, @@ -464,6 +468,7 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning() approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, sandbox_policy: config.permissions.sandbox_policy.get().clone(), + permission_profile: None, model: requested_model.to_string(), effort: None, summary: Some( @@ -631,6 +636,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: REMOTE_MODEL_SLUG.to_string(), effort: None, summary: Some(ReasoningSummary::Auto), @@ -857,6 +863,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: model.to_string(), effort: None, summary: Some(ReasoningSummary::Auto), diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index 614cd10b9f..96394c4b06 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -196,6 +196,7 @@ async fn submit_turn( approval_policy, approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/request_permissions_tool.rs b/codex-rs/core/tests/suite/request_permissions_tool.rs index 0578441e99..d8951f0fdc 100644 --- a/codex-rs/core/tests/suite/request_permissions_tool.rs +++ b/codex-rs/core/tests/suite/request_permissions_tool.rs @@ -147,6 +147,7 @@ async fn submit_turn( approval_policy, approvals_reviewer: None, sandbox_policy, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 8e30b37c21..7b6a0680a7 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -140,6 +140,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -258,6 +259,7 @@ where approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs index 67f6e86ab0..b72e191850 100644 --- a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs +++ b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs @@ -143,6 +143,7 @@ async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 7875f09810..faa4dcb7cb 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -423,6 +423,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, @@ -791,6 +792,7 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow:: approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, @@ -923,6 +925,7 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1022,6 +1025,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1173,6 +1177,7 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1410,6 +1415,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: text_only_model_slug.to_string(), effort: None, summary: None, @@ -1518,6 +1524,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1887,6 +1894,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, @@ -2103,6 +2111,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index 51a88ef16a..3c6ca51938 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -47,6 +47,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: REQUESTED_MODEL.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -146,6 +147,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: REQUESTED_MODEL.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -232,6 +234,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: REQUESTED_MODEL.to_string(), effort: test.config.model_reasoning_effort, summary: None, @@ -282,6 +285,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: REQUESTED_MODEL.to_string(), effort: test.config.model_reasoning_effort, summary: None, diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index 516bc049f5..a6399c3c00 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -166,6 +166,7 @@ async fn run_snapshot_command_with_options( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -257,6 +258,7 @@ async fn run_shell_command_snapshot_with_options( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -328,6 +330,7 @@ async fn run_tool_turn_on_harness( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -563,6 +566,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index d77d736f94..4aeb9dfdc1 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -53,6 +53,7 @@ async fn submit_turn_with_policies( approval_policy, approvals_reviewer: None, sandbox_policy, + permission_profile: None, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 1b2ac71643..90919bf01e 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -113,6 +113,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index e7125210b2..06e8452393 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -406,6 +406,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result< approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index 67a7275693..bc6a27bb77 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -87,6 +87,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -158,6 +159,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -239,6 +241,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -335,6 +338,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -439,6 +443,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index 2628136a15..80011f0438 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -44,6 +44,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -361,6 +362,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index c12dd58191..9c6d7cb5f2 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -509,6 +509,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index be6fa73a40..87ba3474a6 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -175,6 +175,7 @@ async fn submit_unified_exec_turn( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -259,6 +260,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1749,6 +1751,7 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1842,6 +1845,7 @@ async fn unified_exec_interrupt_preserves_long_running_session() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -2315,6 +2319,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { approvals_reviewer: None, // Important! sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, @@ -2423,6 +2428,7 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: read_only_policy, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -2551,6 +2557,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 807c746905..9aaee17e38 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -179,6 +179,7 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: fixture.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 9f53e60d72..a867a3dfb5 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -168,6 +168,7 @@ async fn assert_user_turn_local_image_resizes_to( approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -288,6 +289,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -419,6 +421,7 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5 approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, service_tier: None, @@ -518,6 +521,7 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, service_tier: None, @@ -608,6 +612,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, service_tier: None, @@ -708,6 +713,7 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, service_tier: None, @@ -812,6 +818,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, service_tier: None, @@ -914,6 +921,7 @@ await codex.emitImage(out); approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1034,6 +1042,7 @@ console.log(out.type); approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, service_tier: None, @@ -1127,6 +1136,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1208,6 +1218,7 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1294,6 +1305,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> { approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, @@ -1431,6 +1443,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: model_slug.to_string(), effort: None, summary: None, @@ -1511,6 +1524,7 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()> approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index c55e72ec64..64ff5e125a 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -159,6 +159,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: None, model: session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 5425366d7a..7594ffb1e8 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -455,6 +455,13 @@ pub enum Op { /// Policy to use for tool calls such as `local_shell`. sandbox_policy: SandboxPolicy, + /// Full permissions profile to use for tool calls such as `local_shell`. + /// + /// When omitted, `sandbox_policy` is used as a legacy compatibility + /// projection. + #[serde(default, skip_serializing_if = "Option::is_none")] + permission_profile: Option, + /// Must be a valid model slug for the configured client session /// associated with this conversation. model: String, diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 11f7638f9d..7b0bb80a3b 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -497,6 +497,7 @@ impl App { approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, model, effort, summary, @@ -580,6 +581,7 @@ impl App { approvals_reviewer .unwrap_or(self.chat_widget.config_ref().approvals_reviewer), sandbox_policy.clone(), + permission_profile.clone(), model.to_string(), effort, *summary, diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index c34df5b765..2860400712 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -8,6 +8,7 @@ use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::mcp::RequestId as McpRequestId; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ConversationAudioParams; @@ -44,6 +45,7 @@ pub(crate) enum AppCommandView<'a> { approval_policy: AskForApproval, approvals_reviewer: &'a Option, sandbox_policy: &'a SandboxPolicy, + permission_profile: &'a Option, model: &'a str, effort: Option, summary: &'a Option, @@ -140,6 +142,7 @@ impl AppCommand { cwd: PathBuf, approval_policy: AskForApproval, sandbox_policy: SandboxPolicy, + permission_profile: Option, model: String, effort: Option, summary: Option, @@ -154,6 +157,7 @@ impl AppCommand { approval_policy, approvals_reviewer: None, sandbox_policy, + permission_profile, model, effort, summary, @@ -290,6 +294,7 @@ impl AppCommand { approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, model, effort, summary, @@ -303,6 +308,7 @@ impl AppCommand { approval_policy: *approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, model, effort: *effort, summary, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 80c1a7494a..3b73eb36a0 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -518,6 +518,7 @@ impl AppServerSession { approval_policy: AskForApproval, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, sandbox_policy: SandboxPolicy, + permission_profile: Option, model: String, effort: Option, summary: Option, @@ -527,14 +528,19 @@ impl AppServerSession { output_schema: Option, ) -> Result { let request_id = self.next_request_id(); - let (sandbox_policy, permission_profile) = match sandbox_policy { - policy @ SandboxPolicy::ExternalSandbox { .. } => (Some(policy.into()), None), - policy => { - let permission_profile = - PermissionProfile::from_legacy_sandbox_policy(&policy, &cwd); + let (sandbox_policy, permission_profile) = + if let Some(permission_profile) = permission_profile { (None, Some(permission_profile.into())) - } - }; + } else { + match sandbox_policy { + policy @ SandboxPolicy::ExternalSandbox { .. } => (Some(policy.into()), None), + policy => { + let permission_profile = + PermissionProfile::from_legacy_sandbox_policy(&policy, &cwd); + (None, Some(permission_profile.into())) + } + } + }; self.client .request_typed(ClientRequest::TurnStart { request_id, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 16bc0ba92f..977c8f69fb 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -5912,11 +5912,20 @@ impl ChatWidget { .filter(|_| self.config.features.enabled(Feature::Personality)) .filter(|_| self.current_model_supports_personality()); let service_tier = Some(self.config.service_tier); + let permission_profile = if matches!( + self.config.permissions.sandbox_policy.get(), + SandboxPolicy::ExternalSandbox { .. } + ) { + None + } else { + Some(self.config.permissions.permission_profile()) + }; let op = AppCommand::user_turn( items, self.config.cwd.to_path_buf(), self.config.permissions.approval_policy.value(), self.config.permissions.sandbox_policy.get().clone(), + permission_profile, effective_mode.model().to_string(), effective_mode.reasoning_effort(), /*summary*/ None, diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index d35c7e9ded..a23f137b40 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -86,6 +86,75 @@ async fn submission_preserves_text_elements_and_local_images() { assert!(stored_remote_image_urls.is_empty()); } +#[tokio::test] +async fn submission_includes_configured_permission_profile() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + let conversation_id = ThreadId::new(); + let rollout_file = NamedTempFile::new().unwrap(); + let expected_permission_profile = PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(false), + }), + file_system: Some(FileSystemPermissions { + entries: vec![ + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::Root, + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::GlobPattern { + pattern: "/home/user/project/secrets/**".to_string(), + }, + access: codex_protocol::permissions::FileSystemAccessMode::None, + }, + ], + glob_scan_max_depth: None, + }), + }; + let configured = codex_protocol::protocol::SessionConfiguredEvent { + session_id: conversation_id, + forked_from_id: None, + thread_name: None, + model: "test-model".to_string(), + model_provider_id: "test-provider".to_string(), + service_tier: None, + approval_policy: AskForApproval::Never, + approvals_reviewer: ApprovalsReviewer::User, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: Some(expected_permission_profile.clone()), + cwd: test_path_buf("/home/user/project").abs(), + reasoning_effort: Some(ReasoningEffortConfig::default()), + history_log_id: 0, + history_entry_count: 0, + initial_messages: None, + network_proxy: None, + rollout_path: Some(rollout_file.path().to_path_buf()), + }; + chat.handle_codex_event(Event { + id: "initial".into(), + msg: EventMsg::SessionConfigured(configured), + }); + drain_insert_history(&mut rx); + + chat.bottom_pane.set_composer_text( + "submit with configured permissions".to_string(), + Vec::new(), + Vec::new(), + ); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + let permission_profile = match next_submit_op(&mut op_rx) { + Op::UserTurn { + permission_profile, .. + } => permission_profile, + other => panic!("expected Op::UserTurn, got {other:?}"), + }; + assert_eq!(permission_profile, Some(expected_permission_profile)); +} + #[tokio::test] async fn submission_with_remote_and_local_images_keeps_local_placeholder_numbering() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; From 71636e4ea76db0e4e8bc94fd4afb743ad65b150f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 14/16] mcp: include permission profiles in sandbox state --- codex-rs/codex-mcp/src/mcp_connection_manager.rs | 3 +++ codex-rs/core/src/mcp_tool_call.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager.rs b/codex-rs/codex-mcp/src/mcp_connection_manager.rs index 10c48e040e..bcc7685a31 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager.rs @@ -42,6 +42,7 @@ use codex_protocol::approvals::ElicitationRequest; use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::mcp::CallToolResult; use codex_protocol::mcp::RequestId as ProtocolRequestId; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -646,6 +647,8 @@ pub const MCP_SANDBOX_STATE_META_CAPABILITY: &str = "codex/sandbox-state-meta"; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SandboxState { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub permission_profile: Option, pub sandbox_policy: SandboxPolicy, pub codex_linux_sandbox_exe: Option, pub sandbox_cwd: PathBuf, diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 32c7cf879e..50e114db9d 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -513,6 +513,7 @@ async fn augment_mcp_tool_request_meta_with_sandbox_state( } let sandbox_state = serde_json::to_value(SandboxState { + permission_profile: Some(turn_context.permission_profile()), sandbox_policy: turn_context.sandbox_policy.get().clone(), codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(), sandbox_cwd: turn_context.cwd.to_path_buf(), From 0f8b8aed8ee0365091852f2ace8029ebad14397d Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 15/16] shell-escalation: carry resolved permission profiles --- .../tools/runtimes/shell/unix_escalation.rs | 38 +++++++++++-------- .../runtimes/shell/unix_escalation_tests.rs | 12 +++--- codex-rs/protocol/src/approvals.rs | 17 +++++---- codex-rs/shell-escalation/src/lib.rs | 4 +- .../src/unix/escalate_server.rs | 16 ++++---- codex-rs/shell-escalation/src/unix/mod.rs | 2 +- 6 files changed, 52 insertions(+), 37 deletions(-) diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 9d3b7c5f27..00363c38c9 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -53,8 +53,8 @@ use codex_shell_escalation::EscalationPolicy; use codex_shell_escalation::EscalationSession; use codex_shell_escalation::ExecParams; use codex_shell_escalation::ExecResult; -use codex_shell_escalation::Permissions as EscalatedPermissions; use codex_shell_escalation::PreparedExec; +use codex_shell_escalation::ResolvedPermissionProfile; use codex_shell_escalation::ShellCommandExecutor; use codex_shell_escalation::Stopwatch; use codex_utils_absolute_path::AbsolutePathBuf; @@ -369,13 +369,17 @@ impl CoreShellActionProvider { .map(|_| { // Shell request additional permissions were already normalized and // merged into the first-attempt sandbox policy. - EscalationExecution::Permissions(EscalationPermissions::Permissions( - EscalatedPermissions { - sandbox_policy: sandbox_policy.clone(), - file_system_sandbox_policy: file_system_sandbox_policy.clone(), - network_sandbox_policy, - }, - )) + EscalationExecution::Permissions( + EscalationPermissions::ResolvedPermissionProfile( + ResolvedPermissionProfile { + permission_profile: PermissionProfile::from_runtime_permissions( + file_system_sandbox_policy, + network_sandbox_policy, + ), + sandbox_policy: sandbox_policy.clone(), + }, + ), + ) }) .unwrap_or(EscalationExecution::TurnDefault), } @@ -841,9 +845,9 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { additional_permissions: None, })? } - EscalationExecution::Permissions(EscalationPermissions::PermissionProfile( - permission_profile, - )) => { + EscalationExecution::Permissions( + EscalationPermissions::AdditionalPermissionProfile(permission_profile), + ) => { // Merge additive permissions into the existing turn/request sandbox policy. self.prepare_sandboxed_exec(PrepareSandboxedExecParams { command, @@ -855,15 +859,19 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { additional_permissions: Some(permission_profile), })? } - EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => { - // Use a fully specified sandbox policy instead of merging into the turn policy. + EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile( + permissions, + )) => { + // Use a fully specified permission profile instead of merging into the turn policy. + let (file_system_sandbox_policy, network_sandbox_policy) = + permissions.permission_profile.to_runtime_permissions(); self.prepare_sandboxed_exec(PrepareSandboxedExecParams { command, workdir, env, sandbox_policy: &permissions.sandbox_policy, - file_system_sandbox_policy: &permissions.file_system_sandbox_policy, - network_sandbox_policy: permissions.network_sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, + network_sandbox_policy, additional_permissions: None, })? } diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index deaaf067ea..a2b6ca145e 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -33,7 +33,7 @@ use codex_sandboxing::SandboxType; use codex_shell_escalation::EscalationExecution; use codex_shell_escalation::EscalationPermissions; use codex_shell_escalation::ExecResult; -use codex_shell_escalation::Permissions as EscalatedPermissions; +use codex_shell_escalation::ResolvedPermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::Value; @@ -317,11 +317,13 @@ fn shell_request_escalation_execution_is_explicit() { network_sandbox_policy, Some(&requested_permissions), ), - EscalationExecution::Permissions(EscalationPermissions::Permissions( - EscalatedPermissions { + EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile( + ResolvedPermissionProfile { + permission_profile: PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + network_sandbox_policy, + ), sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, }, )), ); diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index b8839b51b6..b7fe8a1b2d 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -1,8 +1,6 @@ use crate::mcp::RequestId; use crate::models::PermissionProfile; use crate::parse_command::ParsedCommand; -use crate::permissions::FileSystemSandboxPolicy; -use crate::permissions::NetworkSandboxPolicy; use crate::protocol::FileChange; use crate::protocol::ReviewDecision; use crate::protocol::SandboxPolicy; @@ -15,18 +13,23 @@ use std::collections::HashMap; use std::path::PathBuf; use ts_rs::TS; +/// Fully resolved permissions for rerunning an intercepted child process. +/// +/// `permission_profile` is the canonical permission model. `sandbox_policy` +/// remains as the legacy adapter for sandbox backends that still require it. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Permissions { +pub struct ResolvedPermissionProfile { + pub permission_profile: PermissionProfile, pub sandbox_policy: SandboxPolicy, - pub file_system_sandbox_policy: FileSystemSandboxPolicy, - pub network_sandbox_policy: NetworkSandboxPolicy, } #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum EscalationPermissions { - PermissionProfile(PermissionProfile), - Permissions(Permissions), + /// Permissions to merge with the active turn permissions. + AdditionalPermissionProfile(PermissionProfile), + /// Fully resolved permissions that should replace the active turn permissions. + ResolvedPermissionProfile(ResolvedPermissionProfile), } /// Proposed execpolicy change to allow commands starting with this prefix. diff --git a/codex-rs/shell-escalation/src/lib.rs b/codex-rs/shell-escalation/src/lib.rs index 6c7e1ec605..0f3c0afd30 100644 --- a/codex-rs/shell-escalation/src/lib.rs +++ b/codex-rs/shell-escalation/src/lib.rs @@ -22,10 +22,10 @@ pub use unix::ExecParams; #[cfg(unix)] pub use unix::ExecResult; #[cfg(unix)] -pub use unix::Permissions; -#[cfg(unix)] pub use unix::PreparedExec; #[cfg(unix)] +pub use unix::ResolvedPermissionProfile; +#[cfg(unix)] pub use unix::ShellCommandExecutor; #[cfg(unix)] pub use unix::Stopwatch; diff --git a/codex-rs/shell-escalation/src/unix/escalate_server.rs b/codex-rs/shell-escalation/src/unix/escalate_server.rs index 42e31f6a2a..ce73f262f4 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_server.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_server.rs @@ -922,7 +922,7 @@ mod tests { server, Arc::new(DeterministicEscalationPolicy { decision: EscalationDecision::escalate(EscalationExecution::Permissions( - EscalationPermissions::PermissionProfile(PermissionProfile { + EscalationPermissions::AdditionalPermissionProfile(PermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), @@ -931,12 +931,14 @@ mod tests { )), }), Arc::new(PermissionAssertingShellCommandExecutor { - expected_permissions: EscalationPermissions::PermissionProfile(PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - ..Default::default() - }), + expected_permissions: EscalationPermissions::AdditionalPermissionProfile( + PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + ..Default::default() + }, + ), }), CancellationToken::new(), CancellationToken::new(), diff --git a/codex-rs/shell-escalation/src/unix/mod.rs b/codex-rs/shell-escalation/src/unix/mod.rs index b5d999411c..8fb8688443 100644 --- a/codex-rs/shell-escalation/src/unix/mod.rs +++ b/codex-rs/shell-escalation/src/unix/mod.rs @@ -76,4 +76,4 @@ pub use self::escalation_policy::EscalationPolicy; pub use self::execve_wrapper::main_execve_wrapper; pub use self::stopwatch::Stopwatch; pub use codex_protocol::approvals::EscalationPermissions; -pub use codex_protocol::approvals::Permissions; +pub use codex_protocol::approvals::ResolvedPermissionProfile; From d9b343612d0bddb973b30016757eb82886f3823b Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 19:42:55 -0700 Subject: [PATCH 16/16] tests: isolate approval fixtures from host rules --- codex-rs/core/tests/suite/approvals.rs | 3 +- codex-rs/core/tests/suite/otel.rs | 36 +++++++++++++++---- .../core/tests/suite/permissions_messages.rs | 2 ++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index c393c539a6..e4583327e4 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -1,6 +1,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] use anyhow::Result; +use codex_config::types::ApprovalsReviewer; use codex_core::CodexThread; use codex_core::config::Constrained; use codex_core::config_loader::ConfigLayerStack; @@ -591,7 +592,7 @@ async fn submit_turn( final_output_json_schema: None, cwd: test.cwd.path().to_path_buf(), approval_policy, - approvals_reviewer: None, + approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy, permission_profile: None, model: session_model, diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index f93945e78f..7dcbaebd24 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -1132,7 +1132,11 @@ async fn handle_container_exec_user_approved_records_tool_decision() { mount_sse_once( &server, sse(vec![ - ev_local_shell_call("user_approved_call", "completed", vec!["/bin/date"]), + ev_local_shell_call( + "user_approved_call", + "completed", + vec!["/usr/bin/touch", "codex-otel-approval-test"], + ), ev_completed("done"), ]), ) @@ -1200,7 +1204,11 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision() mount_sse_once( &server, sse(vec![ - ev_local_shell_call("user_approved_session_call", "completed", vec!["/bin/date"]), + ev_local_shell_call( + "user_approved_session_call", + "completed", + vec!["/usr/bin/touch", "codex-otel-approval-test"], + ), ev_completed("done"), ]), ) @@ -1267,7 +1275,11 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() { mount_sse_once( &server, sse(vec![ - ev_local_shell_call("sandbox_retry_call", "completed", vec!["/bin/date"]), + ev_local_shell_call( + "sandbox_retry_call", + "completed", + vec!["/usr/bin/touch", "codex-otel-approval-test"], + ), ev_completed("done"), ]), ) @@ -1334,7 +1346,11 @@ async fn handle_container_exec_user_denies_records_tool_decision() { mount_sse_once( &server, sse(vec![ - ev_local_shell_call("user_denied_call", "completed", vec!["/bin/date"]), + ev_local_shell_call( + "user_denied_call", + "completed", + vec!["/usr/bin/touch", "codex-otel-approval-test"], + ), ev_completed("done"), ]), ) @@ -1401,7 +1417,11 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision() mount_sse_once( &server, sse(vec![ - ev_local_shell_call("sandbox_session_call", "completed", vec!["/bin/date"]), + ev_local_shell_call( + "sandbox_session_call", + "completed", + vec!["/usr/bin/touch", "codex-otel-approval-test"], + ), ev_completed("done"), ]), ) @@ -1468,7 +1488,11 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() { mount_sse_once( &server, sse(vec![ - ev_local_shell_call("sandbox_deny_call", "completed", vec!["/bin/date"]), + ev_local_shell_call( + "sandbox_deny_call", + "completed", + vec!["/usr/bin/touch", "codex-otel-approval-test"], + ), ev_completed("done"), ]), ) diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 2f28a28b55..a2203ea626 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -1,6 +1,7 @@ use anyhow::Result; use codex_core::ForkSnapshot; use codex_core::config::Constrained; +use codex_core::config_loader::ConfigLayerStack; use codex_execpolicy::Policy; use codex_protocol::models::DeveloperInstructions; use codex_protocol::protocol::AskForApproval; @@ -536,6 +537,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config.config_layer_stack = ConfigLayerStack::default(); }); let test = builder.build(&server).await?;