Add requirements.toml support for managed config fields

This commit is contained in:
Abhinav Vedmala
2026-06-11 20:31:29 +00:00
parent 9d88e299a7
commit 6092d88f23
20 changed files with 1821 additions and 43 deletions
@@ -6687,6 +6687,15 @@
}
]
},
"AuthCredentialsStoreMode": {
"enum": [
"file",
"keyring",
"auto",
"ephemeral"
],
"type": "string"
},
"AuthMode": {
"description": "Authentication mode for OpenAI-backed providers.",
"oneOf": [
@@ -8082,6 +8091,12 @@
"null"
]
},
"allowLoginShell": {
"type": [
"boolean",
"null"
]
},
"allowManagedHooksOnly": {
"type": [
"boolean",
@@ -8097,6 +8112,24 @@
"null"
]
},
"allowedChatgptWorkspaces": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"object",
"null"
]
},
"allowedLoginMethods": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"object",
"null"
]
},
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
@@ -8133,6 +8166,28 @@
"null"
]
},
"chatgptBaseUrl": {
"type": [
"string",
"null"
]
},
"checkForUpdateOnStartup": {
"type": [
"boolean",
"null"
]
},
"cliAuthCredentialsStore": {
"anyOf": [
{
"$ref": "#/definitions/v2/AuthCredentialsStoreMode"
},
{
"type": "null"
}
]
},
"computerUse": {
"anyOf": [
{
@@ -8167,6 +8222,58 @@
"object",
"null"
]
},
"feedback": {
"anyOf": [
{
"$ref": "#/definitions/v2/FeedbackRequirements"
},
{
"type": "null"
}
]
},
"logDir": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"modelCatalogJson": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"otel": {
"description": "Lossless config-native OTEL requirements, including exporter headers."
},
"shellEnvironmentPolicy": {
"description": "Lossless config-native shell environment policy requirements."
},
"sqliteHome": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"windowsSandboxPrivateDesktop": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
@@ -8920,6 +9027,17 @@
],
"type": "string"
},
"FeedbackRequirements": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"FeedbackUploadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -965,6 +965,15 @@
}
]
},
"AuthCredentialsStoreMode": {
"enum": [
"file",
"keyring",
"auto",
"ephemeral"
],
"type": "string"
},
"AuthMode": {
"description": "Authentication mode for OpenAI-backed providers.",
"oneOf": [
@@ -4404,6 +4413,12 @@
"null"
]
},
"allowLoginShell": {
"type": [
"boolean",
"null"
]
},
"allowManagedHooksOnly": {
"type": [
"boolean",
@@ -4419,6 +4434,24 @@
"null"
]
},
"allowedChatgptWorkspaces": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"object",
"null"
]
},
"allowedLoginMethods": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"object",
"null"
]
},
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
@@ -4455,6 +4488,28 @@
"null"
]
},
"chatgptBaseUrl": {
"type": [
"string",
"null"
]
},
"checkForUpdateOnStartup": {
"type": [
"boolean",
"null"
]
},
"cliAuthCredentialsStore": {
"anyOf": [
{
"$ref": "#/definitions/AuthCredentialsStoreMode"
},
{
"type": "null"
}
]
},
"computerUse": {
"anyOf": [
{
@@ -4489,6 +4544,58 @@
"object",
"null"
]
},
"feedback": {
"anyOf": [
{
"$ref": "#/definitions/FeedbackRequirements"
},
{
"type": "null"
}
]
},
"logDir": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"modelCatalogJson": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"otel": {
"description": "Lossless config-native OTEL requirements, including exporter headers."
},
"shellEnvironmentPolicy": {
"description": "Lossless config-native shell environment policy requirements."
},
"sqliteHome": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"windowsSandboxPrivateDesktop": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
@@ -5242,6 +5349,17 @@
],
"type": "string"
},
"FeedbackRequirements": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"FeedbackUploadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -1,6 +1,10 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -60,6 +64,15 @@
}
]
},
"AuthCredentialsStoreMode": {
"enum": [
"file",
"keyring",
"auto",
"ephemeral"
],
"type": "string"
},
"ComputerUseRequirements": {
"properties": {
"allowLockedComputerUse": {
@@ -79,6 +92,12 @@
"null"
]
},
"allowLoginShell": {
"type": [
"boolean",
"null"
]
},
"allowManagedHooksOnly": {
"type": [
"boolean",
@@ -94,6 +113,24 @@
"null"
]
},
"allowedChatgptWorkspaces": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"object",
"null"
]
},
"allowedLoginMethods": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"object",
"null"
]
},
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
@@ -130,6 +167,28 @@
"null"
]
},
"chatgptBaseUrl": {
"type": [
"string",
"null"
]
},
"checkForUpdateOnStartup": {
"type": [
"boolean",
"null"
]
},
"cliAuthCredentialsStore": {
"anyOf": [
{
"$ref": "#/definitions/AuthCredentialsStoreMode"
},
{
"type": "null"
}
]
},
"computerUse": {
"anyOf": [
{
@@ -164,6 +223,58 @@
"object",
"null"
]
},
"feedback": {
"anyOf": [
{
"$ref": "#/definitions/FeedbackRequirements"
},
{
"type": "null"
}
]
},
"logDir": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"modelCatalogJson": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"otel": {
"description": "Lossless config-native OTEL requirements, including exporter headers."
},
"shellEnvironmentPolicy": {
"description": "Lossless config-native shell environment policy requirements."
},
"sqliteHome": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"windowsSandboxPrivateDesktop": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
@@ -268,6 +379,17 @@
],
"type": "object"
},
"FeedbackRequirements": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"ManagedHooksRequirements": {
"properties": {
"PermissionRequest": {
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AuthCredentialsStoreMode = "file" | "keyring" | "auto" | "ephemeral";
@@ -1,11 +1,21 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { WebSearchMode } from "../WebSearchMode";
import type { JsonValue } from "../serde_json/JsonValue";
import type { AskForApproval } from "./AskForApproval";
import type { AuthCredentialsStoreMode } from "./AuthCredentialsStoreMode";
import type { ComputerUseRequirements } from "./ComputerUseRequirements";
import type { FeedbackRequirements } from "./FeedbackRequirements";
import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, allowedLoginMethods: { [key in string]?: boolean } | null, allowedChatgptWorkspaces: { [key in string]?: boolean } | null, cliAuthCredentialsStore: AuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, sqliteHome: AbsolutePathBuf | null, logDir: AbsolutePathBuf | null, modelCatalogJson: AbsolutePathBuf | null, checkForUpdateOnStartup: boolean | null, /**
* Lossless config-native OTEL requirements, including exporter headers.
*/
otel: JsonValue | null, allowLoginShell: boolean | null, /**
* Lossless config-native shell environment policy requirements.
*/
shellEnvironmentPolicy: JsonValue | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FeedbackRequirements = { enabled: boolean | null, };
@@ -35,6 +35,7 @@ export type { AppsListResponse } from "./AppsListResponse";
export type { AskForApproval } from "./AskForApproval";
export type { AttestationGenerateParams } from "./AttestationGenerateParams";
export type { AttestationGenerateResponse } from "./AttestationGenerateResponse";
export type { AuthCredentialsStoreMode } from "./AuthCredentialsStoreMode";
export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource";
export type { ByteRange } from "./ByteRange";
export type { CancelLoginAccountParams } from "./CancelLoginAccountParams";
@@ -108,6 +109,7 @@ export type { ExternalAgentConfigImportParams } from "./ExternalAgentConfigImpor
export type { ExternalAgentConfigImportResponse } from "./ExternalAgentConfigImportResponse";
export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem";
export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType";
export type { FeedbackRequirements } from "./FeedbackRequirements";
export type { FeedbackUploadParams } from "./FeedbackUploadParams";
export type { FeedbackUploadResponse } from "./FeedbackUploadResponse";
export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision";
@@ -387,6 +387,38 @@ pub struct ConfigRequirements {
pub enforce_residency: Option<ResidencyRequirement>,
#[experimental("configRequirements/read.network")]
pub network: Option<NetworkRequirements>,
pub allowed_login_methods: Option<BTreeMap<String, bool>>,
pub allowed_chatgpt_workspaces: Option<BTreeMap<String, bool>>,
pub cli_auth_credentials_store: Option<AuthCredentialsStoreMode>,
pub chatgpt_base_url: Option<String>,
pub sqlite_home: Option<AbsolutePathBuf>,
pub log_dir: Option<AbsolutePathBuf>,
pub model_catalog_json: Option<AbsolutePathBuf>,
pub check_for_update_on_startup: Option<bool>,
/// Lossless config-native OTEL requirements, including exporter headers.
pub otel: Option<JsonValue>,
pub allow_login_shell: Option<bool>,
/// Lossless config-native shell environment policy requirements.
pub shell_environment_policy: Option<JsonValue>,
pub feedback: Option<FeedbackRequirements>,
pub windows_sandbox_private_desktop: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export_to = "v2/")]
pub enum AuthCredentialsStoreMode {
File,
Keyring,
Auto,
Ephemeral,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct FeedbackRequirements {
pub enabled: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
@@ -1708,6 +1708,19 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
hooks: None,
enforce_residency: None,
network: None,
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
otel: None,
allow_login_shell: None,
shell_environment_policy: None,
feedback: None,
windows_sandbox_private_desktop: None,
});
assert_eq!(reason, Some("askForApproval.granular"));
+1 -1
View File
@@ -234,7 +234,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/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish).
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`, `allowedLoginMethods`, `allowedChatgptWorkspaces`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, `network` constraints, and pinned runtime settings such as credential storage, ChatGPT base URL, state/log/model-catalog paths, update checks, login-shell and environment policy, feedback, Windows private-desktop use, and OTEL configuration. OTEL exporter responses include their complete headers map.
### Example: Start or resume a thread
@@ -7,6 +7,7 @@ use crate::error_code::invalid_request;
use crate::outgoing_message::ConnectionRequestId;
use crate::outgoing_message::OutgoingMessageSender;
use codex_analytics::AnalyticsEventsClient;
use codex_app_server_protocol::AuthCredentialsStoreMode as ApiAuthCredentialsStoreMode;
use codex_app_server_protocol::ClientResponsePayload;
use codex_app_server_protocol::ComputerUseRequirements;
use codex_app_server_protocol::ConfigBatchWriteParams;
@@ -21,6 +22,7 @@ use codex_app_server_protocol::ConfiguredHookHandler;
use codex_app_server_protocol::ConfiguredHookMatcherGroup;
use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams;
use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse;
use codex_app_server_protocol::FeedbackRequirements;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::ManagedHooksRequirements;
use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse;
@@ -314,6 +316,11 @@ impl ConfigRequestProcessor {
}
fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigRequirements {
let windows_sandbox_private_desktop = requirements
.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop);
ConfigRequirements {
allowed_approval_policies: requirements.allowed_approval_policies.map(|policies| {
policies
@@ -375,6 +382,40 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
.enforce_residency
.map(map_residency_requirement_to_api),
network: requirements.network.map(map_network_requirements_to_api),
allowed_login_methods: requirements.allowed_login_methods,
allowed_chatgpt_workspaces: requirements.allowed_chatgpt_workspaces,
cli_auth_credentials_store: requirements
.cli_auth_credentials_store
.map(map_auth_credentials_store_mode_to_api),
chatgpt_base_url: requirements.chatgpt_base_url,
sqlite_home: requirements.sqlite_home,
log_dir: requirements.log_dir,
model_catalog_json: requirements.model_catalog_json,
check_for_update_on_startup: requirements.check_for_update_on_startup,
otel: requirements.otel.map(|otel| json!(otel)),
allow_login_shell: requirements.allow_login_shell,
shell_environment_policy: requirements
.shell_environment_policy
.map(|policy| json!(policy)),
feedback: requirements.feedback.map(|feedback| FeedbackRequirements {
enabled: feedback.enabled,
}),
windows_sandbox_private_desktop,
}
}
fn map_auth_credentials_store_mode_to_api(
mode: codex_config::types::AuthCredentialsStoreMode,
) -> ApiAuthCredentialsStoreMode {
match mode {
codex_config::types::AuthCredentialsStoreMode::File => ApiAuthCredentialsStoreMode::File,
codex_config::types::AuthCredentialsStoreMode::Keyring => {
ApiAuthCredentialsStoreMode::Keyring
}
codex_config::types::AuthCredentialsStoreMode::Auto => ApiAuthCredentialsStoreMode::Auto,
codex_config::types::AuthCredentialsStoreMode::Ephemeral => {
ApiAuthCredentialsStoreMode::Ephemeral
}
}
}
@@ -569,8 +610,15 @@ mod tests {
use codex_config::ComputerUseRequirementsToml;
use codex_config::ConfigRequirementsToml;
use codex_config::WindowsRequirementsToml;
use codex_config::types::OtelConfigToml;
use codex_config::types::OtelExporterKind;
use codex_config::types::OtelHttpProtocol as CoreOtelHttpProtocol;
use codex_config::types::ShellEnvironmentPolicyToml;
use codex_protocol::config_types::ShellEnvironmentPolicyInherit as CoreShellEnvironmentPolicyInherit;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
use std::collections::HashMap;
#[test]
fn requirements_api_includes_allow_managed_hooks_only() {
@@ -643,6 +691,7 @@ mod tests {
codex_config::types::WindowsSandboxModeToml::Elevated,
codex_config::types::WindowsSandboxModeToml::Unelevated,
]),
sandbox_private_desktop: Some(false),
}),
..ConfigRequirementsToml::default()
});
@@ -654,5 +703,73 @@ mod tests {
WindowsSandboxSetupMode::Unelevated,
])
);
assert_eq!(mapped.windows_sandbox_private_desktop, Some(false));
}
#[test]
fn requirements_api_preserves_complex_config_values_and_otel_headers() {
let headers = HashMap::from([("Authorization".to_string(), "Bearer secret".to_string())]);
let shell_set = HashMap::from([("MANAGED".to_string(), "true".to_string())]);
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
otel: Some(OtelConfigToml {
log_user_prompt: Some(false),
environment: Some("production".to_string()),
exporter: Some(OtelExporterKind::OtlpHttp {
endpoint: "https://otel.example.com/v1/logs".to_string(),
headers,
protocol: CoreOtelHttpProtocol::Json,
tls: None,
}),
trace_exporter: None,
metrics_exporter: None,
span_attributes: Some(BTreeMap::from([("team".to_string(), "codex".to_string())])),
tracestate: None,
}),
shell_environment_policy: Some(ShellEnvironmentPolicyToml {
inherit: Some(CoreShellEnvironmentPolicyInherit::Core),
ignore_default_excludes: Some(false),
exclude: Some(vec!["*SECRET*".to_string()]),
r#set: Some(shell_set.clone()),
include_only: None,
experimental_use_profile: Some(false),
}),
..ConfigRequirementsToml::default()
});
assert_eq!(
mapped.otel,
Some(json!({
"log_user_prompt": false,
"environment": "production",
"exporter": {
"otlp-http": {
"endpoint": "https://otel.example.com/v1/logs",
"headers": {
"Authorization": "Bearer secret"
},
"protocol": "json",
"tls": null
}
},
"trace_exporter": null,
"metrics_exporter": null,
"span_attributes": {
"team": "codex"
},
"tracestate": null
}))
);
assert_eq!(
mapped.shell_environment_policy,
Some(json!({
"inherit": "core",
"ignore_default_excludes": false,
"exclude": ["*SECRET*"],
"set": shell_set,
"include_only": null,
"experimental_use_profile": false
}))
);
}
}
+496 -30
View File
@@ -20,6 +20,10 @@ use crate::ConstraintError;
use crate::ManagedHooksRequirementsToml;
use crate::mcp_types::AppToolApproval;
use crate::permissions_toml::PermissionProfileToml;
use crate::types::AuthCredentialsStoreMode;
use crate::types::FeedbackConfigToml;
use crate::types::OtelConfigToml;
use crate::types::ShellEnvironmentPolicyToml;
use crate::types::WindowsSandboxModeToml;
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -142,10 +146,23 @@ impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
/// normalization.
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigRequirements {
pub allowed_login_methods: Option<Sourced<BTreeMap<String, bool>>>,
pub allowed_chatgpt_workspaces: Option<Sourced<BTreeMap<String, bool>>>,
pub cli_auth_credentials_store: Option<Sourced<AuthCredentialsStoreMode>>,
pub chatgpt_base_url: Option<Sourced<String>>,
pub sqlite_home: Option<Sourced<AbsolutePathBuf>>,
pub log_dir: Option<Sourced<AbsolutePathBuf>>,
pub model_catalog_json: Option<Sourced<AbsolutePathBuf>>,
pub check_for_update_on_startup: Option<Sourced<bool>>,
pub otel: Option<Sourced<OtelConfigToml>>,
pub allow_login_shell: Option<Sourced<bool>>,
pub shell_environment_policy: Option<Sourced<ShellEnvironmentPolicyToml>>,
pub feedback: Option<Sourced<FeedbackConfigToml>>,
pub approval_policy: ConstrainedWithSource<AskForApproval>,
pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
pub permission_profile: ConstrainedWithSource<PermissionProfile>,
pub windows_sandbox_mode: ConstrainedWithSource<Option<WindowsSandboxModeToml>>,
pub windows_sandbox_private_desktop: Option<Sourced<bool>>,
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
pub allow_managed_hooks_only: Option<Sourced<bool>>,
pub allow_appshots: Option<Sourced<bool>>,
@@ -167,6 +184,18 @@ pub struct ConfigRequirements {
impl Default for ConfigRequirements {
fn default() -> Self {
Self {
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
otel: None,
allow_login_shell: None,
shell_environment_policy: None,
feedback: None,
approval_policy: ConstrainedWithSource::new(
Constrained::allow_any_from_default(),
/*source*/ None,
@@ -183,6 +212,7 @@ impl Default for ConfigRequirements {
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
),
windows_sandbox_private_desktop: None,
web_search_mode: ConstrainedWithSource::new(
Constrained::allow_any(WebSearchMode::Cached),
/*source*/ None,
@@ -714,11 +744,12 @@ impl ComputerUseRequirementsToml {
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct WindowsRequirementsToml {
pub allowed_sandbox_implementations: Option<Vec<WindowsSandboxModeToml>>,
pub sandbox_private_desktop: Option<bool>,
}
impl WindowsRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allowed_sandbox_implementations.is_none()
self.allowed_sandbox_implementations.is_none() && self.sandbox_private_desktop.is_none()
}
}
@@ -819,6 +850,18 @@ pub(crate) fn merge_app_requirements_descending(
/// Base config deserialized from system `requirements.toml` or MDM.
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsToml {
pub allowed_login_methods: Option<BTreeMap<String, bool>>,
pub allowed_chatgpt_workspaces: Option<BTreeMap<String, bool>>,
pub cli_auth_credentials_store: Option<AuthCredentialsStoreMode>,
pub chatgpt_base_url: Option<String>,
pub sqlite_home: Option<AbsolutePathBuf>,
pub log_dir: Option<AbsolutePathBuf>,
pub model_catalog_json: Option<AbsolutePathBuf>,
pub check_for_update_on_startup: Option<bool>,
pub otel: Option<OtelConfigToml>,
pub allow_login_shell: Option<bool>,
pub shell_environment_policy: Option<ShellEnvironmentPolicyToml>,
pub feedback: Option<FeedbackConfigToml>,
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
@@ -874,6 +917,18 @@ impl<T> std::ops::Deref for Sourced<T> {
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsWithSources {
pub allowed_login_methods: Option<Sourced<BTreeMap<String, bool>>>,
pub allowed_chatgpt_workspaces: Option<Sourced<BTreeMap<String, bool>>>,
pub cli_auth_credentials_store: Option<Sourced<AuthCredentialsStoreMode>>,
pub chatgpt_base_url: Option<Sourced<String>>,
pub sqlite_home: Option<Sourced<AbsolutePathBuf>>,
pub log_dir: Option<Sourced<AbsolutePathBuf>>,
pub model_catalog_json: Option<Sourced<AbsolutePathBuf>>,
pub check_for_update_on_startup: Option<Sourced<bool>>,
pub otel: Option<Sourced<OtelConfigToml>>,
pub allow_login_shell: Option<Sourced<bool>>,
pub shell_environment_policy: Option<Sourced<ShellEnvironmentPolicyToml>>,
pub feedback: Option<Sourced<FeedbackConfigToml>>,
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
@@ -915,6 +970,18 @@ impl ConfigRequirementsWithSources {
// Destructure without `..` so adding fields to `ConfigRequirementsToml`
// forces this merge logic to be updated.
let ConfigRequirementsToml {
allowed_login_methods: _,
allowed_chatgpt_workspaces: _,
cli_auth_credentials_store: _,
chatgpt_base_url: _,
sqlite_home: _,
log_dir: _,
model_catalog_json: _,
check_for_update_on_startup: _,
otel: _,
allow_login_shell: _,
shell_environment_policy: _,
feedback: _,
allowed_approval_policies: _,
allowed_approvals_reviewers: _,
allowed_sandbox_modes: _,
@@ -951,6 +1018,18 @@ impl ConfigRequirementsWithSources {
other,
source,
{
allowed_login_methods,
allowed_chatgpt_workspaces,
cli_auth_credentials_store,
chatgpt_base_url,
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -984,6 +1063,18 @@ impl ConfigRequirementsWithSources {
pub fn into_toml(self) -> ConfigRequirementsToml {
let ConfigRequirementsWithSources {
allowed_login_methods,
allowed_chatgpt_workspaces,
cli_auth_credentials_store,
chatgpt_base_url,
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -1006,6 +1097,18 @@ impl ConfigRequirementsWithSources {
guardian_policy_config,
} = self;
ConfigRequirementsToml {
allowed_login_methods: allowed_login_methods.map(|sourced| sourced.value),
allowed_chatgpt_workspaces: allowed_chatgpt_workspaces.map(|sourced| sourced.value),
cli_auth_credentials_store: cli_auth_credentials_store.map(|sourced| sourced.value),
chatgpt_base_url: chatgpt_base_url.map(|sourced| sourced.value),
sqlite_home: sqlite_home.map(|sourced| sourced.value),
log_dir: log_dir.map(|sourced| sourced.value),
model_catalog_json: model_catalog_json.map(|sourced| sourced.value),
check_for_update_on_startup: check_for_update_on_startup.map(|sourced| sourced.value),
otel: otel.map(|sourced| sourced.value),
allow_login_shell: allow_login_shell.map(|sourced| sourced.value),
shell_environment_policy: shell_environment_policy.map(|sourced| sourced.value),
feedback: feedback.map(|sourced| sourced.value),
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),
@@ -1095,7 +1198,37 @@ impl ConfigRequirementsToml {
}
pub fn is_empty(&self) -> bool {
self.allowed_approval_policies.is_none()
self.allowed_login_methods.is_none()
&& self.allowed_chatgpt_workspaces.is_none()
&& self.cli_auth_credentials_store.is_none()
&& self.chatgpt_base_url.is_none()
&& self.sqlite_home.is_none()
&& self.log_dir.is_none()
&& self.model_catalog_json.is_none()
&& self.check_for_update_on_startup.is_none()
&& self.otel.as_ref().is_none_or(|otel| {
otel.log_user_prompt.is_none()
&& otel.environment.is_none()
&& otel.exporter.is_none()
&& otel.trace_exporter.is_none()
&& otel.metrics_exporter.is_none()
&& otel.span_attributes.is_none()
&& otel.tracestate.is_none()
})
&& self.allow_login_shell.is_none()
&& self.shell_environment_policy.as_ref().is_none_or(|policy| {
policy.inherit.is_none()
&& policy.ignore_default_excludes.is_none()
&& policy.exclude.is_none()
&& policy.r#set.is_none()
&& policy.include_only.is_none()
&& policy.experimental_use_profile.is_none()
})
&& self
.feedback
.as_ref()
.is_none_or(|feedback| feedback.enabled.is_none())
&& self.allowed_approval_policies.is_none()
&& self.allowed_approvals_reviewers.is_none()
&& self.allowed_sandbox_modes.is_none()
&& self.allowed_permission_profiles.is_none()
@@ -1148,6 +1281,18 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
// config loading and requirements API projection. The normalized
// constraints below only need the compiled PermissionProfile envelope.
let ConfigRequirementsWithSources {
allowed_login_methods,
allowed_chatgpt_workspaces,
cli_auth_credentials_store,
chatgpt_base_url,
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -1170,6 +1315,37 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
guardian_policy_config,
} = toml;
if let Some(Sourced {
value: methods,
source,
}) = &allowed_login_methods
&& let Some(unknown) = methods
.keys()
.find(|method| method.as_str() != "api" && method.as_str() != "chatgpt")
{
return Err(ConstraintError::InvalidValue {
field_name: "allowed_login_methods",
candidate: unknown.clone(),
allowed: "[\"api\", \"chatgpt\"]".to_string(),
requirement_source: source.clone(),
});
}
if let Some(Sourced {
value: workspaces,
source,
}) = &allowed_chatgpt_workspaces
&& let Some(empty) = workspaces
.keys()
.find(|workspace| workspace.trim().is_empty())
{
return Err(ConstraintError::InvalidValue {
field_name: "allowed_chatgpt_workspaces",
candidate: empty.clone(),
allowed: "non-empty workspace identifiers".to_string(),
requirement_source: source.clone(),
});
}
let approval_policy = match allowed_approval_policies {
Some(Sourced {
value: policies,
@@ -1267,42 +1443,60 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
/*source*/ None,
),
};
let windows_sandbox_mode = match windows {
let (windows_sandbox_mode, windows_sandbox_private_desktop) = match windows {
Some(Sourced {
value:
WindowsRequirementsToml {
allowed_sandbox_implementations: Some(implementations),
allowed_sandbox_implementations,
sandbox_private_desktop,
},
source: requirement_source,
}) => {
if implementations.is_empty() {
return Err(ConstraintError::empty_field(
"windows.allowed_sandbox_implementations",
));
}
// Prefer elevated when both Windows sandbox implementations are allowed.
let initial_value = if implementations.contains(&WindowsSandboxModeToml::Elevated) {
WindowsSandboxModeToml::Elevated
} else {
WindowsSandboxModeToml::Unelevated
};
let sandbox_private_desktop = sandbox_private_desktop
.map(|value| Sourced::new(value, requirement_source.clone()));
let sandbox_mode = match allowed_sandbox_implementations {
Some(implementations) => {
if implementations.is_empty() {
return Err(ConstraintError::empty_field(
"windows.allowed_sandbox_implementations",
));
}
// Prefer elevated when both Windows sandbox implementations are allowed.
let initial_value =
if implementations.contains(&WindowsSandboxModeToml::Elevated) {
WindowsSandboxModeToml::Elevated
} else {
WindowsSandboxModeToml::Unelevated
};
let requirement_source_for_error = requirement_source.clone();
let constrained =
Constrained::new(Some(initial_value), move |candidate| match candidate {
Some(candidate) if implementations.contains(candidate) => Ok(()),
_ => Err(ConstraintError::InvalidValue {
field_name: "windows.sandbox",
candidate: format!("{candidate:?}"),
allowed: format!("{implementations:?}"),
requirement_source: requirement_source_for_error.clone(),
}),
})?;
ConstrainedWithSource::new(constrained, Some(requirement_source))
let requirement_source_for_error = requirement_source.clone();
let constrained = Constrained::new(
Some(initial_value),
move |candidate| match candidate {
Some(candidate) if implementations.contains(candidate) => Ok(()),
_ => Err(ConstraintError::InvalidValue {
field_name: "windows.sandbox",
candidate: format!("{candidate:?}"),
allowed: format!("{implementations:?}"),
requirement_source: requirement_source_for_error.clone(),
}),
},
)?;
ConstrainedWithSource::new(constrained, Some(requirement_source))
}
None => ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
),
};
(sandbox_mode, sandbox_private_desktop)
}
Some(_) | None => ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
None => (
ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
),
None,
),
};
let exec_policy = match rules {
@@ -1427,10 +1621,23 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
});
let guardian_policy_config_source = guardian_policy_config.map(|sourced| sourced.source);
Ok(ConfigRequirements {
allowed_login_methods,
allowed_chatgpt_workspaces,
cli_auth_credentials_store,
chatgpt_base_url,
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
approval_policy,
approvals_reviewer,
permission_profile,
windows_sandbox_mode,
windows_sandbox_private_desktop,
web_search_mode,
allow_managed_hooks_only,
allow_appshots,
@@ -1516,6 +1723,18 @@ mod tests {
fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources {
let ConfigRequirementsToml {
allowed_login_methods,
allowed_chatgpt_workspaces,
cli_auth_credentials_store,
chatgpt_base_url,
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -1539,6 +1758,26 @@ mod tests {
guardian_policy_config,
} = toml;
ConfigRequirementsWithSources {
allowed_login_methods: allowed_login_methods
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_chatgpt_workspaces: allowed_chatgpt_workspaces
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
cli_auth_credentials_store: cli_auth_credentials_store
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
chatgpt_base_url: chatgpt_base_url
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
sqlite_home: sqlite_home.map(|value| Sourced::new(value, RequirementSource::Unknown)),
log_dir: log_dir.map(|value| Sourced::new(value, RequirementSource::Unknown)),
model_catalog_json: model_catalog_json
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
check_for_update_on_startup: check_for_update_on_startup
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
otel: otel.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allow_login_shell: allow_login_shell
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
shell_environment_policy: shell_environment_policy
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
feedback: feedback.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_approval_policies: allowed_approval_policies
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_approvals_reviewers: allowed_approvals_reviewers
@@ -1707,6 +1946,152 @@ mod tests {
Ok(())
}
#[test]
fn deserialize_managed_config_replacement_requirements() -> Result<()> {
let sqlite_home = std::env::temp_dir().join("codex-sqlite");
let log_dir = std::env::temp_dir().join("codex-logs");
let model_catalog_json = std::env::temp_dir().join("models.json");
let requirements: ConfigRequirementsToml = from_str(&format!(
r#"
allowed_login_methods = {{ chatgpt = true, api = false }}
allowed_chatgpt_workspaces = {{ workspace_1 = true, workspace_2 = false }}
cli_auth_credentials_store = "keyring"
chatgpt_base_url = "https://chatgpt.example.com"
sqlite_home = {:?}
log_dir = {:?}
model_catalog_json = {:?}
check_for_update_on_startup = false
allow_login_shell = false
[otel]
log_user_prompt = false
environment = "prod"
span_attributes = {{ team = "codex" }}
[shell_environment_policy]
ignore_default_excludes = false
set = {{ MANAGED_ENV = "1" }}
[feedback]
enabled = false
[windows]
sandbox_private_desktop = false
"#,
sqlite_home.display().to_string(),
log_dir.display().to_string(),
model_catalog_json.display().to_string(),
))?;
assert_eq!(
requirements.allowed_login_methods,
Some(BTreeMap::from([
("api".to_string(), false),
("chatgpt".to_string(), true),
]))
);
assert_eq!(
requirements.allowed_chatgpt_workspaces,
Some(BTreeMap::from([
("workspace_1".to_string(), true),
("workspace_2".to_string(), false),
]))
);
assert_eq!(
requirements.cli_auth_credentials_store,
Some(AuthCredentialsStoreMode::Keyring)
);
assert_eq!(
requirements.chatgpt_base_url.as_deref(),
Some("https://chatgpt.example.com")
);
assert_eq!(
requirements.sqlite_home.as_deref(),
Some(sqlite_home.as_path())
);
assert_eq!(requirements.log_dir.as_deref(), Some(log_dir.as_path()));
assert_eq!(
requirements.model_catalog_json.as_deref(),
Some(model_catalog_json.as_path())
);
assert_eq!(requirements.check_for_update_on_startup, Some(false));
assert_eq!(requirements.allow_login_shell, Some(false));
assert_eq!(
requirements
.otel
.as_ref()
.and_then(|otel| otel.span_attributes.as_ref()),
Some(&BTreeMap::from([("team".to_string(), "codex".to_string())]))
);
assert_eq!(
requirements
.shell_environment_policy
.as_ref()
.and_then(|policy| policy.r#set.as_ref())
.and_then(|set| set.get("MANAGED_ENV"))
.map(String::as_str),
Some("1")
);
assert_eq!(
requirements.feedback,
Some(FeedbackConfigToml {
enabled: Some(false),
})
);
assert_eq!(
requirements.windows,
Some(WindowsRequirementsToml {
allowed_sandbox_implementations: None,
sandbox_private_desktop: Some(false),
})
);
assert!(!requirements.is_empty());
Ok(())
}
#[test]
fn normalize_rejects_unknown_login_method() -> Result<()> {
let config: ConfigRequirementsToml = from_str(
r#"
[allowed_login_methods]
chatgpt = true
sso = true
"#,
)?;
assert_eq!(
ConfigRequirements::try_from(with_unknown_source(config)),
Err(ConstraintError::InvalidValue {
field_name: "allowed_login_methods",
candidate: "sso".to_string(),
allowed: "[\"api\", \"chatgpt\"]".to_string(),
requirement_source: RequirementSource::Unknown,
})
);
Ok(())
}
#[test]
fn normalize_rejects_blank_chatgpt_workspace_id() -> Result<()> {
let config: ConfigRequirementsToml = from_str(
r#"
[allowed_chatgpt_workspaces]
" " = true
"#,
)?;
assert_eq!(
ConfigRequirements::try_from(with_unknown_source(config)),
Err(ConstraintError::InvalidValue {
field_name: "allowed_chatgpt_workspaces",
candidate: " ".to_string(),
allowed: "non-empty workspace identifiers".to_string(),
requirement_source: RequirementSource::Unknown,
})
);
Ok(())
}
#[test]
fn merge_unset_fields_copies_every_field_and_sets_sources() {
let mut target = ConfigRequirementsWithSources::default();
@@ -1729,6 +2114,27 @@ mod tests {
let computer_use = ComputerUseRequirementsToml {
allow_locked_computer_use: Some(false),
};
let allowed_login_methods =
BTreeMap::from([("api".to_string(), true), ("chatgpt".to_string(), false)]);
let allowed_chatgpt_workspaces = BTreeMap::from([("workspace-1".to_string(), true)]);
let sqlite_home = AbsolutePathBuf::try_from(std::env::temp_dir().join("sqlite"))
.expect("absolute sqlite home");
let log_dir = AbsolutePathBuf::try_from(std::env::temp_dir().join("logs"))
.expect("absolute log directory");
let model_catalog_json =
AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-model-catalog.json"))
.expect("absolute model catalog path");
let otel = OtelConfigToml {
log_user_prompt: Some(false),
..Default::default()
};
let shell_environment_policy = ShellEnvironmentPolicyToml {
ignore_default_excludes: Some(false),
..Default::default()
};
let feedback = FeedbackConfigToml {
enabled: Some(false),
};
let enforce_residency = ResidencyRequirement::Us;
let enforce_source = source.clone();
let guardian_policy_config = "Use the company-managed guardian policy.".to_string();
@@ -1736,6 +2142,18 @@ mod tests {
// Intentionally constructed without `..Default::default()` so adding a new field to
// `ConfigRequirementsToml` forces this test to be updated.
let other = ConfigRequirementsToml {
allowed_login_methods: Some(allowed_login_methods.clone()),
allowed_chatgpt_workspaces: Some(allowed_chatgpt_workspaces.clone()),
cli_auth_credentials_store: Some(AuthCredentialsStoreMode::Keyring),
chatgpt_base_url: Some("https://chatgpt.example.com".to_string()),
sqlite_home: Some(sqlite_home.clone()),
log_dir: Some(log_dir.clone()),
model_catalog_json: Some(model_catalog_json.clone()),
check_for_update_on_startup: Some(false),
otel: Some(otel.clone()),
allow_login_shell: Some(false),
shell_environment_policy: Some(shell_environment_policy.clone()),
feedback: Some(feedback.clone()),
allowed_approval_policies: Some(allowed_approval_policies.clone()),
allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()),
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
@@ -1764,6 +2182,30 @@ mod tests {
assert_eq!(
target,
ConfigRequirementsWithSources {
allowed_login_methods: Some(Sourced::new(allowed_login_methods, source.clone(),)),
allowed_chatgpt_workspaces: Some(Sourced::new(
allowed_chatgpt_workspaces,
source.clone(),
)),
cli_auth_credentials_store: Some(Sourced::new(
AuthCredentialsStoreMode::Keyring,
source.clone(),
)),
chatgpt_base_url: Some(Sourced::new(
"https://chatgpt.example.com".to_string(),
source.clone(),
)),
sqlite_home: Some(Sourced::new(sqlite_home, source.clone())),
log_dir: Some(Sourced::new(log_dir, source.clone())),
model_catalog_json: Some(Sourced::new(model_catalog_json, source.clone(),)),
check_for_update_on_startup: Some(Sourced::new(false, source.clone())),
otel: Some(Sourced::new(otel, source.clone())),
allow_login_shell: Some(Sourced::new(false, source.clone())),
shell_environment_policy: Some(Sourced::new(
shell_environment_policy,
source.clone(),
)),
feedback: Some(Sourced::new(feedback, source.clone())),
allowed_approval_policies: Some(Sourced::new(
allowed_approval_policies,
source.clone()
@@ -1824,6 +2266,18 @@ mod tests {
assert_eq!(
empty_target,
ConfigRequirementsWithSources {
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
otel: None,
allow_login_shell: None,
shell_environment_policy: None,
feedback: None,
allowed_approval_policies: Some(Sourced::new(
vec![AskForApproval::OnRequest],
source_location,
@@ -1877,6 +2331,18 @@ mod tests {
assert_eq!(
populated_target,
ConfigRequirementsWithSources {
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
otel: None,
allow_login_shell: None,
shell_environment_policy: None,
feedback: None,
allowed_approval_policies: Some(Sourced::new(
vec![AskForApproval::Never],
existing_source,
@@ -173,6 +173,18 @@ fn populate_merged_regular_fields_with_sources(
// Destructure without `..` so every new requirements field must choose
// whether it belongs in the regular TOML merge path or in a special merger.
let ConfigRequirementsToml {
allowed_login_methods,
allowed_chatgpt_workspaces,
cli_auth_credentials_store,
chatgpt_base_url,
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -196,6 +208,21 @@ fn populate_merged_regular_fields_with_sources(
guardian_policy_config,
} = requirements;
set_sourced!(allowed_login_methods, &["allowed_login_methods"]);
set_sourced!(allowed_chatgpt_workspaces, &["allowed_chatgpt_workspaces"]);
set_sourced!(cli_auth_credentials_store, &["cli_auth_credentials_store"]);
set_sourced!(chatgpt_base_url, &["chatgpt_base_url"]);
set_sourced!(sqlite_home, &["sqlite_home"]);
set_sourced!(log_dir, &["log_dir"]);
set_sourced!(model_catalog_json, &["model_catalog_json"]);
set_sourced!(
check_for_update_on_startup,
&["check_for_update_on_startup"]
);
set_sourced!(otel, &["otel"]);
set_sourced!(allow_login_shell, &["allow_login_shell"]);
set_sourced!(shell_environment_policy, &["shell_environment_policy"]);
set_sourced!(feedback, &["feedback"]);
set_sourced!(allowed_approval_policies, &["allowed_approval_policies"]);
set_sourced!(
allowed_approvals_reviewers,
@@ -291,6 +291,107 @@ approval_mode = "approve"
);
}
#[test]
fn managed_config_replacement_tables_merge_recursively() {
let composed = compose_requirements_for_hostname(
vec![
layer(
"req_low",
"Low",
r#"
[allowed_login_methods]
chatgpt = true
api = true
[allowed_chatgpt_workspaces]
workspace_a = true
workspace_b = true
[otel.span_attributes]
region = "us"
team = "low"
[shell_environment_policy.set]
LOW = "1"
SHARED = "low"
[windows]
sandbox_private_desktop = true
"#,
),
layer(
"req_high",
"High",
r#"
[allowed_login_methods]
api = false
[allowed_chatgpt_workspaces]
workspace_b = false
[otel.span_attributes]
team = "high"
[shell_environment_policy.set]
HIGH = "1"
SHARED = "high"
[feedback]
enabled = false
"#,
),
],
/*hostname*/ None,
)
.expect("compose requirements")
.expect("requirements present");
assert_eq!(
composed.clone().into_toml(),
expected_requirements(
r#"
[allowed_login_methods]
chatgpt = true
api = false
[allowed_chatgpt_workspaces]
workspace_a = true
workspace_b = false
[otel.span_attributes]
region = "us"
team = "high"
[shell_environment_policy.set]
LOW = "1"
HIGH = "1"
SHARED = "high"
[feedback]
enabled = false
[windows]
sandbox_private_desktop = true
"#,
)
);
assert_eq!(
composed.allowed_login_methods.map(|sourced| sourced.source),
Some(RequirementSource::Composite {
sources: vec![
RequirementSource::EnterpriseManaged {
id: "req_high".to_string(),
name: "High".to_string(),
},
RequirementSource::EnterpriseManaged {
id: "req_low".to_string(),
name: "Low".to_string(),
},
],
})
);
}
#[test]
fn merged_table_source_is_composite_in_priority_order() {
let high_source = RequirementSource::EnterpriseManaged {
+262
View File
@@ -8343,6 +8343,18 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
let fixture = create_test_fixture()?;
let requirements_toml = codex_config::ConfigRequirementsToml {
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
otel: None,
allow_login_shell: None,
shell_environment_policy: None,
feedback: None,
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
@@ -9335,6 +9347,256 @@ async fn explicit_approval_policy_falls_back_when_disallowed_by_requirements() -
Ok(())
}
#[tokio::test]
async fn exact_requirements_override_only_managed_leaves() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let catalog_path = codex_home.path().join("required-models.json");
let mut catalog = bundled_models_response()
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
catalog.models = catalog.models.into_iter().take(1).collect();
std::fs::write(
&catalog_path,
serde_json::to_string(&catalog).expect("serialize catalog"),
)?;
let required_sqlite_home = codex_home.path().join("required-state");
let required_log_dir = codex_home.path().join("required-logs");
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
forced_chatgpt_workspace_id = ["A", "C"]
cli_auth_credentials_store = "file"
chatgpt_base_url = "https://user.example/backend-api"
check_for_update_on_startup = true
allow_login_shell = true
[shell_environment_policy]
ignore_default_excludes = true
exclude = ["USER_*"]
[otel]
environment = "user"
[otel.span_attributes]
configured = "kept"
managed = "old"
[feedback]
enabled = true
[windows]
sandbox_private_desktop = true
"#,
)?;
let requirements = format!(
r#"
cli_auth_credentials_store = "keyring"
chatgpt_base_url = "https://managed.example/backend-api"
sqlite_home = "{}"
log_dir = "{}"
model_catalog_json = "{}"
check_for_update_on_startup = false
allow_login_shell = false
[allowed_login_methods]
chatgpt = true
api = false
[allowed_chatgpt_workspaces]
"A" = true
"B" = true
[shell_environment_policy]
ignore_default_excludes = false
experimental_use_profile = true
[otel]
log_user_prompt = false
environment = "managed"
[otel.span_attributes]
managed = "required"
[feedback]
enabled = false
[windows]
sandbox_private_desktop = false
"#,
required_sqlite_home.display(),
required_log_dir.display(),
catalog_path.display(),
);
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements),
)
.build()
.await?;
let expected_shell_environment_policy: ShellEnvironmentPolicy =
codex_config::types::ShellEnvironmentPolicyToml {
ignore_default_excludes: Some(false),
exclude: Some(vec!["USER_*".to_string()]),
experimental_use_profile: Some(true),
..Default::default()
}
.into();
assert_eq!(
(
config.forced_login_method,
config.forced_chatgpt_workspace_id.clone(),
config.cli_auth_credentials_store_mode,
config.chatgpt_base_url.clone(),
config.sqlite_home.clone(),
config.log_dir.clone(),
config.model_catalog.clone(),
config.check_for_update_on_startup,
config.permissions.allow_login_shell,
config.permissions.shell_environment_policy.clone(),
config.feedback_enabled,
config.permissions.windows_sandbox_private_desktop,
),
(
Some(ForcedLoginMethod::Chatgpt),
Some(vec!["A".to_string()]),
AuthCredentialsStoreMode::Keyring,
"https://managed.example/backend-api".to_string(),
required_sqlite_home,
required_log_dir,
Some(catalog),
false,
false,
expected_shell_environment_policy,
false,
false,
)
);
assert_eq!(
config.otel.span_attributes,
BTreeMap::from([
("configured".to_string(), "kept".to_string()),
("managed".to_string(), "required".to_string()),
])
);
Ok(())
}
#[tokio::test]
async fn invalid_required_otel_trace_metadata_fails_closed() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let err = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(
"[otel.span_attributes]\n\"\" = \"missing-key\"\n",
),
)
.build()
.await
.expect_err("invalid managed OTEL metadata should fail startup");
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert!(
err.to_string()
.contains("invalid required `otel.span_attributes`")
);
Ok(())
}
#[tokio::test]
async fn conflicting_login_method_requirements_fail_closed() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
"forced_login_method = \"api\"\n",
)?;
let err = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(
"[allowed_login_methods]\nchatgpt = true\n",
),
)
.build()
.await
.expect_err("conflicting login restrictions should fail closed");
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert!(
err.to_string()
.contains("conflicts with `allowed_login_methods`")
);
Ok(())
}
#[tokio::test]
async fn workspace_requirements_can_disable_all_chatgpt_workspaces() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
[allowed_chatgpt_workspaces]
"A" = false
"#,
),
)
.build()
.await?;
assert_eq!(config.forced_chatgpt_workspace_id, Some(Vec::new()));
Ok(())
}
#[tokio::test]
async fn bootstrap_config_applies_local_auth_requirements() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let requirements_path = codex_home.path().join("requirements.toml");
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
cli_auth_credentials_store = "file"
chatgpt_base_url = "https://user.example/backend-api"
"#,
)?;
std::fs::write(
&requirements_path,
r#"
cli_auth_credentials_store = "keyring"
chatgpt_base_url = "https://managed.example/backend-api"
"#,
)?;
let mut loader_overrides = LoaderOverrides::without_managed_config_for_tests();
loader_overrides.system_requirements_path = Some(requirements_path);
let config = load_bootstrap_config_as_toml_with_cli_and_load_options(
codex_home.path(),
Some(&codex_home.path().abs()),
Vec::new(),
ConfigLoadOptions {
loader_overrides,
..Default::default()
},
)
.await?;
assert_eq!(
(config.cli_auth_credentials_store, config.chatgpt_base_url,),
(
Some(AuthCredentialsStoreMode::Keyring),
Some("https://managed.example/backend-api".to_string()),
)
);
Ok(())
}
#[tokio::test]
async fn feature_requirements_normalize_effective_feature_values() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
+356 -6
View File
@@ -110,7 +110,9 @@ use rmcp::model::FormElicitationCapability;
use rmcp::model::UrlElicitationCapability;
use serde::Deserialize;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::hash_map::Entry;
@@ -1661,6 +1663,41 @@ pub async fn load_config_as_toml_with_cli_and_load_options(
Ok(cfg)
}
/// Load the raw merged config needed to bootstrap cloud requirements, applying
/// exact auth requirements from every layer available to this load.
///
/// The initial bootstrap call has no cloud loader, so system and MDM
/// requirements determine how the cloud bundle is fetched. A later call may
/// include cloud requirements, but those values cannot retroactively change
/// the loader that fetched them.
pub async fn load_bootstrap_config_as_toml_with_cli_and_load_options(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
options: impl Into<ConfigLoadOptions>,
) -> std::io::Result<ConfigToml> {
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home,
cwd.cloned(),
&cli_overrides,
options,
&codex_config::NoopThreadConfigLoader,
)
.await?;
let merged_toml = config_layer_stack.effective_config();
let mut cfg = deserialize_config_toml_with_base(merged_toml, codex_home)?;
let requirements = config_layer_stack.requirements_toml();
if let Some(required) = requirements.cli_auth_credentials_store {
cfg.cli_auth_credentials_store = Some(required);
}
if let Some(required) = requirements.chatgpt_base_url.as_ref() {
cfg.chatgpt_base_url = Some(required.clone());
}
Ok(cfg)
}
pub fn deserialize_config_toml_with_base(
root_value: TomlValue,
config_base_dir: &Path,
@@ -1818,6 +1855,171 @@ where
Ok(false)
}
fn apply_exact_requirement<T>(
field_name: &'static str,
configured_value: &mut Option<T>,
requirement: Option<Sourced<T>>,
startup_warnings: &mut Vec<String>,
) where
T: Clone + PartialEq + std::fmt::Debug,
{
let Some(Sourced { value, source }) = requirement else {
return;
};
if configured_value
.as_ref()
.is_some_and(|configured| configured != &value)
{
tracing::warn!(
?source,
?value,
"configured value is overridden by an exact requirement for {field_name}"
);
startup_warnings.push(format!(
"Configured value for `{field_name}` is overridden by the required value {value:?} from {source}."
));
}
*configured_value = Some(value);
}
fn merge_exact_requirement<T>(
field_name: &'static str,
configured_value: T,
requirement: Option<Sourced<T>>,
startup_warnings: &mut Vec<String>,
) -> std::io::Result<T>
where
T: Clone + PartialEq + std::fmt::Debug + Serialize + DeserializeOwned,
{
let Some(Sourced { value, source }) = requirement else {
return Ok(configured_value);
};
let mut configured_toml = TomlValue::try_from(configured_value).map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to serialize configured `{field_name}` value: {err}"),
)
})?;
let required_toml = TomlValue::try_from(value).map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to serialize required `{field_name}` value: {err}"),
)
})?;
if toml_values_conflict(&configured_toml, &required_toml) {
tracing::warn!(
?source,
"configured value contains leaves overridden by exact requirements for {field_name}"
);
startup_warnings.push(format!(
"Configured leaves under `{field_name}` are overridden by requirements from {source}."
));
}
codex_config::merge_toml_values(&mut configured_toml, &required_toml);
configured_toml.try_into().map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to deserialize required `{field_name}` value: {err}"),
)
})
}
fn toml_values_conflict(configured: &TomlValue, required: &TomlValue) -> bool {
match (configured, required) {
(TomlValue::Table(configured), TomlValue::Table(required)) => {
required.iter().any(|(key, required)| {
configured
.get(key)
.is_some_and(|configured| toml_values_conflict(configured, required))
})
}
_ => configured != required,
}
}
fn validate_otel_requirement(
requirement: &codex_config::types::OtelConfigToml,
) -> std::io::Result<()> {
if let Some(span_attributes) = requirement.span_attributes.as_ref() {
codex_otel::validate_span_attributes(span_attributes).map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid required `otel.span_attributes`: {err}"),
)
})?;
}
if let Some(tracestate) = requirement.tracestate.as_ref() {
codex_otel::validate_tracestate_entries(tracestate).map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid required `otel.tracestate`: {err}"),
)
})?;
}
Ok(())
}
fn apply_otel_requirement(
configured: &mut Option<codex_config::types::OtelConfigToml>,
requirement: Sourced<codex_config::types::OtelConfigToml>,
startup_warnings: &mut Vec<String>,
) {
let Sourced {
value: required,
source,
} = requirement;
let configured = configured.get_or_insert_default();
let mut conflict = false;
macro_rules! replace_leaf {
($field:ident) => {
if let Some(required) = required.$field {
conflict |= configured
.$field
.as_ref()
.is_some_and(|configured| configured != &required);
configured.$field = Some(required);
}
};
}
replace_leaf!(log_user_prompt);
replace_leaf!(environment);
replace_leaf!(exporter);
replace_leaf!(trace_exporter);
replace_leaf!(metrics_exporter);
if let Some(required) = required.span_attributes {
let configured = configured.span_attributes.get_or_insert_default();
conflict |= required
.iter()
.any(|(key, value)| configured.get(key).is_some_and(|current| current != value));
configured.extend(required);
}
if let Some(required) = required.tracestate {
let configured = configured.tracestate.get_or_insert_default();
for (member, required_fields) in required {
let configured_fields = configured.entry(member).or_default();
conflict |= required_fields.iter().any(|(key, value)| {
configured_fields
.get(key)
.is_some_and(|current| current != value)
});
configured_fields.extend(required_fields);
}
}
if conflict {
tracing::warn!(
?source,
"configured OTEL leaves are overridden by exact requirements"
);
startup_warnings.push(format!(
"Configured leaves under `otel` are overridden by requirements from {source}."
));
}
}
fn mcp_server_matches_requirement(
requirement: &McpServerRequirement,
server: &McpServerConfig,
@@ -2566,7 +2768,7 @@ impl Config {
pub(crate) async fn load_config_with_layer_stack(
fs: &dyn ExecutorFileSystem,
cfg: ConfigToml,
mut cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
config_layer_stack: ConfigLayerStack,
@@ -2590,6 +2792,19 @@ impl Config {
permission_profile: mut constrained_permission_profile,
windows_sandbox_mode: mut constrained_windows_sandbox_mode,
web_search_mode: mut constrained_web_search_mode,
allowed_login_methods,
allowed_chatgpt_workspaces,
cli_auth_credentials_store: required_cli_auth_credentials_store,
chatgpt_base_url: required_chatgpt_base_url,
sqlite_home: required_sqlite_home,
log_dir: required_log_dir,
model_catalog_json: required_model_catalog_json,
check_for_update_on_startup: required_check_for_update_on_startup,
otel: required_otel,
allow_login_shell: required_allow_login_shell,
shell_environment_policy: required_shell_environment_policy,
feedback: required_feedback,
windows_sandbox_private_desktop: required_windows_sandbox_private_desktop,
allow_managed_hooks_only: _,
allow_appshots: _,
computer_use: _,
@@ -2608,6 +2823,77 @@ impl Config {
.startup_warnings()
.unwrap_or_default()
.to_vec();
let cli_auth_credentials_store_is_required =
required_cli_auth_credentials_store.is_some();
apply_exact_requirement(
"cli_auth_credentials_store",
&mut cfg.cli_auth_credentials_store,
required_cli_auth_credentials_store,
&mut startup_warnings,
);
apply_exact_requirement(
"chatgpt_base_url",
&mut cfg.chatgpt_base_url,
required_chatgpt_base_url,
&mut startup_warnings,
);
apply_exact_requirement(
"sqlite_home",
&mut cfg.sqlite_home,
required_sqlite_home,
&mut startup_warnings,
);
apply_exact_requirement(
"log_dir",
&mut cfg.log_dir,
required_log_dir,
&mut startup_warnings,
);
apply_exact_requirement(
"model_catalog_json",
&mut cfg.model_catalog_json,
required_model_catalog_json,
&mut startup_warnings,
);
apply_exact_requirement(
"check_for_update_on_startup",
&mut cfg.check_for_update_on_startup,
required_check_for_update_on_startup,
&mut startup_warnings,
);
apply_exact_requirement(
"allow_login_shell",
&mut cfg.allow_login_shell,
required_allow_login_shell,
&mut startup_warnings,
);
cfg.shell_environment_policy = merge_exact_requirement(
"shell_environment_policy",
cfg.shell_environment_policy,
required_shell_environment_policy,
&mut startup_warnings,
)?;
if let Some(required_otel) = required_otel {
validate_otel_requirement(&required_otel.value)?;
apply_otel_requirement(&mut cfg.otel, required_otel, &mut startup_warnings);
}
if let Some(required_feedback) = required_feedback {
cfg.feedback = Some(merge_exact_requirement(
"feedback",
cfg.feedback.take().unwrap_or_default(),
Some(required_feedback),
&mut startup_warnings,
)?);
}
if let Some(required) = required_windows_sandbox_private_desktop {
let windows = cfg.windows.get_or_insert_default();
apply_exact_requirement(
"windows.sandbox_private_desktop",
&mut windows.sandbox_private_desktop,
Some(required),
&mut startup_warnings,
);
}
let user_instructions = AgentsMdManager::load_global_instructions(
LOCAL_FS.as_ref(),
Some(&codex_home),
@@ -3202,8 +3488,68 @@ impl Config {
.collect::<Vec<_>>()
})
.filter(|values| !values.is_empty());
let forced_chatgpt_workspace_id = match allowed_chatgpt_workspaces {
Some(requirement) => {
let allowed = requirement
.value
.into_iter()
.filter_map(|(workspace, allowed)| allowed.then_some(workspace))
.collect::<BTreeSet<_>>();
Some(match forced_chatgpt_workspace_id {
Some(configured) => configured
.into_iter()
.filter(|workspace| allowed.contains(workspace))
.collect(),
None => allowed.into_iter().collect(),
})
}
None => forced_chatgpt_workspace_id,
};
let forced_login_method = cfg.forced_login_method;
let forced_login_method = match allowed_login_methods {
Some(requirement) => {
let chatgpt_allowed = requirement
.value
.get("chatgpt")
.copied()
.unwrap_or(false);
let api_allowed = requirement
.value
.get("api")
.copied()
.unwrap_or(false);
match cfg.forced_login_method {
Some(ForcedLoginMethod::Chatgpt) if chatgpt_allowed => {
Some(ForcedLoginMethod::Chatgpt)
}
Some(ForcedLoginMethod::Api) if api_allowed => Some(ForcedLoginMethod::Api),
Some(configured) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"configured `forced_login_method = \"{configured}\"` conflicts with `allowed_login_methods` from {}",
requirement.source
),
));
}
None => match (chatgpt_allowed, api_allowed) {
(true, true) => None,
(true, false) => Some(ForcedLoginMethod::Chatgpt),
(false, true) => Some(ForcedLoginMethod::Api),
(false, false) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"`allowed_login_methods` from {} does not allow any login method",
requirement.source
),
));
}
},
}
}
None => cfg.forced_login_method,
};
let model = model.or(cfg.model);
let notices = cfg.notice.unwrap_or_default();
@@ -3464,10 +3810,14 @@ impl Config {
include_environment_context,
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
cli_auth_credentials_store_mode: resolve_cli_auth_credentials_store_mode(
cfg.cli_auth_credentials_store.unwrap_or_default(),
env!("CARGO_PKG_VERSION"),
),
cli_auth_credentials_store_mode: {
let configured = cfg.cli_auth_credentials_store.unwrap_or_default();
if cli_auth_credentials_store_is_required {
configured
} else {
resolve_cli_auth_credentials_store_mode(configured, env!("CARGO_PKG_VERSION"))
}
},
mcp_servers,
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
+2 -2
View File
@@ -63,7 +63,7 @@ use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_and_load_options;
use codex_core::config::load_bootstrap_config_as_toml_with_cli_and_load_options;
use codex_core::config::resolve_oss_provider;
use codex_core::config::resolve_profile_v2_config_path;
use codex_core::find_thread_meta_by_name_str;
@@ -617,7 +617,7 @@ async fn load_config_toml_or_exit(
strict_config: bool,
cloud_config_bundle: CloudConfigBundleLoader,
) -> codex_config::config_toml::ConfigToml {
match load_config_as_toml_with_cli_and_load_options(
match load_bootstrap_config_as_toml_with_cli_and_load_options(
codex_home,
cwd,
cli_kv_overrides,
@@ -54,6 +54,7 @@ fn windows_sandbox_requirements_stack(
let requirements_toml = codex_config::ConfigRequirementsToml {
windows: Some(codex_config::WindowsRequirementsToml {
allowed_sandbox_implementations: Some(allowed_sandbox_implementations),
sandbox_private_desktop: None,
}),
..Default::default()
};
+24
View File
@@ -744,6 +744,18 @@ mod tests {
};
let requirements_toml = ConfigRequirementsToml {
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
otel: None,
allow_login_shell: None,
shell_environment_policy: None,
feedback: None,
allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]),
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
@@ -1103,6 +1115,18 @@ approval_policy = "never"
};
let requirements_toml = ConfigRequirementsToml {
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
otel: None,
allow_login_shell: None,
shell_environment_policy: None,
feedback: None,
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
+8 -3
View File
@@ -7,7 +7,7 @@ use crate::legacy_core::check_execpolicy_for_warnings;
use crate::legacy_core::config::Config;
use crate::legacy_core::config::ConfigBuilder;
use crate::legacy_core::config::ConfigOverrides;
use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options;
use crate::legacy_core::config::load_bootstrap_config_as_toml_with_cli_and_load_options;
use crate::legacy_core::config::resolve_oss_provider;
use crate::legacy_core::config::resolve_profile_v2_config_path;
use crate::legacy_core::format_exec_policy_error_with_source;
@@ -1190,7 +1190,12 @@ pub async fn run_main(
.config_layer_stack
.effective_config()
.as_table()
.is_some_and(|table| table.contains_key("log_dir"));
.is_some_and(|table| table.contains_key("log_dir"))
|| config
.config_layer_stack
.requirements_toml()
.log_dir
.is_some();
#[allow(clippy::print_stderr)]
match check_execpolicy_for_warnings(&config.config_layer_stack).await {
@@ -1997,7 +2002,7 @@ async fn load_config_toml_or_exit(
strict_config: bool,
cloud_config_bundle: CloudConfigBundleLoader,
) -> codex_config::config_toml::ConfigToml {
match load_config_as_toml_with_cli_and_load_options(
match load_bootstrap_config_as_toml_with_cli_and_load_options(
codex_home,
cwd,
cli_kv_overrides,