mirror of
https://github.com/openai/codex.git
synced 2026-03-23 08:36:30 +03:00
Compare commits
1 Commits
starr/exec
...
dh--sandbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d619f32b7 |
@@ -885,6 +885,9 @@ impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
},
|
||||
codex_protocol::protocol::SandboxPolicy::Custom { .. } => {
|
||||
panic!("SandboxPolicy::Custom is internal-only and not supported in app-server v2")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +451,13 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
SandboxPolicy::ExternalSandbox { .. } => {
|
||||
SandboxModeRequirement::ExternalSandbox
|
||||
}
|
||||
SandboxPolicy::Custom { writable_roots, .. } => {
|
||||
if writable_roots.is_empty() {
|
||||
SandboxModeRequirement::ReadOnly
|
||||
} else {
|
||||
SandboxModeRequirement::WorkspaceWrite
|
||||
}
|
||||
}
|
||||
};
|
||||
if modes.contains(&mode) {
|
||||
Ok(())
|
||||
|
||||
@@ -493,7 +493,9 @@ pub fn render_decision_for_unmatched_command(
|
||||
// command has not been flagged as dangerous.
|
||||
Decision::Allow
|
||||
}
|
||||
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
SandboxPolicy::ReadOnly { .. }
|
||||
| SandboxPolicy::WorkspaceWrite { .. }
|
||||
| SandboxPolicy::Custom { .. } => {
|
||||
// In restricted sandboxes (ReadOnly/WorkspaceWrite), do not prompt for
|
||||
// non‑escalated, non‑dangerous commands — let the sandbox enforce
|
||||
// restrictions (e.g., block network/write) without a user prompt.
|
||||
@@ -511,7 +513,9 @@ pub fn render_decision_for_unmatched_command(
|
||||
// by `prompt_is_rejected_by_policy`.
|
||||
Decision::Allow
|
||||
}
|
||||
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
SandboxPolicy::ReadOnly { .. }
|
||||
| SandboxPolicy::WorkspaceWrite { .. }
|
||||
| SandboxPolicy::Custom { .. } => {
|
||||
if sandbox_permissions.requires_escalated_permissions() {
|
||||
Decision::Prompt
|
||||
} else {
|
||||
|
||||
@@ -133,7 +133,9 @@ fn is_write_patch_constrained_to_writable_paths(
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
return true;
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => sandbox_policy.get_writable_roots_with_cwd(cwd),
|
||||
SandboxPolicy::WorkspaceWrite { .. } | SandboxPolicy::Custom { .. } => {
|
||||
sandbox_policy.get_writable_roots_with_cwd(cwd)
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize a path by removing `.` and resolving `..` without touching the
|
||||
|
||||
@@ -310,6 +310,7 @@ fn sandbox_policy_tag(policy: &SandboxPolicy) -> &'static str {
|
||||
match policy {
|
||||
SandboxPolicy::ReadOnly { .. } => "read-only",
|
||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
|
||||
SandboxPolicy::Custom { .. } => "custom",
|
||||
SandboxPolicy::DangerFullAccess => "danger-full-access",
|
||||
SandboxPolicy::ExternalSandbox { .. } => "external-sandbox",
|
||||
}
|
||||
|
||||
@@ -321,6 +321,14 @@ impl DeveloperInstructions {
|
||||
let roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
(SandboxMode::WorkspaceWrite, Some(roots))
|
||||
}
|
||||
SandboxPolicy::Custom { .. } => {
|
||||
let roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
if roots.is_empty() {
|
||||
(SandboxMode::ReadOnly, None)
|
||||
} else {
|
||||
(SandboxMode::WorkspaceWrite, Some(roots))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DeveloperInstructions::from_permissions_with_network(
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
@@ -610,6 +611,25 @@ pub enum SandboxPolicy {
|
||||
#[serde(default)]
|
||||
exclude_slash_tmp: bool,
|
||||
},
|
||||
|
||||
/// Internal-only exact path policy with explicit read/write roots.
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
/// Read access granted while running under this policy.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "ReadOnlyAccess::has_full_disk_read_access"
|
||||
)]
|
||||
read_only_access: ReadOnlyAccess,
|
||||
|
||||
/// Exact writable roots and custom read-only subpaths under each root.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
writable_roots: Vec<CustomWritableRoot>,
|
||||
|
||||
/// Whether outbound network access is allowed.
|
||||
#[serde(default)]
|
||||
network_access: NetworkAccess,
|
||||
},
|
||||
}
|
||||
|
||||
/// A writable root path accompanied by a list of subpaths that should remain
|
||||
@@ -625,6 +645,16 @@ pub struct WritableRoot {
|
||||
pub read_only_subpaths: Vec<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
/// Declarative writable root configuration for [`SandboxPolicy::Custom`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct CustomWritableRoot {
|
||||
pub root: AbsolutePathBuf,
|
||||
|
||||
/// Relative subpaths under `root` that should remain read-only.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub read_only_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl WritableRoot {
|
||||
pub fn is_path_writable(&self, path: &Path) -> bool {
|
||||
// Check if the path is under the root.
|
||||
@@ -643,6 +673,90 @@ impl WritableRoot {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_relative_subpath(path: &Path) -> Option<PathBuf> {
|
||||
if path.as_os_str().is_empty() || path.is_absolute() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::CurDir => {}
|
||||
Component::Normal(part) => normalized.push(part),
|
||||
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
if normalized.as_os_str().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_writable_root(
|
||||
writable_root: AbsolutePathBuf,
|
||||
explicit_read_only_paths: &[PathBuf],
|
||||
) -> WritableRoot {
|
||||
let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for read_only_path in explicit_read_only_paths {
|
||||
let Some(normalized) = normalize_relative_subpath(read_only_path) else {
|
||||
error!(
|
||||
"Ignoring invalid custom read-only subpath {:?} under {}",
|
||||
read_only_path,
|
||||
writable_root.as_path().display()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
#[allow(clippy::expect_used)]
|
||||
let absolute_subpath = writable_root
|
||||
.join(normalized)
|
||||
.expect("normalized relative path is valid");
|
||||
if seen.insert(absolute_subpath.clone()) {
|
||||
subpaths.push(absolute_subpath);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_git = writable_root
|
||||
.join(".git")
|
||||
.expect(".git is a valid relative path");
|
||||
// This applies to typical repos (directory .git), worktrees/submodules
|
||||
// (file .git with gitdir pointer), and bare repos when the gitdir is the
|
||||
// writable root itself.
|
||||
let top_level_git_is_file = top_level_git.as_path().is_file();
|
||||
let top_level_git_is_dir = top_level_git.as_path().is_dir();
|
||||
if top_level_git_is_dir || top_level_git_is_file {
|
||||
if top_level_git_is_file
|
||||
&& is_git_pointer_file(&top_level_git)
|
||||
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
|
||||
&& seen.insert(gitdir.clone())
|
||||
{
|
||||
subpaths.push(gitdir);
|
||||
}
|
||||
if seen.insert(top_level_git.clone()) {
|
||||
subpaths.push(top_level_git);
|
||||
}
|
||||
}
|
||||
|
||||
// Make .agents/skills and .codex/config.toml and related files read-only
|
||||
// to the agent, by default.
|
||||
for subdir in &[".agents", ".codex"] {
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_codex = writable_root.join(subdir).expect("valid relative path");
|
||||
if top_level_codex.as_path().is_dir() && seen.insert(top_level_codex.clone()) {
|
||||
subpaths.push(top_level_codex);
|
||||
}
|
||||
}
|
||||
|
||||
WritableRoot {
|
||||
root: writable_root,
|
||||
read_only_subpaths: subpaths,
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SandboxPolicy {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
@@ -680,6 +794,9 @@ impl SandboxPolicy {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
read_only_access, ..
|
||||
} => read_only_access.has_full_disk_read_access(),
|
||||
SandboxPolicy::Custom {
|
||||
read_only_access, ..
|
||||
} => read_only_access.has_full_disk_read_access(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,6 +806,7 @@ impl SandboxPolicy {
|
||||
SandboxPolicy::ExternalSandbox { .. } => true,
|
||||
SandboxPolicy::ReadOnly { .. } => false,
|
||||
SandboxPolicy::WorkspaceWrite { .. } => false,
|
||||
SandboxPolicy::Custom { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,6 +816,7 @@ impl SandboxPolicy {
|
||||
SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(),
|
||||
SandboxPolicy::ReadOnly { .. } => false,
|
||||
SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
|
||||
SandboxPolicy::Custom { network_access, .. } => network_access.is_enabled(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,6 +830,9 @@ impl SandboxPolicy {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
read_only_access, ..
|
||||
} => read_only_access.include_platform_defaults(),
|
||||
SandboxPolicy::Custom {
|
||||
read_only_access, ..
|
||||
} => read_only_access.include_platform_defaults(),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => false,
|
||||
}
|
||||
}
|
||||
@@ -726,6 +848,9 @@ impl SandboxPolicy {
|
||||
SandboxPolicy::ReadOnly { access } => access.get_readable_roots_with_cwd(cwd),
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
read_only_access, ..
|
||||
}
|
||||
| SandboxPolicy::Custom {
|
||||
read_only_access, ..
|
||||
} => {
|
||||
let mut roots = read_only_access.get_readable_roots_with_cwd(cwd);
|
||||
roots.extend(
|
||||
@@ -812,48 +937,19 @@ impl SandboxPolicy {
|
||||
// For each root, compute subpaths that should remain read-only.
|
||||
roots
|
||||
.into_iter()
|
||||
.map(|writable_root| {
|
||||
let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_git = writable_root
|
||||
.join(".git")
|
||||
.expect(".git is a valid relative path");
|
||||
// This applies to typical repos (directory .git), worktrees/submodules
|
||||
// (file .git with gitdir pointer), and bare repos when the gitdir is the
|
||||
// writable root itself.
|
||||
let top_level_git_is_file = top_level_git.as_path().is_file();
|
||||
let top_level_git_is_dir = top_level_git.as_path().is_dir();
|
||||
if top_level_git_is_dir || top_level_git_is_file {
|
||||
if top_level_git_is_file
|
||||
&& is_git_pointer_file(&top_level_git)
|
||||
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
|
||||
&& !subpaths
|
||||
.iter()
|
||||
.any(|subpath| subpath.as_path() == gitdir.as_path())
|
||||
{
|
||||
subpaths.push(gitdir);
|
||||
}
|
||||
subpaths.push(top_level_git);
|
||||
}
|
||||
|
||||
// Make .agents/skills and .codex/config.toml and
|
||||
// related files read-only to the agent, by default.
|
||||
for subdir in &[".agents", ".codex"] {
|
||||
#[allow(clippy::expect_used)]
|
||||
let top_level_codex =
|
||||
writable_root.join(subdir).expect("valid relative path");
|
||||
if top_level_codex.as_path().is_dir() {
|
||||
subpaths.push(top_level_codex);
|
||||
}
|
||||
}
|
||||
|
||||
WritableRoot {
|
||||
root: writable_root,
|
||||
read_only_subpaths: subpaths,
|
||||
}
|
||||
})
|
||||
.map(|writable_root| build_writable_root(writable_root, &[]))
|
||||
.collect()
|
||||
}
|
||||
SandboxPolicy::Custom {
|
||||
writable_roots,
|
||||
read_only_access: _,
|
||||
network_access: _,
|
||||
} => writable_roots
|
||||
.iter()
|
||||
.map(|writable_root| {
|
||||
build_writable_root(writable_root.root.clone(), &writable_root.read_only_paths)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3071,6 +3167,79 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_writable_roots_are_readable_and_exact_only() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let custom_root_path = tmp.path().join("custom");
|
||||
std::fs::create_dir_all(&custom_root_path).expect("custom root");
|
||||
let custom_root = AbsolutePathBuf::try_from(custom_root_path.as_path()).expect("absolute");
|
||||
let cwd = tmp.path().join("cwd");
|
||||
std::fs::create_dir_all(&cwd).expect("cwd");
|
||||
|
||||
let policy = SandboxPolicy::Custom {
|
||||
read_only_access: ReadOnlyAccess::FullAccess,
|
||||
writable_roots: vec![CustomWritableRoot {
|
||||
root: custom_root.clone(),
|
||||
read_only_paths: vec![PathBuf::from("Cargo.lock")],
|
||||
}],
|
||||
network_access: NetworkAccess::Restricted,
|
||||
};
|
||||
|
||||
let writable_roots = policy.get_writable_roots_with_cwd(&cwd);
|
||||
assert_eq!(writable_roots.len(), 1);
|
||||
assert_eq!(writable_roots[0].root, custom_root);
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.iter()
|
||||
.any(|path| path.as_path() == custom_root_path.join("Cargo.lock"))
|
||||
);
|
||||
|
||||
let readable_roots = policy.get_readable_roots_with_cwd(&cwd);
|
||||
assert!(
|
||||
readable_roots
|
||||
.iter()
|
||||
.any(|path| path.as_path() == custom_root_path)
|
||||
);
|
||||
assert!(!readable_roots.iter().any(|path| path.as_path() == cwd));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_ignores_invalid_read_only_subpaths() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let root_path = tmp.path().join("custom");
|
||||
std::fs::create_dir_all(&root_path).expect("custom root");
|
||||
let root = AbsolutePathBuf::try_from(root_path.as_path()).expect("absolute");
|
||||
|
||||
let policy = SandboxPolicy::Custom {
|
||||
read_only_access: ReadOnlyAccess::FullAccess,
|
||||
writable_roots: vec![CustomWritableRoot {
|
||||
root: root,
|
||||
read_only_paths: vec![
|
||||
PathBuf::from("."),
|
||||
PathBuf::from("../escape"),
|
||||
PathBuf::from("/absolute"),
|
||||
PathBuf::from("ok"),
|
||||
],
|
||||
}],
|
||||
network_access: NetworkAccess::Enabled,
|
||||
};
|
||||
|
||||
let writable_roots = policy.get_writable_roots_with_cwd(tmp.path());
|
||||
assert_eq!(writable_roots.len(), 1);
|
||||
let subpaths = &writable_roots[0].read_only_subpaths;
|
||||
assert!(
|
||||
subpaths
|
||||
.iter()
|
||||
.any(|path| path.as_path() == root_path.join("ok"))
|
||||
);
|
||||
assert!(
|
||||
!subpaths
|
||||
.iter()
|
||||
.any(|path| path.as_path() == root_path.join("escape"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_started_event_from_web_search_emits_begin_event() {
|
||||
let event = ItemStartedEvent {
|
||||
|
||||
@@ -16,7 +16,9 @@ pub fn add_dir_warning_message(
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
| SandboxPolicy::DangerFullAccess
|
||||
| SandboxPolicy::ExternalSandbox { .. } => None,
|
||||
SandboxPolicy::ReadOnly { .. } => Some(format_warning(additional_dirs)),
|
||||
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::Custom { .. } => {
|
||||
Some(format_warning(additional_dirs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,13 @@ impl StatusHistoryCell {
|
||||
..
|
||||
} => "workspace-write with network access".to_string(),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
|
||||
SandboxPolicy::Custom { network_access, .. } => {
|
||||
if matches!(network_access, NetworkAccess::Enabled) {
|
||||
"custom sandbox with network access".to_string()
|
||||
} else {
|
||||
"custom sandbox".to_string()
|
||||
}
|
||||
}
|
||||
SandboxPolicy::ExternalSandbox { network_access } => {
|
||||
if matches!(network_access, NetworkAccess::Enabled) {
|
||||
"external-sandbox (network access enabled)".to_string()
|
||||
|
||||
@@ -41,12 +41,31 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
|
||||
}
|
||||
summary
|
||||
}
|
||||
SandboxPolicy::Custom {
|
||||
writable_roots,
|
||||
network_access,
|
||||
read_only_access: _,
|
||||
} => {
|
||||
let mut summary = "custom".to_string();
|
||||
if !writable_roots.is_empty() {
|
||||
let writable_entries = writable_roots
|
||||
.iter()
|
||||
.map(|root| root.root.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
summary.push_str(&format!(" [{}]", writable_entries.join(", ")));
|
||||
}
|
||||
if matches!(network_access, NetworkAccess::Enabled) {
|
||||
summary.push_str(" (network access enabled)");
|
||||
}
|
||||
summary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::protocol::CustomWritableRoot;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -85,4 +104,25 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_summary_lists_explicit_roots_without_workspace_defaults() {
|
||||
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
|
||||
let writable_root = AbsolutePathBuf::try_from(root).unwrap();
|
||||
let summary = summarize_sandbox_policy(&SandboxPolicy::Custom {
|
||||
read_only_access: Default::default(),
|
||||
writable_roots: vec![CustomWritableRoot {
|
||||
root: writable_root.clone(),
|
||||
read_only_paths: vec![],
|
||||
}],
|
||||
network_access: NetworkAccess::Enabled,
|
||||
});
|
||||
assert_eq!(
|
||||
summary,
|
||||
format!(
|
||||
"custom [{}] (network access enabled)",
|
||||
writable_root.to_string_lossy()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,13 @@ pub fn compute_allow_paths(
|
||||
}
|
||||
);
|
||||
|
||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
if matches!(
|
||||
policy,
|
||||
SandboxPolicy::WorkspaceWrite { .. } | SandboxPolicy::Custom { .. }
|
||||
) {
|
||||
let add_writable_root =
|
||||
|root: PathBuf,
|
||||
read_only_subpaths: &[PathBuf],
|
||||
policy_cwd: &Path,
|
||||
add_allow: &mut dyn FnMut(PathBuf),
|
||||
add_deny: &mut dyn FnMut(PathBuf)| {
|
||||
@@ -52,30 +56,34 @@ pub fn compute_allow_paths(
|
||||
let canonical = canonicalize(&candidate).unwrap_or(candidate);
|
||||
add_allow(canonical.clone());
|
||||
|
||||
for protected_subdir in [".git", ".codex", ".agents"] {
|
||||
let protected_entry = canonical.join(protected_subdir);
|
||||
if protected_entry.exists() {
|
||||
add_deny(protected_entry);
|
||||
}
|
||||
for read_only_subpath in read_only_subpaths {
|
||||
add_deny(canonicalize(read_only_subpath).unwrap_or_else(|_| read_only_subpath.clone()));
|
||||
}
|
||||
};
|
||||
|
||||
add_writable_root(
|
||||
command_cwd.to_path_buf(),
|
||||
policy_cwd,
|
||||
&mut add_allow_path,
|
||||
&mut add_deny_path,
|
||||
);
|
||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
add_writable_root(
|
||||
command_cwd.to_path_buf(),
|
||||
&[],
|
||||
policy_cwd,
|
||||
&mut add_allow_path,
|
||||
&mut add_deny_path,
|
||||
);
|
||||
}
|
||||
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy {
|
||||
for root in writable_roots {
|
||||
add_writable_root(
|
||||
root.clone().into(),
|
||||
policy_cwd,
|
||||
&mut add_allow_path,
|
||||
&mut add_deny_path,
|
||||
);
|
||||
}
|
||||
for writable_root in policy.get_writable_roots_with_cwd(policy_cwd) {
|
||||
let read_only_subpaths: Vec<PathBuf> = writable_root
|
||||
.read_only_subpaths
|
||||
.iter()
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect();
|
||||
add_writable_root(
|
||||
writable_root.root.to_path_buf(),
|
||||
&read_only_subpaths,
|
||||
policy_cwd,
|
||||
&mut add_allow_path,
|
||||
&mut add_deny_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
if include_tmp_env_vars {
|
||||
|
||||
@@ -251,15 +251,32 @@ pub fn apply_capability_denies_for_world_writable(
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
std::fs::write(&cap_path, serde_json::to_string(&caps)?)?;
|
||||
let (active_sid, workspace_roots): (*mut c_void, Vec<PathBuf>) = match sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let sid = unsafe { convert_string_sid_to_sid(&caps.workspace) }
|
||||
.ok_or_else(|| anyhow!("ConvertStringSidToSidW failed for workspace capability"))?;
|
||||
let mut roots: Vec<PathBuf> =
|
||||
vec![dunce::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf())];
|
||||
for root in writable_roots {
|
||||
let candidate = root.as_path();
|
||||
roots.push(dunce::canonicalize(candidate).unwrap_or_else(|_| root.to_path_buf()));
|
||||
}
|
||||
let mut roots: Vec<PathBuf> = sandbox_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
.map(|root| dunce::canonicalize(root.root.as_path()).unwrap_or_else(|_| root.root.to_path_buf()))
|
||||
.collect();
|
||||
roots.push(dunce::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf()));
|
||||
(sid, roots)
|
||||
}
|
||||
SandboxPolicy::Custom { writable_roots, .. } => {
|
||||
let sid = if writable_roots.is_empty() {
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly) }.ok_or_else(|| {
|
||||
anyhow!("ConvertStringSidToSidW failed for readonly capability")
|
||||
})?
|
||||
} else {
|
||||
unsafe { convert_string_sid_to_sid(&caps.workspace) }.ok_or_else(|| {
|
||||
anyhow!("ConvertStringSidToSidW failed for workspace capability")
|
||||
})?
|
||||
};
|
||||
let roots = sandbox_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
.map(|root| dunce::canonicalize(root.root.as_path()).unwrap_or_else(|_| root.root.to_path_buf()))
|
||||
.collect();
|
||||
(sid, roots)
|
||||
}
|
||||
SandboxPolicy::ReadOnly { .. } => (
|
||||
|
||||
@@ -140,6 +140,13 @@ pub fn main() -> Result<()> {
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
create_workspace_write_token_with_caps_from(base, &cap_psids)
|
||||
}
|
||||
SandboxPolicy::Custom { writable_roots, .. } => {
|
||||
if writable_roots.is_empty() {
|
||||
create_readonly_token_with_caps_from(base, &cap_psids)
|
||||
} else {
|
||||
create_workspace_write_token_with_caps_from(base, &cap_psids)
|
||||
}
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
@@ -256,6 +256,22 @@ mod windows_impl {
|
||||
crate::cap::workspace_cap_sid_for_cwd(codex_home, cwd)?,
|
||||
],
|
||||
),
|
||||
SandboxPolicy::Custom { writable_roots, .. } => {
|
||||
if writable_roots.is_empty() {
|
||||
(
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() },
|
||||
vec![caps.readonly.clone()],
|
||||
)
|
||||
} else {
|
||||
(
|
||||
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() },
|
||||
vec![
|
||||
caps.workspace.clone(),
|
||||
crate::cap::workspace_cap_sid_for_cwd(codex_home, cwd)?,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
}
|
||||
|
||||
@@ -259,7 +259,11 @@ mod windows_impl {
|
||||
std::fs::create_dir_all(&sandbox_base)?;
|
||||
let logs_base_dir = Some(sandbox_base.as_path());
|
||||
log_start(&command, logs_base_dir);
|
||||
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
|
||||
let is_write_capable = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. })
|
||||
|| matches!(
|
||||
&policy,
|
||||
SandboxPolicy::Custom { writable_roots, .. } if !writable_roots.is_empty()
|
||||
);
|
||||
|
||||
if matches!(
|
||||
&policy,
|
||||
@@ -293,6 +297,25 @@ mod windows_impl {
|
||||
let h = h_res?;
|
||||
(h, psid_generic, Some(psid_workspace))
|
||||
}
|
||||
SandboxPolicy::Custom { writable_roots, .. } => {
|
||||
if writable_roots.is_empty() {
|
||||
let psid = convert_string_sid_to_sid(&caps.readonly).unwrap();
|
||||
let (h, _) = super::token::create_readonly_token_with_cap(psid)?;
|
||||
(h, psid, None)
|
||||
} else {
|
||||
let psid_generic = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||
let ws_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
|
||||
let psid_workspace = convert_string_sid_to_sid(&ws_sid).unwrap();
|
||||
let base = super::token::get_current_token_for_restriction()?;
|
||||
let h_res = create_workspace_write_token_with_caps_from(
|
||||
base,
|
||||
&[psid_generic, psid_workspace],
|
||||
);
|
||||
windows_sys::Win32::Foundation::CloseHandle(base);
|
||||
let h = h_res?;
|
||||
(h, psid_generic, Some(psid_workspace))
|
||||
}
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
}
|
||||
@@ -300,7 +323,7 @@ mod windows_impl {
|
||||
};
|
||||
|
||||
unsafe {
|
||||
if is_workspace_write {
|
||||
if is_write_capable {
|
||||
if let Ok(base) = super::token::get_current_token_for_restriction() {
|
||||
if let Ok(bytes) = super::token::get_logon_sid_bytes(base) {
|
||||
let mut tmp = bytes.clone();
|
||||
@@ -312,7 +335,7 @@ mod windows_impl {
|
||||
}
|
||||
}
|
||||
|
||||
let persist_aces = is_workspace_write;
|
||||
let persist_aces = is_write_capable;
|
||||
let AllowDenyPaths { allow, deny } =
|
||||
compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||
let canonical_cwd = canonicalize_path(¤t_dir);
|
||||
@@ -524,8 +547,12 @@ mod windows_impl {
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
let is_workspace_write = matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. });
|
||||
if !is_workspace_write {
|
||||
let is_write_capable = matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. })
|
||||
|| matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::Custom { writable_roots, .. } if !writable_roots.is_empty()
|
||||
);
|
||||
if !is_write_capable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -264,10 +264,20 @@ pub(crate) fn gather_read_roots(command_cwd: &Path, policy: &SandboxPolicy) -> V
|
||||
roots.push(PathBuf::from(up));
|
||||
}
|
||||
roots.push(command_cwd.to_path_buf());
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy {
|
||||
for root in writable_roots {
|
||||
roots.push(root.to_path_buf());
|
||||
match policy {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
for root in writable_roots {
|
||||
roots.push(root.to_path_buf());
|
||||
}
|
||||
}
|
||||
SandboxPolicy::Custom { writable_roots, .. } => {
|
||||
for root in writable_roots {
|
||||
roots.push(root.root.to_path_buf());
|
||||
}
|
||||
}
|
||||
SandboxPolicy::ReadOnly { .. }
|
||||
| SandboxPolicy::DangerFullAccess
|
||||
| SandboxPolicy::ExternalSandbox { .. } => {}
|
||||
}
|
||||
canonical_existing(&roots)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user