mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
permissions: support workspace roots in profiles (#22610)
## Why This is the configuration/model half of the alternative permissions migration we discussed as a comparison point for [#22401](https://github.com/openai/codex/pull/22401) and [#22402](https://github.com/openai/codex/pull/22402). The old `workspace-write` model mixes three concerns that we want to keep separate: - reusable profile rules that should stay immutable once selected - user/runtime workspace roots from `cwd`, `--add-dir`, and legacy workspace-write config - internal Codex writable roots such as memories, which should not be shown as user workspace roots This PR gives permission profiles first-class `workspace_roots` so users can opt multiple repositories into the same `:workspace_roots` rules without using broad absolute-path write grants. It also starts separating the raw selected profile from the effective runtime profile by making `Permissions` expose explicit accessors instead of public mutable fields. A representative `config.toml` looks like this: ```toml default_permissions = "dev" [permissions.dev.workspace_roots] "~/code/openai" = true "~/code/developers-website" = true [permissions.dev.filesystem.":workspace_roots"] "." = "write" ".codex" = "read" ".git" = "read" ".vscode" = "read" ``` If Codex starts in `~/code/codex` with that profile selected, the effective workspace-root set becomes: - `~/code/codex` from the runtime `cwd` - `~/code/openai` from the profile - `~/code/developers-website` from the profile The `:workspace_roots` rules are materialized across each root, so `.git`, `.codex`, and `.vscode` stay scoped the same way everywhere. Runtime additions such as `--add-dir` can still layer on later stack entries without mutating the selected profile. ## Stack Shape This PR intentionally stops before the profile-identity cleanup in [#22683](https://github.com/openai/codex/pull/22683) so the base review stays focused on config loading, workspace-root materialization, and compatibility with legacy `workspace-write`. The representation in this PR is therefore transitional: `Permissions` carries enough state to distinguish the raw constrained profile from the effective runtime profile, and there are still call sites that must keep the active profile identity and constrained profile value in sync. The follow-up PR replaces that with a single resolved profile state (`ResolvedPermissionProfile` / `PermissionProfileState`) that keeps the profile id, immutable `PermissionProfile`, and profile-declared workspace roots together. That follow-up removes APIs such as `set_constrained_permission_profile_with_active_profile()` where separate arguments could drift out of sync. Downstream PRs then build on this base to switch app-server turn updates to profile ids plus runtime workspace roots and to finish the user-visible summary behavior. Reviewers should judge this PR as the workspace-roots foundation, not as the final in-memory shape of selected permission profiles. ## Review Guide Suggested review order: 1. Start with `codex-rs/core/src/config/mod.rs`. This is the main shape change in the base slice. `Permissions` now stores a private raw `Constrained<PermissionProfile>` plus runtime `workspace_roots`. Callers use `permission_profile()` when they need the raw constrained value and `effective_permission_profile()` when they need a materialized runtime profile. As noted above, [#22683](https://github.com/openai/codex/pull/22683) replaces this transitional shape with a resolved profile state that keeps identity and profile data together. 2. Review `codex-rs/config/src/permissions_toml.rs` and `codex-rs/core/src/config/permissions.rs`. These add `[permissions.<id>.workspace_roots]`, resolve enabled entries relative to the policy cwd, and keep `:workspace_roots` deny-read glob patterns symbolic until the actual roots are known. 3. Review `codex-rs/protocol/src/permissions.rs` and `codex-rs/protocol/src/models.rs`. These add the policy/profile materialization helpers that expand exact `:workspace_roots` entries and scoped deny-read globs over every workspace root. This is also where `ActivePermissionProfileModification` is removed from the core model. 4. Review the legacy bridge in `Config::load_from_base_config_with_overrides` and `Config::set_legacy_sandbox_policy`. This is where legacy `workspace-write` roots become runtime workspace roots, while Codex internal writable roots stay internal and do not appear as user-facing workspace roots. 5. Then skim downstream call sites. The interesting pattern is raw-vs-effective access: state/proxy/bwrap paths keep the raw constrained profile, while execution, summaries, and user-visible status use the effective profile and workspace-root list. ## What Changed - added `[permissions.<id>.workspace_roots]` to the config model and schema - added runtime `workspace_roots` state to `Config`/`Permissions` and `ConfigOverrides` - made `Permissions` profile fields private and replaced direct mutation with accessors/setters - added `PermissionProfile` and `FileSystemSandboxPolicy` helpers for materializing `:workspace_roots` exact paths and deny-read globs across all roots - moved legacy additional writable roots into runtime workspace-root state instead of active profile modifications - removed `ActivePermissionProfileModification` and its app-server protocol/schema export - updated sandbox/status summary paths so internal writable roots are not reported as user workspace roots ## Verification Strategy The targeted tests cover the behavior at the layers where regressions are most likely: - `codex-rs/core/src/config/config_tests.rs` verifies config loading, legacy workspace-root seeding, effective profile materialization, and memory-root handling. - `codex-rs/core/src/config/permissions_tests.rs` verifies profile `workspace_roots` parsing and `:workspace_roots` scoped/glob compilation. - `codex-rs/protocol/src/permissions.rs` unit tests verify exact and glob materialization over multiple workspace roots. - `codex-rs/tui/src/status/tests.rs` and `codex-rs/utils/sandbox-summary/src/sandbox_summary.rs` verify the user-facing summaries show effective workspace roots and hide internal writes. I also ran `cargo check --tests` locally after the latest stack refresh to catch cross-crate API breakage from the private-field/accessor changes. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22610). * #22612 * #22611 * #22683 * __->__ #22610
This commit is contained in:
committed by
GitHub
Unverified
parent
7dbe1c9498
commit
c25d905f61
@@ -29,6 +29,7 @@ use codex_config::permissions_toml::NetworkDomainPermissionsToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::WorkspaceRootsToml;
|
||||
use codex_config::profile_toml::ConfigProfile;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
@@ -70,7 +71,6 @@ use codex_model_provider_info::WireApi;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
@@ -722,6 +722,10 @@ fn config_toml_deserializes_permission_profiles() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.workspace_roots]
|
||||
"~/code/openai" = true
|
||||
"~/code/ignored" = false
|
||||
|
||||
[permissions.workspace.filesystem]
|
||||
":minimal" = "read"
|
||||
|
||||
@@ -748,6 +752,12 @@ allow_upstream_proxy = false
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: Some(WorkspaceRootsToml {
|
||||
entries: BTreeMap::from([
|
||||
("~/code/ignored".to_string(), false),
|
||||
("~/code/openai".to_string(), true),
|
||||
]),
|
||||
}),
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([
|
||||
@@ -803,6 +813,7 @@ async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -956,6 +967,7 @@ async fn network_proxy_feature_matrix_preserves_sandbox_network_semantics() -> s
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1106,6 +1118,7 @@ async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io:
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1208,6 +1221,7 @@ enabled = false
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1256,6 +1270,7 @@ async fn permissions_profiles_network_disabled_by_default_does_not_start_proxy()
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1302,6 +1317,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([
|
||||
@@ -1335,6 +1351,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cwd_root = cwd.path().abs();
|
||||
let memories_root = codex_home.path().join("memories").abs();
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
@@ -1346,14 +1363,14 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
path: FileSystemPath::Path {
|
||||
path: cwd_root.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(Some("docs".into())),
|
||||
path: FileSystemPath::Path {
|
||||
path: cwd_root.join("docs"),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
@@ -1374,6 +1391,12 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
!config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path())
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Restricted
|
||||
@@ -1406,7 +1429,10 @@ async fn permission_profile_override_populates_runtime_permissions() -> std::io:
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.permissions.permission_profile(), permission_profile);
|
||||
assert_eq!(
|
||||
config.permissions.effective_permission_profile(),
|
||||
permission_profile
|
||||
);
|
||||
assert_eq!(config.permissions.active_permission_profile(), None);
|
||||
assert_eq!(
|
||||
&config.legacy_sandbox_policy(),
|
||||
@@ -1436,7 +1462,10 @@ async fn permission_profile_override_preserves_managed_unrestricted_filesystem()
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.permissions.permission_profile(), permission_profile);
|
||||
assert_eq!(
|
||||
config.permissions.effective_permission_profile(),
|
||||
permission_profile
|
||||
);
|
||||
assert_eq!(
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
@@ -1568,6 +1597,7 @@ async fn permission_profile_override_preserves_configured_network_policy_without
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1605,7 +1635,10 @@ async fn permission_profile_override_preserves_configured_network_policy_without
|
||||
config.permissions.network.is_none(),
|
||||
"profile network.enabled should not start the managed network proxy"
|
||||
);
|
||||
assert_eq!(config.permissions.permission_profile(), permission_profile);
|
||||
assert_eq!(
|
||||
config.permissions.effective_permission_profile(),
|
||||
permission_profile
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1613,7 +1646,9 @@ async fn permission_profile_override_preserves_configured_network_policy_without
|
||||
async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let extra_root = TempDir::new()?;
|
||||
tokio::fs::write(cwd.path().join(".git"), "gitdir: nowhere").await?;
|
||||
tokio::fs::write(extra_root.path().join(".git"), "gitdir: nowhere").await?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
@@ -1622,6 +1657,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: Some(2),
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1640,6 +1676,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
additional_writable_roots: vec![extra_root.path().to_path_buf()],
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
@@ -1653,21 +1690,23 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
|
||||
.glob_scan_max_depth,
|
||||
Some(2)
|
||||
);
|
||||
let expected_pattern = AbsolutePathBuf::resolve_path_against_base("**/*.env", cwd.path())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.entries
|
||||
.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: expected_pattern,
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
})
|
||||
);
|
||||
for root in [cwd.path(), extra_root.path()] {
|
||||
let expected_pattern = AbsolutePathBuf::resolve_path_against_base("**/*.env", root)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.entries
|
||||
.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: expected_pattern,
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
})
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!config
|
||||
.permissions
|
||||
@@ -1697,6 +1736,7 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<(
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1767,8 +1807,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_permissions_read_only_applies_additional_writable_roots_as_modifications()
|
||||
-> std::io::Result<()> {
|
||||
async fn default_permissions_read_only_keeps_add_dir_read_only() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let extra_root = TempDir::new()?;
|
||||
@@ -1790,20 +1829,88 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
|
||||
"expected additional writable root to modify :read-only, policy: {policy:?}"
|
||||
!policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
|
||||
"expected :read-only to stay read-only for runtime workspace roots, policy: {policy:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.active_permission_profile(),
|
||||
Some(
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY).with_modifications(
|
||||
vec![
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot {
|
||||
path: extra_root,
|
||||
Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots()
|
||||
-> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let codex_home = temp_dir.path().join("codex-home");
|
||||
let cwd = temp_dir.path().join("frontend");
|
||||
let runtime_root = temp_dir.path().join("backend");
|
||||
let profile_root = temp_dir.path().join("shared");
|
||||
for root in [&cwd, &runtime_root, &profile_root] {
|
||||
std::fs::create_dir_all(root.join(".git"))?;
|
||||
std::fs::create_dir_all(root.join(".codex"))?;
|
||||
}
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("dev".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"dev".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: Some(WorkspaceRootsToml {
|
||||
entries: BTreeMap::from([(
|
||||
profile_root.to_string_lossy().into_owned(),
|
||||
true,
|
||||
)]),
|
||||
}),
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
":workspace_roots".to_string(),
|
||||
FilesystemPermissionToml::Scoped(BTreeMap::from([
|
||||
(".".to_string(), FileSystemAccessMode::Write),
|
||||
(".git".to_string(), FileSystemAccessMode::Read),
|
||||
(".codex".to_string(), FileSystemAccessMode::Read),
|
||||
])),
|
||||
)]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.clone()),
|
||||
additional_writable_roots: vec![runtime_root.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
for root in [cwd.abs(), runtime_root.abs(), profile_root.abs()] {
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(root.as_path(), cwd.as_path()),
|
||||
"expected workspace root to be writable, policy: {policy:?}"
|
||||
);
|
||||
assert!(
|
||||
!policy.can_write_path_with_cwd(&root.join(".git"), cwd.as_path()),
|
||||
"expected .git carveout under {root:?}, policy: {policy:?}"
|
||||
);
|
||||
assert!(
|
||||
!policy.can_write_path_with_cwd(&root.join(".codex"), cwd.as_path()),
|
||||
"expected .codex carveout under {root:?}, policy: {policy:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
config.permissions.active_permission_profile(),
|
||||
Some(ActivePermissionProfile::new("dev"))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -2071,7 +2178,7 @@ async fn default_permissions_can_select_builtin_full_access_profile() -> std::io
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.permission_profile(),
|
||||
config.permissions.effective_permission_profile(),
|
||||
PermissionProfile::Disabled
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2188,6 +2295,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2244,6 +2352,7 @@ async fn permissions_profiles_reject_nested_entries_for_non_workspace_roots() ->
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2304,6 +2413,7 @@ async fn load_workspace_permission_profile(
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2347,6 +2457,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<(
|
||||
async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
|
||||
-> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2383,6 +2494,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: None,
|
||||
network: None,
|
||||
})
|
||||
@@ -2411,6 +2523,7 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::new(),
|
||||
@@ -2446,6 +2559,7 @@ async fn permissions_profiles_reject_workspace_root_parent_traversal() -> std::i
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2492,6 +2606,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()>
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -3087,13 +3202,15 @@ exclude_slash_tmp = true
|
||||
);
|
||||
continue;
|
||||
}
|
||||
assert_eq!(
|
||||
config.permissions.workspace_roots(),
|
||||
&[cwd.abs(), extra_root.clone()]
|
||||
);
|
||||
assert!(
|
||||
file_system_policy
|
||||
.entries
|
||||
.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
},
|
||||
path: FileSystemPath::Path { path: cwd.abs() },
|
||||
access: FileSystemAccessMode::Write,
|
||||
})
|
||||
);
|
||||
@@ -3112,15 +3229,16 @@ exclude_slash_tmp = true
|
||||
file_system_policy
|
||||
.entries
|
||||
.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(Some(
|
||||
subpath.into()
|
||||
)),
|
||||
path: FileSystemPath::Path {
|
||||
path: AbsolutePathBuf::resolve_path_against_base(
|
||||
subpath,
|
||||
cwd.path()
|
||||
),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}),
|
||||
"case `{name}` should preserve `{subpath}` as a symbolic project-root \
|
||||
metadata carveout"
|
||||
"case `{name}` should materialize `{subpath}` for the runtime workspace \
|
||||
root"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7423,10 +7541,13 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
constrained_permissions_profile: Constrained::allow_any(
|
||||
PermissionProfile::read_only()
|
||||
),
|
||||
active_permission_profile: Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -7438,6 +7559,8 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
workspace_roots_explicit: false,
|
||||
cli_auth_credentials_store_mode: Default::default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
@@ -7870,10 +7993,11 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
model_provider: fixture.openai_custom_provider.clone(),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
constrained_permissions_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
active_permission_profile: Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -7885,6 +8009,8 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
workspace_roots_explicit: false,
|
||||
cli_auth_credentials_store_mode: Default::default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
@@ -8031,10 +8157,11 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
constrained_permissions_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
active_permission_profile: Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -8046,6 +8173,8 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
workspace_roots_explicit: false,
|
||||
cli_auth_credentials_store_mode: Default::default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
@@ -8177,10 +8306,11 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
constrained_permissions_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
active_permission_profile: Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -8192,6 +8322,8 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
workspace_roots_explicit: false,
|
||||
cli_auth_credentials_store_mode: Default::default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
@@ -9127,7 +9259,7 @@ async fn permission_profile_override_falls_back_when_disallowed_by_requirements(
|
||||
let expected_sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
assert_eq!(config.legacy_sandbox_policy(), expected_sandbox_policy);
|
||||
assert_eq!(
|
||||
config.permissions.permission_profile(),
|
||||
config.permissions.effective_permission_profile(),
|
||||
PermissionProfile::read_only()
|
||||
);
|
||||
Ok(())
|
||||
@@ -9155,7 +9287,7 @@ async fn active_profile_is_cleared_when_requirements_force_fallback() -> std::io
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.permission_profile(),
|
||||
config.permissions.effective_permission_profile(),
|
||||
PermissionProfile::read_only()
|
||||
);
|
||||
assert_eq!(config.permissions.active_permission_profile(), None);
|
||||
@@ -9275,7 +9407,7 @@ async fn requirements_web_search_mode_overrides_danger_full_access_default() ->
|
||||
assert_eq!(
|
||||
resolve_web_search_mode_for_turn(
|
||||
&config.web_search_mode,
|
||||
&config.permissions.permission_profile(),
|
||||
&config.permissions.effective_permission_profile(),
|
||||
),
|
||||
WebSearchMode::Cached,
|
||||
);
|
||||
|
||||
+226
-93
@@ -89,7 +89,6 @@ use codex_protocol::config_types::WebSearchConfig;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
@@ -117,6 +116,7 @@ use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE;
|
||||
use crate::config::permissions::apply_network_proxy_feature_config;
|
||||
use crate::config::permissions::builtin_permission_profile;
|
||||
use crate::config::permissions::compile_permission_profile_selection;
|
||||
use crate::config::permissions::compile_permission_profile_workspace_roots;
|
||||
use crate::config::permissions::default_builtin_permission_profile_name;
|
||||
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
|
||||
use crate::config::permissions::network_proxy_config_for_profile_selection;
|
||||
@@ -247,12 +247,16 @@ pub(crate) async fn test_config() -> Config {
|
||||
pub struct Permissions {
|
||||
/// Approval policy for executing commands.
|
||||
pub approval_policy: Constrained<AskForApproval>,
|
||||
/// Canonical effective runtime permissions after config requirements and
|
||||
/// runtime readable-root additions have been applied.
|
||||
pub permission_profile: Constrained<PermissionProfile>,
|
||||
/// Canonical constrained permissions profile before runtime workspace-root
|
||||
/// materialization has been applied.
|
||||
constrained_permissions_profile: Constrained<PermissionProfile>,
|
||||
/// Named or implicit built-in profile selected by config, rather than an
|
||||
/// ad-hoc override.
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
/// Thread-scoped runtime workspace roots. Symbolic `:workspace_roots`
|
||||
/// entries in `constrained_permissions_profile` are materialized against
|
||||
/// these roots.
|
||||
workspace_roots: Vec<AbsolutePathBuf>,
|
||||
/// Effective network configuration applied to all spawned processes.
|
||||
pub network: Option<NetworkProxySpec>,
|
||||
/// Whether the model may request a login shell for shell-based tools.
|
||||
@@ -274,10 +278,67 @@ pub struct Permissions {
|
||||
}
|
||||
|
||||
impl Permissions {
|
||||
/// Build permissions from the constrained values required for a minimal
|
||||
/// in-process configuration.
|
||||
pub fn from_approval_and_profile(
|
||||
approval_policy: Constrained<AskForApproval>,
|
||||
permission_profile: Constrained<PermissionProfile>,
|
||||
) -> Self {
|
||||
Self {
|
||||
approval_policy,
|
||||
constrained_permissions_profile: permission_profile,
|
||||
active_permission_profile: None,
|
||||
workspace_roots: Vec::new(),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
windows_sandbox_private_desktop: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrow the constrained canonical profile. This preserves the raw
|
||||
/// symbolic `:workspace_roots` form for session/thread state.
|
||||
pub fn permission_profile(&self) -> &Constrained<PermissionProfile> {
|
||||
&self.constrained_permissions_profile
|
||||
}
|
||||
|
||||
/// Set the full constrained profile value and preserve the active profile
|
||||
/// sidecar when the caller has already validated both together.
|
||||
pub fn set_constrained_permission_profile_with_active_profile(
|
||||
&mut self,
|
||||
permission_profile: Constrained<PermissionProfile>,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
) {
|
||||
self.constrained_permissions_profile = permission_profile;
|
||||
self.active_permission_profile = active_permission_profile;
|
||||
}
|
||||
|
||||
pub fn set_workspace_roots(&mut self, workspace_roots: Vec<AbsolutePathBuf>) {
|
||||
self.workspace_roots = workspace_roots;
|
||||
}
|
||||
|
||||
pub fn workspace_roots(&self) -> &[AbsolutePathBuf] {
|
||||
&self.workspace_roots
|
||||
}
|
||||
|
||||
/// Workspace roots that came from user-visible configuration or runtime
|
||||
/// selection. Internal Codex-only writable roots are intentionally excluded.
|
||||
pub fn user_visible_workspace_roots(&self) -> &[AbsolutePathBuf] {
|
||||
&self.workspace_roots
|
||||
}
|
||||
|
||||
fn materialized_permission_profile(&self) -> PermissionProfile {
|
||||
self.constrained_permissions_profile
|
||||
.get()
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
|
||||
}
|
||||
|
||||
/// Effective runtime permissions after config requirements and runtime
|
||||
/// readable-root additions have been applied.
|
||||
pub fn permission_profile(&self) -> PermissionProfile {
|
||||
self.permission_profile.get().clone()
|
||||
/// workspace-root materialization have been applied.
|
||||
pub fn effective_permission_profile(&self) -> PermissionProfile {
|
||||
self.materialized_permission_profile()
|
||||
}
|
||||
|
||||
/// Named profile selected by config, if the current profile has one.
|
||||
@@ -287,20 +348,23 @@ impl Permissions {
|
||||
|
||||
/// Effective filesystem sandbox policy derived from the canonical profile.
|
||||
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
|
||||
self.permission_profile.get().file_system_sandbox_policy()
|
||||
self.materialized_permission_profile()
|
||||
.file_system_sandbox_policy()
|
||||
}
|
||||
|
||||
/// Effective network sandbox policy derived from the canonical profile.
|
||||
pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
|
||||
self.permission_profile.get().network_sandbox_policy()
|
||||
self.constrained_permissions_profile
|
||||
.get()
|
||||
.network_sandbox_policy()
|
||||
}
|
||||
|
||||
/// Legacy compatibility projection derived from the canonical profile.
|
||||
pub fn legacy_sandbox_policy(&self, cwd: &Path) -> SandboxPolicy {
|
||||
let permission_profile = self.permission_profile.get();
|
||||
let permission_profile = self.materialized_permission_profile();
|
||||
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
|
||||
compatibility_sandbox_policy_for_permission_profile(
|
||||
permission_profile,
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
permission_profile.network_sandbox_policy(),
|
||||
cwd,
|
||||
@@ -322,11 +386,12 @@ impl Permissions {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
self.permission_profile.can_set(&permission_profile)
|
||||
self.constrained_permissions_profile
|
||||
.can_set(&permission_profile)
|
||||
}
|
||||
|
||||
/// Replace permissions from a legacy sandbox policy and keep every
|
||||
/// permission projection in sync.
|
||||
/// Set permissions from a legacy sandbox policy and keep every permission
|
||||
/// projection in sync.
|
||||
pub fn set_legacy_sandbox_policy(
|
||||
&mut self,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
@@ -341,13 +406,34 @@ impl Permissions {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
self.workspace_roots = match &sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
let mut workspace_roots = vec![
|
||||
AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.unwrap_or_else(|_| AbsolutePathBuf::resolve_path_against_base(cwd, "/")),
|
||||
];
|
||||
for root in writable_roots {
|
||||
if !workspace_roots.iter().any(|existing| existing == root) {
|
||||
workspace_roots.push(root.clone());
|
||||
}
|
||||
}
|
||||
workspace_roots
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess
|
||||
| SandboxPolicy::ExternalSandbox { .. }
|
||||
| SandboxPolicy::ReadOnly { .. } => vec![
|
||||
AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.unwrap_or_else(|_| AbsolutePathBuf::resolve_path_against_base(cwd, "/")),
|
||||
],
|
||||
};
|
||||
|
||||
self.permission_profile.set(permission_profile)?;
|
||||
self.constrained_permissions_profile
|
||||
.set(permission_profile)?;
|
||||
self.active_permission_profile = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace permissions from the canonical profile.
|
||||
/// Set permissions from the canonical profile.
|
||||
pub fn set_permission_profile(
|
||||
&mut self,
|
||||
permission_profile: PermissionProfile,
|
||||
@@ -358,16 +444,15 @@ impl Permissions {
|
||||
)
|
||||
}
|
||||
|
||||
/// Replace permissions from the canonical profile and record the named
|
||||
/// source profile, if one is known.
|
||||
/// Set permissions from the canonical profile and record the named source
|
||||
/// profile, if one is known.
|
||||
pub fn set_permission_profile_with_active_profile(
|
||||
&mut self,
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
) -> ConstraintResult<()> {
|
||||
self.permission_profile.can_set(&permission_profile)?;
|
||||
|
||||
self.permission_profile.set(permission_profile)?;
|
||||
self.constrained_permissions_profile
|
||||
.set(permission_profile)?;
|
||||
self.active_permission_profile = active_permission_profile;
|
||||
Ok(())
|
||||
}
|
||||
@@ -577,6 +662,15 @@ pub struct Config {
|
||||
/// layer are resolved against this path.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
|
||||
/// Absolute runtime workspace roots for the session. Symbolic
|
||||
/// `:workspace_roots` permission entries are materialized against these
|
||||
/// roots while profile-defined workspace roots remain encoded directly in
|
||||
/// the permission profile.
|
||||
pub workspace_roots: Vec<AbsolutePathBuf>,
|
||||
/// Whether runtime workspace roots were supplied explicitly by the caller
|
||||
/// or legacy config, rather than defaulting to `cwd`.
|
||||
pub workspace_roots_explicit: bool,
|
||||
|
||||
/// Preferred store for CLI auth credentials.
|
||||
/// file (default): Use a file in the Codex home directory.
|
||||
/// keyring: Use an OS-specific keyring service.
|
||||
@@ -1076,8 +1170,14 @@ impl Config {
|
||||
&mut self,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
) -> ConstraintResult<()> {
|
||||
self.workspace_roots_explicit = matches!(
|
||||
&sandbox_policy,
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } if !writable_roots.is_empty()
|
||||
);
|
||||
self.permissions
|
||||
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())
|
||||
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())?;
|
||||
self.workspace_roots = self.permissions.workspace_roots().to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_models_manager_config(&self) -> ModelsManagerConfig {
|
||||
@@ -1926,6 +2026,14 @@ pub struct ConfigOverrides {
|
||||
pub bypass_hook_trust: Option<bool>,
|
||||
/// Additional directories that should be treated as writable roots for this session.
|
||||
pub additional_writable_roots: Vec<PathBuf>,
|
||||
/// Explicit runtime workspace roots for this session. When set, this is
|
||||
/// the full runtime root list rather than an additive override.
|
||||
pub workspace_roots: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
|
||||
let mut seen = HashSet::new();
|
||||
paths.retain(|path| seen.insert(path.clone()));
|
||||
}
|
||||
|
||||
/// Resolves the OSS provider from CLI override, profile config, or global config.
|
||||
@@ -2239,6 +2347,7 @@ impl Config {
|
||||
ephemeral,
|
||||
bypass_hook_trust,
|
||||
additional_writable_roots,
|
||||
workspace_roots: workspace_roots_override,
|
||||
} = overrides;
|
||||
let bypass_hook_trust = bypass_hook_trust.unwrap_or_default();
|
||||
|
||||
@@ -2329,11 +2438,10 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}))?;
|
||||
let mut additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
|
||||
let requested_additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
|
||||
.into_iter()
|
||||
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
|
||||
.collect();
|
||||
let requested_additional_writable_roots = additional_writable_roots.clone();
|
||||
let repo_root = resolve_root_git_project_for_trust(fs, &resolved_cwd).await;
|
||||
let active_project = cfg
|
||||
.get_active_project(
|
||||
@@ -2375,12 +2483,7 @@ impl Config {
|
||||
};
|
||||
let memories_root = memory_root(&codex_home);
|
||||
std::fs::create_dir_all(&memories_root)?;
|
||||
if !additional_writable_roots
|
||||
.iter()
|
||||
.any(|existing| existing == &memories_root)
|
||||
{
|
||||
additional_writable_roots.push(memories_root);
|
||||
}
|
||||
let internal_writable_roots = vec![memories_root];
|
||||
|
||||
let profiles_are_active = default_permissions_override.is_some()
|
||||
|| matches!(
|
||||
@@ -2390,6 +2493,40 @@ impl Config {
|
||||
|| permission_config_syntax.is_none();
|
||||
let using_implicit_builtin_profile =
|
||||
permission_config_syntax.is_none() && default_permissions.is_none();
|
||||
let should_seed_legacy_workspace_roots = default_permissions.is_none()
|
||||
&& matches!(
|
||||
permission_config_syntax,
|
||||
None | Some(PermissionConfigSyntax::Legacy)
|
||||
);
|
||||
let legacy_workspace_roots_explicit = should_seed_legacy_workspace_roots
|
||||
&& cfg
|
||||
.sandbox_workspace_write
|
||||
.as_ref()
|
||||
.is_some_and(|sandbox_workspace_write| {
|
||||
!sandbox_workspace_write.writable_roots.is_empty()
|
||||
});
|
||||
let workspace_roots_explicit = workspace_roots_override.is_some()
|
||||
|| !requested_additional_writable_roots.is_empty()
|
||||
|| legacy_workspace_roots_explicit;
|
||||
let mut workspace_roots = match workspace_roots_override {
|
||||
Some(workspace_roots) => workspace_roots
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path())
|
||||
})
|
||||
.collect(),
|
||||
None => {
|
||||
let mut workspace_roots = vec![resolved_cwd.clone()];
|
||||
workspace_roots.extend(requested_additional_writable_roots.clone());
|
||||
if should_seed_legacy_workspace_roots
|
||||
&& let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
|
||||
{
|
||||
workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
|
||||
}
|
||||
workspace_roots
|
||||
}
|
||||
};
|
||||
dedupe_absolute_paths(&mut workspace_roots);
|
||||
let (
|
||||
mut configured_network_proxy_config,
|
||||
permission_profile,
|
||||
@@ -2418,18 +2555,24 @@ impl Config {
|
||||
} else {
|
||||
NetworkProxyConfig::default()
|
||||
};
|
||||
let materialized_file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots);
|
||||
let materialized_permission_profile =
|
||||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&materialized_file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&materialized_permission_profile,
|
||||
&materialized_file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
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,
|
||||
);
|
||||
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
|
||||
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
@@ -2463,6 +2606,22 @@ impl Config {
|
||||
resolved_cwd.as_path(),
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let mut configured_workspace_roots = compile_permission_profile_workspace_roots(
|
||||
cfg.permissions.as_ref(),
|
||||
default_permissions,
|
||||
resolved_cwd.as_path(),
|
||||
)?;
|
||||
if using_implicit_builtin_profile
|
||||
&& default_permissions == BUILT_IN_WORKSPACE_PROFILE
|
||||
&& let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
|
||||
{
|
||||
configured_workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
|
||||
}
|
||||
dedupe_absolute_paths(&mut configured_workspace_roots);
|
||||
workspace_roots.extend(configured_workspace_roots.iter().cloned());
|
||||
dedupe_absolute_paths(&mut workspace_roots);
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_materialized_project_roots_for_workspace_roots(&configured_workspace_roots);
|
||||
let mut permission_profile = if let Some(permission_profile) =
|
||||
builtin_permission_profile(default_permissions, builtin_workspace_write_settings)
|
||||
{
|
||||
@@ -2473,36 +2632,26 @@ impl Config {
|
||||
network_sandbox_policy,
|
||||
)
|
||||
};
|
||||
let materialized_file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots);
|
||||
let materialized_permission_profile =
|
||||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&materialized_file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&materialized_permission_profile,
|
||||
&materialized_file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
file_system_sandbox_policy = if using_implicit_builtin_profile {
|
||||
file_system_sandbox_policy
|
||||
.with_additional_legacy_workspace_writable_roots(
|
||||
&additional_writable_roots,
|
||||
)
|
||||
} else {
|
||||
file_system_sandbox_policy.with_additional_writable_roots(
|
||||
resolved_cwd.as_path(),
|
||||
&additional_writable_roots,
|
||||
)
|
||||
};
|
||||
permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
} else if matches!(permission_profile, PermissionProfile::Managed { .. })
|
||||
&& !requested_additional_writable_roots.is_empty()
|
||||
{
|
||||
file_system_sandbox_policy = file_system_sandbox_policy.with_additional_writable_roots(
|
||||
resolved_cwd.as_path(),
|
||||
&requested_additional_writable_roots,
|
||||
);
|
||||
permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
|
||||
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
@@ -2518,22 +2667,7 @@ impl Config {
|
||||
// when doing so would lose roots, network, or tmp settings.
|
||||
None
|
||||
} else {
|
||||
let active_permission_profile = if !requested_additional_writable_roots.is_empty()
|
||||
&& matches!(permission_profile, PermissionProfile::Managed { .. })
|
||||
{
|
||||
ActivePermissionProfile::new(default_permissions).with_modifications(
|
||||
requested_additional_writable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|path| {
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot { path }
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
ActivePermissionProfile::new(default_permissions)
|
||||
};
|
||||
Some(active_permission_profile)
|
||||
Some(ActivePermissionProfile::new(default_permissions))
|
||||
};
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
@@ -2572,25 +2706,21 @@ impl Config {
|
||||
}
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
// `additional_writable_roots` is a legacy workspace-write knob. It
|
||||
// only applies when the derived managed profile has workspace-style
|
||||
// write access to the project roots; read-only, disabled, external,
|
||||
// and future non-workspace profiles must not silently grow extra
|
||||
// write access.
|
||||
let materialized_file_system_sandbox_policy = permission_profile
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots)
|
||||
.file_system_sandbox_policy();
|
||||
if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed)
|
||||
&& file_system_sandbox_policy.can_write_path_with_cwd(
|
||||
&& materialized_file_system_sandbox_policy.can_write_path_with_cwd(
|
||||
resolved_cwd.as_path(),
|
||||
resolved_cwd.as_path(),
|
||||
)
|
||||
&& !file_system_sandbox_policy.has_full_disk_write_access()
|
||||
&& !materialized_file_system_sandbox_policy.has_full_disk_write_access()
|
||||
{
|
||||
// Keep legacy behavior for extra writable roots while storing
|
||||
// the result as the canonical permission profile. Explicit
|
||||
// extra roots are concrete paths, so their metadata carveouts
|
||||
// are also concrete rather than symbolic `:workspace_roots`
|
||||
// entries.
|
||||
// Keep Codex runtime write access while storing the runtime
|
||||
// workspace roots separately on the thread.
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_legacy_workspace_writable_roots(&additional_writable_roots);
|
||||
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
|
||||
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
@@ -3105,11 +3235,14 @@ impl Config {
|
||||
model_provider_id,
|
||||
model_provider,
|
||||
cwd: resolved_cwd,
|
||||
workspace_roots: workspace_roots.clone(),
|
||||
workspace_roots_explicit,
|
||||
startup_warnings,
|
||||
permissions: Permissions {
|
||||
approval_policy: constrained_approval_policy.value,
|
||||
permission_profile: constrained_permission_profile.value,
|
||||
constrained_permissions_profile: constrained_permission_profile.value,
|
||||
active_permission_profile,
|
||||
workspace_roots,
|
||||
network,
|
||||
allow_login_shell,
|
||||
shell_environment_policy,
|
||||
@@ -3393,7 +3526,7 @@ impl Config {
|
||||
|
||||
pub fn managed_network_requirements_enabled(&self) -> bool {
|
||||
!matches!(
|
||||
self.permissions.permission_profile.get(),
|
||||
self.permissions.permission_profile().get(),
|
||||
PermissionProfile::Disabled
|
||||
) && self
|
||||
.config_layer_stack
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_config::permissions_toml::NetworkUnixSocketPermissionToml;
|
||||
use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::WorkspaceRootsToml;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_features::NetworkProxyConfigToml;
|
||||
use codex_features::NetworkProxyDomainPermissionToml;
|
||||
@@ -33,6 +34,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::permissions::project_roots_glob_pattern;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::ProjectConfig;
|
||||
@@ -72,12 +74,12 @@ pub(crate) fn builtin_permission_profile(
|
||||
BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()),
|
||||
BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write {
|
||||
Some(SandboxWorkspaceWrite {
|
||||
writable_roots,
|
||||
writable_roots: _,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
}) => PermissionProfile::workspace_write_with(
|
||||
writable_roots,
|
||||
&[],
|
||||
if *network_access {
|
||||
NetworkSandboxPolicy::Enabled
|
||||
} else {
|
||||
@@ -303,6 +305,41 @@ pub(crate) fn compile_permission_profile_selection(
|
||||
compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings)
|
||||
}
|
||||
|
||||
pub(crate) fn compile_permission_profile_workspace_roots(
|
||||
permissions: Option<&PermissionsToml>,
|
||||
profile_name: &str,
|
||||
policy_cwd: &Path,
|
||||
) -> io::Result<Vec<AbsolutePathBuf>> {
|
||||
if is_builtin_permission_profile_name(profile_name) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
reject_unknown_builtin_permission_profile(profile_name)?;
|
||||
|
||||
let permissions = permissions.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a `[permissions]` table",
|
||||
)
|
||||
})?;
|
||||
let profile = resolve_permission_profile(permissions, profile_name)?;
|
||||
Ok(compile_workspace_roots(
|
||||
profile.workspace_roots.as_ref(),
|
||||
policy_cwd,
|
||||
))
|
||||
}
|
||||
|
||||
fn compile_workspace_roots(
|
||||
workspace_roots: Option<&WorkspaceRootsToml>,
|
||||
policy_cwd: &Path,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
workspace_roots.map_or_else(Vec::new, |workspace_roots| {
|
||||
workspace_roots
|
||||
.enabled_roots()
|
||||
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, policy_cwd))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn reject_unknown_builtin_permission_profile(profile_name: &str) -> io::Result<()> {
|
||||
if profile_name.starts_with(':') {
|
||||
return Err(io::Error::new(
|
||||
@@ -478,7 +515,7 @@ fn compile_scoped_filesystem_pattern(
|
||||
path: &str,
|
||||
subpath: &str,
|
||||
access: FileSystemAccessMode,
|
||||
policy_cwd: &Path,
|
||||
_policy_cwd: &Path,
|
||||
) -> io::Result<String> {
|
||||
// Pattern entries currently mean deny-read only. Supporting broader access
|
||||
// modes here would imply glob-based read/write allow semantics that the
|
||||
@@ -493,15 +530,10 @@ fn compile_scoped_filesystem_pattern(
|
||||
|
||||
match parse_special_path(path) {
|
||||
Some(FileSystemSpecialPath::ProjectRoots { .. }) => {
|
||||
// `:workspace_roots` is represented as a special path, but current
|
||||
// filesystem-policy resolution defines it relative to the session
|
||||
// cwd. Use the same policy cwd here so glob entries and exact
|
||||
// scoped entries resolve consistently.
|
||||
Ok(
|
||||
AbsolutePathBuf::resolve_path_against_base(&subpath, policy_cwd)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
// Keep `:workspace_roots` glob patterns symbolic until the active
|
||||
// workspace roots are known, then materialize them for cwd and any
|
||||
// runtime/profile-added workspace roots together.
|
||||
Ok(project_roots_glob_pattern(&subpath))
|
||||
}
|
||||
Some(_) => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_config::permissions_toml::NetworkUnixSocketPermissionToml;
|
||||
use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::WorkspaceRootsToml;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -66,6 +67,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::new(),
|
||||
@@ -275,6 +277,39 @@ fn profile_network_proxy_config_keeps_proxy_disabled_for_proxy_policy() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_permission_profile_workspace_roots_resolves_enabled_entries() -> std::io::Result<()> {
|
||||
let cwd = TempDir::new()?;
|
||||
let workspace_roots = compile_permission_profile_workspace_roots(
|
||||
Some(&PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: Some(WorkspaceRootsToml {
|
||||
entries: BTreeMap::from([
|
||||
("backend".to_string(), true),
|
||||
("disabled".to_string(), false),
|
||||
]),
|
||||
}),
|
||||
filesystem: None,
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
"workspace",
|
||||
cwd.path(),
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
workspace_roots,
|
||||
vec![AbsolutePathBuf::resolve_path_against_base(
|
||||
"backend",
|
||||
cwd.path()
|
||||
)]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths() {
|
||||
let filesystem = FilesystemPermissionsToml {
|
||||
@@ -359,6 +394,7 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()>
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
|
||||
@@ -251,10 +251,11 @@ fn sandbox_text(mode: SandboxMode, network_access: NetworkAccess) -> String {
|
||||
}
|
||||
|
||||
fn writable_roots_text(writable_roots: Option<Vec<WritableRoot>>) -> Option<String> {
|
||||
let roots = writable_roots?;
|
||||
let mut roots = writable_roots?;
|
||||
if roots.is_empty() {
|
||||
return None;
|
||||
}
|
||||
roots.sort_by(|left, right| left.root.as_path().cmp(right.root.as_path()));
|
||||
|
||||
let roots_list: Vec<String> = roots
|
||||
.iter()
|
||||
|
||||
@@ -9,7 +9,6 @@ use codex_analytics::GuardianReviewAnalyticsResult;
|
||||
use codex_analytics::GuardianReviewSessionKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -894,9 +893,6 @@ pub(crate) fn build_guardian_review_session_config(
|
||||
guardian_config.developer_instructions = None;
|
||||
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
guardian_config.permissions.permission_profile = Constrained::allow_only(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
);
|
||||
guardian_config
|
||||
.permissions
|
||||
.set_legacy_sandbox_policy(sandbox_policy, guardian_config.cwd.as_path())
|
||||
@@ -922,7 +918,7 @@ pub(crate) fn build_guardian_review_session_config(
|
||||
guardian_config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints(
|
||||
live_network_config,
|
||||
network_constraints,
|
||||
guardian_config.permissions.permission_profile.get(),
|
||||
guardian_config.permissions.permission_profile().get(),
|
||||
)?);
|
||||
}
|
||||
for feature in [
|
||||
|
||||
@@ -2163,7 +2163,7 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
parent_config.permissions.permission_profile.get(),
|
||||
parent_config.permissions.permission_profile().get(),
|
||||
)
|
||||
.expect("network proxy spec");
|
||||
parent_config.permissions.network = Some(network.clone());
|
||||
@@ -2190,8 +2190,8 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
Constrained::allow_only(AskForApproval::Never)
|
||||
);
|
||||
assert_eq!(
|
||||
guardian_config.permissions.permission_profile,
|
||||
Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
|
||||
guardian_config.permissions.permission_profile(),
|
||||
&Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
))
|
||||
);
|
||||
@@ -2230,7 +2230,7 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
NetworkProxySpec::from_config_and_constraints(
|
||||
parent_network,
|
||||
/*requirements*/ None,
|
||||
parent_config.permissions.permission_profile.get(),
|
||||
parent_config.permissions.permission_profile().get(),
|
||||
)
|
||||
.expect("parent network proxy spec"),
|
||||
);
|
||||
|
||||
@@ -617,7 +617,7 @@ impl Codex {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: session_permission_profile_from_config(&config)?,
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -818,6 +818,20 @@ fn get_service_tier(
|
||||
.then_some(ServiceTier::Fast.request_value().to_string())
|
||||
}
|
||||
|
||||
fn session_permission_profile_from_config(
|
||||
config: &Config,
|
||||
) -> CodexResult<codex_config::Constrained<PermissionProfile>> {
|
||||
let mut session_permission_profile = config.permissions.permission_profile().clone();
|
||||
session_permission_profile
|
||||
.set(config.permissions.effective_permission_profile())
|
||||
.map_err(|err| {
|
||||
CodexErr::Fatal(format!(
|
||||
"failed to materialize workspace roots for session permissions: {err}"
|
||||
))
|
||||
})?;
|
||||
Ok(session_permission_profile)
|
||||
}
|
||||
|
||||
fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool {
|
||||
plan_type == AccountPlanType::Enterprise
|
||||
|| plan_type.is_business_like()
|
||||
|
||||
@@ -756,7 +756,7 @@ impl Session {
|
||||
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
|
||||
spec,
|
||||
current_exec_policy.as_ref(),
|
||||
config.permissions.permission_profile.get(),
|
||||
config.permissions.permission_profile().get(),
|
||||
network_policy_decider.as_ref().map(Arc::clone),
|
||||
blocked_request_observer.as_ref().map(Arc::clone),
|
||||
managed_network_requirements_configured,
|
||||
@@ -820,7 +820,7 @@ impl Session {
|
||||
// setup is straightforward enough and performs well.
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
&config.permissions.approval_policy,
|
||||
&config.permissions.permission_profile,
|
||||
config.permissions.permission_profile(),
|
||||
))),
|
||||
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
|
||||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||||
|
||||
@@ -36,6 +36,7 @@ use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
@@ -2128,9 +2129,12 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
|
||||
};
|
||||
let expected_sandbox_policy = sandbox_policy.clone();
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.permissions.permission_profile = codex_config::Constrained::allow_any(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
);
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::from_legacy_sandbox_policy(
|
||||
&sandbox_policy,
|
||||
))
|
||||
.expect("set permission profile");
|
||||
config
|
||||
.set_legacy_sandbox_policy(sandbox_policy)
|
||||
.expect("set sandbox policy");
|
||||
@@ -2149,6 +2153,33 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn session_permission_profile_materializes_runtime_workspace_roots() -> anyhow::Result<()> {
|
||||
let codex_home = tempfile::TempDir::new()?;
|
||||
let cwd = tempfile::TempDir::new()?;
|
||||
let extra_root = tempfile::TempDir::new()?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(crate::config::ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
|
||||
additional_writable_roots: vec![extra_root.path().to_path_buf()],
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
let session_permission_profile = session_permission_profile_from_config(&config)?;
|
||||
let file_system_policy = session_permission_profile
|
||||
.get()
|
||||
.file_system_sandbox_policy();
|
||||
|
||||
assert!(
|
||||
file_system_policy.can_write_path_with_cwd(extra_root.path(), config.cwd.as_path()),
|
||||
"session permission profile should carry materialized runtime workspace roots"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
@@ -2884,7 +2915,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -2988,7 +3019,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -3461,7 +3492,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -3994,7 +4025,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -4103,7 +4134,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -4150,7 +4181,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let services = SessionServices {
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
&config.permissions.approval_policy,
|
||||
&config.permissions.permission_profile,
|
||||
config.permissions.permission_profile(),
|
||||
))),
|
||||
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
|
||||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||||
@@ -4335,7 +4366,7 @@ async fn make_session_with_config_and_rx(
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -4438,7 +4469,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -5955,7 +5986,7 @@ where
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -6002,7 +6033,7 @@ where
|
||||
let services = SessionServices {
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
&config.permissions.approval_policy,
|
||||
&config.permissions.permission_profile,
|
||||
config.permissions.permission_profile(),
|
||||
))),
|
||||
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
|
||||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||||
|
||||
@@ -438,8 +438,12 @@ impl Session {
|
||||
per_turn_config.service_tier = session_configuration.service_tier.clone();
|
||||
per_turn_config.personality = session_configuration.personality;
|
||||
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
|
||||
per_turn_config.permissions.permission_profile =
|
||||
session_configuration.permission_profile.clone();
|
||||
per_turn_config
|
||||
.permissions
|
||||
.set_constrained_permission_profile_with_active_profile(
|
||||
session_configuration.permission_profile.clone(),
|
||||
session_configuration.active_permission_profile.clone(),
|
||||
);
|
||||
let permission_profile = session_configuration.permission_profile();
|
||||
let resolved_web_search_mode =
|
||||
resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &permission_profile);
|
||||
@@ -466,8 +470,6 @@ impl Session {
|
||||
Self::build_per_turn_config(session_configuration, session_configuration.cwd.clone());
|
||||
config.model = Some(session_configuration.collaboration_mode.model().to_string());
|
||||
config.permissions.approval_policy = session_configuration.approval_policy.clone();
|
||||
config.permissions.active_permission_profile =
|
||||
session_configuration.active_permission_profile.clone();
|
||||
config
|
||||
}
|
||||
|
||||
|
||||
@@ -2111,7 +2111,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
|
||||
turn.permission_profile = expected_permission_profile.clone();
|
||||
assert_ne!(
|
||||
expected_permission_profile,
|
||||
turn.config.permissions.permission_profile(),
|
||||
turn.config.permissions.effective_permission_profile(),
|
||||
"test requires a runtime profile override that differs from base config"
|
||||
);
|
||||
|
||||
@@ -3948,7 +3948,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
#[allow(deprecated)]
|
||||
let turn_cwd = turn.cwd.clone();
|
||||
let sandbox_policy = pick_allowed_sandbox_policy(
|
||||
&turn.config.permissions.permission_profile,
|
||||
turn.config.permissions.permission_profile(),
|
||||
turn.config.legacy_sandbox_policy(),
|
||||
turn_cwd.as_path(),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user