Add remote_sandbox_config to our config requirements (#18763)

## Why

Customers need finer-grained control over allowed sandbox modes based on
the host Codex is running on. For example, they may want stricter
sandbox limits on devboxes while keeping a different default elsewhere.

Our current cloud requirements can target user/account groups, but they
cannot vary sandbox requirements by host. That makes remote development
environments awkward because the same top-level `allowed_sandbox_modes`
has to apply everywhere.

## What

Adds a new `remote_sandbox_config` section to `requirements.toml`:

```toml
allowed_sandbox_modes = ["read-only"]

[[remote_sandbox_config]]
hostname_patterns = ["*.org"]
allowed_sandbox_modes = ["read-only", "workspace-write"]

[[remote_sandbox_config]]
hostname_patterns = ["*.sh", "runner-*.ci"]
allowed_sandbox_modes = ["read-only", "danger-full-access"]
```

During requirements resolution, Codex resolves the local host name once,
preferring the machine FQDN when available and falling back to the
cleaned kernel hostname. This host classification is best effort rather
than authenticated device proof.

Each requirements source applies its first matching
`remote_sandbox_config` entry before it is merged with other sources.
The shared merge helper keeps that `apply_remote_sandbox_config` step
paired with requirements merging so new requirements sources do not have
to remember the extra call.

That preserves source precedence: a lower-precedence requirements file
with a matching `remote_sandbox_config` cannot override a
higher-precedence source that already set `allowed_sandbox_modes`.

This also wires the hostname-aware resolution through app-server,
CLI/TUI config loading, config API reads, and config layer metadata so
they all evaluate remote sandbox requirements consistently.

## Verification

- `cargo test -p codex-config remote_sandbox_config`
- `cargo test -p codex-config host_name`
- `cargo test -p codex-core
load_config_layers_applies_matching_remote_sandbox_config`
- `cargo test -p codex-core
system_remote_sandbox_config_keeps_cloud_sandbox_modes`
- `cargo test -p codex-config`
- `cargo test -p codex-core` unit tests passed; `tests/all.rs`
integration matrix was intentionally stopped after the relevant focused
tests passed
- `just fix -p codex-config`
- `just fix -p codex-core`
- `cargo check -p codex-app-server`
This commit is contained in:
Abhinav
2026-04-20 22:05:02 -07:00
committed by GitHub
Unverified
parent 86535c9901
commit ab26554a3a
21 changed files with 598 additions and 27 deletions
+1
View File
@@ -831,6 +831,7 @@
"dispatch2_0.3.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bitflags\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"block2\",\"optional\":true,\"req\":\">=0.6.1, <0.8.0\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.80\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"objc2\",\"optional\":true,\"req\":\">=0.6.1, <0.8.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"alloc\":[],\"block2\":[\"dep:block2\"],\"default\":[\"std\",\"block2\",\"libc\",\"objc2\"],\"libc\":[\"dep:libc\"],\"objc2\":[\"dep:objc2\"],\"std\":[\"alloc\"]}}",
"display_container_0.9.0": "{\"dependencies\":[{\"name\":\"either\",\"req\":\"^1.8\"},{\"name\":\"indenter\",\"req\":\"^0.3.3\"}],\"features\":{}}",
"displaydoc_0.2.5": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^1.0.24\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
"dns-lookup_3.0.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"features\":[\"Win32_Networking_WinSock\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\"^0.60\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
"dotenvy_0.15.7": "{\"dependencies\":[{\"name\":\"clap\",\"optional\":true,\"req\":\"^3.2\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.16.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.3.0\"}],\"features\":{\"cli\":[\"clap\"]}}",
"downcast-rs_1.2.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
"dtor-proc-macro_0.0.6": "{\"dependencies\":[],\"features\":{\"default\":[]}}",
+29 -13
View File
@@ -449,7 +449,7 @@ dependencies = [
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -1372,7 +1372,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
"unicode-width 0.1.14",
"unicode-width 0.2.1",
]
[[package]]
@@ -1877,7 +1877,10 @@ dependencies = [
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-path",
"dns-lookup",
"futures",
"gethostname",
"libc",
"multimap",
"pretty_assertions",
"schemars 0.8.22",
@@ -1892,6 +1895,7 @@ dependencies = [
"toml_edit 0.24.0+spec-1.1.0",
"tracing",
"wildmatch",
"winapi-util",
]
[[package]]
@@ -4324,6 +4328,18 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "dns-lookup"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e39034cee21a2f5bbb66ba0e3689819c4bb5d00382a282006e802a7ffa6c41d"
dependencies = [
"cfg-if",
"libc",
"socket2 0.6.2",
"windows-sys 0.60.2",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -4548,7 +4564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5110,7 +5126,7 @@ dependencies = [
"gobject-sys",
"libc",
"system-deps",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5554,7 +5570,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.6.2",
"system-configuration",
"tokio",
"tower-service",
@@ -5640,7 +5656,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.58.0",
"windows-core 0.62.2",
]
[[package]]
@@ -6060,7 +6076,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7550,7 +7566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -8237,7 +8253,7 @@ dependencies = [
"quinn-udp",
"rustc-hash 2.1.1",
"rustls",
"socket2 0.5.10",
"socket2 0.6.2",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -8274,9 +8290,9 @@ dependencies = [
"cfg_aliases 0.2.1",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2 0.6.2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -9118,7 +9134,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -10577,7 +10593,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.3",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
+2
View File
@@ -236,6 +236,7 @@ deno_core_icudata = "0.77.0"
derive_more = "2"
diffy = "0.4.2"
dirs = "6"
dns-lookup = "3.0.1"
dotenvy = "0.15.7"
dunce = "1.0.4"
ed25519-dalek = { version = "2.2.0", features = ["pkcs8"] }
@@ -373,6 +374,7 @@ webbrowser = "1.0"
which = "8"
whoami = "1.6.1"
wildmatch = "2.6.1"
winapi-util = "0.1.11"
zip = "2.4.2"
zstd = "0.13"
@@ -6577,6 +6577,7 @@ impl CodexMessageProcessor {
}
};
let cli_overrides = self.current_cli_overrides();
let host_name = codex_config::host_name();
let mut data = Vec::new();
for cwd in cwds {
let extra_roots = extra_roots_by_cwd
@@ -6605,6 +6606,7 @@ impl CodexMessageProcessor {
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
self.thread_config_loader.as_ref(),
host_name.as_deref(),
)
.await
{
+4
View File
@@ -108,6 +108,7 @@ impl ConfigApi {
self.current_cli_overrides(),
self.loader_overrides.clone(),
self.current_cloud_requirements(),
codex_config::host_name(),
)
}
@@ -563,6 +564,7 @@ mod tests {
CoreSandboxModeRequirement::ReadOnly,
CoreSandboxModeRequirement::ExternalSandbox,
]),
remote_sandbox_config: None,
allowed_web_search_modes: Some(vec![
codex_core::config_loader::WebSearchModeRequirement::Cached,
]),
@@ -675,6 +677,7 @@ mod tests {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -733,6 +736,7 @@ mod tests {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: Some(Vec::new()),
guardian_policy_config: None,
feature_requirements: None,
+16
View File
@@ -1187,6 +1187,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1217,6 +1218,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1247,6 +1249,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1294,6 +1297,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1377,6 +1381,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1450,6 +1455,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1521,6 +1527,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1719,6 +1726,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1755,6 +1763,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1811,6 +1820,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1862,6 +1872,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1917,6 +1928,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1973,6 +1985,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2029,6 +2042,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2118,6 +2132,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2146,6 +2161,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
+8
View File
@@ -19,6 +19,7 @@ codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path = { workspace = true }
futures = { workspace = true, features = ["alloc", "std"] }
gethostname = { workspace = true }
multimap = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
@@ -32,6 +33,13 @@ toml_edit = { workspace = true }
tracing = { workspace = true }
wildmatch = { workspace = true }
[target.'cfg(unix)'.dependencies]
dns-lookup = { workspace = true }
libc = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
winapi-util = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
+208
View File
@@ -11,6 +11,7 @@ use serde::de::value::Error as ValueDeserializerError;
use serde::de::value::StrDeserializer;
use std::collections::BTreeMap;
use std::fmt;
use wildmatch::WildMatchPattern;
use super::requirements_exec_policy::RequirementsExecPolicy;
use super::requirements_exec_policy::RequirementsExecPolicyToml;
@@ -620,6 +621,7 @@ pub struct ConfigRequirementsToml {
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
pub remote_sandbox_config: Option<Vec<RemoteSandboxConfigToml>>,
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
#[serde(rename = "features", alias = "feature_requirements")]
pub feature_requirements: Option<FeatureRequirementsToml>,
@@ -633,6 +635,12 @@ pub struct ConfigRequirementsToml {
pub guardian_policy_config: Option<String>,
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct RemoteSandboxConfigToml {
pub hostname_patterns: Vec<String>,
pub allowed_sandbox_modes: Vec<SandboxModeRequirement>,
}
/// Value paired with the requirement source it came from, for better error
/// messages.
#[derive(Debug, Clone, PartialEq)]
@@ -693,6 +701,7 @@ impl ConfigRequirementsWithSources {
allowed_approval_policies: _,
allowed_approvals_reviewers: _,
allowed_sandbox_modes: _,
remote_sandbox_config: _,
allowed_web_search_modes: _,
feature_requirements: _,
mcp_servers: _,
@@ -759,6 +768,7 @@ impl ConfigRequirementsWithSources {
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value),
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
remote_sandbox_config: None,
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
feature_requirements: feature_requirements.map(|sourced| sourced.value),
mcp_servers: mcp_servers.map(|sourced| sourced.value),
@@ -772,6 +782,19 @@ impl ConfigRequirementsWithSources {
}
}
fn normalize_hostname(hostname: &str) -> Option<String> {
let hostname = hostname.trim().trim_end_matches('.');
(!hostname.is_empty()).then(|| hostname.to_ascii_lowercase())
}
fn hostname_matches_any_pattern(hostname: &str, patterns: &[String]) -> bool {
patterns.iter().any(|pattern| {
normalize_hostname(pattern)
.map(|pattern| WildMatchPattern::<'*', '?'>::new_case_insensitive(&pattern))
.is_some_and(|pattern| pattern.matches(hostname))
})
}
/// Currently, `external-sandbox` is not supported in config.toml, but it is
/// supported through programmatic use.
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
@@ -806,10 +829,27 @@ pub enum ResidencyRequirement {
}
impl ConfigRequirementsToml {
pub fn apply_remote_sandbox_config(&mut self, hostname: Option<&str>) {
let Some(hostname) = hostname.and_then(normalize_hostname) else {
return;
};
let Some(remote_sandbox_config) = self.remote_sandbox_config.as_ref() else {
return;
};
let Some(matched_config) = remote_sandbox_config
.iter()
.find(|config| hostname_matches_any_pattern(&hostname, &config.hostname_patterns))
else {
return;
};
self.allowed_sandbox_modes = Some(matched_config.allowed_sandbox_modes.clone());
}
pub fn is_empty(&self) -> bool {
self.allowed_approval_policies.is_none()
&& self.allowed_approvals_reviewers.is_none()
&& self.allowed_sandbox_modes.is_none()
&& self.remote_sandbox_config.is_none()
&& self.allowed_web_search_modes.is_none()
&& self
.feature_requirements
@@ -1099,6 +1139,7 @@ mod tests {
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
remote_sandbox_config: _,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
@@ -1161,6 +1202,7 @@ mod tests {
allowed_approval_policies: Some(allowed_approval_policies.clone()),
allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()),
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
remote_sandbox_config: None,
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
feature_requirements: Some(feature_requirements.clone()),
mcp_servers: None,
@@ -1883,6 +1925,172 @@ allowed_approvals_reviewers = ["user"]
Ok(())
}
#[test]
fn deserialize_remote_sandbox_config_requires_hostname_patterns_list() -> Result<()> {
let toml_str = r#"
[[remote_sandbox_config]]
hostname_patterns = ["*.org", "runner-??.ci"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
assert_eq!(
config.remote_sandbox_config,
Some(vec![RemoteSandboxConfigToml {
hostname_patterns: vec!["*.org".to_string(), "runner-??.ci".to_string()],
allowed_sandbox_modes: vec![
SandboxModeRequirement::ReadOnly,
SandboxModeRequirement::WorkspaceWrite,
],
}])
);
let err = from_str::<ConfigRequirementsToml>(
r#"
[[remote_sandbox_config]]
hostname_patterns = "*.org"
allowed_sandbox_modes = ["read-only"]
"#,
)
.expect_err("hostname_patterns should be list-only");
assert!(
err.to_string().contains("invalid type: string"),
"unexpected error: {err}"
);
Ok(())
}
#[test]
fn remote_sandbox_config_first_match_overrides_top_level() -> Result<()> {
let source = RequirementSource::CloudRequirements;
let mut requirements_toml: ConfigRequirementsToml = from_str(
r#"
allowed_sandbox_modes = ["read-only"]
[[remote_sandbox_config]]
hostname_patterns = ["build-*.example.com"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
[[remote_sandbox_config]]
hostname_patterns = ["build-01.example.com"]
allowed_sandbox_modes = ["read-only", "danger-full-access"]
"#,
)?;
requirements_toml.apply_remote_sandbox_config(Some("BUILD-01.EXAMPLE.COM."));
let mut requirements_with_sources = ConfigRequirementsWithSources::default();
requirements_with_sources.merge_unset_fields(source.clone(), requirements_toml);
assert_eq!(
requirements_with_sources
.allowed_sandbox_modes
.as_ref()
.map(|sourced| sourced.value.clone()),
Some(vec![
SandboxModeRequirement::ReadOnly,
SandboxModeRequirement::WorkspaceWrite,
])
);
let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
assert!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
read_only_access: Default::default(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})
.is_ok()
);
assert_eq!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::DangerFullAccess),
Err(ConstraintError::InvalidValue {
field_name: "sandbox_mode",
candidate: "DangerFullAccess".into(),
allowed: "[ReadOnly, WorkspaceWrite]".into(),
requirement_source: source,
})
);
Ok(())
}
#[test]
fn remote_sandbox_config_non_match_preserves_top_level() -> Result<()> {
let mut requirements_toml: ConfigRequirementsToml = from_str(
r#"
allowed_sandbox_modes = ["read-only"]
[[remote_sandbox_config]]
hostname_patterns = ["build-*.example.com"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
"#,
)?;
requirements_toml.apply_remote_sandbox_config(Some("laptop.example.com"));
let mut requirements_with_sources = ConfigRequirementsWithSources::default();
requirements_with_sources.merge_unset_fields(RequirementSource::Unknown, requirements_toml);
let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
assert_eq!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::DangerFullAccess),
Err(ConstraintError::InvalidValue {
field_name: "sandbox_mode",
candidate: "DangerFullAccess".into(),
allowed: "[ReadOnly]".into(),
requirement_source: RequirementSource::Unknown,
})
);
Ok(())
}
#[test]
fn remote_sandbox_config_does_not_override_higher_precedence_sandbox_modes() -> Result<()> {
let high_source = RequirementSource::CloudRequirements;
let mut high_precedence: ConfigRequirementsToml = from_str(
r#"
allowed_sandbox_modes = ["read-only"]
"#,
)?;
high_precedence.apply_remote_sandbox_config(Some("runner-01.ci.example.com"));
let mut low_precedence: ConfigRequirementsToml = from_str(
r#"
[[remote_sandbox_config]]
hostname_patterns = ["runner-*.ci.example.com"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
"#,
)?;
low_precedence.apply_remote_sandbox_config(Some("runner-01.ci.example.com"));
let mut requirements_with_sources = ConfigRequirementsWithSources::default();
requirements_with_sources.merge_unset_fields(high_source.clone(), high_precedence);
requirements_with_sources.merge_unset_fields(RequirementSource::Unknown, low_precedence);
let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
assert_eq!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::new_workspace_write_policy()),
Err(ConstraintError::InvalidValue {
field_name: "sandbox_mode",
candidate: "WorkspaceWrite".into(),
allowed: "[ReadOnly]".into(),
requirement_source: high_source,
})
);
Ok(())
}
#[test]
fn deserialize_allowed_web_search_modes() -> Result<()> {
let toml_str = r#"
+97
View File
@@ -0,0 +1,97 @@
#[cfg(unix)]
use dns_lookup::AddrInfoHints;
#[cfg(unix)]
use dns_lookup::getaddrinfo;
use std::sync::LazyLock;
#[cfg(windows)]
use winapi_util::sysinfo::ComputerNameKind;
#[cfg(windows)]
use winapi_util::sysinfo::get_computer_name;
static HOST_NAME: LazyLock<Option<String>> = LazyLock::new(compute_host_name);
pub fn host_name() -> Option<String> {
HOST_NAME.clone()
}
fn compute_host_name() -> Option<String> {
let kernel_hostname = gethostname::gethostname();
let kernel_hostname = normalize_host_name(&kernel_hostname.to_string_lossy())?;
// Remote sandbox requirements are meant to target remote hosts by DNS name,
// so prefer the canonical FQDN when the local resolver can provide one.
// This is best-effort host classification, not authenticated device proof.
if let Some(fqdn) = local_fqdn_for_hostname(&kernel_hostname) {
return Some(fqdn);
}
// Some machines have only a short local hostname or resolver setup that
// does not return AI_CANONNAME. Keep matching behavior best-effort by
// falling back to the cleaned kernel hostname instead of returning None.
Some(kernel_hostname)
}
fn normalize_host_name(hostname: &str) -> Option<String> {
let hostname = hostname.trim().trim_end_matches('.');
(!hostname.is_empty()).then(|| hostname.to_ascii_lowercase())
}
#[cfg(unix)]
fn local_fqdn_for_hostname(hostname: &str) -> Option<String> {
let hints = AddrInfoHints {
flags: libc::AI_CANONNAME,
..AddrInfoHints::default()
};
getaddrinfo(Some(hostname), /*service*/ None, Some(hints))
.ok()?
.filter_map(Result::ok)
.filter_map(|addr| addr.canonname)
// getaddrinfo may return the short hostname as canonname when no FQDN
// is available. Treat only DNS-qualified names as an FQDN result.
.find_map(|hostname| normalize_fqdn_candidate(&hostname))
}
#[cfg(windows)]
fn local_fqdn_for_hostname(_hostname: &str) -> Option<String> {
get_computer_name(ComputerNameKind::PhysicalDnsFullyQualified)
.ok()
.and_then(|hostname| hostname.into_string().ok())
.and_then(|hostname| normalize_fqdn_candidate(&hostname))
}
#[cfg(not(any(unix, windows)))]
fn local_fqdn_for_hostname(_hostname: &str) -> Option<String> {
None
}
fn normalize_fqdn_candidate(hostname: &str) -> Option<String> {
normalize_host_name(hostname).filter(|hostname| hostname.contains('.'))
}
#[cfg(test)]
mod tests {
use super::normalize_fqdn_candidate;
use pretty_assertions::assert_eq;
#[test]
fn normalize_fqdn_candidate_accepts_dns_qualified_name() {
assert_eq!(
normalize_fqdn_candidate("runner-01.ci.example.com"),
Some("runner-01.ci.example.com".to_string())
);
}
#[test]
fn normalize_fqdn_candidate_rejects_short_name() {
assert_eq!(normalize_fqdn_candidate("runner-01"), None);
}
#[test]
fn normalize_fqdn_candidate_trims_trailing_dot_and_normalizes_case() {
assert_eq!(
normalize_fqdn_candidate("RUNNER-01.CI.EXAMPLE.COM."),
Some("runner-01.ci.example.com".to_string())
);
}
}
+3
View File
@@ -4,6 +4,7 @@ pub mod config_toml;
mod constraint;
mod diagnostics;
mod fingerprint;
mod host_name;
mod key_aliases;
mod marketplace_edit;
mod mcp_edit;
@@ -43,6 +44,7 @@ pub use config_requirements::NetworkDomainPermissionsToml;
pub use config_requirements::NetworkRequirementsToml;
pub use config_requirements::NetworkUnixSocketPermissionToml;
pub use config_requirements::NetworkUnixSocketPermissionsToml;
pub use config_requirements::RemoteSandboxConfigToml;
pub use config_requirements::RequirementSource;
pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement;
@@ -63,6 +65,7 @@ pub use diagnostics::format_config_error;
pub use diagnostics::format_config_error_with_source;
pub use diagnostics::io_error_from_config_error;
pub use fingerprint::version_for_toml;
pub use host_name::host_name;
pub use marketplace_edit::MarketplaceConfigUpdate;
pub use marketplace_edit::RemoveMarketplaceConfigOutcome;
pub use marketplace_edit::record_user_marketplace;
+4
View File
@@ -2213,6 +2213,7 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> {
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
let cfg =
@@ -2348,6 +2349,7 @@ async fn managed_config_wins_over_cli_overrides() -> anyhow::Result<()> {
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -5484,6 +5486,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: Some(vec![
crate::config_loader::WebSearchModeRequirement::Cached,
]),
@@ -6160,6 +6163,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: Some(vec![crate::config_loader::SandboxModeRequirement::ReadOnly]),
remote_sandbox_config: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
+26 -1
View File
@@ -649,7 +649,7 @@ impl AuthManagerConfig for Config {
}
}
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct ConfigBuilder {
codex_home: Option<PathBuf>,
cli_overrides: Option<Vec<(String, TomlValue)>>,
@@ -658,6 +658,22 @@ pub struct ConfigBuilder {
cloud_requirements: CloudRequirementsLoader,
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
fallback_cwd: Option<PathBuf>,
host_name: Option<String>,
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self {
codex_home: None,
cli_overrides: None,
harness_overrides: None,
loader_overrides: None,
cloud_requirements: CloudRequirementsLoader::default(),
thread_config_loader: None,
fallback_cwd: None,
host_name: codex_config::host_name(),
}
}
}
impl ConfigBuilder {
@@ -699,6 +715,11 @@ impl ConfigBuilder {
self
}
pub fn host_name(mut self, host_name: Option<String>) -> Self {
self.host_name = host_name;
self
}
pub async fn build(self) -> std::io::Result<Config> {
let Self {
codex_home,
@@ -708,6 +729,7 @@ impl ConfigBuilder {
cloud_requirements,
thread_config_loader,
fallback_cwd,
host_name,
} = self;
let codex_home = match codex_home {
Some(codex_home) => AbsolutePathBuf::from_absolute_path(codex_home)?,
@@ -732,6 +754,7 @@ impl ConfigBuilder {
thread_config_loader
.as_deref()
.unwrap_or(&codex_config::NoopThreadConfigLoader),
host_name.as_deref(),
)
.await?;
let merged_toml = config_layer_stack.effective_config();
@@ -911,6 +934,7 @@ pub async fn load_config_as_toml_with_cli_and_loader_overrides(
loader_overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -1083,6 +1107,7 @@ pub async fn load_global_mcp_servers(
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
let merged_toml = config_layer_stack.effective_config();
+7
View File
@@ -115,6 +115,7 @@ pub struct ConfigService {
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
host_name: Option<String>,
}
impl ConfigService {
@@ -123,12 +124,14 @@ impl ConfigService {
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
host_name: Option<String>,
) -> Self {
Self {
codex_home,
cli_overrides,
loader_overrides,
cloud_requirements,
host_name,
}
}
@@ -138,6 +141,7 @@ impl ConfigService {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
host_name: codex_config::host_name(),
}
}
@@ -148,6 +152,7 @@ impl ConfigService {
Vec::new(),
LoaderOverrides::without_managed_config_for_tests(),
CloudRequirementsLoader::default(),
/*host_name*/ None,
)
}
@@ -166,6 +171,7 @@ impl ConfigService {
.loader_overrides(self.loader_overrides.clone())
.fallback_cwd(Some(cwd.to_path_buf()))
.cloud_requirements(self.cloud_requirements.clone())
.host_name(self.host_name.clone())
.build()
.await
.map_err(|err| {
@@ -432,6 +438,7 @@ impl ConfigService {
self.loader_overrides.clone(),
self.cloud_requirements.clone(),
&codex_config::NoopThreadConfigLoader,
self.host_name.as_deref(),
)
.await
}
@@ -223,6 +223,7 @@ async fn read_includes_origins_and_layers() {
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
/*host_name*/ None,
);
let response = service
@@ -301,6 +302,7 @@ writable_roots = ["~/code"]
vec![],
loader_overrides,
CloudRequirementsLoader::default(),
/*host_name*/ None,
);
let response = service
@@ -341,6 +343,7 @@ async fn write_value_reports_override() {
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
/*host_name*/ None,
);
let result = service
@@ -440,6 +443,7 @@ async fn invalid_user_value_rejected_even_if_overridden_by_managed() {
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
/*host_name*/ None,
);
let error = service
@@ -507,6 +511,7 @@ async fn write_value_rejects_feature_requirement_conflict() {
..Default::default()
}))
}),
/*host_name*/ None,
);
let error = service
@@ -553,6 +558,7 @@ async fn write_value_rejects_profile_feature_requirement_conflict() {
..Default::default()
}))
}),
/*host_name*/ None,
);
let error = service
@@ -603,6 +609,7 @@ async fn read_reports_managed_overrides_user_and_session_flags() {
cli_overrides,
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
/*host_name*/ None,
);
let response = service
@@ -656,6 +663,7 @@ async fn write_value_reports_managed_override() {
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
/*host_name*/ None,
);
let result = service
+3 -4
View File
@@ -10,7 +10,7 @@ This module is the canonical place to **load and describe Codex configuration la
Exported from `codex_core::config_loader`:
- `load_config_layers_state(fs, codex_home, cwd_opt, cli_overrides, overrides, cloud_requirements, thread_config_loader) -> ConfigLayerStack`
- `load_config_layers_state(fs, codex_home, cwd_opt, cli_overrides, overrides, cloud_requirements, thread_config_loader, host_name) -> ConfigLayerStack`
- `ConfigLayerStack`
- `effective_config() -> toml::Value`
- `origins() -> HashMap<String, ConfigLayerMetadata>`
@@ -41,9 +41,7 @@ computing the effective config and origins metadata. This is what
Most callers want the effective config plus metadata:
```rust
use codex_core::config_loader::{
CloudRequirementsLoader, LoaderOverrides, load_config_layers_state,
};
use codex_core::config_loader::{CloudRequirementsLoader, LoaderOverrides, load_config_layers_state};
use codex_config::NoopThreadConfigLoader;
use codex_exec_server::LOCAL_FS;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -59,6 +57,7 @@ let layers = load_config_layers_state(
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&NoopThreadConfigLoader,
/*host_name*/ None,
).await?;
let effective = layers.effective_config();
+11 -2
View File
@@ -1,6 +1,7 @@
use super::ConfigRequirementsToml;
use super::ConfigRequirementsWithSources;
use super::RequirementSource;
use super::merge_requirements_with_remote_sandbox_config;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use core_foundation::base::TCFType;
@@ -64,6 +65,7 @@ fn load_managed_admin_config() -> io::Result<Option<ManagedAdminConfigLayer>> {
pub(crate) async fn load_managed_admin_requirements_toml(
target: &mut ConfigRequirementsWithSources,
override_base64: Option<&str>,
host_name: Option<&str>,
) -> io::Result<()> {
if let Some(encoded) = override_base64 {
let trimmed = encoded.trim();
@@ -71,9 +73,11 @@ pub(crate) async fn load_managed_admin_requirements_toml(
return Ok(());
}
target.merge_unset_fields(
merge_requirements_with_remote_sandbox_config(
target,
managed_preferences_requirements_source(),
parse_managed_requirements_base64(trimmed)?,
host_name,
);
return Ok(());
}
@@ -81,7 +85,12 @@ pub(crate) async fn load_managed_admin_requirements_toml(
match task::spawn_blocking(load_managed_admin_requirements).await {
Ok(result) => {
if let Some(requirements) = result? {
target.merge_unset_fields(managed_preferences_requirements_source(), requirements);
merge_requirements_with_remote_sandbox_config(
target,
managed_preferences_requirements_source(),
requirements,
host_name,
);
}
Ok(())
}
+39 -6
View File
@@ -54,6 +54,7 @@ pub use codex_config::NetworkDomainPermissionsToml;
pub use codex_config::NetworkRequirementsToml;
pub use codex_config::NetworkUnixSocketPermissionToml;
pub use codex_config::NetworkUnixSocketPermissionsToml;
pub use codex_config::RemoteSandboxConfigToml;
pub use codex_config::RequirementSource;
pub use codex_config::ResidencyRequirement;
pub use codex_config::SandboxModeRequirement;
@@ -122,6 +123,7 @@ pub(crate) async fn first_layer_config_error_from_entries(
/// associated with it such that `cwd` should be `Some(...)`. Only for
/// thread-agnostic config loading (e.g., for the app server's `/config`
/// endpoint) should `cwd` be `None`.
#[allow(clippy::too_many_arguments)]
pub async fn load_config_layers_state(
fs: &dyn ExecutorFileSystem,
codex_home: &Path,
@@ -130,6 +132,7 @@ pub async fn load_config_layers_state(
overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
thread_config_loader: &dyn ThreadConfigLoader,
host_name: Option<&str>,
) -> io::Result<ConfigLayerStack> {
let ignore_user_config = overrides.ignore_user_config;
let ignore_user_and_project_exec_policy_rules =
@@ -137,8 +140,12 @@ pub async fn load_config_layers_state(
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
if let Some(requirements) = cloud_requirements.get().await.map_err(io::Error::other)? {
config_requirements_toml
.merge_unset_fields(RequirementSource::CloudRequirements, requirements);
merge_requirements_with_remote_sandbox_config(
&mut config_requirements_toml,
RequirementSource::CloudRequirements,
requirements,
host_name,
);
}
#[cfg(target_os = "macos")]
@@ -147,12 +154,19 @@ pub async fn load_config_layers_state(
overrides
.macos_managed_config_requirements_base64
.as_deref(),
host_name,
)
.await?;
// Honor the system requirements.toml location.
let requirements_toml_file = system_requirements_toml_file()?;
load_requirements_toml(fs, &mut config_requirements_toml, &requirements_toml_file).await?;
load_requirements_toml(
fs,
&mut config_requirements_toml,
&requirements_toml_file,
host_name,
)
.await?;
// Make a best-effort to support the legacy `managed_config.toml` as a
// requirements specification.
@@ -161,6 +175,7 @@ pub async fn load_config_layers_state(
load_requirements_from_legacy_scheme(
&mut config_requirements_toml,
loaded_config_layers.clone(),
host_name,
)
.await?;
@@ -413,6 +428,7 @@ async fn load_requirements_toml(
fs: &dyn ExecutorFileSystem,
config_requirements_toml: &mut ConfigRequirementsWithSources,
requirements_toml_file: &AbsolutePathBuf,
host_name: Option<&str>,
) -> io::Result<()> {
match fs
.read_file_text(requirements_toml_file, /*sandbox*/ None)
@@ -439,11 +455,13 @@ async fn load_requirements_toml(
),
)
})?;
config_requirements_toml.merge_unset_fields(
merge_requirements_with_remote_sandbox_config(
config_requirements_toml,
RequirementSource::SystemRequirementsToml {
file: requirements_toml_file.clone(),
},
requirements_config,
host_name,
);
}
Err(e) => {
@@ -558,6 +576,7 @@ fn windows_program_data_dir_from_known_folder() -> io::Result<PathBuf> {
async fn load_requirements_from_legacy_scheme(
config_requirements_toml: &mut ConfigRequirementsWithSources,
loaded_config_layers: LoadedConfigLayers,
host_name: Option<&str>,
) -> io::Result<()> {
// In this implementation, earlier layers cannot be overwritten by later
// layers, so list managed_config_from_mdm first because it has the highest
@@ -590,13 +609,27 @@ async fn load_requirements_from_legacy_scheme(
)
})?;
let new_requirements_toml = ConfigRequirementsToml::from(legacy_config);
config_requirements_toml.merge_unset_fields(source, new_requirements_toml);
merge_requirements_with_remote_sandbox_config(
config_requirements_toml,
source,
ConfigRequirementsToml::from(legacy_config),
host_name,
);
}
Ok(())
}
pub(super) fn merge_requirements_with_remote_sandbox_config(
target: &mut ConfigRequirementsWithSources,
source: RequirementSource,
mut requirements: ConfigRequirementsToml,
host_name: Option<&str>,
) {
requirements.apply_remote_sandbox_config(host_name);
target.merge_unset_fields(source, requirements);
}
struct ProjectTrustContext {
project_root: AbsolutePathBuf,
project_root_key: String,
+126 -1
View File
@@ -24,7 +24,6 @@ use codex_exec_server::LOCAL_FS;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::protocol::AskForApproval;
#[cfg(target_os = "macos")]
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
@@ -104,6 +103,7 @@ async fn returns_config_error_for_invalid_user_config_toml() {
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await
.expect_err("expected error");
@@ -136,6 +136,7 @@ async fn ignore_user_config_keeps_empty_user_layer() -> std::io::Result<()> {
},
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -167,6 +168,7 @@ async fn ignore_rules_marks_config_stack_for_exec_policy_rule_skip() -> std::io:
},
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -192,6 +194,7 @@ async fn returns_config_error_for_invalid_managed_config_toml() {
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await
.expect_err("expected error");
@@ -278,6 +281,7 @@ extra = true
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await
.expect("load config");
@@ -312,6 +316,7 @@ async fn returns_empty_when_all_layers_missing() {
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await
.expect("load layers");
@@ -380,6 +385,7 @@ async fn includes_thread_config_layers_in_stack() -> anyhow::Result<()> {
features: BTreeMap::from([("plugins".to_string(), false)]),
..Default::default()
})]),
/*host_name*/ None,
)
.await?;
@@ -456,6 +462,7 @@ flag = false
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await
.expect("load config");
@@ -559,6 +566,7 @@ allowed_sandbox_modes = ["read-only"]
loader_overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -622,6 +630,7 @@ allowed_approval_policies = ["never"]
loader_overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -663,6 +672,7 @@ personality = true
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&requirements_file,
/*host_name*/ None,
)
.await?;
@@ -764,6 +774,7 @@ allowed_approval_policies = ["on-request"]
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
@@ -776,6 +787,7 @@ allowed_approval_policies = ["on-request"]
}))
}),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -818,6 +830,7 @@ allowed_approval_policies = ["on-request"]
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
@@ -833,6 +846,7 @@ allowed_approval_policies = ["on-request"]
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&AbsolutePathBuf::try_from(requirements_file)?,
/*host_name*/ None,
)
.await?;
@@ -854,6 +868,54 @@ allowed_approval_policies = ["on-request"]
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn system_remote_sandbox_config_keeps_cloud_sandbox_modes() -> anyhow::Result<()> {
let tmp = tempdir()?;
let requirements_file = tmp.path().join("requirements.toml");
tokio::fs::write(
&requirements_file,
r#"
[[remote_sandbox_config]]
hostname_patterns = ["runner-*.ci.example.com"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
"#,
)
.await?;
let cloud_source = RequirementSource::CloudRequirements;
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
config_requirements_toml.merge_unset_fields(
cloud_source.clone(),
toml::from_str(
r#"
allowed_sandbox_modes = ["read-only"]
"#,
)?,
);
load_requirements_toml(
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&AbsolutePathBuf::try_from(requirements_file)?,
Some("runner-01.ci.example.com"),
)
.await?;
let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?;
assert_eq!(
config_requirements
.sandbox_policy
.can_set(&SandboxPolicy::new_workspace_write_policy()),
Err(ConstraintError::InvalidValue {
field_name: "sandbox_mode",
candidate: "WorkspaceWrite".into(),
allowed: "[ReadOnly]".into(),
requirement_source: cloud_source,
})
);
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn load_requirements_toml_resolves_deny_read_against_parent() -> anyhow::Result<()> {
let tmp = tempdir()?;
@@ -875,6 +937,7 @@ deny_read = ["./sensitive", "../shared/secret.txt"]
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&requirements_file,
/*host_name*/ None,
)
.await?;
@@ -929,6 +992,7 @@ deny_read = ["./sensitive/**/*.txt"]
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&requirements_file,
/*host_name*/ None,
)
.await?;
@@ -974,6 +1038,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
@@ -995,6 +1060,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
LoaderOverrides::default(),
cloud_requirements,
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -1018,6 +1084,53 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
Ok(())
}
#[tokio::test]
async fn load_config_layers_applies_matching_remote_sandbox_config() -> anyhow::Result<()> {
let tmp = tempdir()?;
let codex_home = tmp.path().join("home");
tokio::fs::create_dir_all(&codex_home).await?;
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?;
let requirements: ConfigRequirementsToml = toml::from_str(
r#"
allowed_sandbox_modes = ["read-only"]
[[remote_sandbox_config]]
hostname_patterns = ["runner-*.ci.example.com"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
"#,
)?;
let cloud_requirements = CloudRequirementsLoader::new(async move { Ok(Some(requirements)) });
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
LoaderOverrides::default(),
cloud_requirements,
&codex_config::NoopThreadConfigLoader,
Some("runner-01.ci.example.com"),
)
.await?;
assert_eq!(
layers.requirements_toml().allowed_sandbox_modes,
Some(vec![
crate::config_loader::SandboxModeRequirement::ReadOnly,
crate::config_loader::SandboxModeRequirement::WorkspaceWrite,
])
);
assert!(
layers
.requirements()
.sandbox_policy
.can_set(&SandboxPolicy::new_workspace_write_policy())
.is_ok()
);
Ok(())
}
#[tokio::test]
async fn load_config_layers_fails_when_cloud_requirements_loader_fails() -> anyhow::Result<()> {
let tmp = tempdir()?;
@@ -1039,6 +1152,7 @@ async fn load_config_layers_fails_when_cloud_requirements_loader_fails() -> anyh
))
}),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await
.expect_err("cloud requirements failure should fail closed");
@@ -1087,6 +1201,7 @@ async fn project_layers_prefer_closest_cwd() -> std::io::Result<()> {
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -1233,6 +1348,7 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -1274,6 +1390,7 @@ async fn codex_home_is_not_loaded_as_project_layer_from_home_dir() -> std::io::R
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -1332,6 +1449,7 @@ async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Resul
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -1404,6 +1522,7 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
let project_layers_untrusted: Vec<_> = layers_untrusted
@@ -1444,6 +1563,7 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
let project_layers_unknown: Vec<_> = layers_unknown
@@ -1511,6 +1631,7 @@ async fn project_trust_does_not_match_configured_alias_for_canonical_cwd() -> st
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -1665,6 +1786,7 @@ async fn invalid_project_config_ignored_when_untrusted_or_unknown() -> std::io::
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
let project_layers: Vec<_> = layers
@@ -1734,6 +1856,7 @@ async fn project_layer_without_config_toml_is_disabled_when_untrusted_or_unknown
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
let project_layers: Vec<_> = layers
@@ -1795,6 +1918,7 @@ async fn cli_overrides_with_relative_paths_do_not_break_trust_check() -> std::io
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -1839,6 +1963,7 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<()
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await?;
@@ -54,6 +54,7 @@ async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await
.context("failed to load Codex config")?;
+1
View File
@@ -546,6 +546,7 @@ pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec<PathBuf>, for
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
/*host_name*/ None,
)
.await
{
+2
View File
@@ -640,6 +640,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::GuardianSubagent]),
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
remote_sandbox_config: None,
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
guardian_policy_config: None,
feature_requirements: Some(FeatureRequirementsToml {
@@ -843,6 +844,7 @@ approval_policy = "never"
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: Some(Vec::new()),
guardian_policy_config: None,
feature_requirements: None,