Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Bolin
4172ee10c7 fix: stabilize SDK CI codex setup 2026-03-27 14:34:29 -07:00
8 changed files with 166 additions and 510 deletions

View File

@@ -33,11 +33,44 @@ jobs:
node-version: 22
cache: pnpm
- uses: dtolnay/rust-toolchain@1.93.0
- name: Set up Bazel CI
id: setup_bazel
uses: ./.github/actions/setup-bazel-ci
with:
target: x86_64-unknown-linux-gnu
- name: build codex
run: cargo build --bin codex
working-directory: codex-rs
- name: Build codex with Bazel
env:
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
shell: bash
run: |
./.github/scripts/run-bazel-ci.sh \
-- \
build \
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
--build_metadata=TAG_job=sdk \
-- \
//codex-rs/cli:codex
- name: Expose Bazel-built codex path
shell: bash
run: |
set -euo pipefail
codex_path="$(
bazel cquery \
--ui_event_filters=-info \
--noshow_progress \
--output=files \
//codex-rs/cli:codex \
| tail -n 1
)"
echo "CODEX_EXEC_PATH=$(realpath "${codex_path}")" >> "$GITHUB_ENV"
- name: Warm up Bazel-built codex
shell: bash
run: |
set -euo pipefail
"${CODEX_EXEC_PATH}" --version
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -50,3 +83,12 @@ jobs:
- name: Test SDK packages
run: pnpm -r --filter ./sdk/typescript run test
- name: Save bazel repository cache
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: |
~/.cache/bazel-repo-cache
key: bazel-cache-x86_64-unknown-linux-gnu-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}

View File

@@ -586,14 +586,13 @@ fn permissions_profiles_require_default_permissions() -> std::io::Result<()> {
}
#[test]
fn permissions_profiles_allow_writes_outside_workspace_root_with_read_only_legacy_fallback()
-> std::io::Result<()> {
fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
let external_write_path = if cfg!(windows) { r"C:\temp" } else { "/tmp" };
let config = Config::load_from_base_config_with_overrides(
let err = Config::load_from_base_config_with_overrides(
ConfigToml {
default_permissions: Some("workspace".to_string()),
permissions: Some(PermissionsToml {
@@ -617,45 +616,14 @@ fn permissions_profiles_allow_writes_outside_workspace_root_with_read_only_legac
..Default::default()
},
codex_home.path().to_path_buf(),
)?;
)
.expect_err("writes outside the workspace root should be rejected");
assert_eq!(
config.permissions.sandbox_policy.get(),
&SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: Vec::new(),
},
network_access: false,
}
);
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert!(
config
.permissions
.file_system_sandbox_policy
.can_write_path_with_cwd(Path::new(external_write_path), cwd.path())
);
assert!(
config
.permissions
.file_system_sandbox_policy
.needs_direct_runtime_enforcement(
config.permissions.network_sandbox_policy,
cwd.path(),
)
);
assert!(
config
.permissions
.file_system_sandbox_policy
.can_write_path_with_cwd(codex_home.path().join("memories").as_path(), cwd.path())
);
assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User);
let warnings = &config.startup_warnings;
assert!(
warnings.is_empty(),
"external writes should no longer fail config load: {warnings:?}"
err.to_string()
.contains("filesystem writes outside the workspace root"),
"{err}"
);
Ok(())
}
@@ -766,133 +734,6 @@ fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> {
Ok(())
}
#[test]
fn permissions_profiles_allow_tmpdir_write_with_read_only_legacy_fallback() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
default_permissions: Some("workspace".to_string()),
permissions: Some(PermissionsToml {
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
filesystem: Some(FilesystemPermissionsToml {
entries: BTreeMap::from([(
":tmpdir".to_string(),
FilesystemPermissionToml::Access(FileSystemAccessMode::Write),
)]),
}),
network: None,
},
)]),
}),
..Default::default()
},
ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
..Default::default()
},
codex_home.path().to_path_buf(),
)?;
let memories_root = codex_home.path().join("memories").abs();
assert!(
config
.permissions
.file_system_sandbox_policy
.entries
.iter()
.any(|entry| {
entry.access == FileSystemAccessMode::Write
&& entry.path
== FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
}
})
);
assert!(
config
.permissions
.file_system_sandbox_policy
.can_write_path_with_cwd(memories_root.as_path(), cwd.path())
);
assert_eq!(
config.permissions.sandbox_policy.get(),
&SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: Vec::new(),
},
network_access: false,
}
);
Ok(())
}
#[cfg(unix)]
#[test]
fn permissions_profiles_allow_slash_tmp_write_with_read_only_legacy_fallback() -> std::io::Result<()>
{
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
default_permissions: Some("workspace".to_string()),
permissions: Some(PermissionsToml {
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
filesystem: Some(FilesystemPermissionsToml {
entries: BTreeMap::from([(
"/tmp".to_string(),
FilesystemPermissionToml::Access(FileSystemAccessMode::Write),
)]),
}),
network: None,
},
)]),
}),
..Default::default()
},
ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
..Default::default()
},
codex_home.path().to_path_buf(),
)?;
assert!(
config
.permissions
.file_system_sandbox_policy
.entries
.iter()
.any(|entry| {
entry.access == FileSystemAccessMode::Write
&& entry.path
== FileSystemPath::Path {
path: test_absolute_path("/tmp"),
}
})
);
assert_eq!(
config.permissions.sandbox_policy.get(),
&SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: Vec::new(),
},
network_access: false,
}
);
Ok(())
}
#[test]
fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() -> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {

View File

@@ -2154,18 +2154,17 @@ impl Config {
default_permissions,
&mut startup_warnings,
)?;
if !file_system_sandbox_policy
.get_writable_roots_with_cwd(resolved_cwd.as_path())
.is_empty()
{
let mut sandbox_policy = file_system_sandbox_policy
.to_legacy_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,
);
sandbox_policy = file_system_sandbox_policy
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
}
let sandbox_policy = file_system_sandbox_policy
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
(
configured_network_proxy_config,
sandbox_policy,

View File

@@ -83,20 +83,7 @@ pub enum FileSystemSpecialPath {
#[ts(optional)]
subpath: Option<PathBuf>,
},
/// Config-facing `:tmpdir` special path.
///
/// This is the broader restricted-policy temp bundle used by filesystem
/// permission profiles. On Unix it expands to the temp env roots plus
/// `/tmp` and `/tmp`'s canonical target. On Windows it expands to the
/// platform temp env roots.
Tmpdir,
/// Legacy env-backed temp roots used when bridging old
/// `SandboxPolicy::WorkspaceWrite` semantics into split filesystem policy.
///
/// Keep this narrower than [`FileSystemSpecialPath::Tmpdir`] so existing
/// `exclude_tmpdir_env_var = false` behavior does not silently widen to the
/// full temp bundle on Unix.
TmpdirEnvVar,
SlashTmp,
/// WARNING: `:special_path` tokens are part of config compatibility.
/// Do not make older runtimes reject newly introduced tokens.
@@ -657,57 +644,50 @@ impl FileSystemSandboxPolicy {
FileSystemSpecialPath::CurrentWorkingDirectory => {
if entry.access.can_write() {
workspace_root_writable = true;
} else if entry.access.can_read() {
readable_roots.extend(resolve_file_system_special_paths(
} else if entry.access.can_read()
&& let Some(path) = resolve_file_system_special_path(
value,
cwd_absolute.as_ref(),
));
)
{
readable_roots.push(path);
}
}
FileSystemSpecialPath::ProjectRoots { subpath } => {
if subpath.is_none() && entry.access.can_write() {
workspace_root_writable = true;
} else {
let resolved_paths = resolve_file_system_special_paths(
value,
cwd_absolute.as_ref(),
);
} else if let Some(path) =
resolve_file_system_special_path(value, cwd_absolute.as_ref())
{
if entry.access.can_write() {
writable_roots.extend(resolved_paths);
writable_roots.push(path);
} else if entry.access.can_read() {
readable_roots.extend(resolved_paths);
readable_roots.push(path);
}
}
}
FileSystemSpecialPath::Tmpdir => {
if entry.access.can_write() {
tmpdir_writable = true;
slash_tmp_writable = true;
} else if entry.access.can_read() {
readable_roots.extend(resolve_file_system_special_paths(
} else if entry.access.can_read()
&& let Some(path) = resolve_file_system_special_path(
value,
cwd_absolute.as_ref(),
));
}
}
FileSystemSpecialPath::TmpdirEnvVar => {
if entry.access.can_write() {
tmpdir_writable = true;
} else if entry.access.can_read() {
readable_roots.extend(resolve_file_system_special_paths(
value,
cwd_absolute.as_ref(),
));
)
{
readable_roots.push(path);
}
}
FileSystemSpecialPath::SlashTmp => {
if entry.access.can_write() {
slash_tmp_writable = true;
} else if entry.access.can_read() {
readable_roots.extend(resolve_file_system_special_paths(
} else if entry.access.can_read()
&& let Some(path) = resolve_file_system_special_path(
value,
cwd_absolute.as_ref(),
));
)
{
readable_roots.push(path);
}
}
FileSystemSpecialPath::Unknown { .. } => {}
@@ -748,6 +728,11 @@ impl FileSystemSandboxPolicy {
exclude_tmpdir_env_var: !tmpdir_writable,
exclude_slash_tmp: !slash_tmp_writable,
}
} else if !writable_roots.is_empty() || tmpdir_writable || slash_tmp_writable {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"permissions profile requests filesystem writes outside the workspace root, which is not supported until the runtime enforces FileSystemSandboxPolicy directly",
));
} else {
SandboxPolicy::ReadOnly {
access: read_only_access,
@@ -762,13 +747,13 @@ impl FileSystemSandboxPolicy {
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
self.entries
.iter()
.flat_map(|entry| {
resolve_entry_paths(&entry.path, cwd_absolute.as_ref())
.into_iter()
.map(|path| ResolvedFileSystemEntry {
.filter_map(|entry| {
resolve_entry_path(&entry.path, cwd_absolute.as_ref()).map(|path| {
ResolvedFileSystemEntry {
path,
access: entry.access,
})
}
})
})
.collect()
}
@@ -890,7 +875,7 @@ impl From<&SandboxPolicy> for FileSystemSandboxPolicy {
if !exclude_tmpdir_env_var {
entries.push(FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::TmpdirEnvVar,
value: FileSystemSpecialPath::Tmpdir,
},
access: FileSystemAccessMode::Write,
});
@@ -913,21 +898,21 @@ impl From<&SandboxPolicy> for FileSystemSandboxPolicy {
fn resolve_file_system_path(
path: &FileSystemPath,
cwd: Option<&AbsolutePathBuf>,
) -> Vec<AbsolutePathBuf> {
) -> Option<AbsolutePathBuf> {
match path {
FileSystemPath::Path { path } => vec![path.clone()],
FileSystemPath::Special { value } => resolve_file_system_special_paths(value, cwd),
FileSystemPath::Path { path } => Some(path.clone()),
FileSystemPath::Special { value } => resolve_file_system_special_path(value, cwd),
}
}
fn resolve_entry_paths(
fn resolve_entry_path(
path: &FileSystemPath,
cwd: Option<&AbsolutePathBuf>,
) -> Vec<AbsolutePathBuf> {
) -> Option<AbsolutePathBuf> {
match path {
FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
} => cwd.map(absolute_root_path_for_cwd).into_iter().collect(),
} => cwd.map(absolute_root_path_for_cwd),
_ => resolve_file_system_path(path, cwd),
}
}
@@ -972,7 +957,6 @@ fn special_paths_share_target(left: &FileSystemSpecialPath, right: &FileSystemSp
FileSystemSpecialPath::CurrentWorkingDirectory,
)
| (FileSystemSpecialPath::Tmpdir, FileSystemSpecialPath::Tmpdir)
| (FileSystemSpecialPath::TmpdirEnvVar, FileSystemSpecialPath::TmpdirEnvVar)
| (FileSystemSpecialPath::SlashTmp, FileSystemSpecialPath::SlashTmp) => true,
(
FileSystemSpecialPath::CurrentWorkingDirectory,
@@ -1009,13 +993,11 @@ fn special_path_matches_absolute_path(
value: &FileSystemSpecialPath,
path: &AbsolutePathBuf,
) -> bool {
if matches!(value, FileSystemSpecialPath::Root) {
return path.as_path().parent().is_none();
match value {
FileSystemSpecialPath::Root => path.as_path().parent().is_none(),
FileSystemSpecialPath::SlashTmp => path.as_path() == Path::new("/tmp"),
_ => false,
}
resolve_cwd_independent_special_paths(value)
.into_iter()
.any(|candidate| candidate == *path)
}
/// Orders resolved entries so the most specific path wins first, then applies
@@ -1035,106 +1017,44 @@ fn absolute_root_path_for_cwd(cwd: &AbsolutePathBuf) -> AbsolutePathBuf {
.unwrap_or_else(|err| panic!("cwd root must be an absolute path: {err}"))
}
fn resolve_file_system_special_paths(
fn resolve_file_system_special_path(
value: &FileSystemSpecialPath,
cwd: Option<&AbsolutePathBuf>,
) -> Vec<AbsolutePathBuf> {
) -> Option<AbsolutePathBuf> {
match value {
FileSystemSpecialPath::Root
| FileSystemSpecialPath::Minimal
| FileSystemSpecialPath::Unknown { .. } => Vec::new(),
| FileSystemSpecialPath::Unknown { .. } => None,
FileSystemSpecialPath::CurrentWorkingDirectory => {
let Some(cwd) = cwd else {
return Vec::new();
};
vec![cwd.clone()]
let cwd = cwd?;
Some(cwd.clone())
}
FileSystemSpecialPath::ProjectRoots { subpath } => {
let Some(cwd) = cwd else {
return Vec::new();
};
let cwd = cwd?;
match subpath.as_ref() {
Some(subpath) => AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path())
.into_iter()
.collect(),
None => vec![cwd.clone()],
Some(subpath) => {
AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path()).ok()
}
None => Some(cwd.clone()),
}
}
FileSystemSpecialPath::Tmpdir => resolve_tmpdir_paths(),
FileSystemSpecialPath::TmpdirEnvVar => resolve_temp_env_paths(),
FileSystemSpecialPath::SlashTmp => resolve_slash_tmp_path().into_iter().collect(),
}
}
fn resolve_cwd_independent_special_paths(value: &FileSystemSpecialPath) -> Vec<AbsolutePathBuf> {
match value {
FileSystemSpecialPath::Root => Vec::new(),
FileSystemSpecialPath::Minimal
| FileSystemSpecialPath::CurrentWorkingDirectory
| FileSystemSpecialPath::ProjectRoots { .. }
| FileSystemSpecialPath::Unknown { .. } => Vec::new(),
FileSystemSpecialPath::Tmpdir => resolve_tmpdir_paths(),
FileSystemSpecialPath::TmpdirEnvVar => resolve_temp_env_paths(),
FileSystemSpecialPath::SlashTmp => resolve_slash_tmp_path().into_iter().collect(),
}
}
fn resolve_tmpdir_paths() -> Vec<AbsolutePathBuf> {
let mut paths = resolve_temp_env_paths();
if let Some(slash_tmp) = resolve_slash_tmp_path() {
paths.push(slash_tmp.clone());
if let Ok(realpath) = slash_tmp.as_path().canonicalize()
&& let Ok(realpath) = AbsolutePathBuf::from_absolute_path(realpath)
{
paths.push(realpath);
FileSystemSpecialPath::Tmpdir => {
let tmpdir = std::env::var_os("TMPDIR")?;
if tmpdir.is_empty() {
None
} else {
let tmpdir = AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok()?;
Some(tmpdir)
}
}
}
dedup_absolute_paths(paths, /*normalize_effective_paths*/ false)
}
pub(crate) fn resolve_temp_env_paths() -> Vec<AbsolutePathBuf> {
dedup_absolute_paths(
temp_env_var_keys()
.iter()
.filter_map(|key| resolve_temp_env_path(key))
.collect(),
/*normalize_effective_paths*/ false,
)
}
#[cfg(windows)]
const fn temp_env_var_keys() -> &'static [&'static str] {
&["TEMP", "TMP"]
}
#[cfg(not(windows))]
const fn temp_env_var_keys() -> &'static [&'static str] {
&["TMPDIR"]
}
fn resolve_temp_env_path(key: &str) -> Option<AbsolutePathBuf> {
let value = std::env::var_os(key)?;
if value.is_empty() {
None
} else {
AbsolutePathBuf::from_absolute_path(PathBuf::from(value)).ok()
}
}
fn resolve_slash_tmp_path() -> Option<AbsolutePathBuf> {
#[cfg(not(unix))]
{
None
}
#[cfg(unix)]
{
#[allow(clippy::expect_used)]
let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
if !slash_tmp.as_path().is_dir() {
return None;
FileSystemSpecialPath::SlashTmp => {
#[allow(clippy::expect_used)]
let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
if !slash_tmp.as_path().is_dir() {
return None;
}
Some(slash_tmp)
}
Some(slash_tmp)
}
}
@@ -1840,14 +1760,12 @@ mod tests {
#[cfg(unix)]
#[test]
fn tmpdir_env_var_special_path_canonicalizes_symlinked_tmpdir() {
fn tmpdir_special_path_canonicalizes_symlinked_tmpdir() {
if std::env::var_os(SYMLINKED_TMPDIR_TEST_ENV).is_none() {
let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
.env(SYMLINKED_TMPDIR_TEST_ENV, "1")
.arg("--exact")
.arg(
"permissions::tests::tmpdir_env_var_special_path_canonicalizes_symlinked_tmpdir",
)
.arg("permissions::tests::tmpdir_special_path_canonicalizes_symlinked_tmpdir")
.output()
.expect("run tmpdir subprocess test");
@@ -1894,7 +1812,7 @@ mod tests {
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::TmpdirEnvVar,
value: FileSystemSpecialPath::Tmpdir,
},
access: FileSystemAccessMode::Write,
},
@@ -1924,143 +1842,6 @@ mod tests {
);
}
#[cfg(unix)]
#[test]
fn tmpdir_special_path_includes_tmpdir_env_and_tmp_namespace() {
if std::env::var_os(SYMLINKED_TMPDIR_TEST_ENV).is_none() {
let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
.env(SYMLINKED_TMPDIR_TEST_ENV, "1")
.arg("--exact")
.arg(
"permissions::tests::tmpdir_special_path_includes_tmpdir_env_and_tmp_namespace",
)
.output()
.expect("run tmpdir subprocess test");
assert!(
output.status.success(),
"tmpdir subprocess test failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return;
}
let cwd = TempDir::new().expect("tempdir");
let real_tmpdir = cwd.path().join("real-tmpdir");
let link_tmpdir = cwd.path().join("link-tmpdir");
fs::create_dir_all(&real_tmpdir).expect("create real tmpdir");
symlink_dir(&real_tmpdir, &link_tmpdir).expect("create symlinked tmpdir");
unsafe {
std::env::set_var("TMPDIR", &link_tmpdir);
}
let slash_tmp =
AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp should be absolute");
let slash_tmp_realpath = AbsolutePathBuf::from_absolute_path(
slash_tmp
.as_path()
.canonicalize()
.expect("canonicalize /tmp"),
)
.expect("absolute canonical /tmp");
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
},
access: FileSystemAccessMode::Write,
}]);
assert!(policy.can_write_path_with_cwd(link_tmpdir.as_path(), cwd.path()));
assert!(policy.can_write_path_with_cwd(Path::new("/tmp/codex-protocol"), cwd.path()));
assert!(
policy.can_write_path_with_cwd(
slash_tmp_realpath
.join("codex-protocol")
.expect("valid tmp child")
.as_path(),
cwd.path(),
)
);
let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
assert!(writable_roots.iter().any(|root| {
root.root
== AbsolutePathBuf::from_absolute_path(
real_tmpdir.canonicalize().expect("canonicalize tmpdir"),
)
.expect("absolute canonical tmpdir")
}));
assert!(
writable_roots
.iter()
.any(|root| root.root == slash_tmp_realpath)
);
}
#[test]
fn tmpdir_write_without_workspace_root_falls_back_to_read_only_legacy_policy()
-> std::io::Result<()> {
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
},
access: FileSystemAccessMode::Write,
}]);
let sandbox_policy = policy.to_legacy_sandbox_policy(
NetworkSandboxPolicy::Restricted,
Path::new("/tmp/workspace"),
)?;
assert_eq!(
sandbox_policy,
SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: Vec::new(),
},
network_access: false,
}
);
assert!(policy.needs_direct_runtime_enforcement(
NetworkSandboxPolicy::Restricted,
Path::new("/tmp/workspace"),
));
Ok(())
}
#[test]
fn legacy_workspace_write_uses_tmpdir_env_var_special_path() {
let file_system_policy = FileSystemSandboxPolicy::from(&SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: Vec::new(),
},
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: true,
});
assert!(file_system_policy.entries.iter().any(|entry| {
entry.access == FileSystemAccessMode::Write
&& entry.path
== FileSystemPath::Special {
value: FileSystemSpecialPath::TmpdirEnvVar,
}
}));
assert!(!file_system_policy.entries.iter().any(|entry| {
entry.access == FileSystemAccessMode::Write
&& entry.path
== FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
}
}));
}
#[test]
fn resolve_access_with_cwd_uses_most_specific_entry() {
let cwd = TempDir::new().expect("tempdir");

View File

@@ -79,7 +79,6 @@ pub use crate::permissions::FileSystemSandboxKind;
pub use crate::permissions::FileSystemSandboxPolicy;
pub use crate::permissions::FileSystemSpecialPath;
pub use crate::permissions::NetworkSandboxPolicy;
use crate::permissions::resolve_temp_env_paths;
pub use crate::request_permissions::RequestPermissionsArgs;
pub use crate::request_user_input::RequestUserInputEvent;
@@ -1048,12 +1047,28 @@ impl SandboxPolicy {
}
}
// Include platform temp env roots unless explicitly excluded.
// On Unix, this keeps the legacy TMPDIR behavior. On Windows,
// this picks up TEMP/TMP so legacy workspace-write policies
// keep matching the host temp directory.
if !exclude_tmpdir_env_var {
roots.extend(resolve_temp_env_paths());
// Include $TMPDIR unless explicitly excluded. On macOS, TMPDIR
// is per-user, so writes to TMPDIR should not be readable by
// other users on the system.
//
// By comparison, TMPDIR is not guaranteed to be defined on
// Linux or Windows, but supporting it here gives users a way to
// provide the model with their own temporary directory without
// having to hardcode it in the config.
if !exclude_tmpdir_env_var
&& let Some(tmpdir) = std::env::var_os("TMPDIR")
&& !tmpdir.is_empty()
{
match AbsolutePathBuf::from_absolute_path(PathBuf::from(&tmpdir)) {
Ok(tmpdir_path) => {
roots.push(tmpdir_path);
}
Err(e) => {
error!(
"Ignoring invalid TMPDIR value {tmpdir:?} for sandbox writable root: {e}",
);
}
}
}
// For each root, compute subpaths that should remain read-only.
@@ -4166,7 +4181,7 @@ mod tests {
}
#[test]
fn file_system_policy_falls_back_to_read_only_legacy_bridge_for_non_workspace_writes() {
fn file_system_policy_rejects_legacy_bridge_for_non_workspace_writes() {
let cwd = if cfg!(windows) {
Path::new(r"C:\workspace")
} else {
@@ -4184,19 +4199,14 @@ mod tests {
access: FileSystemAccessMode::Write,
}]);
let legacy_policy = policy
let err = policy
.to_legacy_sandbox_policy(NetworkSandboxPolicy::Restricted, cwd)
.expect("non-workspace writes should fall back to read-only");
.expect_err("non-workspace writes should be rejected");
assert_eq!(
legacy_policy,
SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: Vec::new(),
},
network_access: false,
}
assert!(
err.to_string()
.contains("filesystem writes outside the workspace root"),
"{err}"
);
}

View File

@@ -90,6 +90,12 @@
(allow file-read* file-test-existence file-write-data file-ioctl
(literal "/dev/dtracehelper"))
; Scratch space so tools can create temp files.
(allow file-read* file-test-existence file-write* (subpath "/tmp"))
(allow file-read* file-write* (subpath "/private/tmp"))
(allow file-read* file-write* (subpath "/var/tmp"))
(allow file-read* file-write* (subpath "/private/var/tmp"))
; Allow reading standard config directories.
(allow file-read* (subpath "/etc"))
(allow file-read* (subpath "/private/etc"))

View File

@@ -60,31 +60,6 @@ fn base_policy_allows_node_cpu_sysctls() {
);
}
#[test]
fn restricted_platform_defaults_do_not_include_tmp_write_rules() {
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Minimal,
},
access: FileSystemAccessMode::Read,
}]);
let args = create_seatbelt_command_args_for_policies(
vec!["/bin/true".to_string()],
&file_system_policy,
NetworkSandboxPolicy::Restricted,
Path::new("/"),
false,
None,
);
let policy = seatbelt_policy_arg(&args);
assert!(!policy.contains("(subpath \"/tmp\")"));
assert!(!policy.contains("(subpath \"/private/tmp\")"));
assert!(!policy.contains("(subpath \"/var/tmp\")"));
assert!(!policy.contains("(subpath \"/private/var/tmp\")"));
}
#[test]
fn create_seatbelt_args_routes_network_through_proxy_ports() {
let policy = dynamic_network_policy(

View File

@@ -3,7 +3,9 @@ import path from "node:path";
import { Codex } from "../src/codex";
import type { CodexConfigObject } from "../src/codexOptions";
export const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
export const codexExecPath =
process.env.CODEX_EXEC_PATH ??
path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
type CreateTestClientOptions = {
apiKey?: string;