feat(config): support managed deny-read requirements (#17740)

## Summary
- adds managed requirements support for deny-read filesystem entries
- constrains config layers so managed deny-read requirements cannot be
widened by user-controlled config
- surfaces managed deny-read requirements through debug/config plumbing

This PR lets managed requirements inject deny-read filesystem
constraints into the effective filesystem sandbox policy.
User-controlled config can still choose the surrounding permission
profile, but it cannot remove or weaken the managed deny-read entries.

## Managed deny-read shape
A managed requirements file can declare exact paths and glob patterns
under `[permissions.filesystem]`:

```toml
# /etc/codex/requirements.toml
[permissions.filesystem]
deny_read = [
  "/Users/alice/.gitconfig",
  "/Users/alice/.ssh",
  "./managed-private/**/*.env",
]
```

Those entries are compiled into the effective filesystem policy as
`access = none` rules, equivalent in shape to filesystem permission
entries like:

```toml
[permissions.workspace.filesystem]
"/Users/alice/.gitconfig" = "none"
"/Users/alice/.ssh" = "none"
"/absolute/path/to/managed-private/**/*.env" = "none"
```

The important difference is that the managed entries come from
requirements, so lower-precedence user config cannot remove them or make
those paths readable again.

Relative managed `deny_read` entries are resolved relative to the
directory containing the managed requirements file. Glob entries keep
their glob suffix after the non-glob prefix is normalized.

## Runtime behavior
- Managed `deny_read` entries are appended to the effective
`FileSystemSandboxPolicy` after the selected permission profile is
resolved.
- Exact paths become `FileSystemPath::Path { access: None }`; glob
patterns become `FileSystemPath::GlobPattern { access: None }`.
- When managed deny-read entries are present, `sandbox_mode` is
constrained to `read-only` or `workspace-write`; `danger-full-access`
and `external-sandbox` cannot silently bypass the managed read-deny
policy.
- On Windows, the managed deny-read policy is enforced for direct file
tools, but shell subprocess reads are not sandboxed yet, so startup
emits a warning for that platform.
- `/debug-config` shows the effective managed requirement as
`permissions.filesystem.deny_read` with its source.

## Stack
1. #15979 - glob deny-read policy/config/direct-tool support
2. #18096 - macOS and Linux sandbox enforcement
3. This PR - managed deny-read requirements

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-04-17 08:40:09 -07:00
committed by GitHub
parent 2dd6734dd3
commit dae0608c06
10 changed files with 509 additions and 1 deletions

View File

@@ -197,6 +197,22 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
));
}
if let Some(filesystem) = requirements.filesystem.as_ref() {
let deny_read = join_or_empty(
filesystem
.value
.deny_read
.iter()
.map(|pattern| pattern.as_str().to_string())
.collect::<Vec<_>>(),
);
requirement_lines.push(requirement_line(
"permissions.filesystem.deny_read",
deny_read,
Some(&filesystem.source),
));
}
if requirement_lines.is_empty() {
lines.push(" <none>".dim().into());
} else {
@@ -458,6 +474,7 @@ mod tests {
use crate::legacy_core::config_loader::ConfigRequirementsToml;
use crate::legacy_core::config_loader::ConstrainedWithSource;
use crate::legacy_core::config_loader::FeatureRequirementsToml;
use crate::legacy_core::config_loader::FilesystemConstraints;
use crate::legacy_core::config_loader::McpServerIdentity;
use crate::legacy_core::config_loader::McpServerRequirement;
use crate::legacy_core::config_loader::NetworkConstraints;
@@ -549,6 +566,11 @@ mod tests {
} else {
absolute_path("/etc/codex/requirements.toml")
};
let denied_path = if cfg!(windows) {
absolute_path("C:\\Users\\alice\\.gitconfig")
} else {
absolute_path("/home/alice/.gitconfig")
};
let requirements = ConfigRequirements {
approval_policy: ConstrainedWithSource::new(
@@ -603,6 +625,14 @@ mod tests {
},
RequirementSource::CloudRequirements,
)),
filesystem: Some(Sourced::new(
FilesystemConstraints {
deny_read: vec![denied_path.clone().into()],
},
RequirementSource::SystemRequirementsToml {
file: requirements_file.clone(),
},
)),
..ConfigRequirements::default()
};
@@ -627,6 +657,7 @@ mod tests {
rules: None,
enforce_residency: Some(ResidencyRequirement::Us),
network: None,
permissions: None,
};
let user_file = if cfg!(windows) {
@@ -671,6 +702,15 @@ mod tests {
assert!(rendered.contains(
"experimental_network: enabled=true, domains={example.com=allow} (source: cloud requirements)"
));
assert!(
rendered.contains(
format!(
"permissions.filesystem.deny_read: {}",
denied_path.as_path().display()
)
.as_str()
)
);
assert!(!rendered.contains(" - rules:"));
}
@@ -811,6 +851,7 @@ approval_policy = "never"
rules: None,
enforce_residency: None,
network: None,
permissions: None,
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)