Render remote permission paths using executor context (#47867)

## What changed

- Resolve remote sandbox modes, writable roots, and denied read paths/globs using the executor's filesystem context, displaying paths in its native format. Preserve local filesystem resolution for local environments.
- Resolve remote read denials individually so missing executor facts omit only the unresolved entries.
- Apply a shared 32 KiB budget to displayed permission paths/globs, omitting whole entries and warning that all restrictions still apply, including omitted read denials.

## Testing

Add coverage for remote Windows writable roots and read denials, missing executor path facts, and whole-entry omission with multibyte paths under the shared budget.

GitOrigin-RevId: c61e7ddc8fa569dbc25e9990d90357c42315188f
This commit is contained in:
iceweasel-oai
2026-09-24 15:57:42 +00:00
committed by copyberry
parent 3e9d1d2937
commit 2deb542491
7 changed files with 253 additions and 48 deletions
@@ -10,6 +10,7 @@ use codex_execpolicy::Policy;
use codex_prompts::ApprovalPromptContext;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::format_allow_prefixes;
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
use codex_protocol::protocol::AskForApproval;
use serde::Deserialize;
use serde::Serialize;
@@ -34,12 +35,14 @@ pub(crate) enum PermissionsSnapshot {
}
impl PermissionsState {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
permission_profile: &PermissionProfile,
approval_policy: AskForApproval,
approval_context: ApprovalPromptContext<'_>,
exec_policy: &Policy,
cwd: &Path,
paths: Option<&FileSystemSandboxPolicyContext<'_>>,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> Self {
@@ -50,6 +53,7 @@ impl PermissionsState {
approval_context,
exec_policy,
cwd,
paths,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
)
@@ -237,6 +237,7 @@ fn permissions_state(
ApprovalPromptContext::new(ApprovalsReviewer::User, model_messages),
&Policy::empty(),
Path::new("/workspace"),
/*paths*/ None,
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
@@ -250,6 +251,7 @@ fn permissions_state_with_default_messages(exec_policy: &Policy) -> PermissionsS
ApprovalPromptContext::new(ApprovalsReviewer::User, model_messages),
exec_policy,
Path::new("/workspace"),
/*paths*/ None,
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
+10
View File
@@ -24,6 +24,7 @@ use crate::context::world_state::WorldState;
use codex_connectors::AppToolPolicyEvaluator;
use codex_extension_api::WorldStateContributionInput;
use codex_features::Feature;
use codex_file_system::FileSystemSandboxContext;
use codex_prompts::ApprovalPromptContext;
use codex_prompts::ResolvedModelMessages;
use codex_prompts::render_model_instructions;
@@ -166,6 +167,14 @@ impl Session {
.current_for_prefix_rules(turn_context.allow_prefix_rules());
if turn_context.config.include_permissions_instructions {
let environment = step_context.environments.primary();
let sandbox = environment
.filter(|environment| environment.environment.is_remote())
.map(|environment| {
environment.sandbox_context(/*additional_permissions*/ None)
});
let paths = sandbox
.as_ref()
.map(FileSystemSandboxContext::policy_context);
let permission_profile =
turn_context.permission_profile_for_environments(&step_context.environments);
#[allow(deprecated)]
@@ -178,6 +187,7 @@ impl Session {
ApprovalPromptContext::new(settings.approvals_reviewer(), model_messages),
exec_policy.as_ref(),
&cwd,
paths.as_ref(),
turn_context
.config
.features
@@ -729,6 +729,28 @@ timeout = 900
)]),
NetworkSandboxPolicy::Enabled,
)
} else if foreign_cwd
&& matches!(
scenario,
PushedExecScenario::Complete | PushedExecScenario::ElevatedPowerShell
)
{
let mut file_system = PermissionProfile::workspace_write().file_system_sandbox_policy();
file_system.entries.extend([
FileSystemSandboxEntry::new(
FileSystemPath::Path {
path: "file:///C:/workspace/blocked".parse()?,
},
FileSystemAccessMode::Deny,
),
FileSystemSandboxEntry::new(
FileSystemPath::GlobPattern {
pattern: r"C:\workspace\private\**".into(),
},
FileSystemAccessMode::Deny,
),
]);
PermissionProfile::from_runtime_permissions(&file_system, NetworkSandboxPolicy::Restricted)
} else if foreign_cwd && !matches!(scenario, PushedExecScenario::UnsandboxedInterceptedPatch) {
PermissionProfile::workspace_write()
} else {
@@ -976,6 +998,28 @@ timeout = 900
"toolCallId": CALL_ID,
}),
);
if foreign_cwd && !managed_network_configured {
let permissions = response_mock.requests()[0]
.message_input_texts("developer")
.join("\n")
.lines()
.filter(|line| {
line.contains("`sandbox_mode`")
|| line.starts_with(" The writable root")
|| line.starts_with("- path `")
|| line.starts_with("- glob `")
})
.collect::<Vec<_>>()
.join("\n");
insta::allow_duplicates! {
insta::assert_snapshot!(permissions, @r#"
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted.
The writable roots are `C:\workspace`, `D:\other-workspace`.
- path `C:\workspace\blocked`
- glob `C:\workspace\private\**`
"#);
}
}
if foreign_cwd {
let params = &exec_server_result.process_start["params"];
assert_eq!(params["cwd"], "file:///C:/workspace");
+100 -27
View File
@@ -1,5 +1,6 @@
//! Composes selected permission text and prepared runtime facts into a stored fragment body.
//! Only the profile adapter inspects the host filesystem; composition uses the supplied facts.
//! Remote paths use executor context; the local adapter retains host filesystem resolution.
//! Displayed paths share a byte budget and are omitted only at whole-entry boundaries.
//! Catalog text uses literal substitution; bundled sandbox templates are parsed and cached here.
use crate::ResolvedMessage;
@@ -16,10 +17,12 @@ use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::ContentItemKind;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::format_allow_prefixes;
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::NetworkAccess;
use codex_utils_template::Template;
use std::collections::HashSet;
use std::path::Path;
use std::sync::LazyLock;
@@ -34,6 +37,8 @@ const GRANULAR_PROMPTED_CATEGORIES: &str =
"These approval categories may still prompt the user when needed:";
const GRANULAR_REJECTED_CATEGORIES: &str =
"These approval categories are automatically rejected instead of prompting the user:";
const MAX_PERMISSION_PATH_BYTES: usize = 32 * 1024;
const OMITTED_PERMISSION_PATHS: &str = "Additional permission paths/globs are omitted. All restrictions still apply; do not use escalation or additional permissions to bypass omitted read denials.";
static DANGER_FULL_ACCESS: LazyLock<Template> = LazyLock::new(|| {
Template::parse(DANGER_FULL_ACCESS_TEMPLATE.trim_end())
@@ -93,6 +98,28 @@ impl PermissionsInstructions {
context: PermissionsRenderContext<'_>,
approval_context: ApprovalPromptContext<'_>,
) -> Self {
let mut remaining_path_bytes =
MAX_PERMISSION_PATH_BYTES - OMITTED_PERMISSION_PATHS.len() - 1;
let mut paths_omitted = false;
let mut bounded_paths = |paths: &[String]| {
paths
.iter()
.filter(|path| {
// Reserve up to ten bytes for each entry's label, quoting, and separator.
if let Some(remaining) = remaining_path_bytes.checked_sub(path.len() + 10) {
remaining_path_bytes = remaining;
true
} else {
paths_omitted = true;
false
}
})
.cloned()
.collect::<Vec<_>>()
};
let writable_roots = bounded_paths(context.writable_roots);
let denied_read_paths = bounded_paths(context.denied_read_paths);
let denied_read_globs = bounded_paths(context.denied_read_globs);
let mut text = String::new();
let sandbox = sandbox_text(
context.sandbox_mode,
@@ -113,54 +140,100 @@ impl PermissionsInstructions {
context.request_permissions_tool_enabled,
),
);
if let Some(writable_roots) = writable_roots_text(context.writable_roots) {
if let Some(writable_roots) = writable_roots_text(&writable_roots) {
append_section(&mut text, &writable_roots);
}
if let Some(denied_reads) =
denied_reads_text(context.denied_read_paths, context.denied_read_globs)
{
if let Some(denied_reads) = denied_reads_text(&denied_read_paths, &denied_read_globs) {
append_section(&mut text, &denied_reads);
}
if paths_omitted {
append_section(&mut text, OMITTED_PERMISSION_PATHS);
}
if !text.ends_with('\n') {
text.push('\n');
}
Self { text }
}
/// Resolves a permission profile against the host filesystem before rendering instructions.
/// Uses executor paths when supplied, otherwise retaining local path resolution.
#[allow(clippy::too_many_arguments)]
pub fn from_permission_profile(
permission_profile: &PermissionProfile,
approval_policy: AskForApproval,
approval_context: ApprovalPromptContext<'_>,
exec_policy: &Policy,
cwd: &Path,
paths: Option<&FileSystemSandboxPolicyContext<'_>>,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> Self {
let file_system_policy = permission_profile.file_system_sandbox_policy();
let (sandbox_mode, mut writable_roots) = if file_system_policy.has_full_disk_write_access()
{
(SandboxMode::DangerFullAccess, Vec::new())
let full_disk_write = paths.map_or_else(
|| file_system_policy.has_full_disk_write_access(),
|paths| file_system_policy.has_full_disk_write_access_with_context(paths),
);
let (writable_roots, denied_read_paths, denied_read_globs) = if let Some(paths) = paths {
let mut writable_roots = file_system_policy
.resolved_entries(paths)
.into_iter()
.filter(|(path, access)| {
!full_disk_write
&& access.can_write()
&& file_system_policy.can_write_path(path, paths)
})
.map(|(path, _)| path)
.collect::<HashSet<_>>()
.into_iter()
.map(|path| path.inferred_native_path_string())
.collect::<Vec<_>>();
writable_roots.sort();
let mut denied_read_paths = Vec::new();
let mut denied_read_globs = Vec::new();
let mut seen = HashSet::new();
let mut entry_policy = file_system_policy.clone();
// Missing executor facts should omit only the unresolved entry from the text.
for entry in &file_system_policy.entries {
entry_policy.entries = vec![entry.clone()];
denied_read_paths.extend(
entry_policy
.get_unreadable_roots_with_context(paths)
.unwrap_or_default()
.into_iter()
.filter(|path| seen.insert(path.clone()))
.map(|path| path.inferred_native_path_string()),
);
denied_read_globs.extend(
entry_policy
.get_unreadable_globs_with_context(paths)
.unwrap_or_default(),
);
}
denied_read_globs.sort();
denied_read_globs.dedup();
(writable_roots, denied_read_paths, denied_read_globs)
} else {
let roots = file_system_policy.get_writable_roots_with_cwd(cwd);
let mode = if roots.is_empty() {
SandboxMode::ReadOnly
} else {
SandboxMode::WorkspaceWrite
};
(mode, roots)
let mut writable_roots = file_system_policy.get_writable_roots_with_cwd(cwd);
writable_roots.sort_by(|left, right| left.root.as_path().cmp(right.root.as_path()));
(
writable_roots
.iter()
.map(|root| root.root.to_string_lossy().into_owned())
.collect(),
file_system_policy
.get_unreadable_roots_with_cwd(cwd)
.iter()
.map(|path| path.to_string_lossy().into_owned())
.collect(),
file_system_policy.get_unreadable_globs_with_cwd(cwd),
)
};
let sandbox_mode = if full_disk_write {
SandboxMode::DangerFullAccess
} else if writable_roots.is_empty() {
SandboxMode::ReadOnly
} else {
SandboxMode::WorkspaceWrite
};
writable_roots.sort_by(|left, right| left.root.as_path().cmp(right.root.as_path()));
let writable_roots = writable_roots
.iter()
.map(|root| root.root.to_string_lossy().into_owned())
.collect::<Vec<_>>();
let denied_read_paths = file_system_policy
.get_unreadable_roots_with_cwd(cwd)
.iter()
.map(|path| path.to_string_lossy().into_owned())
.collect::<Vec<_>>();
let denied_read_globs = file_system_policy.get_unreadable_globs_with_cwd(cwd);
Self::from_resolved(
PermissionsRenderContext {
sandbox_mode,
@@ -10,6 +10,7 @@ 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 codex_utils_absolute_path::test_support::test_path_buf;
@@ -33,17 +34,38 @@ fn user_approval_context() -> ApprovalPromptContext<'static> {
#[test]
fn builds_permissions_from_profile() {
let cwd = test_path_buf("/tmp");
let cwd = test_path_buf("/workspace");
let writable_root =
AbsolutePathBuf::from_absolute_path(cwd.join("repo")).expect("absolute path");
let denied_root =
AbsolutePathBuf::from_absolute_path(cwd.join("blocked")).expect("absolute path");
let denied_glob = cwd
.join("blocked")
.join("**")
.to_string_lossy()
.into_owned();
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: writable_root.clone().into(),
&FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: writable_root.clone().into(),
},
access: FileSystemAccessMode::Write,
missing_path_behavior: None,
},
access: FileSystemAccessMode::Write,
missing_path_behavior: None,
}]),
FileSystemSandboxEntry::new(
FileSystemPath::Path {
path: denied_root.clone().into(),
},
FileSystemAccessMode::Deny,
),
FileSystemSandboxEntry::new(
FileSystemPath::GlobPattern {
pattern: denied_glob.clone(),
},
FileSystemAccessMode::Deny,
),
]),
NetworkSandboxPolicy::Enabled,
);
@@ -53,6 +75,7 @@ fn builds_permissions_from_profile() {
user_approval_context(),
&Policy::empty(),
&cwd,
/*paths*/ None,
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
@@ -60,33 +83,50 @@ fn builds_permissions_from_profile() {
assert!(text.contains("`sandbox_mode` is `workspace-write`"));
assert!(text.contains("Network access is enabled."));
assert!(text.contains(writable_root.to_string_lossy().as_ref()));
assert!(text.contains("Do not request escalation or additional permissions"));
assert!(text.contains(&format!("path `{}`", denied_root.display())));
assert!(text.contains(&format!("glob `{denied_glob}`")));
}
#[test]
fn builds_permissions_from_profile_with_denied_reads() {
let cwd = test_path_buf("/tmp");
let denied_root =
AbsolutePathBuf::from_absolute_path(cwd.join("blocked")).expect("absolute path");
let denied_glob = cwd.join("blocked").join("**");
fn builds_permissions_from_profile_with_executor_denied_reads() {
let paths = FileSystemSandboxPolicyContext {
cwd: &"file:///C:/workspace".parse().unwrap(),
workspace_roots: &[],
user_home_dir: None,
temporary_directories: None,
};
let denied_root = paths.cwd.join("blocked").unwrap();
let denied_glob = r"C:\workspace\blocked\**";
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Root,
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: denied_root.clone().into(),
FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
},
FileSystemAccessMode::Deny,
),
FileSystemSandboxEntry::new(
FileSystemPath::GlobPattern {
pattern: "~/private/**".into(),
},
FileSystemAccessMode::Deny,
),
FileSystemSandboxEntry {
path: FileSystemPath::Path { path: denied_root },
access: FileSystemAccessMode::Deny,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: denied_glob.to_string_lossy().into_owned(),
pattern: denied_glob.into(),
},
access: FileSystemAccessMode::Deny,
missing_path_behavior: None,
@@ -103,15 +143,16 @@ fn builds_permissions_from_profile_with_denied_reads() {
ResolvedModelMessages::bundled(),
),
&Policy::empty(),
&cwd,
&test_path_buf("/unused-local-cwd"),
Some(&paths),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
let text = instructions.body();
assert!(text.contains("## Denied filesystem reads"));
assert!(text.contains("Do not request escalation or additional permissions"));
assert!(text.contains(denied_root.to_string_lossy().as_ref()));
assert!(text.contains(&format!("glob `{}`", denied_glob.to_string_lossy())));
assert!(text.contains(r"path `C:\workspace\blocked`"));
assert!(text.contains(&format!("glob `{denied_glob}`")));
}
#[test]
@@ -704,3 +745,32 @@ fn preserves_supplied_path_spellings_and_order() {
),
);
}
#[test]
fn bounds_permission_paths_at_whole_entry_boundaries() {
let render =
|writable_roots: &[String], denied_read_paths: &[String], denied_read_globs: &[String]| {
PermissionsInstructions::from_resolved(
PermissionsRenderContext {
writable_roots,
denied_read_paths,
denied_read_globs,
..WORKSPACE_CONTEXT
},
user_approval_context(),
)
.body()
};
// Any two fit, but all three exceed the shared budget. Include multibyte paths.
let writable = format!("/writable/{}", "é".repeat(6_000));
let denied = format!("/denied/{}", "é".repeat(6_000));
let glob = format!("/glob/{}/**", "é".repeat(6_000));
let oversized = format!("/oversized/{}", "x".repeat(MAX_PERMISSION_PATH_BYTES));
let text = render(
&[oversized, writable.clone()],
std::slice::from_ref(&denied),
&[glob, "/short/**".into()],
);
let expected = render(&[writable], &[denied], &["/short/**".into()]);
assert_eq!(text, format!("{expected}{OMITTED_PERMISSION_PATHS}\n"));
}
+3 -1
View File
@@ -1827,7 +1827,9 @@ impl FileSystemSandboxPolicy {
.collect()
}
fn resolved_entries(
/// Resolves configured roots using executor paths without inspecting the filesystem.
/// Glob patterns are excluded; access precedence is evaluated by `resolve_access`.
pub fn resolved_entries(
&self,
context: &FileSystemSandboxPolicyContext<'_>,
) -> Vec<(PathUri, FileSystemAccessMode)> {