Preserve legacy sandbox templates in TUI permission conversions (#60936)

## What changed

Add lossless conversions between legacy sandbox templates and `PermissionProfile` without probing workspace metadata, collapsing fixed roots into current-directory access, or deduplicating writable roots. Reject profiles whose restrictions cannot survive the round trip.

Use these conversions for sandbox inheritance in `create_thread` and for validating read-only temporary structured threads.

## Testing

Extend coverage for duplicate writable roots, existing `.git` metadata, unrepresentable restrictions, and child threads inheriting writable roots, network access, and temporary-directory exclusions.

GitOrigin-RevId: cb37acaf615b09f45680b4b0c4152470fbc04abb
This commit is contained in:
ioanbudea-OAI
2026-11-12 00:13:27 +00:00
committed by copyberry
parent 27294165dc
commit c93732887e
7 changed files with 240 additions and 48 deletions
+40
View File
@@ -21,6 +21,7 @@ use crate::permissions::FileSystemSandboxEntry;
use crate::permissions::FileSystemSandboxKind;
use crate::permissions::FileSystemSandboxPolicy;
use crate::permissions::FileSystemSpecialPath;
use crate::permissions::LegacySandboxPolicyProjection;
use crate::permissions::NetworkSandboxPolicy;
use crate::permissions::RawFileSystemSandboxEntry;
use crate::protocol::SandboxPolicy;
@@ -662,6 +663,28 @@ impl PermissionProfile {
)
}
/// Decodes a portable legacy template without probing workspace metadata.
pub fn from_legacy_sandbox_policy_template(sandbox_policy: &SandboxPolicy) -> Self {
if let SandboxPolicy::WorkspaceWrite {
writable_roots,
exclude_tmpdir_env_var,
exclude_slash_tmp,
..
} = sandbox_policy
{
Self::from_runtime_permissions(
&FileSystemSandboxPolicy::workspace_write_template(
writable_roots,
*exclude_tmpdir_env_var,
*exclude_slash_tmp,
),
NetworkSandboxPolicy::from(sandbox_policy),
)
} else {
Self::from_legacy_sandbox_policy(sandbox_policy)
}
}
pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
Self::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy),
@@ -736,6 +759,23 @@ impl PermissionProfile {
}
}
/// Encodes an unresolved template without collapsing fixed roots into cwd access.
pub fn to_legacy_sandbox_policy_template(&self) -> io::Result<SandboxPolicy> {
let policy = self
.file_system_sandbox_policy()
.to_legacy_sandbox_policy_with_projection(
self.network_sandbox_policy(),
LegacySandboxPolicyProjection::Template,
)?;
if Self::from_legacy_sandbox_policy_template(&policy) != *self {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"permission profile cannot be represented by a legacy sandbox template",
));
}
Ok(policy)
}
pub fn to_runtime_permissions(&self) -> (FileSystemSandboxPolicy, NetworkSandboxPolicy) {
(
self.file_system_sandbox_policy(),
+59 -16
View File
@@ -40,6 +40,13 @@ pub use local_aliases::LocalFileSystemPolicyMatcher;
pub use windows_glob::WindowsDenyReadGlobScan;
pub use windows_glob::windows_deny_read_glob_scan;
/// Selects native execution projection or an unresolved, lossless transport template.
#[derive(Clone, Copy)]
pub(crate) enum LegacySandboxPolicyProjection<'a> {
Cwd(&'a Path),
Template,
}
const PROTECTED_METADATA_GIT_PATH_NAME: &str = ".git";
const PROTECTED_METADATA_AGENTS_PATH_NAME: &str = ".agents";
const PROTECTED_METADATA_CODEX_PATH_NAME: &str = ".codex";
@@ -805,6 +812,32 @@ impl FileSystemSandboxPolicy {
writable_roots: &[AbsolutePathBuf],
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
) -> Self {
let mut policy = Self::workspace_write_template(
writable_roots,
exclude_tmpdir_env_var,
exclude_slash_tmp,
);
for writable_root in writable_roots {
for protected_path in default_read_only_subpaths_for_writable_root(
writable_root,
/*protect_missing_dot_codex*/ false,
) {
append_default_read_only_path_if_no_explicit_rule(
&mut policy.entries,
protected_path,
);
}
}
policy
}
/// Builds an unresolved template; admission must bind native workspace metadata.
pub(crate) fn workspace_write_template(
writable_roots: &[AbsolutePathBuf],
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
) -> Self {
let mut entries = vec![FileSystemSandboxEntry::new(
FileSystemPath::Special {
@@ -846,16 +879,7 @@ impl FileSystemSandboxPolicy {
append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".agents");
append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".codex");
append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".aws");
for writable_root in writable_roots {
for protected_path in default_read_only_subpaths_for_writable_root(
writable_root,
/*protect_missing_dot_codex*/ false,
) {
append_default_read_only_path_if_no_explicit_rule(&mut entries, protected_path);
}
}
FileSystemSandboxPolicy::restricted(entries)
Self::restricted(entries)
}
/// Converts a legacy sandbox policy into an equivalent filesystem policy
@@ -1899,6 +1923,17 @@ impl FileSystemSandboxPolicy {
&self,
network_policy: NetworkSandboxPolicy,
cwd: &Path,
) -> io::Result<SandboxPolicy> {
self.to_legacy_sandbox_policy_with_projection(
network_policy,
LegacySandboxPolicyProjection::Cwd(cwd),
)
}
pub(crate) fn to_legacy_sandbox_policy_with_projection(
&self,
network_policy: NetworkSandboxPolicy,
projection: LegacySandboxPolicyProjection<'_>,
) -> io::Result<SandboxPolicy> {
Ok(match self.kind {
FileSystemSandboxKind::ExternalSandbox => SandboxPolicy::ExternalSandbox {
@@ -1918,7 +1953,12 @@ impl FileSystemSandboxPolicy {
}
}
FileSystemSandboxKind::Restricted => {
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
let cwd = match projection {
LegacySandboxPolicyProjection::Cwd(cwd) => Some(cwd),
LegacySandboxPolicyProjection::Template => None,
};
let cwd_absolute =
cwd.and_then(|cwd| AbsolutePathBuf::from_absolute_path(cwd).ok());
let has_full_disk_write_access = self.has_full_disk_write_access();
let mut workspace_root_writable = false;
let mut writable_roots = Vec::new();
@@ -1976,15 +2016,18 @@ impl FileSystemSandboxPolicy {
if workspace_root_writable {
SandboxPolicy::WorkspaceWrite {
writable_roots: dedup_absolute_paths(
writable_roots,
/*normalize_effective_paths*/ false,
),
writable_roots: match projection {
LegacySandboxPolicyProjection::Cwd(_) => dedup_absolute_paths(
writable_roots,
/*normalize_effective_paths*/ false,
),
LegacySandboxPolicyProjection::Template => writable_roots,
},
network_access: network_policy.is_enabled(),
exclude_tmpdir_env_var: !tmpdir_writable,
exclude_slash_tmp: !slash_tmp_writable,
}
} else if self.has_legacy_write_intent(cwd) {
} else if cwd.is_some_and(|cwd| self.has_legacy_write_intent(cwd)) {
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",