mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Enforce exact values from managed config requirements (#34597)
## What changed - Apply managed values for `sqlite_home`, `log_dir`, `model_catalog_json`, `check_for_update_on_startup`, `allow_login_shell`, the report-submission opt-in, and `windows.sandbox_private_desktop` to the runtime configuration, with source-aware warnings when they override configured values. - Expose these values through `configRequirements/read`, return them from `config/read`, include them in session config locks, and show their sources in the TUI config debug view. - Reject app-server writes that overlap an exact managed requirement with `configRequirementReadonly` while leaving unrelated sibling keys writable. ## Testing - Cover runtime overrides, requirement API mapping, config reads and writes, session lock serialization, cross-platform path URIs, and debug output. GitOrigin-RevId: d4e23b6fd7775c1a6dfe2eab313cdcee50b07081
This commit is contained in:
+60
@@ -8542,6 +8542,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowLoginShell": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowManagedHooksOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -8599,6 +8605,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"checkForUpdateOnStartup": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"computerUse": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -8634,6 +8646,28 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"feedback": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/FeedbackRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"logDir": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelCatalogJson": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"models": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -8643,6 +8677,18 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sqliteHome": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"windowsSandboxPrivateDesktop": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -9943,6 +9989,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FeedbackRequirements": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FeedbackUploadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -13724,6 +13781,9 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PathUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionProfileListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
+60
@@ -4720,6 +4720,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowLoginShell": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowManagedHooksOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -4777,6 +4783,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"checkForUpdateOnStartup": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"computerUse": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4812,6 +4824,28 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"feedback": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FeedbackRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"logDir": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelCatalogJson": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"models": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4821,6 +4855,18 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sqliteHome": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"windowsSandboxPrivateDesktop": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -6121,6 +6167,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FeedbackRequirements": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FeedbackUploadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -10066,6 +10123,9 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PathUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionProfileListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
+57
@@ -78,6 +78,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowLoginShell": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowManagedHooksOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -135,6 +141,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"checkForUpdateOnStartup": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"computerUse": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -170,6 +182,28 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"feedback": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FeedbackRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"logDir": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelCatalogJson": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"models": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -179,6 +213,18 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sqliteHome": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"windowsSandboxPrivateDesktop": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -292,6 +338,17 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FeedbackRequirements": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ManagedHooksRequirements": {
|
||||
"properties": {
|
||||
"PermissionRequest": {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* An immutable, cross-platform representation of a `file:` URI.
|
||||
*
|
||||
* Only the `file:` scheme is currently accepted. Construction validates the
|
||||
* URL, and the URI cannot be mutated after construction. [`Self::basename`],
|
||||
* [`Self::parent`], and [`Self::join`] operate on URI path segments without
|
||||
* interpreting them using the operating system running Codex. Fallback URIs
|
||||
* created by [`Self::from_abs_path`] are opaque to these lexical operations.
|
||||
*
|
||||
* `file:` paths retain their URI spelling so they can be parsed independently
|
||||
* of the current host. A local POSIX `file:` URI can also retain
|
||||
* percent-encoded non-UTF-8 bytes for lossless native round trips.
|
||||
*
|
||||
* Like [VS Code resources], path operations use `/` URI separators on every
|
||||
* host. Lexical path operations preserve a URL authority without interpreting
|
||||
* Windows drive or UNC roots from path text. Native path normalization,
|
||||
* filesystem aliases, symlinks, case sensitivity, and Unicode normalization
|
||||
* are not resolved.
|
||||
*
|
||||
* Serde represents a `PathUri` as its canonical URI string. Deserialization
|
||||
* accepts only valid `file:` URI strings. These strings round-trip through
|
||||
* their canonical URL form, including encoded non-UTF-8 path bytes.
|
||||
*
|
||||
* [VS Code resources]: https://github.com/microsoft/vscode/blob/main/src/vs/base/common/resources.ts
|
||||
*/
|
||||
export type PathUri = string;
|
||||
@@ -55,6 +55,7 @@ export type { MultiAgentMode } from "./MultiAgentMode";
|
||||
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
|
||||
export type { ParsedCommand } from "./ParsedCommand";
|
||||
export type { PathUri } from "./PathUri";
|
||||
export type { Personality } from "./Personality";
|
||||
export type { PlanType } from "./PlanType";
|
||||
export type { RealtimeConversationVersion } from "./RealtimeConversationVersion";
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// 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 { PathUri } from "../PathUri";
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { ComputerUseRequirements } from "./ComputerUseRequirements";
|
||||
import type { FeedbackRequirements } from "./FeedbackRequirements";
|
||||
import type { ModelsRequirements } from "./ModelsRequirements";
|
||||
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, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, models: ModelsRequirements | 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, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | 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, };
|
||||
@@ -129,6 +129,7 @@ export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigr
|
||||
export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType";
|
||||
export type { ExternalAgentImportedConnectorCandidate } from "./ExternalAgentImportedConnectorCandidate";
|
||||
export type { ExternalAgentImportedConnectorSource } from "./ExternalAgentImportedConnectorSource";
|
||||
export type { FeedbackRequirements } from "./FeedbackRequirements";
|
||||
export type { FeedbackUploadParams } from "./FeedbackUploadParams";
|
||||
export type { FeedbackUploadResponse } from "./FeedbackUploadResponse";
|
||||
export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision";
|
||||
|
||||
@@ -46,7 +46,6 @@ const JSON_V1_ALLOWLIST: &[&str] = &["InitializeParams", "InitializeResponse"];
|
||||
const EXPERIMENTAL_CLIENT_METHOD_DEPENDENCY_TYPES: &[&str] = &[
|
||||
"EnvironmentShellInfo",
|
||||
"EnvironmentStatusKind",
|
||||
"PathUri",
|
||||
"RemoteControlClient",
|
||||
"RemoteControlClientsListOrder",
|
||||
"ThreadBackgroundTerminal",
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WebSearchToolConfig;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -337,6 +338,7 @@ pub struct ConfigWriteResponse {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ConfigWriteErrorCode {
|
||||
ConfigLayerReadonly,
|
||||
ConfigRequirementReadonly,
|
||||
ConfigVersionConflict,
|
||||
ConfigValidationError,
|
||||
ConfigPathNotFound,
|
||||
@@ -392,6 +394,16 @@ pub struct ConfigRequirements {
|
||||
#[experimental("configRequirements/read.network")]
|
||||
pub network: Option<NetworkRequirements>,
|
||||
pub models: Option<ModelsRequirements>,
|
||||
#[schemars(with = "Option<String>")]
|
||||
pub sqlite_home: Option<PathUri>,
|
||||
#[schemars(with = "Option<String>")]
|
||||
pub log_dir: Option<PathUri>,
|
||||
#[schemars(with = "Option<String>")]
|
||||
pub model_catalog_json: Option<PathUri>,
|
||||
pub check_for_update_on_startup: Option<bool>,
|
||||
pub allow_login_shell: Option<bool>,
|
||||
pub feedback: Option<FeedbackRequirements>,
|
||||
pub windows_sandbox_private_desktop: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -410,6 +422,13 @@ pub struct NewThreadModelDefaults {
|
||||
pub service_tier: Option<String>,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -1807,11 +1807,46 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
models: None,
|
||||
sqlite_home: None,
|
||||
log_dir: None,
|
||||
model_catalog_json: None,
|
||||
check_for_update_on_startup: None,
|
||||
allow_login_shell: None,
|
||||
feedback: None,
|
||||
windows_sandbox_private_desktop: None,
|
||||
});
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.granular"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_requirements_read_accepts_foreign_path_uris() {
|
||||
let response: ConfigRequirementsReadResponse = serde_json::from_value(json!({
|
||||
"requirements": {
|
||||
"sqliteHome": "file:///C:/Users/alice/.codex/state",
|
||||
"logDir": "file:///C:/Users/alice/.codex/logs",
|
||||
"modelCatalogJson": "file:///C:/Users/alice/.codex/models.json"
|
||||
}
|
||||
}))
|
||||
.expect("requirements response with foreign paths should deserialize");
|
||||
let requirements = response
|
||||
.requirements
|
||||
.expect("requirements should be present");
|
||||
|
||||
assert_eq!(
|
||||
requirements.sqlite_home,
|
||||
Some(PathUri::parse("file:///C:/Users/alice/.codex/state").expect("valid URI"))
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.log_dir,
|
||||
Some(PathUri::parse("file:///C:/Users/alice/.codex/logs").expect("valid URI"))
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.model_catalog_json,
|
||||
Some(PathUri::parse("file:///C:/Users/alice/.codex/models.json").expect("valid URI"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_thread_start_granular_approval_policy_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(
|
||||
|
||||
@@ -16,6 +16,14 @@ fn typescript_schema_fixtures_match_generated() -> Result<()> {
|
||||
.context("generate in-memory typescript schema fixtures")?;
|
||||
|
||||
assert_schema_trees_match("typescript", &fixture_tree, &generated_tree)?;
|
||||
let config_requirements = generated_tree
|
||||
.get(Path::new("v2/ConfigRequirements.ts"))
|
||||
.context("generated ConfigRequirements.ts should exist")?;
|
||||
anyhow::ensure!(
|
||||
!String::from_utf8_lossy(config_requirements).contains("../PathUri")
|
||||
|| generated_tree.contains_key(Path::new("PathUri.ts")),
|
||||
"stable ConfigRequirements.ts imports PathUri but PathUri.ts was not generated"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -242,13 +242,13 @@ Example with notification opt-out:
|
||||
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
|
||||
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
|
||||
- `config/read` — fetch the effective config on disk after resolving config layering, including opaque `desktop` values stored in `config.toml`.
|
||||
- `config/read` — fetch the runtime-effective config after resolving config layering and managed requirements, including opaque `desktop` values stored in `config.toml`.
|
||||
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome`, optional `cwds`, and an optional `migrationSource` selector. Omitted, `null`, or unrecognized migration-source values retain the default behavior. The deprecated optional `source` field remains accepted for compatibility but does not select the migration source. Each detected item includes `cwd` (`null` for home), and multi-item migrations may additionally include structured `details` with plugin ids, skill names, memory, session metadata, or other artifact names.
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Pass the same optional `migrationSource` used for detection so the server reads from the matching source; omitted, `null`, or unrecognized values retain the default behavior. The optional `source` identifies the product that initiated the import, while the optional opaque `providerId` attributes analytics to the provider selected by that product without affecting migration-source selection. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
|
||||
- `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection.
|
||||
- `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/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. Writes that overlap a managed requirement are rejected with `configRequirementReadonly`.
|
||||
- `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`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`, including each command handler's optional `additionalContextLimit`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), 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 exact managed values (`sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`, including each command handler's optional `additionalContextLimit`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||
|
||||
### Example: Start or resume a thread
|
||||
|
||||
|
||||
@@ -130,19 +130,31 @@ impl ConfigManager {
|
||||
};
|
||||
|
||||
let effective = layers.effective_config();
|
||||
let effective_config_toml: ConfigToml = effective
|
||||
let mut effective_config_toml: ConfigToml = effective
|
||||
.try_into()
|
||||
.map_err(|err| ConfigManagerError::toml("invalid configuration", err))?;
|
||||
layers
|
||||
.requirements_toml()
|
||||
.apply_exact_to_config(&mut effective_config_toml);
|
||||
effective_config_toml.allow_login_shell.get_or_insert(true);
|
||||
|
||||
let json_value = serde_json::to_value(&effective_config_toml)
|
||||
.map_err(|err| ConfigManagerError::json("failed to serialize configuration", err))?;
|
||||
let config: ApiConfig = serde_json::from_value(json_value)
|
||||
.map_err(|err| ConfigManagerError::json("failed to deserialize configuration", err))?;
|
||||
|
||||
let mut origins = layers.origins();
|
||||
origins.retain(|path, _| {
|
||||
let segments = path.split('.').map(str::to_string).collect::<Vec<_>>();
|
||||
layers
|
||||
.requirements_toml()
|
||||
.exact_requirement_for_config_path(&segments)
|
||||
.is_none()
|
||||
});
|
||||
|
||||
Ok(ConfigReadResponse {
|
||||
config,
|
||||
origins: layers
|
||||
.origins()
|
||||
origins: origins
|
||||
.into_iter()
|
||||
.map(|(path, metadata)| (path, config_layer_metadata_to_api(metadata)))
|
||||
.collect(),
|
||||
@@ -283,6 +295,15 @@ impl ConfigManager {
|
||||
let mut segments = parse_key_path(&key_path).map_err(|message| {
|
||||
ConfigManagerError::write(ConfigWriteErrorCode::ConfigValidationError, message)
|
||||
})?;
|
||||
if let Some(field) = layers
|
||||
.requirements_toml()
|
||||
.exact_requirement_for_config_path(&segments)
|
||||
{
|
||||
return Err(ConfigManagerError::write(
|
||||
ConfigWriteErrorCode::ConfigRequirementReadonly,
|
||||
format!("`{field}` is managed by requirements and cannot be changed"),
|
||||
));
|
||||
}
|
||||
if (value.is_null() || matches!(strategy, MergeStrategy::Upsert))
|
||||
&& let Some(pattern) = shell_environment_filter_entry(&user_config, &segments)
|
||||
.map(|(pattern, _)| pattern.clone())
|
||||
|
||||
@@ -777,6 +777,164 @@ personality = true
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_value_rejects_exact_managed_requirement() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let path = tmp.path().join(CONFIG_TOML_FILE);
|
||||
std::fs::write(&path, "allow_login_shell = true\n").unwrap();
|
||||
|
||||
let service = ConfigManager::new_for_tests(
|
||||
tmp.path().to_path_buf(),
|
||||
vec![],
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement("allow_login_shell = false"),
|
||||
);
|
||||
|
||||
let error = service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(path.display().to_string()),
|
||||
key_path: "allow_login_shell".to_string(),
|
||||
value: serde_json::json!(true),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.expect_err("managed exact field should be read-only");
|
||||
|
||||
assert_eq!(
|
||||
error.write_error_code(),
|
||||
Some(ConfigWriteErrorCode::ConfigRequirementReadonly)
|
||||
);
|
||||
assert!(error.to_string().contains("`allow_login_shell`"));
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(path).unwrap(),
|
||||
"allow_login_shell = true\n"
|
||||
);
|
||||
}
|
||||
|
||||
fn toml_path(tmp: &Path, name: &str) -> String {
|
||||
tmp.join(name).to_string_lossy().replace('\\', "\\\\")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_omits_origins_for_exact_managed_values() {
|
||||
for has_user_values in [true, false] {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let user_config = if has_user_values {
|
||||
format!(
|
||||
r#"model = "user-model"
|
||||
sqlite_home = "{}"
|
||||
allow_login_shell = true
|
||||
|
||||
[feedback]
|
||||
enabled = true
|
||||
"#,
|
||||
toml_path(tmp.path(), "user-sqlite"),
|
||||
)
|
||||
} else {
|
||||
"model = \"user-model\"\n".to_string()
|
||||
};
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), user_config).unwrap();
|
||||
|
||||
let requirements = format!(
|
||||
r#"sqlite_home = "{}"
|
||||
allow_login_shell = false
|
||||
|
||||
[feedback]
|
||||
enabled = false
|
||||
"#,
|
||||
toml_path(tmp.path(), "managed-sqlite"),
|
||||
);
|
||||
let service = ConfigManager::new_for_tests(
|
||||
tmp.path().to_path_buf(),
|
||||
vec![],
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements),
|
||||
);
|
||||
|
||||
let response = service
|
||||
.read(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await
|
||||
.expect("config read should succeed");
|
||||
|
||||
assert_eq!(
|
||||
response.config.additional.get("sqlite_home"),
|
||||
Some(&serde_json::json!(tmp.path().join("managed-sqlite")))
|
||||
);
|
||||
assert_eq!(
|
||||
response.config.additional.get("allow_login_shell"),
|
||||
Some(&serde_json::json!(false))
|
||||
);
|
||||
assert_eq!(
|
||||
response.config.additional.get("feedback"),
|
||||
Some(&serde_json::json!({"enabled": false}))
|
||||
);
|
||||
for path in ["sqlite_home", "allow_login_shell", "feedback.enabled"] {
|
||||
assert!(!response.origins.contains_key(path), "origin for {path}");
|
||||
}
|
||||
assert!(response.origins.contains_key("model"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_materializes_default_allow_login_shell() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap();
|
||||
|
||||
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
|
||||
let response = service
|
||||
.read(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await
|
||||
.expect("config read should succeed");
|
||||
|
||||
assert_eq!(
|
||||
response.config.additional.get("allow_login_shell"),
|
||||
Some(&serde_json::json!(true))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_value_allows_unmanaged_sibling_of_exact_requirement() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let path = tmp.path().join(CONFIG_TOML_FILE);
|
||||
std::fs::write(&path, "").unwrap();
|
||||
|
||||
let service = ConfigManager::new_for_tests(
|
||||
tmp.path().to_path_buf(),
|
||||
vec![],
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
r#"
|
||||
[windows]
|
||||
sandbox_private_desktop = false
|
||||
"#,
|
||||
),
|
||||
);
|
||||
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(path.display().to_string()),
|
||||
key_path: "windows.sandbox".to_string(),
|
||||
value: serde_json::json!("elevated"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.expect("unmanaged sibling should remain writable");
|
||||
|
||||
assert!(
|
||||
std::fs::read_to_string(path)
|
||||
.unwrap()
|
||||
.contains("sandbox = \"elevated\"")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_reports_managed_overrides_user_and_session_flags() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -21,6 +21,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;
|
||||
@@ -315,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
|
||||
@@ -384,6 +390,15 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
||||
service_tier: new_thread.service_tier,
|
||||
}),
|
||||
}),
|
||||
sqlite_home: requirements.sqlite_home.map(Into::into),
|
||||
log_dir: requirements.log_dir.map(Into::into),
|
||||
model_catalog_json: requirements.model_catalog_json.map(Into::into),
|
||||
check_for_update_on_startup: requirements.check_for_update_on_startup,
|
||||
allow_login_shell: requirements.allow_login_shell,
|
||||
feedback: requirements.feedback.map(|feedback| FeedbackRequirements {
|
||||
enabled: feedback.enabled,
|
||||
}),
|
||||
windows_sandbox_private_desktop,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,13 +593,17 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::map_requirements_toml_to_api;
|
||||
use codex_app_server_protocol::FeedbackRequirements;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
||||
use codex_config::ComputerUseRequirementsToml;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::ModelsRequirementsToml;
|
||||
use codex_config::NewThreadModelDefaultsToml;
|
||||
use codex_config::WindowsRequirementsToml;
|
||||
use codex_config::types::FeedbackConfigToml;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -694,6 +713,7 @@ mod tests {
|
||||
codex_config::types::WindowsSandboxModeToml::Elevated,
|
||||
codex_config::types::WindowsSandboxModeToml::Unelevated,
|
||||
]),
|
||||
sandbox_private_desktop: Some(false),
|
||||
}),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
@@ -705,5 +725,43 @@ mod tests {
|
||||
WindowsSandboxSetupMode::Unelevated,
|
||||
])
|
||||
);
|
||||
assert_eq!(mapped.windows_sandbox_private_desktop, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_exact_managed_values() {
|
||||
let sqlite_home = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-state"))
|
||||
.expect("managed sqlite home should be absolute");
|
||||
let log_dir = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-logs"))
|
||||
.expect("managed log dir should be absolute");
|
||||
let model_catalog_json =
|
||||
AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-models.json"))
|
||||
.expect("managed model catalog path should be absolute");
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
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),
|
||||
allow_login_shell: Some(false),
|
||||
feedback: Some(FeedbackConfigToml {
|
||||
enabled: Some(false),
|
||||
}),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
|
||||
assert_eq!(mapped.sqlite_home, Some(PathUri::from(sqlite_home)));
|
||||
assert_eq!(mapped.log_dir, Some(PathUri::from(log_dir)));
|
||||
assert_eq!(
|
||||
mapped.model_catalog_json,
|
||||
Some(PathUri::from(model_catalog_json))
|
||||
);
|
||||
assert_eq!(mapped.check_for_update_on_startup, Some(false));
|
||||
assert_eq!(mapped.allow_login_shell, Some(false));
|
||||
assert_eq!(
|
||||
mapped.feedback,
|
||||
Some(FeedbackRequirements {
|
||||
enabled: Some(false),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@ use super::requirements_exec_policy::RequirementsExecPolicyToml;
|
||||
use crate::Constrained;
|
||||
use crate::ConstraintError;
|
||||
use crate::ManagedHooksRequirementsToml;
|
||||
use crate::config_toml::ConfigToml;
|
||||
use crate::mcp_requirements::McpServerRequirement;
|
||||
use crate::mcp_types::AppToolApproval;
|
||||
use crate::permissions_toml::PermissionProfileToml;
|
||||
use crate::types::FeedbackConfigToml;
|
||||
use crate::types::WindowsSandboxModeToml;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -145,10 +147,17 @@ impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
|
||||
/// normalization.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConfigRequirements {
|
||||
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 allow_login_shell: Option<Sourced<bool>>,
|
||||
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>>,
|
||||
@@ -172,6 +181,12 @@ pub struct ConfigRequirements {
|
||||
impl Default for ConfigRequirements {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sqlite_home: None,
|
||||
log_dir: None,
|
||||
model_catalog_json: None,
|
||||
check_for_update_on_startup: None,
|
||||
allow_login_shell: None,
|
||||
feedback: None,
|
||||
approval_policy: ConstrainedWithSource::new(
|
||||
Constrained::allow_any_from_default(),
|
||||
/*source*/ None,
|
||||
@@ -188,6 +203,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,
|
||||
@@ -740,11 +756,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,6 +862,12 @@ 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 sqlite_home: Option<AbsolutePathBuf>,
|
||||
pub log_dir: Option<AbsolutePathBuf>,
|
||||
pub model_catalog_json: Option<AbsolutePathBuf>,
|
||||
pub check_for_update_on_startup: Option<bool>,
|
||||
pub allow_login_shell: Option<bool>,
|
||||
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>>,
|
||||
@@ -929,6 +952,12 @@ impl<T> std::ops::Deref for Sourced<T> {
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct ConfigRequirementsWithSources {
|
||||
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 allow_login_shell: Option<Sourced<bool>>,
|
||||
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>>>,
|
||||
@@ -973,6 +1002,12 @@ impl ConfigRequirementsWithSources {
|
||||
// Destructure without `..` so adding fields to `ConfigRequirementsToml`
|
||||
// forces this merge logic to be updated.
|
||||
let ConfigRequirementsToml {
|
||||
sqlite_home: _,
|
||||
log_dir: _,
|
||||
model_catalog_json: _,
|
||||
check_for_update_on_startup: _,
|
||||
allow_login_shell: _,
|
||||
feedback: _,
|
||||
allowed_approval_policies: _,
|
||||
allowed_approvals_reviewers: _,
|
||||
allowed_sandbox_modes: _,
|
||||
@@ -1012,6 +1047,12 @@ impl ConfigRequirementsWithSources {
|
||||
other,
|
||||
source,
|
||||
{
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
check_for_update_on_startup,
|
||||
allow_login_shell,
|
||||
feedback,
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
@@ -1048,6 +1089,12 @@ impl ConfigRequirementsWithSources {
|
||||
|
||||
pub fn into_toml(self) -> ConfigRequirementsToml {
|
||||
let ConfigRequirementsWithSources {
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
check_for_update_on_startup,
|
||||
allow_login_shell,
|
||||
feedback,
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
@@ -1073,6 +1120,12 @@ impl ConfigRequirementsWithSources {
|
||||
guardian_policy_config,
|
||||
} = self;
|
||||
ConfigRequirementsToml {
|
||||
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),
|
||||
allow_login_shell: allow_login_shell.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),
|
||||
@@ -1165,7 +1218,16 @@ impl ConfigRequirementsToml {
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.allowed_approval_policies.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.allow_login_shell.is_none()
|
||||
&& self
|
||||
.feedback
|
||||
.as_ref()
|
||||
.is_none_or(|feedback| feedback == &FeedbackConfigToml::default())
|
||||
&& self.allowed_approval_policies.is_none()
|
||||
&& self.allowed_approvals_reviewers.is_none()
|
||||
&& self.allowed_sandbox_modes.is_none()
|
||||
&& self.allowed_permission_profiles.is_none()
|
||||
@@ -1217,6 +1279,92 @@ impl ConfigRequirementsToml {
|
||||
.as_deref()
|
||||
.is_none_or(|value| value.trim().is_empty())
|
||||
}
|
||||
|
||||
/// Applies the requirements whose values replace config values.
|
||||
///
|
||||
/// This projection is shared by config/read and config-lock export so
|
||||
/// both surfaces describe the same behavior as the final runtime config.
|
||||
pub fn apply_exact_to_config(&self, config: &mut ConfigToml) {
|
||||
macro_rules! apply_exact {
|
||||
($field:ident) => {
|
||||
if let Some(value) = self.$field.as_ref() {
|
||||
config.$field = Some(value.clone());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
apply_exact!(sqlite_home);
|
||||
apply_exact!(log_dir);
|
||||
apply_exact!(model_catalog_json);
|
||||
apply_exact!(check_for_update_on_startup);
|
||||
apply_exact!(allow_login_shell);
|
||||
|
||||
if let Some(enabled) = self.feedback.as_ref().and_then(|feedback| feedback.enabled) {
|
||||
config.feedback.get_or_insert_default().enabled = Some(enabled);
|
||||
}
|
||||
if let Some(sandbox_private_desktop) = self
|
||||
.windows
|
||||
.as_ref()
|
||||
.and_then(|windows| windows.sandbox_private_desktop)
|
||||
{
|
||||
config
|
||||
.windows
|
||||
.get_or_insert_default()
|
||||
.sandbox_private_desktop = Some(sandbox_private_desktop);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the exact managed field affected by editing `segments`.
|
||||
pub fn exact_requirement_for_config_path(&self, segments: &[String]) -> Option<&'static str> {
|
||||
let managed_fields: [(bool, &[&str], &'static str); 7] = [
|
||||
(self.sqlite_home.is_some(), &["sqlite_home"], "sqlite_home"),
|
||||
(self.log_dir.is_some(), &["log_dir"], "log_dir"),
|
||||
(
|
||||
self.model_catalog_json.is_some(),
|
||||
&["model_catalog_json"],
|
||||
"model_catalog_json",
|
||||
),
|
||||
(
|
||||
self.check_for_update_on_startup.is_some(),
|
||||
&["check_for_update_on_startup"],
|
||||
"check_for_update_on_startup",
|
||||
),
|
||||
(
|
||||
self.allow_login_shell.is_some(),
|
||||
&["allow_login_shell"],
|
||||
"allow_login_shell",
|
||||
),
|
||||
(
|
||||
self.feedback
|
||||
.as_ref()
|
||||
.and_then(|feedback| feedback.enabled)
|
||||
.is_some(),
|
||||
&["feedback", "enabled"],
|
||||
"feedback.enabled",
|
||||
),
|
||||
(
|
||||
self.windows
|
||||
.as_ref()
|
||||
.and_then(|windows| windows.sandbox_private_desktop)
|
||||
.is_some(),
|
||||
&["windows", "sandbox_private_desktop"],
|
||||
"windows.sandbox_private_desktop",
|
||||
),
|
||||
];
|
||||
|
||||
managed_fields
|
||||
.into_iter()
|
||||
.find_map(|(is_managed, managed_path, field)| {
|
||||
(is_managed && config_paths_overlap(segments, managed_path)).then_some(field)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn config_paths_overlap(segments: &[String], managed_path: &[&str]) -> bool {
|
||||
segments
|
||||
.iter()
|
||||
.zip(managed_path)
|
||||
.all(|(segment, managed_segment)| segment == managed_segment)
|
||||
}
|
||||
|
||||
fn validate_mcp_server_requirements(
|
||||
@@ -1247,6 +1395,12 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
// defaults also remain there because they are initialization values,
|
||||
// not runtime constraints.
|
||||
let ConfigRequirementsWithSources {
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
check_for_update_on_startup,
|
||||
allow_login_shell,
|
||||
feedback,
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
@@ -1388,42 +1542,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 {
|
||||
@@ -1550,10 +1722,17 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
});
|
||||
let guardian_policy_config_source = guardian_policy_config.map(|sourced| sourced.source);
|
||||
Ok(ConfigRequirements {
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
check_for_update_on_startup,
|
||||
allow_login_shell,
|
||||
feedback,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
permission_profile,
|
||||
windows_sandbox_mode,
|
||||
windows_sandbox_private_desktop,
|
||||
web_search_mode,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
@@ -1623,6 +1802,59 @@ mod tests {
|
||||
)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_requirement_for_config_path_matches_overlapping_paths() {
|
||||
let managed_path = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed"))
|
||||
.expect("managed path should be absolute");
|
||||
let requirements = ConfigRequirementsToml {
|
||||
sqlite_home: Some(managed_path.clone()),
|
||||
log_dir: Some(managed_path.clone()),
|
||||
model_catalog_json: Some(managed_path),
|
||||
check_for_update_on_startup: Some(false),
|
||||
allow_login_shell: Some(false),
|
||||
feedback: Some(FeedbackConfigToml {
|
||||
enabled: Some(false),
|
||||
}),
|
||||
windows: Some(WindowsRequirementsToml {
|
||||
sandbox_private_desktop: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let cases: &[(&[&str], Option<&str>)] = &[
|
||||
(&["sqlite_home"], Some("sqlite_home")),
|
||||
(&["log_dir"], Some("log_dir")),
|
||||
(&["model_catalog_json"], Some("model_catalog_json")),
|
||||
(
|
||||
&["check_for_update_on_startup"],
|
||||
Some("check_for_update_on_startup"),
|
||||
),
|
||||
(&["allow_login_shell"], Some("allow_login_shell")),
|
||||
(&["feedback", "enabled"], Some("feedback.enabled")),
|
||||
(
|
||||
&["windows", "sandbox_private_desktop"],
|
||||
Some("windows.sandbox_private_desktop"),
|
||||
),
|
||||
(&[], Some("sqlite_home")),
|
||||
(&["feedback"], Some("feedback.enabled")),
|
||||
(
|
||||
&["windows", "sandbox_private_desktop", "value"],
|
||||
Some("windows.sandbox_private_desktop"),
|
||||
),
|
||||
(&["feedback", "other"], None),
|
||||
(&["windows", "sandbox"], None),
|
||||
];
|
||||
|
||||
for (segments, expected) in cases {
|
||||
let segments = segments.iter().map(ToString::to_string).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
requirements.exact_requirement_for_config_path(&segments),
|
||||
*expected,
|
||||
"segments: {segments:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_requirement_source_flattens_and_deduplicates_sources() {
|
||||
let mdm_source = RequirementSource::MdmManagedPreferences {
|
||||
@@ -1644,6 +1876,12 @@ mod tests {
|
||||
|
||||
fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources {
|
||||
let ConfigRequirementsToml {
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
check_for_update_on_startup,
|
||||
allow_login_shell,
|
||||
feedback,
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
@@ -1670,6 +1908,15 @@ mod tests {
|
||||
guardian_policy_config,
|
||||
} = toml;
|
||||
ConfigRequirementsWithSources {
|
||||
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)),
|
||||
allow_login_shell: allow_login_shell
|
||||
.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
|
||||
@@ -1909,6 +2156,20 @@ mod tests {
|
||||
service_tier: Some("fast".to_string()),
|
||||
}),
|
||||
};
|
||||
let sqlite_home = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-state"))
|
||||
.expect("managed sqlite home should be absolute");
|
||||
let log_dir = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-logs"))
|
||||
.expect("managed log dir should be absolute");
|
||||
let model_catalog_json =
|
||||
AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-models.json"))
|
||||
.expect("managed model catalog path should be absolute");
|
||||
let feedback = FeedbackConfigToml {
|
||||
enabled: Some(false),
|
||||
};
|
||||
let windows = WindowsRequirementsToml {
|
||||
allowed_sandbox_implementations: None,
|
||||
sandbox_private_desktop: Some(true),
|
||||
};
|
||||
let enforce_residency = ResidencyRequirement::Us;
|
||||
let enforce_source = source.clone();
|
||||
let guardian_policy_config = "Use the company-managed guardian policy.".to_string();
|
||||
@@ -1916,6 +2177,12 @@ mod tests {
|
||||
// Intentionally constructed without `..Default::default()` so adding a new field to
|
||||
// `ConfigRequirementsToml` forces this test to be updated.
|
||||
let other = ConfigRequirementsToml {
|
||||
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),
|
||||
allow_login_shell: Some(false),
|
||||
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()),
|
||||
@@ -1927,7 +2194,7 @@ mod tests {
|
||||
allow_appshots: Some(false),
|
||||
allow_remote_control: Some(false),
|
||||
computer_use: Some(computer_use.clone()),
|
||||
windows: None,
|
||||
windows: Some(windows.clone()),
|
||||
feature_requirements: Some(feature_requirements.clone()),
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
@@ -1947,6 +2214,15 @@ mod tests {
|
||||
assert_eq!(
|
||||
target,
|
||||
ConfigRequirementsWithSources {
|
||||
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(
|
||||
/*value*/ false,
|
||||
source.clone(),
|
||||
)),
|
||||
allow_login_shell: Some(Sourced::new(/*value*/ false, source.clone())),
|
||||
feedback: Some(Sourced::new(feedback, source.clone())),
|
||||
allowed_approval_policies: Some(Sourced::new(
|
||||
allowed_approval_policies,
|
||||
source.clone()
|
||||
@@ -1975,7 +2251,7 @@ mod tests {
|
||||
enforce_source.clone(),
|
||||
)),
|
||||
computer_use: Some(Sourced::new(computer_use, enforce_source.clone())),
|
||||
windows: None,
|
||||
windows: Some(Sourced::new(windows, enforce_source.clone())),
|
||||
feature_requirements: Some(Sourced::new(
|
||||
feature_requirements,
|
||||
enforce_source.clone(),
|
||||
@@ -2039,6 +2315,7 @@ mod tests {
|
||||
permissions: None,
|
||||
models: None,
|
||||
guardian_policy_config: None,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
@@ -2095,6 +2372,7 @@ mod tests {
|
||||
permissions: None,
|
||||
models: None,
|
||||
guardian_policy_config: None,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -67,10 +67,6 @@ const RESERVED_MODEL_PROVIDER_IDS: [&str; 4] = [
|
||||
|
||||
pub const DEFAULT_PROJECT_DOC_MAX_BYTES: usize = 32 * 1024;
|
||||
|
||||
const fn default_allow_login_shell() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
fn default_history() -> Option<History> {
|
||||
Some(History::default())
|
||||
}
|
||||
@@ -193,7 +189,6 @@ pub struct ConfigToml {
|
||||
/// If `false`, the model can never use a login shell: `login = true`
|
||||
/// requests are rejected, and omitting `login` defaults to a non-login
|
||||
/// shell.
|
||||
#[serde(default = "default_allow_login_shell")]
|
||||
pub allow_login_shell: Option<bool>,
|
||||
|
||||
/// Sandbox mode to use.
|
||||
|
||||
@@ -157,12 +157,14 @@ pub async fn load_config_layers_state(
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let managed_preferences_base_dir = AbsolutePathBuf::from_absolute_path(codex_home)?;
|
||||
managed_preferences_requirements_layer = macos::load_managed_admin_requirements_layer(
|
||||
overrides
|
||||
.macos_managed_config_requirements_base64
|
||||
.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.map(|layer| layer.with_base_dir(managed_preferences_base_dir));
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
|
||||
@@ -77,6 +77,7 @@ impl ComposableRequirementsLayer {
|
||||
.as_ref()
|
||||
.and_then(|_| hostname_resolver());
|
||||
requirements.apply_remote_sandbox_config(hostname.as_deref());
|
||||
materialize_resolved_path_requirements(&mut regular_toml, &requirements)?;
|
||||
materialize_remote_sandbox_config(&mut regular_toml, &requirements)?;
|
||||
strip_special_fields(&mut regular_toml);
|
||||
|
||||
@@ -140,6 +141,30 @@ fn parse_layer_requirements(
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_resolved_path_requirements(
|
||||
layer_toml: &mut TomlValue,
|
||||
requirements: &ConfigRequirementsToml,
|
||||
) -> Result<(), RequirementsCompositionError> {
|
||||
let Some(table) = layer_toml.as_table_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for (key, value) in [
|
||||
("sqlite_home", requirements.sqlite_home.as_ref()),
|
||||
("log_dir", requirements.log_dir.as_ref()),
|
||||
(
|
||||
"model_catalog_json",
|
||||
requirements.model_catalog_json.as_ref(),
|
||||
),
|
||||
] {
|
||||
if let Some(value) = value {
|
||||
table.insert(key.to_string(), toml_value_from_serializable(value)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn materialize_remote_sandbox_config(
|
||||
layer_toml: &mut TomlValue,
|
||||
requirements: &ConfigRequirementsToml,
|
||||
|
||||
@@ -206,6 +206,12 @@ 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 {
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
check_for_update_on_startup,
|
||||
allow_login_shell,
|
||||
feedback,
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
@@ -232,6 +238,15 @@ fn populate_merged_regular_fields_with_sources(
|
||||
guardian_policy_config,
|
||||
} = requirements;
|
||||
|
||||
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!(allow_login_shell, &["allow_login_shell"]);
|
||||
set_sourced!(feedback, &["feedback"]);
|
||||
set_sourced!(allowed_approval_policies, &["allowed_approval_policies"]);
|
||||
set_sourced!(
|
||||
allowed_approvals_reviewers,
|
||||
|
||||
@@ -14,6 +14,7 @@ use pretty_assertions::assert_eq;
|
||||
use std::cell::Cell;
|
||||
use std::collections::BTreeMap;
|
||||
use tempfile::TempDir;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn layer(id: &str, name: &str, contents: &str) -> RequirementsLayerEntry {
|
||||
RequirementsLayerEntry::from_toml(
|
||||
@@ -149,6 +150,45 @@ service_tier = "fast"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_paths_resolve_against_their_own_layer_base() {
|
||||
let low_dir = tempdir().expect("low-priority requirements directory");
|
||||
let high_dir = tempdir().expect("high-priority requirements directory");
|
||||
let low_base = AbsolutePathBuf::from_absolute_path(low_dir.path()).expect("absolute low base");
|
||||
let high_base =
|
||||
AbsolutePathBuf::from_absolute_path(high_dir.path()).expect("absolute high base");
|
||||
|
||||
let composed = compose(vec![
|
||||
layer(
|
||||
"req_low",
|
||||
"Low",
|
||||
"sqlite_home = \"state\"\nlog_dir = \"low-logs\"",
|
||||
)
|
||||
.with_base_dir(low_base),
|
||||
layer(
|
||||
"req_high",
|
||||
"High",
|
||||
"log_dir = \"high-logs\"\nmodel_catalog_json = \"models.json\"",
|
||||
)
|
||||
.with_base_dir(high_base),
|
||||
])
|
||||
.expect("compose requirements")
|
||||
.expect("requirements present");
|
||||
|
||||
assert_eq!(
|
||||
composed.sqlite_home.as_deref(),
|
||||
Some(low_dir.path().join("state").as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
composed.log_dir.as_deref(),
|
||||
Some(high_dir.path().join("high-logs").as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
composed.model_catalog_json.as_deref(),
|
||||
Some(high_dir.path().join("models.json").as_path())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_strategy_applies_to_non_cloud_layers() {
|
||||
let mdm_source = RequirementSource::MdmManagedPreferences {
|
||||
|
||||
@@ -4726,7 +4726,6 @@
|
||||
"description": "Agent-related settings (thread limits, etc.)."
|
||||
},
|
||||
"allow_login_shell": {
|
||||
"default": true,
|
||||
"description": "Whether the model may request a login shell for shell-based tools. Default to `true`\n\nIf `true`, the model may request a login shell (`login = true`), and omitting `login` defaults to using a login shell. If `false`, the model can never use a login shell: `login = true` requests are rejected, and omitting `login` defaults to a non-login shell.",
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -1164,6 +1164,56 @@ allowed_sandbox_modes = ["read-only"]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn managed_preferences_requirements_resolve_paths_against_codex_home() -> anyhow::Result<()> {
|
||||
use base64::Engine;
|
||||
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("codex-home");
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
|
||||
let mut loader_overrides =
|
||||
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml"));
|
||||
loader_overrides.macos_managed_config_requirements_base64 = Some(
|
||||
base64::prelude::BASE64_STANDARD.encode(
|
||||
r#"
|
||||
sqlite_home = "state"
|
||||
log_dir = "~/.codex/logs"
|
||||
model_catalog_json = "models.json"
|
||||
"#
|
||||
.as_bytes(),
|
||||
),
|
||||
);
|
||||
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(AbsolutePathBuf::try_from(tmp.path())?),
|
||||
&[] as &[(String, TomlValue)],
|
||||
loader_overrides,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let expected_log_dir = AbsolutePathBuf::resolve_path_against_base("~/.codex/logs", &codex_home);
|
||||
let requirements = layers.requirements_toml();
|
||||
|
||||
assert_eq!(
|
||||
requirements.sqlite_home.as_deref(),
|
||||
Some(codex_home.join("state").as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.log_dir.as_deref(),
|
||||
Some(expected_log_dir.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.model_catalog_json.as_deref(),
|
||||
Some(codex_home.join("models.json").as_path())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn managed_preferences_requirements_take_precedence() -> anyhow::Result<()> {
|
||||
|
||||
@@ -9093,6 +9093,12 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let requirements_toml = codex_config::ConfigRequirementsToml {
|
||||
sqlite_home: None,
|
||||
log_dir: None,
|
||||
model_catalog_json: None,
|
||||
check_for_update_on_startup: None,
|
||||
allow_login_shell: None,
|
||||
feedback: None,
|
||||
allowed_approval_policies: None,
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
@@ -11710,3 +11716,134 @@ fn test_tui_notification_condition_rejects_unknown_value() {
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
async fn load_with_enterprise_requirement(
|
||||
codex_home: &TempDir,
|
||||
requirements: impl Into<String>,
|
||||
) -> std::io::Result<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
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exact_requirements_apply_to_runtime_config() -> 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"),
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
check_for_update_on_startup = true
|
||||
allow_login_shell = true
|
||||
|
||||
[feedback]
|
||||
enabled = true
|
||||
|
||||
[windows]
|
||||
sandbox_private_desktop = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let required_sqlite_home = codex_home.path().join("required-state");
|
||||
let required_log_dir = codex_home.path().join("required-logs");
|
||||
let requirements = format!(
|
||||
r#"
|
||||
sqlite_home = {:?}
|
||||
log_dir = {:?}
|
||||
model_catalog_json = {:?}
|
||||
check_for_update_on_startup = false
|
||||
allow_login_shell = false
|
||||
|
||||
[feedback]
|
||||
enabled = false
|
||||
|
||||
[windows]
|
||||
sandbox_private_desktop = false
|
||||
"#,
|
||||
required_sqlite_home.display(),
|
||||
required_log_dir.display(),
|
||||
catalog_path.display(),
|
||||
);
|
||||
let config = load_with_enterprise_requirement(&codex_home, requirements).await?;
|
||||
|
||||
assert_eq!(config.sqlite_home, required_sqlite_home);
|
||||
assert_eq!(config.log_dir, required_log_dir);
|
||||
assert_eq!(config.model_catalog, Some(catalog));
|
||||
assert!(!config.check_for_update_on_startup);
|
||||
assert!(!config.permissions.allow_login_shell);
|
||||
assert!(!config.feedback_enabled);
|
||||
assert!(!config.permissions.windows_sandbox_private_desktop);
|
||||
assert!(config.startup_warnings.iter().any(|warning| {
|
||||
warning.contains("Configured value for `check_for_update_on_startup` is overridden")
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn absent_allow_login_shell_does_not_report_an_override() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = load_with_enterprise_requirement(&codex_home, "allow_login_shell = false").await?;
|
||||
|
||||
assert!(!config.permissions.allow_login_shell);
|
||||
assert!(
|
||||
config
|
||||
.startup_warnings
|
||||
.iter()
|
||||
.all(|warning| !warning.contains("allow_login_shell"))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlite_home_env_conflict_reports_an_override() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let required = AbsolutePathBuf::try_from(codex_home.path().join("required-state"))?;
|
||||
let environment = codex_home.path().join("environment-state");
|
||||
let requirement = Sourced::new(required.clone(), RequirementSource::Unknown);
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
super::requirements::push_sqlite_home_env_override_warning(
|
||||
/*configured_sqlite_home*/ None,
|
||||
Some(environment.as_path()),
|
||||
Some(&requirement),
|
||||
&mut warnings,
|
||||
);
|
||||
assert_eq!(
|
||||
warnings,
|
||||
vec![format!(
|
||||
"Environment value for `$CODEX_SQLITE_HOME` is overridden by the required `sqlite_home` value {required:?} from {}.",
|
||||
RequirementSource::Unknown
|
||||
)]
|
||||
);
|
||||
|
||||
warnings.clear();
|
||||
super::requirements::push_sqlite_home_env_override_warning(
|
||||
/*configured_sqlite_home*/ None,
|
||||
Some(required.as_path()),
|
||||
Some(&requirement),
|
||||
&mut warnings,
|
||||
);
|
||||
assert!(warnings.is_empty());
|
||||
|
||||
super::requirements::push_sqlite_home_env_override_warning(
|
||||
Some(&required),
|
||||
Some(environment.as_path()),
|
||||
Some(&requirement),
|
||||
&mut warnings,
|
||||
);
|
||||
assert!(warnings.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ mod network_proxy_spec;
|
||||
mod otel;
|
||||
mod permission_profile_catalog;
|
||||
mod permissions;
|
||||
mod requirements;
|
||||
mod resolved_permission_profile;
|
||||
#[cfg(test)]
|
||||
mod schema;
|
||||
@@ -3037,7 +3038,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,
|
||||
@@ -3058,13 +3059,31 @@ impl Config {
|
||||
resolve_orchestrator_feature_enabled(orchestrator.and_then(|value| value.skills.as_ref()));
|
||||
let orchestrator_mcp_enabled =
|
||||
resolve_orchestrator_feature_enabled(orchestrator.and_then(|value| value.mcp.as_ref()));
|
||||
// Ensure that every field of ConfigRequirements is applied to the final
|
||||
// Config.
|
||||
let mut startup_warnings = config_layer_stack
|
||||
.startup_warnings()
|
||||
.unwrap_or_default()
|
||||
.to_vec();
|
||||
let configured_sqlite_home = cfg.sqlite_home.clone();
|
||||
requirements::apply_to_config(
|
||||
&mut cfg,
|
||||
config_layer_stack.requirements(),
|
||||
&mut startup_warnings,
|
||||
);
|
||||
|
||||
// Destructure every field to ensure ConfigRequirements additions are
|
||||
// either applied above or handled while constructing the final Config.
|
||||
let ConfigRequirements {
|
||||
sqlite_home: _,
|
||||
log_dir: _,
|
||||
model_catalog_json: _,
|
||||
check_for_update_on_startup: _,
|
||||
allow_login_shell: _,
|
||||
feedback: _,
|
||||
approval_policy: mut constrained_approval_policy,
|
||||
approvals_reviewer: mut constrained_approvals_reviewer,
|
||||
permission_profile: mut constrained_permission_profile,
|
||||
windows_sandbox_mode: mut constrained_windows_sandbox_mode,
|
||||
windows_sandbox_private_desktop: _,
|
||||
web_search_mode: mut constrained_web_search_mode,
|
||||
allow_managed_hooks_only: _,
|
||||
allow_appshots: _,
|
||||
@@ -3082,11 +3101,6 @@ impl Config {
|
||||
guardian_policy_config_source: _,
|
||||
} = config_layer_stack.requirements().clone();
|
||||
|
||||
let mut startup_warnings = config_layer_stack
|
||||
.startup_warnings()
|
||||
.unwrap_or_default()
|
||||
.to_vec();
|
||||
|
||||
// Destructure ConfigOverrides fully to ensure all overrides are applied.
|
||||
let ConfigOverrides {
|
||||
model,
|
||||
@@ -3754,11 +3768,18 @@ impl Config {
|
||||
.as_ref()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.unwrap_or_else(|| codex_home.join("log").to_path_buf());
|
||||
let sqlite_home_env = resolve_sqlite_home_env(&resolved_cwd);
|
||||
requirements::push_sqlite_home_env_override_warning(
|
||||
configured_sqlite_home.as_ref(),
|
||||
sqlite_home_env.as_deref(),
|
||||
config_layer_stack.requirements().sqlite_home.as_ref(),
|
||||
&mut startup_warnings,
|
||||
);
|
||||
let sqlite_home = cfg
|
||||
.sqlite_home
|
||||
.as_ref()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.or_else(|| resolve_sqlite_home_env(&resolved_cwd))
|
||||
.or(sqlite_home_env)
|
||||
.unwrap_or_else(|| codex_home.to_path_buf());
|
||||
let original_permission_profile = permission_profile.clone();
|
||||
apply_requirement_constrained_value(
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::types::FeedbackConfigToml;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::path::Path;
|
||||
|
||||
/// Applies managed requirements to regular config before final config construction.
|
||||
///
|
||||
/// Managed values replace their configured counterparts, and conflicts produce
|
||||
/// source-aware startup warnings.
|
||||
pub(super) fn apply_to_config(
|
||||
config: &mut ConfigToml,
|
||||
requirements: &ConfigRequirements,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) {
|
||||
macro_rules! apply_exact {
|
||||
($field:ident) => {
|
||||
apply_exact_requirement(
|
||||
stringify!($field),
|
||||
&mut config.$field,
|
||||
requirements.$field.as_ref(),
|
||||
startup_warnings,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
apply_exact!(sqlite_home);
|
||||
apply_exact!(log_dir);
|
||||
apply_exact!(model_catalog_json);
|
||||
apply_exact!(check_for_update_on_startup);
|
||||
apply_exact!(allow_login_shell);
|
||||
apply_feedback_requirement(
|
||||
&mut config.feedback,
|
||||
requirements.feedback.as_ref(),
|
||||
startup_warnings,
|
||||
);
|
||||
if let Some(requirement) = requirements.windows_sandbox_private_desktop.as_ref() {
|
||||
apply_exact_requirement(
|
||||
"windows.sandbox_private_desktop",
|
||||
&mut config
|
||||
.windows
|
||||
.get_or_insert_default()
|
||||
.sandbox_private_desktop,
|
||||
Some(requirement),
|
||||
startup_warnings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.clone());
|
||||
}
|
||||
|
||||
fn replace_required_leaf<T: Clone + PartialEq>(
|
||||
configured: &mut Option<T>,
|
||||
required: &Option<T>,
|
||||
) -> bool {
|
||||
let Some(required) = required else {
|
||||
return false;
|
||||
};
|
||||
let conflict = configured
|
||||
.as_ref()
|
||||
.is_some_and(|configured| configured != required);
|
||||
*configured = Some(required.clone());
|
||||
conflict
|
||||
}
|
||||
|
||||
fn apply_feedback_requirement(
|
||||
configured: &mut Option<FeedbackConfigToml>,
|
||||
requirement: Option<&Sourced<FeedbackConfigToml>>,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) {
|
||||
let Some(Sourced { value, source }) = requirement else {
|
||||
return;
|
||||
};
|
||||
let FeedbackConfigToml { enabled } = value;
|
||||
let configured = configured.get_or_insert_default();
|
||||
let conflict = replace_required_leaf(&mut configured.enabled, enabled);
|
||||
push_structured_requirement_override_warning("feedback", conflict, source, startup_warnings);
|
||||
}
|
||||
|
||||
pub(super) fn push_sqlite_home_env_override_warning(
|
||||
configured_sqlite_home: Option<&AbsolutePathBuf>,
|
||||
sqlite_home_env: Option<&Path>,
|
||||
requirement: Option<&Sourced<AbsolutePathBuf>>,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) {
|
||||
if configured_sqlite_home.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(sqlite_home_env) = sqlite_home_env else {
|
||||
return;
|
||||
};
|
||||
let Some(Sourced { value, source }) = requirement else {
|
||||
return;
|
||||
};
|
||||
if sqlite_home_env == value.as_path() {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
?source,
|
||||
?value,
|
||||
"`CODEX_SQLITE_HOME` is overridden by an exact requirement for sqlite_home"
|
||||
);
|
||||
startup_warnings.push(format!(
|
||||
"Environment value for `$CODEX_SQLITE_HOME` is overridden by the required `sqlite_home` value {value:?} from {source}."
|
||||
));
|
||||
}
|
||||
|
||||
/// Emits one source-aware warning when a structured requirement replaces one
|
||||
/// or more configured values.
|
||||
fn push_structured_requirement_override_warning(
|
||||
field_name: &str,
|
||||
conflict: bool,
|
||||
source: &RequirementSource,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) {
|
||||
if !conflict {
|
||||
return;
|
||||
}
|
||||
tracing::warn!(
|
||||
?source,
|
||||
"configured values are overridden by requirements for {field_name}"
|
||||
);
|
||||
startup_warnings.push(format!(
|
||||
"Configured values under `{field_name}` are overridden by requirements from {source}."
|
||||
));
|
||||
}
|
||||
@@ -93,13 +93,17 @@ fn session_configuration_to_lock_config_toml(
|
||||
.effective_config()
|
||||
.try_into()
|
||||
.context("failed to deserialize effective config for config lock")?;
|
||||
|
||||
if config.config_lock_save_fields_resolved_from_model_catalog {
|
||||
save_session_resolved_fields(sc, &mut lock_config);
|
||||
}
|
||||
|
||||
save_config_resolved_fields(config, &mut lock_config)?;
|
||||
drop_lockfile_inputs(&mut lock_config);
|
||||
// Apply exact managed values last so cleanup cannot discard their runtime-effective values.
|
||||
config
|
||||
.config_layer_stack
|
||||
.requirements_toml()
|
||||
.apply_exact_to_config(&mut lock_config);
|
||||
|
||||
Ok(lock_config)
|
||||
}
|
||||
@@ -235,9 +239,23 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn write_model_catalog(path: &Path) {
|
||||
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(
|
||||
path,
|
||||
serde_json::to_string(&catalog).expect("serialize model catalog"),
|
||||
)
|
||||
.expect("write model catalog");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lock_contains_prompts_and_materializes_features() {
|
||||
let mut sc = crate::session::tests::make_session_configuration_for_tests().await;
|
||||
@@ -395,6 +413,90 @@ mod tests {
|
||||
assert_eq!(lock.approvals_reviewer, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lock_contains_exact_managed_requirements() {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let sqlite_home = codex_home.path().join("managed-state");
|
||||
let log_dir = codex_home.path().join("managed-logs");
|
||||
let catalog_path = codex_home.path().join("managed-models.json");
|
||||
write_model_catalog(&catalog_path);
|
||||
let requirements = format!(
|
||||
r#"
|
||||
sqlite_home = {:?}
|
||||
log_dir = {:?}
|
||||
model_catalog_json = {:?}
|
||||
check_for_update_on_startup = false
|
||||
allow_login_shell = false
|
||||
|
||||
[feedback]
|
||||
enabled = false
|
||||
|
||||
[windows]
|
||||
sandbox_private_desktop = false
|
||||
"#,
|
||||
sqlite_home.display(),
|
||||
log_dir.display(),
|
||||
catalog_path.display(),
|
||||
);
|
||||
let mut config = crate::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
|
||||
.expect("config should load");
|
||||
config.config_lock_save_fields_resolved_from_model_catalog = false;
|
||||
let mut sc = crate::session::tests::make_session_configuration_for_tests().await;
|
||||
sc.original_config_do_not_use = Arc::new(config);
|
||||
|
||||
let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize");
|
||||
let lock = &lockfile.config;
|
||||
|
||||
assert_eq!(lock.sqlite_home.as_deref(), Some(sqlite_home.as_path()));
|
||||
assert_eq!(lock.log_dir.as_deref(), Some(log_dir.as_path()));
|
||||
assert_eq!(
|
||||
lock.model_catalog_json.as_deref(),
|
||||
Some(catalog_path.as_path())
|
||||
);
|
||||
assert_eq!(lock.check_for_update_on_startup, Some(false));
|
||||
assert_eq!(lock.allow_login_shell, Some(false));
|
||||
assert_eq!(
|
||||
lock.feedback.as_ref().and_then(|feedback| feedback.enabled),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
lock.windows
|
||||
.as_ref()
|
||||
.and_then(|windows| windows.sandbox_private_desktop),
|
||||
Some(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lock_drops_unmanaged_model_catalog_input() {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let catalog_path = codex_home.path().join("user-models.json");
|
||||
write_model_catalog(&catalog_path);
|
||||
let config = crate::config::ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cli_overrides(vec![(
|
||||
"model_catalog_json".to_string(),
|
||||
toml::Value::String(catalog_path.display().to_string()),
|
||||
)])
|
||||
.build()
|
||||
.await
|
||||
.expect("config should load");
|
||||
let mut sc = crate::session::tests::make_session_configuration_for_tests().await;
|
||||
sc.original_config_do_not_use = Arc::new(config);
|
||||
|
||||
let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize");
|
||||
|
||||
assert_eq!(lockfile.config.model_catalog_json, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lock_validation_reports_config_diff() {
|
||||
let sc = crate::session::tests::make_session_configuration_for_tests().await;
|
||||
|
||||
@@ -1244,6 +1244,39 @@ async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_shell_commands_remain_login_shells_when_model_login_shells_are_disabled()
|
||||
-> anyhow::Result<()> {
|
||||
let (session, rx) = make_session_with_config_and_rx(|config| {
|
||||
config.permissions.allow_login_shell = false;
|
||||
})
|
||||
.await?;
|
||||
let turn_context = session.new_default_turn().await;
|
||||
let command = "echo managed-login-shell".to_string();
|
||||
let expected_command = session
|
||||
.user_shell()
|
||||
.derive_exec_args(&command, /*use_login_shell*/ true);
|
||||
|
||||
execute_user_shell_command(
|
||||
Arc::clone(&session),
|
||||
turn_context,
|
||||
command,
|
||||
CancellationToken::new(),
|
||||
UserShellCommandMode::StandaloneTurn,
|
||||
)
|
||||
.await;
|
||||
|
||||
loop {
|
||||
let event = rx.recv().await.expect("channel open");
|
||||
if let EventMsg::ExecCommandBegin(event) = event.msg {
|
||||
assert_eq!(event.command, expected_command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_base_instructions_no_user_content() {
|
||||
let prompt_with_apply_patch_instructions =
|
||||
|
||||
@@ -1419,21 +1419,21 @@ mod tests {
|
||||
.expect("session flags layer stack");
|
||||
assert_eq!(app.resume_model_settings(), expected);
|
||||
|
||||
app.config.config_layer_stack = ConfigLayerStack::default().with_user_config_profile(
|
||||
&profile_path,
|
||||
Some(&profile),
|
||||
config,
|
||||
);
|
||||
app.config.config_layer_stack = ConfigLayerStack::default()
|
||||
.with_user_config_profile(&profile_path, Some(&profile), config)
|
||||
.expect("user config profile layer stack");
|
||||
assert_eq!(app.resume_model_settings(), expected);
|
||||
}
|
||||
|
||||
app.config.config_layer_stack = ConfigLayerStack::default().with_user_config(
|
||||
&profile_path,
|
||||
TomlValue::Table(toml::map::Map::from_iter([(
|
||||
"model_reasoning_effort".to_string(),
|
||||
TomlValue::String("high".to_string()),
|
||||
)])),
|
||||
);
|
||||
app.config.config_layer_stack = ConfigLayerStack::default()
|
||||
.with_user_config(
|
||||
&profile_path,
|
||||
TomlValue::Table(toml::map::Map::from_iter([(
|
||||
"model_reasoning_effort".to_string(),
|
||||
TomlValue::String("high".to_string()),
|
||||
)])),
|
||||
)
|
||||
.expect("user config layer stack");
|
||||
assert_eq!(
|
||||
app.resume_model_settings(),
|
||||
crate::app_server_session::ResumeModelSettings::RestoreFromThread
|
||||
|
||||
@@ -55,6 +55,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()
|
||||
};
|
||||
|
||||
@@ -160,6 +160,64 @@ fn render_debug_config_lines(
|
||||
lines.push("Requirements:".bold().into());
|
||||
let mut requirement_lines = Vec::new();
|
||||
|
||||
if let Some(sqlite_home) = requirements.sqlite_home.as_ref() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"sqlite_home",
|
||||
sqlite_home.value.display().to_string(),
|
||||
Some(&sqlite_home.source),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(log_dir) = requirements.log_dir.as_ref() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"log_dir",
|
||||
log_dir.value.display().to_string(),
|
||||
Some(&log_dir.source),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(model_catalog_json) = requirements.model_catalog_json.as_ref() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"model_catalog_json",
|
||||
model_catalog_json.value.display().to_string(),
|
||||
Some(&model_catalog_json.source),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(check_for_update_on_startup) = requirements.check_for_update_on_startup.as_ref() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"check_for_update_on_startup",
|
||||
check_for_update_on_startup.value.to_string(),
|
||||
Some(&check_for_update_on_startup.source),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(allow_login_shell) = requirements.allow_login_shell.as_ref() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"allow_login_shell",
|
||||
allow_login_shell.value.to_string(),
|
||||
Some(&allow_login_shell.source),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(feedback) = requirements.feedback.as_ref()
|
||||
&& let Some(enabled) = feedback.value.enabled
|
||||
{
|
||||
requirement_lines.push(requirement_line(
|
||||
"feedback.enabled",
|
||||
enabled.to_string(),
|
||||
Some(&feedback.source),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(sandbox_private_desktop) = requirements.windows_sandbox_private_desktop.as_ref() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"windows.sandbox_private_desktop",
|
||||
sandbox_private_desktop.value.to_string(),
|
||||
Some(&sandbox_private_desktop.source),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(policies) = requirements_toml.allowed_approval_policies.as_ref() {
|
||||
let value = join_or_empty(policies.iter().map(ToString::to_string).collect::<Vec<_>>());
|
||||
requirement_lines.push(requirement_line(
|
||||
@@ -626,7 +684,9 @@ mod tests {
|
||||
use codex_config::SandboxModeRequirement;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::WebSearchModeRequirement;
|
||||
use codex_config::WindowsRequirementsToml;
|
||||
use codex_config::sandbox_mode_requirement_for_permission_profile;
|
||||
use codex_config::types::FeedbackConfigToml;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -749,8 +809,53 @@ interrupt_message = false
|
||||
} else {
|
||||
absolute_path("/home/alice/.gitconfig")
|
||||
};
|
||||
let sqlite_home = if cfg!(windows) {
|
||||
absolute_path("C:\\Users\\alice\\.codex\\state")
|
||||
} else {
|
||||
absolute_path("/home/alice/.codex/state")
|
||||
};
|
||||
let log_dir = if cfg!(windows) {
|
||||
absolute_path("C:\\Users\\alice\\.codex\\logs")
|
||||
} else {
|
||||
absolute_path("/home/alice/.codex/logs")
|
||||
};
|
||||
let model_catalog_json = if cfg!(windows) {
|
||||
absolute_path("C:\\Users\\alice\\.codex\\models.json")
|
||||
} else {
|
||||
absolute_path("/home/alice/.codex/models.json")
|
||||
};
|
||||
|
||||
let requirements = ConfigRequirements {
|
||||
sqlite_home: Some(Sourced::new(
|
||||
sqlite_home.clone(),
|
||||
RequirementSource::LegacyManagedConfigTomlFromMdm,
|
||||
)),
|
||||
log_dir: Some(Sourced::new(
|
||||
log_dir.clone(),
|
||||
RequirementSource::LegacyManagedConfigTomlFromMdm,
|
||||
)),
|
||||
model_catalog_json: Some(Sourced::new(
|
||||
model_catalog_json.clone(),
|
||||
RequirementSource::LegacyManagedConfigTomlFromMdm,
|
||||
)),
|
||||
check_for_update_on_startup: Some(Sourced::new(
|
||||
/*value*/ false,
|
||||
RequirementSource::LegacyManagedConfigTomlFromMdm,
|
||||
)),
|
||||
allow_login_shell: Some(Sourced::new(
|
||||
/*value*/ false,
|
||||
RequirementSource::LegacyManagedConfigTomlFromMdm,
|
||||
)),
|
||||
feedback: Some(Sourced::new(
|
||||
FeedbackConfigToml {
|
||||
enabled: Some(false),
|
||||
},
|
||||
RequirementSource::LegacyManagedConfigTomlFromMdm,
|
||||
)),
|
||||
windows_sandbox_private_desktop: Some(Sourced::new(
|
||||
/*value*/ false,
|
||||
RequirementSource::LegacyManagedConfigTomlFromMdm,
|
||||
)),
|
||||
approval_policy: ConstrainedWithSource::new(
|
||||
Constrained::allow_any(AskForApproval::OnRequest.to_core()),
|
||||
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
|
||||
@@ -828,6 +933,14 @@ interrupt_message = false
|
||||
};
|
||||
|
||||
let requirements_toml = ConfigRequirementsToml {
|
||||
sqlite_home: Some(sqlite_home),
|
||||
log_dir: Some(log_dir),
|
||||
model_catalog_json: Some(model_catalog_json),
|
||||
check_for_update_on_startup: Some(false),
|
||||
allow_login_shell: Some(false),
|
||||
feedback: Some(FeedbackConfigToml {
|
||||
enabled: Some(false),
|
||||
}),
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]),
|
||||
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
|
||||
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
|
||||
@@ -839,7 +952,10 @@ interrupt_message = false
|
||||
allow_appshots: Some(false),
|
||||
allow_remote_control: Some(false),
|
||||
computer_use: None,
|
||||
windows: None,
|
||||
windows: Some(WindowsRequirementsToml {
|
||||
allowed_sandbox_implementations: None,
|
||||
sandbox_private_desktop: Some(false),
|
||||
}),
|
||||
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
|
||||
feature_requirements: Some(FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
||||
@@ -1220,6 +1336,7 @@ approval_policy = "never"
|
||||
network: None,
|
||||
permissions: None,
|
||||
models: None,
|
||||
..ConfigRequirementsToml::default()
|
||||
};
|
||||
|
||||
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||
|
||||
@@ -1166,7 +1166,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();
|
||||
|
||||
set_default_client_residency_requirement(config.enforce_residency.value());
|
||||
|
||||
|
||||
+7
@@ -8,6 +8,13 @@ Config layer stack (lowest precedence first):
|
||||
1. user (/home/alice/.codex/config.toml) (enabled)
|
||||
|
||||
Requirements:
|
||||
- sqlite_home: /home/alice/.codex/state (source: MDM managed_config.toml (legacy))
|
||||
- log_dir: /home/alice/.codex/logs (source: MDM managed_config.toml (legacy))
|
||||
- model_catalog_json: /home/alice/.codex/models.json (source: MDM managed_config.toml (legacy))
|
||||
- check_for_update_on_startup: false (source: MDM managed_config.toml (legacy))
|
||||
- allow_login_shell: false (source: MDM managed_config.toml (legacy))
|
||||
- feedback.enabled: false (source: MDM managed_config.toml (legacy))
|
||||
- windows.sandbox_private_desktop: false (source: MDM managed_config.toml (legacy))
|
||||
- allowed_approval_policies: on-request (source: MDM managed_config.toml (legacy))
|
||||
- allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))
|
||||
- allowed_sandbox_modes: read-only (source: /etc/codex/requirements.toml)
|
||||
|
||||
Reference in New Issue
Block a user