mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why AWS profiles can select credential helpers that the application executes. Granting write access to a containing directory should therefore keep `.aws` protected by default. ## What changed Add `.aws` to the protected metadata paths alongside `.git`, `.agents`, and `.codex`, including workspace roots and additional writable roots. Preserve explicit user rules that override the default protection. ## Testing Extend policy and sandbox tests to cover `.aws` protection. Add macOS coverage for existing and absent `.aws` directories, deletion and replacement attempts, and symlink targets. Verify that Linux sandbox writes to protected configuration files fail while sibling files remain writable. GitOrigin-RevId: 31ea287bd0dd9b13aff893783fba9ca3aed0c89d
205 lines
7.4 KiB
Rust
205 lines
7.4 KiB
Rust
//! Mounts for writable roots must preserve read-only metadata protections.
|
|
|
|
use super::LONG_TIMEOUT_MS;
|
|
use super::create_env_from_core_vars;
|
|
use super::run_cmd_result_with_cwd_and_writable_roots;
|
|
use super::run_cmd_result_with_permission_profile_for_cwd;
|
|
use super::should_skip_bwrap_tests;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::permissions::FileSystemAccessMode;
|
|
use codex_protocol::permissions::FileSystemPath;
|
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
enum RootLayout {
|
|
Directory,
|
|
Symlink,
|
|
}
|
|
|
|
#[test_case::test_case(RootLayout::Directory, ""; "home_cwd")]
|
|
#[test_case::test_case(RootLayout::Directory, ".codex/visualizations/thread"; "visualization_cwd")]
|
|
#[test_case::test_case(RootLayout::Symlink, ""; "symlink_home_cwd")]
|
|
#[test_case::test_case(RootLayout::Symlink, ".codex/visualizations/thread"; "symlink_visualization_cwd")]
|
|
#[tokio::test]
|
|
async fn sandbox_starts_with_nested_writable_metadata(layout: RootLayout, relative_cwd: &str) {
|
|
if should_skip_bwrap_tests().await {
|
|
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
|
|
return;
|
|
}
|
|
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let home =
|
|
AbsolutePathBuf::from_absolute_path(temp.path().join("home")).expect("absolute home");
|
|
match layout {
|
|
RootLayout::Directory => std::fs::create_dir(&home).expect("create home"),
|
|
RootLayout::Symlink => {
|
|
let target = temp.path().join("real-home");
|
|
std::fs::create_dir(&target).expect("create real home");
|
|
std::os::unix::fs::symlink(target, &home).expect("create home alias");
|
|
}
|
|
}
|
|
let codex_home = home.join(".codex");
|
|
let visualization = codex_home.join("visualizations/thread");
|
|
std::fs::create_dir_all(&visualization).expect("create visualization directory");
|
|
let protected_file = codex_home.join("config.toml");
|
|
std::fs::write(&protected_file, "unchanged").expect("write protected config");
|
|
|
|
let mut entries = vec![
|
|
FileSystemSandboxEntry::new(
|
|
FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
FileSystemAccessMode::Read,
|
|
),
|
|
FileSystemSandboxEntry::new(home.clone().into(), FileSystemAccessMode::Write),
|
|
FileSystemSandboxEntry::new(codex_home.clone().into(), FileSystemAccessMode::Read),
|
|
FileSystemSandboxEntry::new(visualization.clone().into(), FileSystemAccessMode::Write),
|
|
];
|
|
// Remote clients expand generated workspace metadata rules to concrete
|
|
// paths, including paths that do not yet exist on the executor.
|
|
for name in [".git", ".agents", ".codex", ".aws"] {
|
|
entries.push(FileSystemSandboxEntry::skip_missing_path(
|
|
visualization.join(name).into(),
|
|
FileSystemAccessMode::Read,
|
|
));
|
|
}
|
|
let permission_profile = PermissionProfile::from_runtime_permissions(
|
|
&FileSystemSandboxPolicy::restricted(entries),
|
|
NetworkSandboxPolicy::Enabled,
|
|
);
|
|
let script = r#"
|
|
set -eu
|
|
workspace="$1"
|
|
visualization="$2"
|
|
printf home > "$workspace/allowed.txt"
|
|
printf visualization > "$visualization/allowed.txt"
|
|
if (rm "$workspace") 2>/dev/null; then
|
|
exit 13
|
|
fi
|
|
if (printf changed > "$workspace/.codex/config.toml") 2>/dev/null; then
|
|
exit 10
|
|
fi
|
|
if (touch "$workspace/.codex/forbidden.txt") 2>/dev/null; then
|
|
exit 11
|
|
fi
|
|
for name in .git .agents .codex .aws; do
|
|
test -d "$visualization/$name"
|
|
if (touch "$visualization/$name/forbidden.txt") 2>/dev/null; then
|
|
exit 12
|
|
fi
|
|
done
|
|
printf nested-metadata-protected
|
|
"#;
|
|
let output = run_cmd_result_with_permission_profile_for_cwd(
|
|
&[
|
|
"/bin/sh",
|
|
"-c",
|
|
script,
|
|
"nested-metadata-test",
|
|
home.to_str().expect("UTF-8 home"),
|
|
visualization
|
|
.to_str()
|
|
.expect("UTF-8 visualization directory"),
|
|
],
|
|
home.join(relative_cwd),
|
|
permission_profile,
|
|
create_env_from_core_vars(),
|
|
LONG_TIMEOUT_MS,
|
|
/*use_legacy_landlock*/ false,
|
|
)
|
|
.await
|
|
.expect("nested writable root should start under bubblewrap");
|
|
|
|
assert_eq!(
|
|
(output.exit_code, output.stdout.text, output.stderr.text),
|
|
(0, "nested-metadata-protected".to_string(), String::new())
|
|
);
|
|
for (path, expected) in [
|
|
(home.join("allowed.txt"), "home"),
|
|
(visualization.join("allowed.txt"), "visualization"),
|
|
(protected_file, "unchanged"),
|
|
] {
|
|
assert_eq!(std::fs::read_to_string(path).expect("read file"), expected);
|
|
}
|
|
for name in [".git", ".agents", ".codex", ".aws"] {
|
|
assert!(
|
|
!visualization.join(name).exists(),
|
|
"temporary {name} mountpoint should be cleaned up"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test_case::test_case(RootLayout::Directory; "direct")]
|
|
#[test_case::test_case(RootLayout::Symlink; "alias_outside_writable_roots")]
|
|
#[tokio::test]
|
|
async fn workspace_write_protects_resolved_gitdir_in_another_writable_root(layout: RootLayout) {
|
|
if should_skip_bwrap_tests().await {
|
|
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
|
|
return;
|
|
}
|
|
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
// Roots at the same depth keep the workspace mount before the additional writable root.
|
|
let workspace = temp.path().join("workspace");
|
|
let writable = temp.path().join("writable");
|
|
let gitdir = writable.join("gitdir");
|
|
std::fs::create_dir(&workspace).expect("create workspace");
|
|
std::fs::create_dir_all(gitdir.join("hooks")).expect("create Git hooks directory");
|
|
let pointer_target = match layout {
|
|
RootLayout::Directory => gitdir.clone(),
|
|
RootLayout::Symlink => {
|
|
let aliases = temp.path().join("readonly-aliases");
|
|
std::fs::create_dir(&aliases).expect("create aliases directory");
|
|
let alias = aliases.join("repo");
|
|
std::os::unix::fs::symlink(&writable, &alias).expect("create gitdir alias");
|
|
alias.join("gitdir")
|
|
}
|
|
};
|
|
std::fs::write(
|
|
workspace.join(".git"),
|
|
format!("gitdir: {}\n", pointer_target.display()),
|
|
)
|
|
.expect("write Git pointer");
|
|
let hook = gitdir.join("hooks/pre-commit");
|
|
std::fs::write(&hook, "unchanged").expect("write Git hook");
|
|
|
|
let script = r#"
|
|
set -eu
|
|
printf allowed > "$1/allowed.txt"
|
|
if (printf changed > "$1/gitdir/hooks/pre-commit") 2>/dev/null; then
|
|
exit 10
|
|
fi
|
|
"#;
|
|
let output = run_cmd_result_with_cwd_and_writable_roots(
|
|
&[
|
|
"/bin/sh",
|
|
"-c",
|
|
script,
|
|
"resolved-gitdir-test",
|
|
writable.to_str().expect("UTF-8 writable root"),
|
|
],
|
|
&workspace,
|
|
std::slice::from_ref(&writable),
|
|
LONG_TIMEOUT_MS,
|
|
/*use_legacy_landlock*/ false,
|
|
/*network_access*/ true,
|
|
)
|
|
.await
|
|
.expect("sandboxed command should complete");
|
|
|
|
assert_eq!(
|
|
(output.exit_code, output.stdout.text, output.stderr.text),
|
|
(0, String::new(), String::new())
|
|
);
|
|
for (path, expected) in [
|
|
(writable.join("allowed.txt"), "allowed"),
|
|
(hook, "unchanged"),
|
|
] {
|
|
assert_eq!(std::fs::read_to_string(path).expect("read file"), expected);
|
|
}
|
|
}
|