Compare commits

...

2 Commits

Author SHA1 Message Date
Abhinav Vedmala
501047b14d Trim remote sandbox requirements tests 2026-04-20 14:55:15 -07:00
Abhinav Vedmala
4c7beb6c08 Add remote sandbox requirements override 2026-04-20 14:39:41 -07:00
16 changed files with 329 additions and 17 deletions

View File

@@ -7020,6 +7020,15 @@
"object",
"null"
]
},
"remoteAllowedSandboxModes": {
"items": {
"$ref": "#/definitions/v2/SandboxMode"
},
"type": [
"array",
"null"
]
}
},
"type": "object"

View File

@@ -3597,6 +3597,15 @@
"object",
"null"
]
},
"remoteAllowedSandboxModes": {
"items": {
"$ref": "#/definitions/SandboxMode"
},
"type": [
"array",
"null"
]
}
},
"type": "object"

View File

@@ -106,6 +106,15 @@
"object",
"null"
]
},
"remoteAllowedSandboxModes": {
"items": {
"$ref": "#/definitions/SandboxMode"
},
"type": [
"array",
"null"
]
}
},
"type": "object"

View File

@@ -6,4 +6,4 @@ import type { AskForApproval } from "./AskForApproval";
import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, remoteAllowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};

View File

@@ -889,6 +889,7 @@ pub struct ConfigRequirements {
#[experimental("configRequirements/read.allowedApprovalsReviewers")]
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
pub remote_allowed_sandbox_modes: Option<Vec<SandboxMode>>,
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
pub feature_requirements: Option<BTreeMap<String, bool>>,
pub enforce_residency: Option<ResidencyRequirement>,
@@ -7884,6 +7885,7 @@ mod tests {
}]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
enforce_residency: None,

View File

@@ -205,7 +205,7 @@ Example with notification opt-out:
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin `details` returned by detect. When a request includes plugin imports, the server emits `externalAgentConfig/import/completed` after the full import finishes (immediately after the response when everything completed synchronously, or after background remote imports finish).
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly`.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `remoteAllowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly`. `allowedSandboxModes` is the effective sandbox allow-list for the current machine; on remote machines it comes from `remote_allowed_sandbox_modes` when set, falling back to `allowed_sandbox_modes` otherwise.
### Example: Start or resume a thread

View File

@@ -381,6 +381,12 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
.filter_map(map_sandbox_mode_requirement_to_api)
.collect()
}),
remote_allowed_sandbox_modes: requirements.remote_allowed_sandbox_modes.map(|modes| {
modes
.into_iter()
.filter_map(map_sandbox_mode_requirement_to_api)
.collect()
}),
allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| {
let mut normalized = modes
.into_iter()
@@ -563,6 +569,10 @@ mod tests {
CoreSandboxModeRequirement::ReadOnly,
CoreSandboxModeRequirement::ExternalSandbox,
]),
remote_allowed_sandbox_modes: Some(vec![
CoreSandboxModeRequirement::ReadOnly,
CoreSandboxModeRequirement::WorkspaceWrite,
]),
allowed_web_search_modes: Some(vec![
codex_core::config_loader::WebSearchModeRequirement::Cached,
]),
@@ -628,6 +638,10 @@ mod tests {
mapped.allowed_sandbox_modes,
Some(vec![SandboxMode::ReadOnly]),
);
assert_eq!(
mapped.remote_allowed_sandbox_modes,
Some(vec![SandboxMode::ReadOnly, SandboxMode::WorkspaceWrite]),
);
assert_eq!(
mapped.allowed_web_search_modes,
Some(vec![WebSearchMode::Cached, WebSearchMode::Disabled]),
@@ -675,6 +689,7 @@ mod tests {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -733,6 +748,7 @@ mod tests {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: Some(Vec::new()),
guardian_policy_config: None,
feature_requirements: None,

View File

@@ -1164,6 +1164,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1194,6 +1195,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1224,6 +1226,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1271,6 +1274,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1354,6 +1358,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1427,6 +1432,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1498,6 +1504,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1696,6 +1703,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1732,6 +1740,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1788,6 +1797,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1839,6 +1849,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1894,6 +1905,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1950,6 +1962,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2006,6 +2019,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2095,6 +2109,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2123,6 +2138,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,

View File

@@ -10,6 +10,7 @@ use serde::de::Error as _;
use serde::de::value::Error as ValueDeserializerError;
use serde::de::value::StrDeserializer;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt;
use super::requirements_exec_policy::RequirementsExecPolicy;
@@ -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_allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
#[serde(rename = "features", alias = "feature_requirements")]
pub feature_requirements: Option<FeatureRequirementsToml>,
@@ -660,6 +662,7 @@ pub struct ConfigRequirementsWithSources {
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
pub remote_allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
@@ -693,6 +696,7 @@ impl ConfigRequirementsWithSources {
allowed_approval_policies: _,
allowed_approvals_reviewers: _,
allowed_sandbox_modes: _,
remote_allowed_sandbox_modes: _,
allowed_web_search_modes: _,
feature_requirements: _,
mcp_servers: _,
@@ -720,6 +724,7 @@ impl ConfigRequirementsWithSources {
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
remote_allowed_sandbox_modes,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
@@ -745,6 +750,7 @@ impl ConfigRequirementsWithSources {
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
remote_allowed_sandbox_modes,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
@@ -759,6 +765,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_allowed_sandbox_modes: remote_allowed_sandbox_modes.map(|sourced| sourced.value),
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),
@@ -810,6 +817,7 @@ impl ConfigRequirementsToml {
self.allowed_approval_policies.is_none()
&& self.allowed_approvals_reviewers.is_none()
&& self.allowed_sandbox_modes.is_none()
&& self.remote_allowed_sandbox_modes.is_none()
&& self.allowed_web_search_modes.is_none()
&& self
.feature_requirements
@@ -831,6 +839,16 @@ impl ConfigRequirementsToml {
}
}
pub fn is_remote_machine() -> bool {
is_remote_machine_with_env_var(|name| std::env::var_os(name))
}
fn is_remote_machine_with_env_var(mut env_var: impl FnMut(&str) -> Option<OsString>) -> bool {
["CI", "SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"]
.into_iter()
.any(|name| env_var(name).is_some_and(|value| !value.as_os_str().is_empty()))
}
impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
type Error = ConstraintError;
@@ -839,6 +857,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
remote_allowed_sandbox_modes,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
@@ -910,6 +929,13 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
),
};
if let Some(modes) = remote_allowed_sandbox_modes.as_ref() {
validate_sandbox_modes_include_read_only("remote_allowed_sandbox_modes", modes)?;
}
if let Some(modes) = allowed_sandbox_modes.as_ref() {
validate_sandbox_modes_include_read_only("allowed_sandbox_modes", modes)?;
}
// TODO(gt): `ConfigRequirementsToml` should let the author specify the
// default `SandboxPolicy`? Should do this for `AskForApproval` too?
//
@@ -923,15 +949,6 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
value: modes,
source: requirement_source,
}) => {
if !modes.contains(&SandboxModeRequirement::ReadOnly) {
return Err(ConstraintError::InvalidValue {
field_name: "allowed_sandbox_modes",
candidate: format!("{modes:?}"),
allowed: "must include 'read-only' to allow any SandboxPolicy".to_string(),
requirement_source,
});
};
let requirement_source_for_error = requirement_source.clone();
let constrained = Constrained::new(default_sandbox_policy, move |candidate| {
let mode = match candidate {
@@ -1071,6 +1088,22 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
}
}
fn validate_sandbox_modes_include_read_only(
field_name: &'static str,
modes: &Sourced<Vec<SandboxModeRequirement>>,
) -> Result<(), ConstraintError> {
if modes.value.contains(&SandboxModeRequirement::ReadOnly) {
Ok(())
} else {
Err(ConstraintError::InvalidValue {
field_name,
candidate: format!("{:?}", modes.value),
allowed: "must include 'read-only' to allow any SandboxPolicy".to_string(),
requirement_source: modes.source.clone(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1099,6 +1132,7 @@ mod tests {
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
remote_allowed_sandbox_modes,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
@@ -1116,6 +1150,8 @@ mod tests {
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_sandbox_modes: allowed_sandbox_modes
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
remote_allowed_sandbox_modes: remote_allowed_sandbox_modes
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_web_search_modes: allowed_web_search_modes
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
feature_requirements: feature_requirements
@@ -1144,6 +1180,10 @@ mod tests {
SandboxModeRequirement::WorkspaceWrite,
SandboxModeRequirement::DangerFullAccess,
];
let remote_allowed_sandbox_modes = vec![
SandboxModeRequirement::ReadOnly,
SandboxModeRequirement::WorkspaceWrite,
];
let allowed_web_search_modes = vec![
WebSearchModeRequirement::Cached,
WebSearchModeRequirement::Live,
@@ -1161,6 +1201,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_allowed_sandbox_modes: Some(remote_allowed_sandbox_modes.clone()),
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
feature_requirements: Some(feature_requirements.clone()),
mcp_servers: None,
@@ -1186,6 +1227,10 @@ mod tests {
source.clone(),
)),
allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)),
remote_allowed_sandbox_modes: Some(Sourced::new(
remote_allowed_sandbox_modes,
source.clone(),
)),
allowed_web_search_modes: Some(Sourced::new(
allowed_web_search_modes,
enforce_source.clone(),
@@ -1229,6 +1274,7 @@ mod tests {
)),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
@@ -1274,6 +1320,7 @@ mod tests {
)),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
@@ -1828,6 +1875,25 @@ allowed_approvals_reviewers = ["user"]
Ok(())
}
#[test]
fn detects_remote_machine_from_non_empty_env_vars() {
assert!(!is_remote_machine_with_env_var(|_| None));
assert!(!is_remote_machine_with_env_var(|name| {
if name == "CI" {
Some(OsString::from(""))
} else {
None
}
}));
assert!(is_remote_machine_with_env_var(|name| {
if name == "SSH_TTY" {
Some(OsString::from("/dev/ttys001"))
} else {
None
}
}));
}
#[test]
fn deserialize_allowed_sandbox_modes() -> Result<()> {
let toml_str = r#"
@@ -1883,6 +1949,47 @@ allowed_approvals_reviewers = ["user"]
Ok(())
}
#[test]
fn deserialize_remote_allowed_sandbox_modes() -> Result<()> {
let toml_str = r#"
remote_allowed_sandbox_modes = ["read-only", "workspace-write", "danger-full-access", "external-sandbox"]
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let expected_modes = Some(vec![
SandboxModeRequirement::ReadOnly,
SandboxModeRequirement::WorkspaceWrite,
SandboxModeRequirement::DangerFullAccess,
SandboxModeRequirement::ExternalSandbox,
]);
assert_eq!(config.remote_allowed_sandbox_modes, expected_modes);
assert!(!config.is_empty());
Ok(())
}
#[test]
fn remote_sandbox_modes_must_include_read_only() {
let mut target = ConfigRequirementsWithSources::default();
target.merge_unset_fields(
RequirementSource::CloudRequirements,
ConfigRequirementsToml {
remote_allowed_sandbox_modes: Some(vec![SandboxModeRequirement::WorkspaceWrite]),
..Default::default()
},
);
assert_eq!(
ConfigRequirements::try_from(target),
Err(ConstraintError::InvalidValue {
field_name: "remote_allowed_sandbox_modes",
candidate: "[WorkspaceWrite]".into(),
allowed: "must include 'read-only' to allow any SandboxPolicy".into(),
requirement_source: RequirementSource::CloudRequirements,
})
);
}
#[test]
fn deserialize_allowed_web_search_modes() -> Result<()> {
let toml_str = r#"

View File

@@ -47,6 +47,7 @@ pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::Sourced;
pub use config_requirements::WebSearchModeRequirement;
pub use config_requirements::is_remote_machine;
pub use constraint::Constrained;
pub use constraint::ConstraintError;
pub use constraint::ConstraintResult;

View File

@@ -22,6 +22,7 @@ pub struct LoaderOverrides {
#[cfg(target_os = "macos")]
pub managed_preferences_base64: Option<String>,
pub macos_managed_config_requirements_base64: Option<String>,
pub requirements_remote_machine: Option<bool>,
}
impl LoaderOverrides {
@@ -45,6 +46,7 @@ impl LoaderOverrides {
#[cfg(target_os = "macos")]
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(String::new()),
requirements_remote_machine: None,
}
}
}

View File

@@ -5367,6 +5367,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_allowed_sandbox_modes: None,
allowed_web_search_modes: Some(vec![
crate::config_loader::WebSearchModeRequirement::Cached,
]),
@@ -6043,6 +6044,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_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
@@ -6069,6 +6071,75 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
Ok(())
}
#[tokio::test]
async fn environment_specific_sandbox_requirements_follow_remote_override() -> std::io::Result<()> {
let local_home = TempDir::new()?;
std::fs::write(
local_home.path().join(CONFIG_TOML_FILE),
r#"sandbox_mode = "danger-full-access"
"#,
)?;
let mut local_loader_overrides =
crate::config_loader::LoaderOverrides::without_managed_config_for_tests();
local_loader_overrides.requirements_remote_machine = Some(false);
let local_config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(local_home.path().to_path_buf())
.fallback_cwd(Some(local_home.path().to_path_buf()))
.loader_overrides(local_loader_overrides)
.cloud_requirements(CloudRequirementsLoader::new(async {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
allowed_sandbox_modes: Some(vec![
crate::config_loader::SandboxModeRequirement::ReadOnly,
]),
remote_allowed_sandbox_modes: Some(vec![
crate::config_loader::SandboxModeRequirement::ReadOnly,
crate::config_loader::SandboxModeRequirement::DangerFullAccess,
]),
..Default::default()
}))
}))
.build()
.await?;
assert_eq!(
*local_config.permissions.sandbox_policy.get(),
SandboxPolicy::new_read_only_policy()
);
let remote_home = TempDir::new()?;
std::fs::write(
remote_home.path().join(CONFIG_TOML_FILE),
r#"sandbox_mode = "danger-full-access"
"#,
)?;
let mut remote_loader_overrides =
crate::config_loader::LoaderOverrides::without_managed_config_for_tests();
remote_loader_overrides.requirements_remote_machine = Some(true);
let remote_config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(remote_home.path().to_path_buf())
.fallback_cwd(Some(remote_home.path().to_path_buf()))
.loader_overrides(remote_loader_overrides)
.cloud_requirements(CloudRequirementsLoader::new(async {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
allowed_sandbox_modes: Some(vec![
crate::config_loader::SandboxModeRequirement::ReadOnly,
]),
remote_allowed_sandbox_modes: Some(vec![
crate::config_loader::SandboxModeRequirement::ReadOnly,
crate::config_loader::SandboxModeRequirement::DangerFullAccess,
]),
..Default::default()
}))
}))
.build()
.await?;
assert_eq!(
*remote_config.permissions.sandbox_policy.get(),
SandboxPolicy::DangerFullAccess
);
Ok(())
}
#[tokio::test]
async fn requirements_web_search_mode_overrides_danger_full_access_default() -> std::io::Result<()>
{

View File

@@ -1,6 +1,7 @@
use super::ConfigRequirementsToml;
use super::ConfigRequirementsWithSources;
use super::RequirementSource;
use super::merge_requirements;
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>,
remote_machine: Option<bool>,
) -> 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(
target,
managed_preferences_requirements_source(),
parse_managed_requirements_base64(trimmed)?,
remote_machine,
);
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(
target,
managed_preferences_requirements_source(),
requirements,
remote_machine,
);
}
Ok(())
}

View File

@@ -11,6 +11,7 @@ use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigRequirementsWithSources;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
use codex_config::is_remote_machine;
use codex_exec_server::ExecutorFileSystem;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_protocol::config_types::ApprovalsReviewer;
@@ -129,10 +130,15 @@ pub async fn load_config_layers_state(
cloud_requirements: CloudRequirementsLoader,
) -> io::Result<ConfigLayerStack> {
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
let requirements_remote_machine = overrides.requirements_remote_machine;
if let Some(requirements) = cloud_requirements.get().await.map_err(io::Error::other)? {
config_requirements_toml
.merge_unset_fields(RequirementSource::CloudRequirements, requirements);
merge_requirements(
&mut config_requirements_toml,
RequirementSource::CloudRequirements,
requirements,
requirements_remote_machine,
);
}
#[cfg(target_os = "macos")]
@@ -141,12 +147,19 @@ pub async fn load_config_layers_state(
overrides
.macos_managed_config_requirements_base64
.as_deref(),
requirements_remote_machine,
)
.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,
requirements_remote_machine,
)
.await?;
// Make a best-effort to support the legacy `managed_config.toml` as a
// requirements specification.
@@ -319,6 +332,21 @@ pub async fn load_config_layers_state(
)
}
pub(super) fn merge_requirements(
target: &mut ConfigRequirementsWithSources,
source: RequirementSource,
mut requirements: ConfigRequirementsToml,
remote_machine: Option<bool>,
) {
if remote_machine.unwrap_or_else(is_remote_machine)
&& let Some(remote_allowed_sandbox_modes) =
requirements.remote_allowed_sandbox_modes.clone()
{
requirements.allowed_sandbox_modes = Some(remote_allowed_sandbox_modes);
}
target.merge_unset_fields(source, requirements);
}
/// Attempts to load a config.toml file from `config_toml`.
/// - If the file exists and is valid TOML, passes the parsed `toml::Value` to
/// `create_entry` and returns the resulting layer entry.
@@ -374,6 +402,7 @@ async fn load_requirements_toml(
fs: &dyn ExecutorFileSystem,
config_requirements_toml: &mut ConfigRequirementsWithSources,
requirements_toml_file: &AbsolutePathBuf,
remote_machine: Option<bool>,
) -> io::Result<()> {
match fs
.read_file_text(requirements_toml_file, /*sandbox*/ None)
@@ -400,11 +429,13 @@ async fn load_requirements_toml(
),
)
})?;
config_requirements_toml.merge_unset_fields(
merge_requirements(
config_requirements_toml,
RequirementSource::SystemRequirementsToml {
file: requirements_toml_file.clone(),
},
requirements_config,
remote_machine,
);
}
Err(e) => {

View File

@@ -544,6 +544,7 @@ personality = true
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&requirements_file,
/*remote_machine*/ None,
)
.await?;
@@ -645,6 +646,7 @@ allowed_approval_policies = ["on-request"]
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
@@ -698,6 +700,7 @@ allowed_approval_policies = ["on-request"]
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
@@ -713,6 +716,7 @@ allowed_approval_policies = ["on-request"]
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&AbsolutePathBuf::try_from(requirements_file)?,
/*remote_machine*/ None,
)
.await?;
@@ -755,6 +759,7 @@ deny_read = ["./sensitive", "../shared/secret.txt"]
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&requirements_file,
/*remote_machine*/ None,
)
.await?;
@@ -809,6 +814,7 @@ deny_read = ["./sensitive/**/*.txt"]
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&requirements_file,
/*remote_machine*/ None,
)
.await?;
@@ -854,6 +860,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_allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,

View File

@@ -129,6 +129,21 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
));
}
if let Some(modes) = requirements_toml.remote_allowed_sandbox_modes.as_ref() {
let value = join_or_empty(
modes
.iter()
.copied()
.map(format_sandbox_mode_requirement)
.collect::<Vec<_>>(),
);
requirement_lines.push(requirement_line(
"remote_allowed_sandbox_modes",
value,
/*source*/ None,
));
}
if let Some(modes) = requirements_toml.allowed_web_search_modes.as_ref() {
let normalized = normalize_allowed_web_search_modes(modes);
let value = join_or_empty(
@@ -640,6 +655,10 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::GuardianSubagent]),
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
remote_allowed_sandbox_modes: Some(vec![
SandboxModeRequirement::ReadOnly,
SandboxModeRequirement::WorkspaceWrite,
]),
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
guardian_policy_config: None,
feature_requirements: Some(FeatureRequirementsToml {
@@ -691,6 +710,9 @@ mod tests {
.as_str(),
)
);
assert!(rendered.contains(
"remote_allowed_sandbox_modes: read-only, workspace-write (source: <unspecified>)"
));
assert!(
rendered.contains(
"allowed_web_search_modes: cached, disabled (source: cloud requirements)"
@@ -843,6 +865,7 @@ approval_policy = "never"
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_allowed_sandbox_modes: None,
allowed_web_search_modes: Some(Vec::new()),
guardian_policy_config: None,
feature_requirements: None,