protocol: preserve glob scan depth in permission profiles (#18713)

## Why

#18274 made `PermissionProfile` the canonical file-system permissions
shape, but the round-trip from `FileSystemSandboxPolicy` to
`PermissionProfile` still dropped one piece of policy metadata:
`glob_scan_max_depth`.

That field is security-relevant for deny-read globs such as `**/*.env`.
On Linux, bubblewrap sandbox construction uses it to bound unreadable
glob expansion. If a profile copied from active runtime permissions
loses this value and is submitted back as an override, the resulting
`FileSystemSandboxPolicy` can behave differently even though the visible
permission entries look equivalent.

## What changed

- Add `glob_scan_max_depth` to protocol `FileSystemPermissions` and
preserve it when converting to/from `FileSystemSandboxPolicy`.
- Keep legacy `read`/`write` JSON for simple path-only permissions, but
force canonical JSON when glob scan depth is present so the metadata is
not silently dropped.
- Carry `globScanMaxDepth` through app-server
`AdditionalFileSystemPermissions`, generated JSON/TypeScript schemas,
and app-server/TUI conversion call sites.
- Preserve the metadata through sandboxing permission normalization,
merging, and intersection.
- Carry the merged scan depth into the effective
`FileSystemSandboxPolicy` used for command execution, so bounded
deny-read globs reach Linux bubblewrap materialization.

## Verification

- `cargo test -p codex-sandboxing glob_scan -- --nocapture`
- `cargo test -p codex-sandboxing policy_transforms -- --nocapture`
- `just fix -p codex-sandboxing`





---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18713).
* #18288
* #18287
* #18286
* #18285
* #18284
* #18283
* #18282
* #18281
* #18280
* #18279
* #18278
* #18277
* #18276
* #18275
* __->__ #18713
This commit is contained in:
Michael Bolin
2026-04-20 19:42:45 -07:00
committed by GitHub
parent 6e9e2c2eef
commit 3d2f123895
18 changed files with 352 additions and 12 deletions
@@ -2056,6 +2056,7 @@ mod tests {
file_system: Some(v2::AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/allowed")]),
write: None,
glob_scan_max_depth: None,
entries: None,
}),
}),
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use crate::RequestId;
@@ -1162,6 +1163,9 @@ pub struct AdditionalFileSystemPermissions {
pub write: Option<Vec<AbsolutePathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub glob_scan_max_depth: Option<NonZeroUsize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub entries: Option<Vec<FileSystemSandboxEntry>>,
}
@@ -1171,12 +1175,14 @@ impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
Self {
read,
write,
glob_scan_max_depth: None,
entries: None,
}
} else {
Self {
read: None,
write: None,
glob_scan_max_depth: value.glob_scan_max_depth,
entries: Some(
value
.entries
@@ -1191,16 +1197,19 @@ impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
impl From<AdditionalFileSystemPermissions> for CoreFileSystemPermissions {
fn from(value: AdditionalFileSystemPermissions) -> Self {
if let Some(entries) = value.entries {
let mut permissions = if let Some(entries) = value.entries {
Self {
entries: entries
.into_iter()
.map(CoreFileSystemSandboxEntry::from)
.collect(),
glob_scan_max_depth: None,
}
} else {
CoreFileSystemPermissions::from_read_write_roots(value.read, value.write)
}
};
permissions.glob_scan_max_depth = value.glob_scan_max_depth;
permissions
}
}
@@ -6950,6 +6959,7 @@ mod tests {
use codex_utils_absolute_path::test_support::test_path_buf;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::num::NonZeroUsize;
use std::path::PathBuf;
fn absolute_path_string(path: &str) -> String {
@@ -7084,6 +7094,7 @@ mod tests {
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
glob_scan_max_depth: None,
entries: None,
}),
}
@@ -7155,6 +7166,7 @@ mod tests {
access: CoreFileSystemAccessMode::None,
},
],
glob_scan_max_depth: NonZeroUsize::new(2),
};
let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone());
@@ -7163,6 +7175,7 @@ mod tests {
AdditionalFileSystemPermissions {
read: None,
write: None,
glob_scan_max_depth: NonZeroUsize::new(2),
entries: Some(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
@@ -7185,6 +7198,17 @@ mod tests {
);
}
#[test]
fn additional_file_system_permissions_rejects_zero_glob_scan_depth() {
serde_json::from_value::<AdditionalFileSystemPermissions>(json!({
"read": null,
"write": null,
"globScanMaxDepth": 0,
"entries": [],
}))
.expect_err("zero glob scan depth should fail deserialization");
}
#[test]
fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() {
let read_only_path = if cfg!(windows) {
@@ -7225,6 +7249,7 @@ mod tests {
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
glob_scan_max_depth: None,
entries: None,
}),
}