mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
## Why Windows and WSL can share `CODEX_HOME` without sharing a lock namespace, allowing cleanup to remove another platform's live helpers. Older clients can also clean helpers stored under the shared `tmp/arg0` root. ## What changed Create helpers under `CODEX_HOME/tmp/arg0-<os>` and limit stale-directory cleanup to that operating system's root. Keep permission discovery compatible with both the current platform's root and legacy `tmp/arg0` directories, while ignoring other platforms' aliases. ## Testing Add regression coverage for cleanup isolation, live helper preservation during legacy cleanup, and selection of the first session alias directory from either supported root. GitOrigin-RevId: f34292748b5b1d3c25629604d849db4b133dfa3a
656 lines
19 KiB
Rust
656 lines
19 KiB
Rust
use super::*;
|
|
use codex_config::permissions_toml::FilesystemPermissionToml;
|
|
use codex_config::permissions_toml::FilesystemPermissionsToml;
|
|
use codex_config::permissions_toml::NetworkDomainPermissionToml;
|
|
use codex_config::permissions_toml::NetworkDomainPermissionsToml;
|
|
use codex_config::permissions_toml::NetworkToml;
|
|
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;
|
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
|
use codex_utils_path_uri::PathConvention;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::BTreeMap;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn active_arg0_helper_dir_selects_only_the_first_session_alias_directory() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let platform_root = home
|
|
.path()
|
|
.join("tmp")
|
|
.join(format!("arg0-{}", std::env::consts::OS));
|
|
let legacy_root = home.path().join("tmp/arg0");
|
|
let foreign_os = if cfg!(windows) { "linux" } else { "windows" };
|
|
let foreign_alias = home
|
|
.path()
|
|
.join("tmp")
|
|
.join(format!("arg0-{foreign_os}"))
|
|
.join("codex-arg0-foreign");
|
|
for arg0_root in [&platform_root, &legacy_root] {
|
|
let active = arg0_root.join("codex-arg0-active");
|
|
let paths = std::env::join_paths([
|
|
home.path().join("bin"),
|
|
foreign_alias.clone(),
|
|
arg0_root.clone(),
|
|
arg0_root.join("unrelated"),
|
|
active.clone(),
|
|
arg0_root.join("codex-arg0-parent"),
|
|
])?;
|
|
assert_eq!(
|
|
active_arg0_helper_dir(home.path(), &paths),
|
|
Some(AbsolutePathBuf::from_absolute_path(active)?)
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn windows_verbatim_path_prefix_does_not_count_as_glob_syntax() {
|
|
assert!(!contains_glob_chars_for_platform(
|
|
r"\\?\D:\c\x\worktrees\2508\swift-base",
|
|
/*is_windows*/ true,
|
|
));
|
|
assert!(contains_glob_chars_for_platform(
|
|
r"\\?\D:\c\x\worktrees\2508\**\*.env",
|
|
/*is_windows*/ true,
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn network_toml_ignores_legacy_network_list_keys() {
|
|
let parsed = toml::from_str::<NetworkToml>(
|
|
r#"
|
|
allowed_domains = ["openai.com"]
|
|
"#,
|
|
)
|
|
.expect("legacy network list keys should be ignored");
|
|
|
|
assert_eq!(parsed, NetworkToml::default());
|
|
}
|
|
|
|
#[test]
|
|
fn network_permission_containers_project_allowed_and_denied_entries() {
|
|
let domains = NetworkDomainPermissionsToml {
|
|
entries: BTreeMap::from([
|
|
(
|
|
"*.openai.com".to_string(),
|
|
NetworkDomainPermissionToml::Allow,
|
|
),
|
|
(
|
|
"api.example.com".to_string(),
|
|
NetworkDomainPermissionToml::Allow,
|
|
),
|
|
(
|
|
"blocked.example.com".to_string(),
|
|
NetworkDomainPermissionToml::Deny,
|
|
),
|
|
]),
|
|
};
|
|
let unix_sockets = NetworkUnixSocketPermissionsToml {
|
|
entries: BTreeMap::from([
|
|
(
|
|
"/tmp/example.sock".to_string(),
|
|
NetworkUnixSocketPermissionToml::Allow,
|
|
),
|
|
(
|
|
"/tmp/ignored.sock".to_string(),
|
|
NetworkUnixSocketPermissionToml::Deny,
|
|
),
|
|
]),
|
|
};
|
|
|
|
assert_eq!(
|
|
domains.allowed_domains(),
|
|
Some(vec![
|
|
"*.openai.com".to_string(),
|
|
"api.example.com".to_string()
|
|
])
|
|
);
|
|
assert_eq!(
|
|
domains.denied_domains(),
|
|
Some(vec!["blocked.example.com".to_string()])
|
|
);
|
|
assert_eq!(
|
|
NetworkDomainPermissionsToml {
|
|
entries: BTreeMap::from([(
|
|
"api.example.com".to_string(),
|
|
NetworkDomainPermissionToml::Allow,
|
|
)]),
|
|
}
|
|
.denied_domains(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
unix_sockets.allow_unix_sockets(),
|
|
vec!["/tmp/example.sock".to_string()]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn network_toml_overlays_unix_socket_permissions_by_path() {
|
|
let mut config = NetworkProxyConfig::default();
|
|
|
|
NetworkToml {
|
|
unix_sockets: Some(NetworkUnixSocketPermissionsToml {
|
|
entries: BTreeMap::from([
|
|
(
|
|
"/tmp/base.sock".to_string(),
|
|
NetworkUnixSocketPermissionToml::Allow,
|
|
),
|
|
(
|
|
"/tmp/override.sock".to_string(),
|
|
NetworkUnixSocketPermissionToml::Allow,
|
|
),
|
|
]),
|
|
}),
|
|
..Default::default()
|
|
}
|
|
.apply_to_network_proxy_config(&mut config);
|
|
|
|
NetworkToml {
|
|
unix_sockets: Some(NetworkUnixSocketPermissionsToml {
|
|
entries: BTreeMap::from([
|
|
(
|
|
"/tmp/extra.sock".to_string(),
|
|
NetworkUnixSocketPermissionToml::Allow,
|
|
),
|
|
(
|
|
"/tmp/override.sock".to_string(),
|
|
NetworkUnixSocketPermissionToml::Deny,
|
|
),
|
|
]),
|
|
}),
|
|
..Default::default()
|
|
}
|
|
.apply_to_network_proxy_config(&mut config);
|
|
|
|
assert_eq!(
|
|
config.unix_sockets,
|
|
Some(codex_network_proxy::NetworkUnixSocketPermissions {
|
|
entries: BTreeMap::from([
|
|
(
|
|
"/tmp/base.sock".to_string(),
|
|
ProxyNetworkUnixSocketPermission::Allow,
|
|
),
|
|
(
|
|
"/tmp/extra.sock".to_string(),
|
|
ProxyNetworkUnixSocketPermission::Allow,
|
|
),
|
|
(
|
|
"/tmp/override.sock".to_string(),
|
|
ProxyNetworkUnixSocketPermission::Deny,
|
|
),
|
|
]),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn permissions_profiles_resolve_extends_parent_first_with_child_overrides() {
|
|
let permissions = toml::from_str::<PermissionsToml>(
|
|
r#"
|
|
[base]
|
|
description = "Base profile"
|
|
|
|
[base.filesystem]
|
|
glob_scan_max_depth = 1
|
|
"/tmp/base" = "read"
|
|
"/tmp/shared" = "read"
|
|
|
|
[base.filesystem.":project_roots"]
|
|
"**/*.env" = "deny"
|
|
docs = "read"
|
|
|
|
[base.network]
|
|
enabled = true
|
|
|
|
[base.network.domains]
|
|
"base.example.com" = "allow"
|
|
"SHARED.EXAMPLE.COM." = "deny"
|
|
|
|
[base.network.unix_sockets]
|
|
"/tmp/base.sock" = "allow"
|
|
"/tmp/blocked.sock" = "deny"
|
|
|
|
[child]
|
|
extends = "base"
|
|
|
|
[child.filesystem]
|
|
glob_scan_max_depth = 3
|
|
"/tmp/shared" = "write"
|
|
|
|
[child.filesystem.":project_roots"]
|
|
docs = "write"
|
|
src = "read"
|
|
|
|
[child.network]
|
|
enabled = false
|
|
allow_local_binding = true
|
|
|
|
[child.network.domains]
|
|
"child.example.com" = "allow"
|
|
"shared.example.com" = "allow"
|
|
|
|
[child.network.unix_sockets]
|
|
"/tmp/child.sock" = "allow"
|
|
"#,
|
|
)
|
|
.expect("permissions should deserialize");
|
|
|
|
let resolved = permissions
|
|
.resolve_profile("child", |_| None)
|
|
.expect("child profile should resolve");
|
|
let expected_profile = toml::from_str::<PermissionProfileToml>(
|
|
r#"
|
|
extends = "base"
|
|
|
|
[filesystem]
|
|
glob_scan_max_depth = 3
|
|
"/tmp/base" = "read"
|
|
"/tmp/shared" = "write"
|
|
|
|
[filesystem.":project_roots"]
|
|
"**/*.env" = "deny"
|
|
docs = "write"
|
|
src = "read"
|
|
|
|
[network]
|
|
enabled = false
|
|
allow_local_binding = true
|
|
|
|
[network.domains]
|
|
"base.example.com" = "allow"
|
|
"child.example.com" = "allow"
|
|
"shared.example.com" = "allow"
|
|
|
|
[network.unix_sockets]
|
|
"/tmp/base.sock" = "allow"
|
|
"/tmp/blocked.sock" = "deny"
|
|
"/tmp/child.sock" = "allow"
|
|
"#,
|
|
)
|
|
.expect("expected profile should deserialize");
|
|
|
|
assert_eq!(resolved, expected_profile);
|
|
}
|
|
|
|
#[test]
|
|
fn permissions_profiles_reject_undefined_extends_parent() {
|
|
let permissions = toml::from_str::<PermissionsToml>(
|
|
r#"
|
|
[child]
|
|
extends = "base"
|
|
"#,
|
|
)
|
|
.expect("permissions should deserialize");
|
|
|
|
let err = permissions
|
|
.resolve_profile("child", |_| None)
|
|
.expect_err("missing parent should be rejected");
|
|
|
|
assert_eq!(
|
|
err.to_string(),
|
|
"permissions profile `child` extends undefined profile `base`"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn permissions_profiles_reject_unsupported_builtin_extends_parent() {
|
|
let permissions = toml::from_str::<PermissionsToml>(
|
|
r#"
|
|
[child]
|
|
extends = ":danger-full-access"
|
|
"#,
|
|
)
|
|
.expect("permissions should deserialize");
|
|
|
|
let err = permissions
|
|
.resolve_profile("child", |_| None)
|
|
.expect_err("unsupported built-in parent should be rejected");
|
|
|
|
assert_eq!(
|
|
err.to_string(),
|
|
"permissions profile `child` cannot extend unsupported built-in profile `:danger-full-access`"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn permissions_profiles_reject_extends_cycles() {
|
|
let permissions = toml::from_str::<PermissionsToml>(
|
|
r#"
|
|
[alpha]
|
|
extends = "beta"
|
|
|
|
[beta]
|
|
extends = "alpha"
|
|
"#,
|
|
)
|
|
.expect("permissions should deserialize");
|
|
|
|
let err = permissions
|
|
.resolve_profile("alpha", |_| None)
|
|
.expect_err("cycle should be rejected");
|
|
|
|
assert_eq!(
|
|
err.to_string(),
|
|
"permissions profile inheritance cycle detected: alpha -> beta -> alpha"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_network_proxy_config_keeps_proxy_disabled_for_bare_network_access() {
|
|
let config = network_proxy_config_from_profile_network(Some(&NetworkToml {
|
|
enabled: Some(true),
|
|
..Default::default()
|
|
}));
|
|
|
|
assert!(!config.enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_network_proxy_config_keeps_proxy_disabled_for_proxy_policy() {
|
|
let config = network_proxy_config_from_profile_network(Some(&NetworkToml {
|
|
enabled: Some(true),
|
|
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
|
enable_socks5: Some(false),
|
|
domains: Some(NetworkDomainPermissionsToml {
|
|
entries: BTreeMap::from([(
|
|
"openai.com".to_string(),
|
|
NetworkDomainPermissionToml::Allow,
|
|
)]),
|
|
}),
|
|
..Default::default()
|
|
}));
|
|
|
|
assert!(!config.enabled);
|
|
assert_eq!(config.proxy_url, "http://127.0.0.1:43128");
|
|
assert!(!config.enable_socks5);
|
|
assert_eq!(
|
|
config.domains,
|
|
Some(codex_network_proxy::NetworkDomainPermissions {
|
|
entries: vec![codex_network_proxy::NetworkDomainPermissionEntry {
|
|
pattern: "openai.com".to_string(),
|
|
permission: codex_network_proxy::NetworkDomainPermission::Allow,
|
|
}],
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compile_permission_profile_resolves_enabled_workspace_roots() -> std::io::Result<()> {
|
|
let cwd = TempDir::new()?;
|
|
let compiled = compile_permission_profile(
|
|
Some(&PermissionsToml {
|
|
entries: BTreeMap::from([(
|
|
"workspace".to_string(),
|
|
PermissionProfileToml {
|
|
description: None,
|
|
extends: None,
|
|
workspace_roots: Some(WorkspaceRootsToml {
|
|
entries: BTreeMap::from([
|
|
("backend".to_string(), true),
|
|
("disabled".to_string(), false),
|
|
]),
|
|
}),
|
|
filesystem: None,
|
|
network: None,
|
|
},
|
|
)]),
|
|
}),
|
|
"workspace",
|
|
&ConfigPathContext::new(
|
|
PathConvention::native(),
|
|
Some(PathUri::from_host_native_path(cwd.path())?),
|
|
/*user_home_dir*/ None,
|
|
),
|
|
/*workspace_write*/ None,
|
|
&mut Vec::new(),
|
|
)?;
|
|
|
|
assert_eq!(
|
|
compiled.workspace_roots,
|
|
vec![
|
|
PathUri::from_host_native_path(cwd.path())?
|
|
.join("backend")
|
|
.unwrap()
|
|
]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_project_roots_restrictions_do_not_fail_open() -> std::io::Result<()> {
|
|
let permissions = toml::from_str::<PermissionsToml>(
|
|
r#"
|
|
[read_deny.filesystem]
|
|
":root" = "read"
|
|
":project_roots" = "none"
|
|
|
|
[write_deny.filesystem]
|
|
":root" = "write"
|
|
":project_roots" = "none"
|
|
|
|
[write_read.filesystem]
|
|
":root" = "write"
|
|
|
|
[write_read.filesystem.":project_roots"]
|
|
docs = "read"
|
|
"#,
|
|
)
|
|
.expect("legacy project roots profiles should deserialize");
|
|
let cwd = TempDir::new()?;
|
|
let docs = cwd.path().join("docs");
|
|
let mut startup_warnings = Vec::new();
|
|
let context = ConfigPathContext::new(
|
|
PathConvention::native(),
|
|
Some(PathUri::from_host_native_path(cwd.path())?),
|
|
/*user_home_dir*/ None,
|
|
);
|
|
|
|
let read_deny_policy = compile_permission_profile(
|
|
Some(&permissions),
|
|
"read_deny",
|
|
&context,
|
|
/*workspace_write*/ None,
|
|
&mut startup_warnings,
|
|
)?
|
|
.permission_profile
|
|
.file_system_sandbox_policy();
|
|
assert_eq!(
|
|
read_deny_policy.resolve_access_for_local_path_with_cwd(cwd.path(), cwd.path()),
|
|
FileSystemAccessMode::Deny
|
|
);
|
|
|
|
let write_deny_policy = compile_permission_profile(
|
|
Some(&permissions),
|
|
"write_deny",
|
|
&context,
|
|
/*workspace_write*/ None,
|
|
&mut startup_warnings,
|
|
)?
|
|
.permission_profile
|
|
.file_system_sandbox_policy();
|
|
assert!(!write_deny_policy.has_full_disk_write_access());
|
|
assert_eq!(
|
|
write_deny_policy.resolve_access_for_local_path_with_cwd(cwd.path(), cwd.path()),
|
|
FileSystemAccessMode::Deny
|
|
);
|
|
|
|
let write_read_policy = compile_permission_profile(
|
|
Some(&permissions),
|
|
"write_read",
|
|
&context,
|
|
/*workspace_write*/ None,
|
|
&mut startup_warnings,
|
|
)?
|
|
.permission_profile
|
|
.file_system_sandbox_policy();
|
|
assert!(!write_read_policy.has_full_disk_write_access());
|
|
assert_eq!(
|
|
write_read_policy.resolve_access_for_local_path_with_cwd(&docs, cwd.path()),
|
|
FileSystemAccessMode::Read
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths() {
|
|
let filesystem = FilesystemPermissionsToml {
|
|
glob_scan_max_depth: None,
|
|
entries: BTreeMap::from([
|
|
(
|
|
"/tmp/**/*.log".to_string(),
|
|
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
|
),
|
|
(
|
|
"/tmp/cache/**".to_string(),
|
|
FilesystemPermissionToml::Access(FileSystemAccessMode::Write),
|
|
),
|
|
(
|
|
":workspace_roots".to_string(),
|
|
FilesystemPermissionToml::Scoped(BTreeMap::from([
|
|
("**/*.env".to_string(), FileSystemAccessMode::Deny),
|
|
("docs/**".to_string(), FileSystemAccessMode::Read),
|
|
("src/**/*.rs".to_string(), FileSystemAccessMode::Write),
|
|
])),
|
|
),
|
|
]),
|
|
};
|
|
|
|
assert_eq!(
|
|
unsupported_read_write_glob_paths(
|
|
&filesystem,
|
|
&ConfigPathContext::new(
|
|
PathConvention::native(),
|
|
/*base_dir*/ None,
|
|
/*user_home_dir*/ None,
|
|
)
|
|
)
|
|
.unwrap(),
|
|
vec![
|
|
"/tmp/**/*.log".to_string(),
|
|
":workspace_roots/src/**/*.rs".to_string()
|
|
],
|
|
"`deny` glob patterns are supported as deny-read rules; only `read`/`write` globs should warn"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unreadable_globstar_warning_is_suppressed_when_scan_depth_is_configured() {
|
|
let filesystem = FilesystemPermissionsToml {
|
|
glob_scan_max_depth: None,
|
|
entries: BTreeMap::from([(
|
|
":workspace_roots".to_string(),
|
|
FilesystemPermissionToml::Scoped(BTreeMap::from([
|
|
("**/*.env".to_string(), FileSystemAccessMode::Deny),
|
|
("*.pem".to_string(), FileSystemAccessMode::Deny),
|
|
])),
|
|
)]),
|
|
};
|
|
|
|
assert_eq!(
|
|
unbounded_unreadable_globstar_paths(&filesystem),
|
|
vec![":workspace_roots/**/*.env".to_string()]
|
|
);
|
|
|
|
let configured_filesystem = FilesystemPermissionsToml {
|
|
glob_scan_max_depth: Some(2),
|
|
..filesystem
|
|
};
|
|
assert_eq!(
|
|
unbounded_unreadable_globstar_paths(&configured_filesystem),
|
|
Vec::<String>::new()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn glob_scan_max_depth_must_be_positive() {
|
|
let err = validate_glob_scan_max_depth(Some(0))
|
|
.expect_err("zero depth would silently skip deny-read glob expansion");
|
|
|
|
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
|
assert_eq!(err.to_string(), "glob_scan_max_depth must be at least 1");
|
|
assert_eq!(
|
|
validate_glob_scan_max_depth(Some(2)).expect("depth should be valid"),
|
|
Some(2)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()> {
|
|
let mut startup_warnings = Vec::new();
|
|
let compiled = compile_permission_profile(
|
|
Some(&PermissionsToml {
|
|
entries: BTreeMap::from([(
|
|
"workspace".to_string(),
|
|
PermissionProfileToml {
|
|
description: None,
|
|
extends: None,
|
|
workspace_roots: None,
|
|
filesystem: Some(FilesystemPermissionsToml {
|
|
glob_scan_max_depth: None,
|
|
entries: BTreeMap::from([(
|
|
":workspace_roots".to_string(),
|
|
FilesystemPermissionToml::Scoped(BTreeMap::from([(
|
|
"docs/**".to_string(),
|
|
FileSystemAccessMode::Read,
|
|
)])),
|
|
)]),
|
|
}),
|
|
network: None,
|
|
},
|
|
)]),
|
|
}),
|
|
"workspace",
|
|
&ConfigPathContext::new(
|
|
PathConvention::native(),
|
|
/*base_dir*/ None,
|
|
/*user_home_dir*/ None,
|
|
),
|
|
/*workspace_write*/ None,
|
|
&mut startup_warnings,
|
|
)?;
|
|
|
|
assert_eq!(
|
|
compiled.permission_profile.file_system_sandbox_policy(),
|
|
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::project_roots(Some("docs".into())),
|
|
},
|
|
access: FileSystemAccessMode::Read,
|
|
missing_path_behavior: None,
|
|
}]),
|
|
"trailing /** should compile as a subtree path instead of a glob pattern"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_write_glob_patterns_still_reject_non_subpath_globs() {
|
|
let err = compile_read_write_glob_path(
|
|
"src/**/*.rs",
|
|
FileSystemAccessMode::Read,
|
|
&ConfigPathContext::new(
|
|
PathConvention::native(),
|
|
/*base_dir*/ None,
|
|
/*user_home_dir*/ None,
|
|
),
|
|
)
|
|
.expect_err("non-subpath read/write glob should be rejected");
|
|
|
|
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
|
assert!(
|
|
err.to_string()
|
|
.contains("filesystem glob path `src/**/*.rs` only supports `deny` access"),
|
|
"{err}"
|
|
);
|
|
}
|