mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## What changed - Pass the selected executor's `PathUri` through user command approval requests instead of converting the working directory to a host-native path. - Make native-path permission helpers explicitly local and require executor-aware permission transforms to receive a `FileSystemSandboxPolicyContext`. - Cover approved remote commands to verify that the approval request reports the selected remote working directory and execution resumes after approval. GitOrigin-RevId: 0aa3042f2d7594af6c7d0abf31d7f99cf67d5556
361 lines
13 KiB
Rust
361 lines
13 KiB
Rust
use super::editor_directory;
|
|
#[cfg(unix)]
|
|
use super::run_editor;
|
|
use codex_protocol::permissions::FileSystemAccessMode;
|
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use pretty_assertions::assert_eq;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use tempfile::TempDir;
|
|
|
|
struct EditorPaths {
|
|
_root: TempDir,
|
|
codex_home: PathBuf,
|
|
cwd: PathBuf,
|
|
}
|
|
|
|
impl EditorPaths {
|
|
fn new() -> Self {
|
|
let root = tempfile::tempdir().expect("create editor test root");
|
|
let canonical_root =
|
|
dunce::canonicalize(root.path()).expect("canonicalize editor test root");
|
|
let codex_home = canonical_root.join("codex-home");
|
|
let cwd = canonical_root.join("workspace");
|
|
fs::create_dir(&codex_home).expect("create Codex home");
|
|
fs::create_dir(&cwd).expect("create workspace");
|
|
|
|
Self {
|
|
_root: root,
|
|
codex_home,
|
|
cwd,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn workspace_write_policy(writable_roots: &[&Path]) -> FileSystemSandboxPolicy {
|
|
let writable_roots = writable_roots
|
|
.iter()
|
|
.map(|root| AbsolutePathBuf::from_absolute_path(root).expect("absolute writable root"))
|
|
.collect::<Vec<_>>();
|
|
|
|
FileSystemSandboxPolicy::workspace_write(
|
|
&writable_roots,
|
|
/*exclude_tmpdir_env_var*/ true,
|
|
/*exclude_slash_tmp*/ true,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn editor_directory_is_inside_isolated_codex_home() {
|
|
let paths = EditorPaths::new();
|
|
let policy = workspace_write_policy(&[]);
|
|
|
|
let directory = editor_directory(&[&paths.codex_home], &policy, &paths.cwd)
|
|
.expect("create isolated editor directory");
|
|
|
|
assert_eq!(directory, paths.codex_home.join("editor"));
|
|
assert!(directory.is_dir());
|
|
}
|
|
|
|
#[test]
|
|
fn editor_directory_rejects_writable_home_editor_and_parent() {
|
|
let paths = EditorPaths::new();
|
|
let editor = paths.codex_home.join("editor");
|
|
fs::create_dir(&editor).expect("create editor directory");
|
|
let parent = paths.codex_home.parent().expect("Codex home parent");
|
|
|
|
for writable_root in [paths.codex_home.as_path(), editor.as_path(), parent] {
|
|
let policy = workspace_write_policy(&[writable_root]);
|
|
|
|
assert!(
|
|
editor_directory(&[&paths.codex_home], &policy, &paths.cwd).is_err(),
|
|
"writable root {} must not expose editor buffers",
|
|
writable_root.display()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn editor_directory_rejects_writable_descendant() {
|
|
let paths = EditorPaths::new();
|
|
let writable_descendant = paths.codex_home.join("editor").join("nested");
|
|
fs::create_dir_all(&writable_descendant).expect("create writable editor descendant");
|
|
let policy = workspace_write_policy(&[&writable_descendant]);
|
|
|
|
assert!(editor_directory(&[&paths.codex_home], &policy, &paths.cwd).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn editor_directory_rejects_read_only_carveout_with_writable_parent() {
|
|
let paths = EditorPaths::new();
|
|
let editor = paths.codex_home.join("editor");
|
|
fs::create_dir(&editor).expect("create editor directory");
|
|
let policy = FileSystemSandboxPolicy::restricted(vec![
|
|
FileSystemSandboxEntry::new(
|
|
AbsolutePathBuf::from_absolute_path(&paths.codex_home)
|
|
.expect("absolute Codex home")
|
|
.into(),
|
|
FileSystemAccessMode::Write,
|
|
),
|
|
FileSystemSandboxEntry::new(
|
|
AbsolutePathBuf::from_absolute_path(&editor)
|
|
.expect("absolute editor directory")
|
|
.into(),
|
|
FileSystemAccessMode::Read,
|
|
),
|
|
]);
|
|
|
|
assert!(editor_directory(&[&paths.codex_home], &policy, &paths.cwd).is_err());
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn editor_directory_rejects_preexisting_symlink() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let paths = EditorPaths::new();
|
|
let outside = paths.cwd.join("outside");
|
|
fs::create_dir(&outside).expect("create editor symlink target");
|
|
symlink(&outside, paths.codex_home.join("editor")).expect("create editor directory symlink");
|
|
let policy = FileSystemSandboxPolicy::read_only();
|
|
|
|
assert!(editor_directory(&[&paths.codex_home], &policy, &paths.cwd).is_err());
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn editor_directory_rejects_writable_codex_home_alias() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let paths = EditorPaths::new();
|
|
let aliased_home = paths.cwd.join("codex-home-link");
|
|
symlink(&paths.codex_home, &aliased_home).expect("create Codex home symlink");
|
|
let policy = workspace_write_policy(&[]);
|
|
|
|
assert!(policy.can_write_local_path_with_cwd(&aliased_home.join("editor"), &paths.cwd));
|
|
assert!(!policy.can_write_local_path_with_cwd(&paths.codex_home.join("editor"), &paths.cwd));
|
|
assert!(editor_directory(&[&aliased_home], &policy, &paths.cwd).is_err());
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn editor_directory_rejects_writable_codex_home_alias_target() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let paths = EditorPaths::new();
|
|
let alias_parent = paths
|
|
.codex_home
|
|
.parent()
|
|
.expect("Codex home parent")
|
|
.join("aliases");
|
|
fs::create_dir(&alias_parent).expect("create protected alias parent");
|
|
let aliased_home = alias_parent.join("codex-home-link");
|
|
symlink(&paths.codex_home, &aliased_home).expect("create Codex home symlink");
|
|
let policy = workspace_write_policy(&[&paths.codex_home]);
|
|
|
|
assert!(!policy.can_write_local_path_with_cwd(&aliased_home.join("editor"), &paths.cwd));
|
|
assert!(policy.can_write_local_path_with_cwd(&paths.codex_home.join("editor"), &paths.cwd));
|
|
assert!(editor_directory(&[&aliased_home], &policy, &paths.cwd).is_err());
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn editor_directory_uses_protected_workspace_fallback_with_default_temporary_grants() {
|
|
let root = tempfile::tempdir().expect("create editor test root");
|
|
let codex_home = root.path().join("codex-home");
|
|
let cwd = root.path().join("workspace");
|
|
fs::create_dir(&codex_home).expect("create Codex home");
|
|
fs::create_dir(&cwd).expect("create workspace");
|
|
let workspace_codex_home = cwd.join(".codex");
|
|
let policy = FileSystemSandboxPolicy::workspace_write(
|
|
&[],
|
|
/*exclude_tmpdir_env_var*/ false,
|
|
/*exclude_slash_tmp*/ false,
|
|
);
|
|
|
|
assert!(!workspace_codex_home.exists());
|
|
assert!(policy.can_write_local_path_with_cwd(&codex_home, &cwd));
|
|
assert!(!policy.can_write_local_path_with_cwd(&workspace_codex_home, &cwd));
|
|
assert!(!policy.can_write_local_path_with_cwd(&workspace_codex_home.join("editor"), &cwd));
|
|
|
|
let directory = editor_directory(&[&codex_home, &workspace_codex_home], &policy, &cwd)
|
|
.expect("use protected workspace metadata directory");
|
|
|
|
assert_eq!(
|
|
directory,
|
|
dunce::canonicalize(&workspace_codex_home)
|
|
.expect("canonicalize workspace metadata directory")
|
|
.join("editor")
|
|
);
|
|
assert!(directory.is_dir());
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn editor_directory_rejects_explicitly_writable_workspace_fallback() {
|
|
let root = tempfile::tempdir().expect("create editor test root");
|
|
let codex_home = root.path().join("codex-home");
|
|
let cwd = root.path().join("workspace");
|
|
fs::create_dir(&codex_home).expect("create Codex home");
|
|
fs::create_dir(&cwd).expect("create workspace");
|
|
let workspace_codex_home = cwd.join(".codex");
|
|
let writable_workspace_codex_home = AbsolutePathBuf::from_absolute_path(&workspace_codex_home)
|
|
.expect("absolute workspace metadata directory");
|
|
let policy = FileSystemSandboxPolicy::workspace_write(
|
|
&[writable_workspace_codex_home],
|
|
/*exclude_tmpdir_env_var*/ false,
|
|
/*exclude_slash_tmp*/ false,
|
|
);
|
|
|
|
assert!(policy.can_write_local_path_with_cwd(&codex_home, &cwd));
|
|
assert!(policy.can_write_local_path_with_cwd(&workspace_codex_home, &cwd));
|
|
assert!(
|
|
editor_directory(&[&codex_home, &workspace_codex_home], &policy, &cwd).is_err(),
|
|
"explicitly writable metadata must not be used for editor buffers"
|
|
);
|
|
assert!(!workspace_codex_home.exists());
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn editor_directory_rejects_workspace_fallback_symlink_to_writable_target() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let paths = EditorPaths::new();
|
|
let workspace_codex_home = paths.cwd.join(".codex");
|
|
symlink(&paths.codex_home, &workspace_codex_home).expect("create workspace metadata symlink");
|
|
let policy = workspace_write_policy(&[&paths.codex_home]);
|
|
|
|
assert!(!policy.can_write_local_path_with_cwd(&workspace_codex_home, &paths.cwd));
|
|
assert!(policy.can_write_local_path_with_cwd(&paths.codex_home, &paths.cwd));
|
|
assert!(
|
|
editor_directory(
|
|
&[&paths.codex_home, &workspace_codex_home],
|
|
&policy,
|
|
&paths.cwd,
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn editor_directory_uses_next_protected_candidate_after_creation_error() {
|
|
let paths = EditorPaths::new();
|
|
let unavailable_home = paths
|
|
.codex_home
|
|
.parent()
|
|
.expect("Codex home parent")
|
|
.join("unavailable-home");
|
|
fs::write(&unavailable_home, "not a directory").expect("create unavailable Codex home");
|
|
let policy = workspace_write_policy(&[]);
|
|
|
|
let directory = editor_directory(&[&unavailable_home, &paths.codex_home], &policy, &paths.cwd)
|
|
.expect("use next protected candidate after directory creation fails");
|
|
|
|
assert_eq!(directory, paths.codex_home.join("editor"));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(windows)]
|
|
fn editor_directory_rejects_windows_temporary_directory_outside_tmpdir_policy_root() {
|
|
let paths = EditorPaths::new();
|
|
let policy = FileSystemSandboxPolicy::workspace_write(
|
|
&[],
|
|
/*exclude_tmpdir_env_var*/ false,
|
|
/*exclude_slash_tmp*/ true,
|
|
);
|
|
|
|
assert!(
|
|
editor_directory(&[&paths.codex_home], &policy, &paths.cwd).is_err(),
|
|
"effective Windows temporary directories must not contain editor buffers"
|
|
);
|
|
assert!(!paths.codex_home.join("editor").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn editor_directory_allows_full_disk_write_policies() {
|
|
let paths = EditorPaths::new();
|
|
|
|
for policy in [
|
|
FileSystemSandboxPolicy::unrestricted(),
|
|
FileSystemSandboxPolicy::external_sandbox(),
|
|
] {
|
|
let directory = editor_directory(&[&paths.codex_home], &policy, &paths.cwd)
|
|
.expect("full-disk-write policies should preserve external editor support");
|
|
|
|
assert_eq!(directory, paths.codex_home.join("editor"));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(unix)]
|
|
async fn editor_process_receives_buffer_in_isolated_codex_home() {
|
|
let paths = EditorPaths::new();
|
|
let policy = workspace_write_policy(&[]);
|
|
let editor_directory = paths.codex_home.join("editor");
|
|
let editor_command = vec![
|
|
"/bin/sh".to_string(),
|
|
"-c".to_string(),
|
|
"case \"$2\" in \"$1\"/*) printf edited > \"$2\" ;; *) exit 88 ;; esac".to_string(),
|
|
"editor".to_string(),
|
|
editor_directory.to_string_lossy().into_owned(),
|
|
];
|
|
|
|
let content = run_editor(
|
|
"seed",
|
|
&editor_command,
|
|
&paths.codex_home,
|
|
&policy,
|
|
&paths.cwd,
|
|
)
|
|
.await
|
|
.expect("run editor with isolated buffer");
|
|
|
|
assert_eq!(content, "edited");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
|
async fn editor_process_uses_protected_workspace_fallback_with_default_temporary_grants() {
|
|
let root = tempfile::tempdir().expect("create editor test root");
|
|
let codex_home = root.path().join("codex-home");
|
|
let cwd = root.path().join("workspace");
|
|
fs::create_dir(&codex_home).expect("create Codex home");
|
|
fs::create_dir(&cwd).expect("create workspace");
|
|
let default_codex_home = dirs::home_dir().expect("home directory").join(".codex");
|
|
let writable_default_codex_home = AbsolutePathBuf::from_absolute_path(&default_codex_home)
|
|
.expect("absolute default Codex home");
|
|
let policy = FileSystemSandboxPolicy::workspace_write(
|
|
&[writable_default_codex_home],
|
|
/*exclude_tmpdir_env_var*/ false,
|
|
/*exclude_slash_tmp*/ false,
|
|
);
|
|
let workspace_codex_home = cwd.join(".codex");
|
|
let editor_directory = dunce::canonicalize(&cwd)
|
|
.expect("canonicalize workspace")
|
|
.join(".codex")
|
|
.join("editor");
|
|
let editor_command = vec![
|
|
"/bin/sh".to_string(),
|
|
"-c".to_string(),
|
|
"case \"$2\" in \"$1\"/*) printf edited > \"$2\" ;; *) exit 88 ;; esac".to_string(),
|
|
"editor".to_string(),
|
|
editor_directory.to_string_lossy().into_owned(),
|
|
];
|
|
|
|
assert!(!workspace_codex_home.exists());
|
|
assert!(policy.can_write_local_path_with_cwd(&codex_home, &cwd));
|
|
assert!(policy.can_write_local_path_with_cwd(&default_codex_home, &cwd));
|
|
assert!(!policy.can_write_local_path_with_cwd(&workspace_codex_home, &cwd));
|
|
|
|
let content = run_editor("seed", &editor_command, &codex_home, &policy, &cwd)
|
|
.await
|
|
.expect("run editor with protected workspace fallback");
|
|
|
|
assert_eq!(content, "edited");
|
|
assert!(editor_directory.is_dir());
|
|
}
|