mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -41,6 +41,8 @@ pub use codex_config::ConfigRequirements;
|
||||
pub use codex_config::ConfigRequirementsToml;
|
||||
pub use codex_config::ConstrainedWithSource;
|
||||
pub use codex_config::FeatureRequirementsToml;
|
||||
pub use codex_config::FilesystemConstraints;
|
||||
pub use codex_config::FilesystemDenyReadPattern;
|
||||
pub use codex_config::LoaderOverrides;
|
||||
pub use codex_config::McpServerIdentity;
|
||||
pub use codex_config::McpServerRequirement;
|
||||
@@ -378,6 +380,16 @@ async fn load_requirements_toml(
|
||||
.await
|
||||
{
|
||||
Ok(contents) => {
|
||||
let requirements_parent = requirements_toml_file.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Requirements file {} has no parent directory",
|
||||
requirements_toml_file.as_ref().display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let _guard = AbsolutePathBufGuard::new(requirements_parent.as_path());
|
||||
let requirements_config: ConfigRequirementsToml =
|
||||
toml::from_str(&contents).map_err(|e| {
|
||||
io::Error::new(
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::config_loader::ConfigLoadError;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConfigRequirementsToml;
|
||||
use crate::config_loader::ConfigRequirementsWithSources;
|
||||
use crate::config_loader::FilesystemDenyReadPattern;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::load_requirements_toml;
|
||||
use crate::config_loader::version_for_toml;
|
||||
@@ -651,6 +652,7 @@ allowed_approval_policies = ["on-request"]
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
permissions: None,
|
||||
guardian_policy_config: None,
|
||||
}))
|
||||
}),
|
||||
@@ -703,6 +705,7 @@ allowed_approval_policies = ["on-request"]
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
permissions: None,
|
||||
guardian_policy_config: None,
|
||||
},
|
||||
);
|
||||
@@ -731,6 +734,103 @@ allowed_approval_policies = ["on-request"]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn load_requirements_toml_resolves_deny_read_against_parent() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let requirements_dir = tmp.path().join("managed");
|
||||
tokio::fs::create_dir_all(&requirements_dir).await?;
|
||||
let requirements_file = requirements_dir.join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_file,
|
||||
r#"
|
||||
[permissions.filesystem]
|
||||
deny_read = ["./sensitive", "../shared/secret.txt"]
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||
load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
|
||||
|
||||
let permissions = config_requirements_toml
|
||||
.permissions
|
||||
.expect("permissions requirements should load");
|
||||
let filesystem = permissions
|
||||
.value
|
||||
.filesystem
|
||||
.expect("filesystem requirements should load");
|
||||
let deny_read = filesystem.deny_read.expect("deny_read paths should load");
|
||||
|
||||
assert_eq!(
|
||||
deny_read,
|
||||
vec![
|
||||
FilesystemDenyReadPattern::from(AbsolutePathBuf::try_from(
|
||||
requirements_dir.join("sensitive")
|
||||
)?,),
|
||||
FilesystemDenyReadPattern::from(AbsolutePathBuf::try_from(
|
||||
tmp.path().join("shared").join("secret.txt"),
|
||||
)?),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
permissions.source,
|
||||
RequirementSource::SystemRequirementsToml {
|
||||
file: AbsolutePathBuf::try_from(requirements_file)?,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn load_requirements_toml_resolves_deny_read_glob_against_parent() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let requirements_dir = tmp.path().join("managed");
|
||||
tokio::fs::create_dir_all(&requirements_dir).await?;
|
||||
let requirements_file = requirements_dir.join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_file,
|
||||
r#"
|
||||
[permissions.filesystem]
|
||||
deny_read = ["./sensitive/**/*.txt"]
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||
load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
|
||||
|
||||
let permissions = config_requirements_toml
|
||||
.permissions
|
||||
.expect("permissions requirements should load");
|
||||
let filesystem = permissions
|
||||
.value
|
||||
.filesystem
|
||||
.expect("filesystem requirements should load");
|
||||
let deny_read = filesystem
|
||||
.deny_read
|
||||
.expect("deny_read patterns should load");
|
||||
|
||||
assert_eq!(
|
||||
deny_read,
|
||||
vec![
|
||||
FilesystemDenyReadPattern::from_input(&format!(
|
||||
"{}/sensitive/**/*.txt",
|
||||
requirements_dir.display()
|
||||
))
|
||||
.expect("normalize glob pattern")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
permissions.source,
|
||||
RequirementSource::SystemRequirementsToml {
|
||||
file: AbsolutePathBuf::try_from(requirements_file)?,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
@@ -749,6 +849,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
permissions: None,
|
||||
guardian_policy_config: None,
|
||||
};
|
||||
let expected = requirements.clone();
|
||||
|
||||
Reference in New Issue
Block a user