Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,14 @@ pub struct ConfigReadResponse {
pub struct ConfigRequirements {
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
pub enforce_residency: Option<ResidencyRequirement>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export_to = "v2/")]
pub enum ResidencyRequirement {
Us,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Example (from OpenAI's official VSCode extension):
- `config/read` — fetch the effective config on disk after resolving config layering.
- `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.
- `configRequirements/read` — fetch the loaded requirements allow-lists from `requirements.toml` and/or MDM (or `null` if none are configured).
- `configRequirements/read` — fetch the loaded requirements allow-lists and `enforceResidency` from `requirements.toml` and/or MDM (or `null` if none are configured).

### Example: Start or resume a thread

Expand Down
17 changes: 17 additions & 0 deletions codex-rs/app-server/src/config_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use codex_core::config::ConfigServiceError;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::ConfigRequirementsToml;
use codex_core::config_loader::LoaderOverrides;
use codex_core::config_loader::ResidencyRequirement as CoreResidencyRequirement;
use codex_core::config_loader::SandboxModeRequirement as CoreSandboxModeRequirement;
use serde_json::json;
use std::path::PathBuf;
Expand Down Expand Up @@ -91,6 +92,9 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
.filter_map(map_sandbox_mode_requirement_to_api)
.collect()
}),
enforce_residency: requirements
.enforce_residency
.map(map_residency_requirement_to_api),
}
}

Expand All @@ -103,6 +107,14 @@ fn map_sandbox_mode_requirement_to_api(mode: CoreSandboxModeRequirement) -> Opti
}
}

fn map_residency_requirement_to_api(
residency: CoreResidencyRequirement,
) -> codex_app_server_protocol::ResidencyRequirement {
match residency {
CoreResidencyRequirement::Us => codex_app_server_protocol::ResidencyRequirement::Us,
}
}

fn map_error(err: ConfigServiceError) -> JSONRPCErrorError {
if let Some(code) = err.write_error_code() {
return config_write_error(code, err.to_string());
Expand Down Expand Up @@ -144,6 +156,7 @@ mod tests {
]),
mcp_servers: None,
rules: None,
enforce_residency: Some(CoreResidencyRequirement::Us),
};

let mapped = map_requirements_toml_to_api(requirements);
Expand All @@ -159,5 +172,9 @@ mod tests {
mapped.allowed_sandbox_modes,
Some(vec![SandboxMode::ReadOnly]),
);
assert_eq!(
mapped.enforce_residency,
Some(codex_app_server_protocol::ResidencyRequirement::Us),
);
}
}
4 changes: 4 additions & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use codex_core::config_loader::LoaderOverrides;
use codex_core::default_client::SetOriginatorError;
use codex_core::default_client::USER_AGENT_SUFFIX;
use codex_core::default_client::get_codex_user_agent;
use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::default_client::set_default_originator;
use codex_feedback::CodexFeedback;
use codex_protocol::ThreadId;
Expand Down Expand Up @@ -104,6 +105,7 @@ pub(crate) struct MessageProcessor {
outgoing: Arc<OutgoingMessageSender>,
codex_message_processor: CodexMessageProcessor,
config_api: ConfigApi,
config: Arc<Config>,
initialized: bool,
config_warnings: Vec<ConfigWarningNotification>,
}
Expand Down Expand Up @@ -169,6 +171,7 @@ impl MessageProcessor {
outgoing,
codex_message_processor,
config_api,
config,
initialized: false,
config_warnings,
}
Expand Down Expand Up @@ -241,6 +244,7 @@ impl MessageProcessor {
}
}
}
set_default_client_residency_requirement(self.config.enforce_residency.value());
let user_agent_suffix = format!("{name}; {version}");
if let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() {
*suffix = Some(user_agent_suffix);
Expand Down
1 change: 1 addition & 0 deletions codex-rs/cloud-requirements/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ mod tests {
allowed_sandbox_modes: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
})
);
}
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::config_loader::ConfigRequirements;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::McpServerIdentity;
use crate::config_loader::McpServerRequirement;
use crate::config_loader::ResidencyRequirement;
use crate::config_loader::Sourced;
use crate::config_loader::load_config_layers_state;
use crate::features::Feature;
Expand Down Expand Up @@ -140,6 +141,11 @@ pub struct Config {

pub sandbox_policy: Constrained<SandboxPolicy>,

/// enforce_residency means web traffic cannot be routed outside of a
/// particular geography. HTTP clients should direct their requests
/// using backend-specific headers or URLs to enforce this.
pub enforce_residency: Constrained<Option<ResidencyRequirement>>,

/// True if the user passed in an override or set a value in config.toml
/// for either of approval_policy or sandbox_mode.
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
Expand Down Expand Up @@ -1516,6 +1522,7 @@ impl Config {
sandbox_policy: mut constrained_sandbox_policy,
mcp_servers,
exec_policy: _,
enforce_residency,
} = requirements;

constrained_approval_policy
Expand All @@ -1538,6 +1545,7 @@ impl Config {
cwd: resolved_cwd,
approval_policy: constrained_approval_policy,
sandbox_policy: constrained_sandbox_policy,
enforce_residency,
did_user_set_custom_approval_policy_or_sandbox_mode,
forced_auto_mode_downgraded_on_windows,
shell_environment_policy,
Expand Down Expand Up @@ -3771,6 +3779,7 @@ model_verbosity = "high"
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::Never),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
enforce_residency: Constrained::allow_any(None),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
Expand Down Expand Up @@ -3855,6 +3864,7 @@ model_verbosity = "high"
model_provider: fixture.openai_chat_completions_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
enforce_residency: Constrained::allow_any(None),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
Expand Down Expand Up @@ -3954,6 +3964,7 @@ model_verbosity = "high"
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
enforce_residency: Constrained::allow_any(None),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
Expand Down Expand Up @@ -4039,6 +4050,7 @@ model_verbosity = "high"
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
enforce_residency: Constrained::allow_any(None),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
Expand Down
46 changes: 46 additions & 0 deletions codex-rs/core/src/config_loader/config_requirements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub struct ConfigRequirements {
pub sandbox_policy: Constrained<SandboxPolicy>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub(crate) exec_policy: Option<Sourced<RequirementsExecPolicy>>,
pub enforce_residency: Constrained<Option<ResidencyRequirement>>,
}

impl Default for ConfigRequirements {
Expand All @@ -61,6 +62,7 @@ impl Default for ConfigRequirements {
sandbox_policy: Constrained::allow_any(SandboxPolicy::ReadOnly),
mcp_servers: None,
exec_policy: None,
enforce_residency: Constrained::allow_any(None),
}
}
}
Expand All @@ -84,6 +86,7 @@ pub struct ConfigRequirementsToml {
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
pub rules: Option<RequirementsExecPolicyToml>,
pub enforce_residency: Option<ResidencyRequirement>,
}

/// Value paired with the requirement source it came from, for better error
Expand Down Expand Up @@ -114,6 +117,7 @@ pub struct ConfigRequirementsWithSources {
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
}

impl ConfigRequirementsWithSources {
Expand Down Expand Up @@ -146,6 +150,7 @@ impl ConfigRequirementsWithSources {
allowed_sandbox_modes,
mcp_servers,
rules,
enforce_residency,
}
);
}
Expand All @@ -156,12 +161,14 @@ impl ConfigRequirementsWithSources {
allowed_sandbox_modes,
mcp_servers,
rules,
enforce_residency,
} = self;
ConfigRequirementsToml {
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
mcp_servers: mcp_servers.map(|sourced| sourced.value),
rules: rules.map(|sourced| sourced.value),
enforce_residency: enforce_residency.map(|sourced| sourced.value),
}
}
}
Expand Down Expand Up @@ -193,12 +200,19 @@ impl From<SandboxMode> for SandboxModeRequirement {
}
}

#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ResidencyRequirement {
Us,
}

impl ConfigRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allowed_approval_policies.is_none()
&& self.allowed_sandbox_modes.is_none()
&& self.mcp_servers.is_none()
&& self.rules.is_none()
&& self.enforce_residency.is_none()
}
}

Expand All @@ -211,6 +225,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
allowed_sandbox_modes,
mcp_servers,
rules,
enforce_residency,
} = toml;

let approval_policy: Constrained<AskForApproval> = match allowed_approval_policies {
Expand Down Expand Up @@ -298,11 +313,33 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
None => None,
};

let enforce_residency: Constrained<Option<ResidencyRequirement>> = match enforce_residency {
Some(Sourced {
value: residency,
source: requirement_source,
}) => {
let required = Some(residency);
Constrained::new(required, move |candidate| {
if candidate == &required {
Ok(())
} else {
Err(ConstraintError::InvalidValue {
field_name: "enforce_residency",
candidate: format!("{candidate:?}"),
allowed: format!("{required:?}"),
requirement_source: requirement_source.clone(),
})
}
})?
}
None => Constrained::allow_any(None),
};
Ok(ConfigRequirements {
approval_policy,
sandbox_policy,
mcp_servers,
exec_policy,
enforce_residency,
})
}
}
Expand All @@ -329,6 +366,7 @@ mod tests {
allowed_sandbox_modes,
mcp_servers,
rules,
enforce_residency,
} = toml;
ConfigRequirementsWithSources {
allowed_approval_policies: allowed_approval_policies
Expand All @@ -337,6 +375,8 @@ mod tests {
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)),
rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
enforce_residency: enforce_residency
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
}
}

Expand All @@ -350,6 +390,8 @@ mod tests {
SandboxModeRequirement::WorkspaceWrite,
SandboxModeRequirement::DangerFullAccess,
];
let enforce_residency = ResidencyRequirement::Us;
let enforce_source = source.clone();

// Intentionally constructed without `..Default::default()` so adding a new field to
// `ConfigRequirementsToml` forces this test to be updated.
Expand All @@ -358,6 +400,7 @@ mod tests {
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
mcp_servers: None,
rules: None,
enforce_residency: Some(enforce_residency),
};

target.merge_unset_fields(source.clone(), other);
Expand All @@ -372,6 +415,7 @@ mod tests {
allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source)),
mcp_servers: None,
rules: None,
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
}
);
}
Expand Down Expand Up @@ -401,6 +445,7 @@ mod tests {
allowed_sandbox_modes: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
}
);
Ok(())
Expand Down Expand Up @@ -438,6 +483,7 @@ mod tests {
allowed_sandbox_modes: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
}
);
Ok(())
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/config_loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub use config_requirements::ConfigRequirementsToml;
pub use config_requirements::McpServerIdentity;
pub use config_requirements::McpServerRequirement;
pub use config_requirements::RequirementSource;
pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::Sourced;
pub use diagnostics::ConfigError;
Expand Down
Loading
Loading