mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
Add requirements.toml support for managed config fields
This commit is contained in:
+118
@@ -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": {
|
||||
|
||||
+118
@@ -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": {
|
||||
|
||||
+122
@@ -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": {
|
||||
|
||||
+5
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user