mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
6e9e2c2eef
commit
3d2f123895
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry";
|
||||
|
||||
export type AdditionalFileSystemPermissions = { read: Array<AbsolutePathBuf> | null, write: Array<AbsolutePathBuf> | null, entries?: Array<FileSystemSandboxEntry>, };
|
||||
export type AdditionalFileSystemPermissions = { read: Array<AbsolutePathBuf> | null, write: Array<AbsolutePathBuf> | null, globScanMaxDepth?: number, entries?: Array<FileSystemSandboxEntry>, };
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -781,6 +781,7 @@ mod tests {
|
||||
codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
write: None,
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
},
|
||||
),
|
||||
@@ -844,6 +845,7 @@ mod tests {
|
||||
codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
write: None,
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -93,6 +93,7 @@ async fn request_permissions_round_trip() -> Result<()> {
|
||||
file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![requested_writes[0].clone()]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
@@ -146,6 +147,7 @@ impl SandboxPermissions {
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, JsonSchema, TS)]
|
||||
pub struct FileSystemPermissions {
|
||||
pub entries: Vec<FileSystemSandboxEntry>,
|
||||
pub glob_scan_max_depth: Option<NonZeroUsize>,
|
||||
}
|
||||
|
||||
pub type LegacyReadWriteRoots = (Option<Vec<AbsolutePathBuf>>, Option<Vec<AbsolutePathBuf>>);
|
||||
@@ -172,7 +174,10 @@ impl FileSystemPermissions {
|
||||
access: FileSystemAccessMode::Write,
|
||||
}));
|
||||
}
|
||||
Self { entries }
|
||||
Self {
|
||||
entries,
|
||||
glob_scan_max_depth: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explicit_path_entries(
|
||||
@@ -190,6 +195,10 @@ impl FileSystemPermissions {
|
||||
}
|
||||
|
||||
fn as_legacy_permissions(&self) -> Option<LegacyFileSystemPermissions> {
|
||||
if self.glob_scan_max_depth.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut read = Vec::new();
|
||||
let mut write = Vec::new();
|
||||
|
||||
@@ -225,6 +234,8 @@ struct LegacyFileSystemPermissions {
|
||||
struct CanonicalFileSystemPermissions {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
entries: Vec<FileSystemSandboxEntry>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
glob_scan_max_depth: Option<NonZeroUsize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -244,6 +255,7 @@ impl Serialize for FileSystemPermissions {
|
||||
} else {
|
||||
CanonicalFileSystemPermissions {
|
||||
entries: self.entries.clone(),
|
||||
glob_scan_max_depth: self.glob_scan_max_depth,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
@@ -256,9 +268,13 @@ impl<'de> Deserialize<'de> for FileSystemPermissions {
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match FileSystemPermissionsDe::deserialize(deserializer)? {
|
||||
FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions { entries }) => {
|
||||
Ok(Self { entries })
|
||||
}
|
||||
FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions {
|
||||
entries,
|
||||
glob_scan_max_depth,
|
||||
}) => Ok(Self {
|
||||
entries,
|
||||
glob_scan_max_depth,
|
||||
}),
|
||||
FileSystemPermissionsDe::Legacy(LegacyFileSystemPermissions { read, write }) => {
|
||||
Ok(Self::from_read_write_roots(read, write))
|
||||
}
|
||||
@@ -352,13 +368,18 @@ impl From<&FileSystemSandboxPolicy> for FileSystemPermissions {
|
||||
}]
|
||||
}
|
||||
};
|
||||
Self { entries }
|
||||
Self {
|
||||
entries,
|
||||
glob_scan_max_depth: value.glob_scan_max_depth.and_then(NonZeroUsize::new),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FileSystemPermissions> for FileSystemSandboxPolicy {
|
||||
fn from(value: &FileSystemPermissions) -> Self {
|
||||
FileSystemSandboxPolicy::restricted(value.entries.clone())
|
||||
let mut policy = FileSystemSandboxPolicy::restricted(value.entries.clone());
|
||||
policy.glob_scan_max_depth = value.glob_scan_max_depth.map(usize::from);
|
||||
policy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1828,6 +1849,69 @@ mod tests {
|
||||
assert_eq!(permission_profile.is_empty(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_profile_round_trip_preserves_glob_scan_max_depth() {
|
||||
let mut file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
}]);
|
||||
file_system_sandbox_policy.glob_scan_max_depth = Some(2);
|
||||
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
permission_profile.file_system_sandbox_policy(),
|
||||
file_system_sandbox_policy
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_permissions_with_glob_scan_depth_uses_canonical_json() -> Result<()> {
|
||||
let path = AbsolutePathBuf::try_from(PathBuf::from(if cfg!(windows) {
|
||||
r"C:\tmp\allowed"
|
||||
} else {
|
||||
"/tmp/allowed"
|
||||
}))
|
||||
.expect("absolute path");
|
||||
let file_system_permissions = FileSystemPermissions {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Read,
|
||||
}],
|
||||
glob_scan_max_depth: NonZeroUsize::new(2),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&file_system_permissions)?;
|
||||
|
||||
assert_eq!(serialized.get("read"), None);
|
||||
assert_eq!(serialized.get("write"), None);
|
||||
assert_eq!(
|
||||
serialized.get("glob_scan_max_depth"),
|
||||
Some(&serde_json::json!(2))
|
||||
);
|
||||
assert!(serialized.get("entries").is_some());
|
||||
assert_eq!(
|
||||
serde_json::from_value::<FileSystemPermissions>(serialized)?,
|
||||
file_system_permissions
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_permissions_rejects_zero_glob_scan_depth() {
|
||||
serde_json::from_value::<FileSystemPermissions>(serde_json::json!({
|
||||
"entries": [],
|
||||
"glob_scan_max_depth": 0,
|
||||
}))
|
||||
.expect_err("zero glob scan depth should fail deserialization");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() {
|
||||
let contents = vec![serde_json::json!({
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
use std::collections::HashSet;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EffectiveSandboxPermissions {
|
||||
@@ -45,6 +46,7 @@ pub fn normalize_additional_permissions(
|
||||
let file_system = match additional_permissions.file_system {
|
||||
Some(file_system) => {
|
||||
let mut entries = Vec::with_capacity(file_system.entries.len());
|
||||
let glob_scan_max_depth = file_system.glob_scan_max_depth;
|
||||
for entry in file_system.entries {
|
||||
if matches!(&entry.path, FileSystemPath::GlobPattern { .. })
|
||||
&& entry.access != FileSystemAccessMode::None
|
||||
@@ -73,7 +75,10 @@ pub fn normalize_additional_permissions(
|
||||
entries.push(normalized_entry);
|
||||
}
|
||||
}
|
||||
let file_system = FileSystemPermissions { entries };
|
||||
let file_system = FileSystemPermissions {
|
||||
entries,
|
||||
glob_scan_max_depth,
|
||||
};
|
||||
(!file_system.is_empty()).then_some(file_system)
|
||||
}
|
||||
None => None,
|
||||
@@ -114,6 +119,13 @@ pub fn merge_permission_profiles(
|
||||
let file_system = match (base.file_system.as_ref(), permissions.file_system.as_ref()) {
|
||||
(Some(base), Some(permissions)) => Some(FileSystemPermissions {
|
||||
entries: merge_permission_entries(&base.entries, &permissions.entries),
|
||||
glob_scan_max_depth: merge_glob_scan_max_depth(
|
||||
&base.entries,
|
||||
base.glob_scan_max_depth.map(usize::from),
|
||||
&permissions.entries,
|
||||
permissions.glob_scan_max_depth.map(usize::from),
|
||||
)
|
||||
.and_then(NonZeroUsize::new),
|
||||
})
|
||||
.filter(|file_system| !file_system.is_empty()),
|
||||
(Some(base), None) => Some(base.clone()),
|
||||
@@ -139,12 +151,21 @@ pub fn intersect_permission_profiles(
|
||||
.file_system
|
||||
.map(|requested_file_system| {
|
||||
let granted_file_system = granted.file_system.unwrap_or_default();
|
||||
let entries = requested_file_system
|
||||
let entries: Vec<_> = requested_file_system
|
||||
.entries
|
||||
.into_iter()
|
||||
.filter(|entry| granted_file_system.entries.contains(entry))
|
||||
.collect();
|
||||
FileSystemPermissions { entries }
|
||||
FileSystemPermissions {
|
||||
glob_scan_max_depth: merge_glob_scan_max_depth(
|
||||
&entries,
|
||||
requested_file_system.glob_scan_max_depth.map(usize::from),
|
||||
&entries,
|
||||
granted_file_system.glob_scan_max_depth.map(usize::from),
|
||||
)
|
||||
.and_then(NonZeroUsize::new),
|
||||
entries,
|
||||
}
|
||||
})
|
||||
.filter(|file_system| !file_system.is_empty());
|
||||
let network = match (requested.network, granted.network) {
|
||||
@@ -167,6 +188,48 @@ pub fn intersect_permission_profiles(
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_glob_scan_max_depth(
|
||||
left_entries: &[FileSystemSandboxEntry],
|
||||
left_depth: Option<usize>,
|
||||
right_entries: &[FileSystemSandboxEntry],
|
||||
right_depth: Option<usize>,
|
||||
) -> Option<usize> {
|
||||
let left_depth = effective_glob_scan_depth(left_entries, left_depth);
|
||||
let right_depth = effective_glob_scan_depth(right_entries, right_depth);
|
||||
|
||||
match (left_depth, right_depth) {
|
||||
(Some(GlobScanDepth::Unbounded), _) | (_, Some(GlobScanDepth::Unbounded)) => None,
|
||||
(Some(GlobScanDepth::Bounded(left)), Some(GlobScanDepth::Bounded(right))) => {
|
||||
Some(left.max(right))
|
||||
}
|
||||
(Some(GlobScanDepth::Bounded(depth)), None)
|
||||
| (None, Some(GlobScanDepth::Bounded(depth))) => Some(depth),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_glob_scan_depth(
|
||||
entries: &[FileSystemSandboxEntry],
|
||||
depth: Option<usize>,
|
||||
) -> Option<GlobScanDepth> {
|
||||
entries
|
||||
.iter()
|
||||
.any(|entry| {
|
||||
entry.access == FileSystemAccessMode::None
|
||||
&& matches!(&entry.path, FileSystemPath::GlobPattern { .. })
|
||||
})
|
||||
.then_some(match depth {
|
||||
Some(depth) => GlobScanDepth::Bounded(depth),
|
||||
None => GlobScanDepth::Unbounded,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GlobScanDepth {
|
||||
Bounded(usize),
|
||||
Unbounded,
|
||||
}
|
||||
|
||||
fn merge_permission_entries(
|
||||
base: &[FileSystemSandboxEntry],
|
||||
permissions: &[FileSystemSandboxEntry],
|
||||
@@ -234,6 +297,12 @@ fn merge_file_system_policy_with_additional_permissions(
|
||||
merged_policy.entries.push(entry.clone());
|
||||
}
|
||||
}
|
||||
merged_policy.glob_scan_max_depth = merge_glob_scan_max_depth(
|
||||
&file_system_policy.entries,
|
||||
file_system_policy.glob_scan_max_depth,
|
||||
&additional_permissions.entries,
|
||||
additional_permissions.glob_scan_max_depth.map(usize::from),
|
||||
);
|
||||
merged_policy
|
||||
}
|
||||
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => {
|
||||
|
||||
@@ -171,6 +171,7 @@ fn normalize_additional_permissions_rejects_glob_read_grants() {
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}],
|
||||
glob_scan_max_depth: None,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -192,6 +193,7 @@ fn normalize_additional_permissions_preserves_deny_globs() {
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
}],
|
||||
glob_scan_max_depth: std::num::NonZeroUsize::new(2),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -207,6 +209,7 @@ fn normalize_additional_permissions_preserves_deny_globs() {
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
}],
|
||||
glob_scan_max_depth: std::num::NonZeroUsize::new(2),
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -288,6 +291,76 @@ fn intersect_permission_profiles_drops_explicit_empty_reads_without_grant() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() {
|
||||
let deny_env_files = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
};
|
||||
let requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![deny_env_files.clone()],
|
||||
glob_scan_max_depth: std::num::NonZeroUsize::new(2),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let granted = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![deny_env_files.clone()],
|
||||
glob_scan_max_depth: std::num::NonZeroUsize::new(4),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
intersect_permission_profiles(requested, granted),
|
||||
PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![deny_env_files],
|
||||
glob_scan_max_depth: std::num::NonZeroUsize::new(4),
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() {
|
||||
let deny_env_files = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
};
|
||||
let requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![deny_env_files.clone()],
|
||||
glob_scan_max_depth: std::num::NonZeroUsize::new(2),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let granted = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![deny_env_files.clone()],
|
||||
glob_scan_max_depth: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
intersect_permission_profiles(requested, granted),
|
||||
PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![deny_env_files],
|
||||
glob_scan_max_depth: None,
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_additional_permissions_can_enable_network_without_writes() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
@@ -402,6 +475,42 @@ fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roo
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_file_system_policy_with_additional_permissions_carries_bounded_glob_scan_depth() {
|
||||
let deny_env_files = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
};
|
||||
let merged_policy = merge_file_system_policy_with_additional_permissions(
|
||||
&FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]),
|
||||
&FileSystemPermissions {
|
||||
entries: vec![deny_env_files.clone()],
|
||||
glob_scan_max_depth: std::num::NonZeroUsize::new(2),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(merged_policy, {
|
||||
let mut policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
deny_env_files,
|
||||
]);
|
||||
policy.glob_scan_max_depth = Some(2);
|
||||
policy
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_file_system_sandbox_policy_returns_base_policy_without_additional_permissions() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
|
||||
@@ -558,6 +558,7 @@ mod tests {
|
||||
file_system: Some(AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path(read_path)]),
|
||||
write: Some(vec![absolute_path(write_path)]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -2601,6 +2601,7 @@ async fn inactive_thread_exec_approval_preserves_context() {
|
||||
file_system: Some(AdditionalFileSystemPermissions {
|
||||
read: Some(vec![test_absolute_path("/tmp/read-only")]),
|
||||
write: Some(vec![test_absolute_path("/tmp/write")]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
});
|
||||
@@ -2708,6 +2709,7 @@ async fn inactive_thread_permissions_approval_preserves_file_system_permissions(
|
||||
file_system: Some(AdditionalFileSystemPermissions {
|
||||
read: Some(vec![test_absolute_path("/tmp/read-only")]),
|
||||
write: Some(vec![test_absolute_path("/tmp/write")]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -92,6 +92,7 @@ mod tests {
|
||||
file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/read-only")]),
|
||||
write: Some(vec![absolute_path("/tmp/write")]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
}
|
||||
@@ -109,6 +110,7 @@ mod tests {
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}],
|
||||
glob_scan_max_depth: None,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -117,6 +119,7 @@ mod tests {
|
||||
file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: None,
|
||||
write: None,
|
||||
glob_scan_max_depth: None,
|
||||
entries: Some(vec![codex_app_server_protocol::FileSystemSandboxEntry {
|
||||
path: codex_app_server_protocol::FileSystemPath::Special {
|
||||
value: codex_app_server_protocol::FileSystemSpecialPath::Root,
|
||||
|
||||
@@ -1372,6 +1372,7 @@ mod tests {
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
],
|
||||
glob_scan_max_depth: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -113,6 +113,7 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
|
||||
file_system: Some(AppServerAdditionalFileSystemPermissions {
|
||||
read: Some(vec![read_path.clone()]),
|
||||
write: Some(vec![write_path.clone()]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
}),
|
||||
@@ -163,6 +164,7 @@ fn app_server_request_permissions_preserves_file_system_permissions() {
|
||||
file_system: Some(AppServerAdditionalFileSystemPermissions {
|
||||
read: Some(vec![read_path.clone()]),
|
||||
write: Some(vec![write_path.clone()]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user