diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 7e431cea8c25..bc7e906d7a2a 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -6765,6 +6765,39 @@ } ] }, + "AuthCredentialsStoreMode": { + "description": "Determines where Codex stores CLI authentication credentials.", + "oneOf": [ + { + "description": "Persist credentials in `CODEX_HOME/auth.json`.", + "enum": [ + "file" + ], + "type": "string" + }, + { + "description": "Persist credentials in the operating system keyring. Fail if unavailable.", + "enum": [ + "keyring" + ], + "type": "string" + }, + { + "description": "Use the operating system keyring when available; otherwise, fall back to `CODEX_HOME/auth.json`.", + "enum": [ + "auto" + ], + "type": "string" + }, + { + "description": "Store credentials in memory only for the current process.", + "enum": [ + "ephemeral" + ], + "type": "string" + } + ] + }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", "oneOf": [ @@ -7662,6 +7695,16 @@ ], "description": "[UNSTABLE] Optional default for where approval requests are routed for review." }, + "cli_auth_credentials_store": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AuthCredentialsStoreMode" + }, + { + "type": "null" + } + ] + }, "compact_prompt": { "type": [ "string", @@ -8160,6 +8203,12 @@ "null" ] }, + "allowLoginShell": { + "type": [ + "boolean", + "null" + ] + }, "allowManagedHooksOnly": { "type": [ "boolean", @@ -8181,6 +8230,24 @@ "null" ] }, + "allowedChatgptWorkspaces": { + "additionalProperties": { + "type": "boolean" + }, + "type": [ + "object", + "null" + ] + }, + "allowedLoginMethods": { + "additionalProperties": { + "type": "boolean" + }, + "type": [ + "object", + "null" + ] + }, "allowedPermissionProfiles": { "additionalProperties": { "type": "boolean" @@ -8217,6 +8284,28 @@ "null" ] }, + "chatgptBaseUrl": { + "type": [ + "string", + "null" + ] + }, + "checkForUpdateOnStartup": { + "type": [ + "boolean", + "null" + ] + }, + "cliAuthCredentialsStore": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AuthCredentialsStoreMode" + }, + { + "type": "null" + } + ] + }, "computerUse": { "anyOf": [ { @@ -8251,6 +8340,66 @@ "object", "null" ] + }, + "feedback": { + "anyOf": [ + { + "$ref": "#/definitions/v2/FeedbackRequirements" + }, + { + "type": "null" + } + ] + }, + "logDir": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "modelCatalogJson": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "otel": { + "description": "Config-native OTEL requirements. Exporter header names are preserved, while their values are redacted." + }, + "shellEnvironmentPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ShellEnvironmentPolicyRequirements" + }, + { + "type": "null" + } + ], + "description": "Lossless config-native shell environment policy requirements." + }, + "sqliteHome": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "windowsSandboxPrivateDesktop": { + "type": [ + "boolean", + "null" + ] } }, "type": "object" @@ -9249,6 +9398,17 @@ ], "type": "string" }, + "FeedbackRequirements": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "FeedbackUploadParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -15834,6 +15994,78 @@ ], "type": "object" }, + "ShellEnvironmentPolicyFilter": { + "description": "Assigns a shell environment variable pattern to the include-only or exclude set. Includes do not re-add variables removed by another exclude pattern.", + "enum": [ + "include", + "exclude" + ], + "type": "string" + }, + "ShellEnvironmentPolicyInherit": { + "oneOf": [ + { + "description": "\"Core\" environment variables for the platform. On UNIX, this would include HOME, LOGNAME, PATH, SHELL, and USER, among others.", + "enum": [ + "core" + ], + "type": "string" + }, + { + "description": "Inherits the full environment from the parent process.", + "enum": [ + "all" + ], + "type": "string" + }, + { + "description": "Do not inherit any environment variables from the parent process.", + "enum": [ + "none" + ], + "type": "string" + } + ] + }, + "ShellEnvironmentPolicyRequirements": { + "properties": { + "filters": { + "additionalProperties": { + "$ref": "#/definitions/v2/ShellEnvironmentPolicyFilter" + }, + "type": [ + "object", + "null" + ] + }, + "ignore_default_excludes": { + "type": [ + "boolean", + "null" + ] + }, + "inherit": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ShellEnvironmentPolicyInherit" + }, + { + "type": "null" + } + ] + }, + "set": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, "SkillDependencies": { "properties": { "tools": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 2733bc271979..f42c4c90da2c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1010,6 +1010,39 @@ } ] }, + "AuthCredentialsStoreMode": { + "description": "Determines where Codex stores CLI authentication credentials.", + "oneOf": [ + { + "description": "Persist credentials in `CODEX_HOME/auth.json`.", + "enum": [ + "file" + ], + "type": "string" + }, + { + "description": "Persist credentials in the operating system keyring. Fail if unavailable.", + "enum": [ + "keyring" + ], + "type": "string" + }, + { + "description": "Use the operating system keyring when available; otherwise, fall back to `CODEX_HOME/auth.json`.", + "enum": [ + "auto" + ], + "type": "string" + }, + { + "description": "Store credentials in memory only for the current process.", + "enum": [ + "ephemeral" + ], + "type": "string" + } + ] + }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", "oneOf": [ @@ -3975,6 +4008,16 @@ ], "description": "[UNSTABLE] Optional default for where approval requests are routed for review." }, + "cli_auth_credentials_store": { + "anyOf": [ + { + "$ref": "#/definitions/AuthCredentialsStoreMode" + }, + { + "type": "null" + } + ] + }, "compact_prompt": { "type": [ "string", @@ -4473,6 +4516,12 @@ "null" ] }, + "allowLoginShell": { + "type": [ + "boolean", + "null" + ] + }, "allowManagedHooksOnly": { "type": [ "boolean", @@ -4494,6 +4543,24 @@ "null" ] }, + "allowedChatgptWorkspaces": { + "additionalProperties": { + "type": "boolean" + }, + "type": [ + "object", + "null" + ] + }, + "allowedLoginMethods": { + "additionalProperties": { + "type": "boolean" + }, + "type": [ + "object", + "null" + ] + }, "allowedPermissionProfiles": { "additionalProperties": { "type": "boolean" @@ -4530,6 +4597,28 @@ "null" ] }, + "chatgptBaseUrl": { + "type": [ + "string", + "null" + ] + }, + "checkForUpdateOnStartup": { + "type": [ + "boolean", + "null" + ] + }, + "cliAuthCredentialsStore": { + "anyOf": [ + { + "$ref": "#/definitions/AuthCredentialsStoreMode" + }, + { + "type": "null" + } + ] + }, "computerUse": { "anyOf": [ { @@ -4564,6 +4653,66 @@ "object", "null" ] + }, + "feedback": { + "anyOf": [ + { + "$ref": "#/definitions/FeedbackRequirements" + }, + { + "type": "null" + } + ] + }, + "logDir": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "modelCatalogJson": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "otel": { + "description": "Config-native OTEL requirements. Exporter header names are preserved, while their values are redacted." + }, + "shellEnvironmentPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/ShellEnvironmentPolicyRequirements" + }, + { + "type": "null" + } + ], + "description": "Lossless config-native shell environment policy requirements." + }, + "sqliteHome": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "windowsSandboxPrivateDesktop": { + "type": [ + "boolean", + "null" + ] } }, "type": "object" @@ -5562,6 +5711,17 @@ ], "type": "string" }, + "FeedbackRequirements": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "FeedbackUploadParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -13642,6 +13802,78 @@ ], "type": "object" }, + "ShellEnvironmentPolicyFilter": { + "description": "Assigns a shell environment variable pattern to the include-only or exclude set. Includes do not re-add variables removed by another exclude pattern.", + "enum": [ + "include", + "exclude" + ], + "type": "string" + }, + "ShellEnvironmentPolicyInherit": { + "oneOf": [ + { + "description": "\"Core\" environment variables for the platform. On UNIX, this would include HOME, LOGNAME, PATH, SHELL, and USER, among others.", + "enum": [ + "core" + ], + "type": "string" + }, + { + "description": "Inherits the full environment from the parent process.", + "enum": [ + "all" + ], + "type": "string" + }, + { + "description": "Do not inherit any environment variables from the parent process.", + "enum": [ + "none" + ], + "type": "string" + } + ] + }, + "ShellEnvironmentPolicyRequirements": { + "properties": { + "filters": { + "additionalProperties": { + "$ref": "#/definitions/ShellEnvironmentPolicyFilter" + }, + "type": [ + "object", + "null" + ] + }, + "ignore_default_excludes": { + "type": [ + "boolean", + "null" + ] + }, + "inherit": { + "anyOf": [ + { + "$ref": "#/definitions/ShellEnvironmentPolicyInherit" + }, + { + "type": "null" + } + ] + }, + "set": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, "SkillDependencies": { "properties": { "tools": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index 3bc0a8e0a9a0..ed8bde40a372 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -208,6 +208,39 @@ } ] }, + "AuthCredentialsStoreMode": { + "description": "Determines where Codex stores CLI authentication credentials.", + "oneOf": [ + { + "description": "Persist credentials in `CODEX_HOME/auth.json`.", + "enum": [ + "file" + ], + "type": "string" + }, + { + "description": "Persist credentials in the operating system keyring. Fail if unavailable.", + "enum": [ + "keyring" + ], + "type": "string" + }, + { + "description": "Use the operating system keyring when available; otherwise, fall back to `CODEX_HOME/auth.json`.", + "enum": [ + "auto" + ], + "type": "string" + }, + { + "description": "Store credentials in memory only for the current process.", + "enum": [ + "ephemeral" + ], + "type": "string" + } + ] + }, "AutoCompactTokenLimitScope": { "description": "Selects which part of the active context is charged against `model_auto_compact_token_limit`.", "oneOf": [ @@ -261,6 +294,16 @@ ], "description": "[UNSTABLE] Optional default for where approval requests are routed for review." }, + "cli_auth_credentials_store": { + "anyOf": [ + { + "$ref": "#/definitions/AuthCredentialsStoreMode" + }, + { + "type": "null" + } + ] + }, "compact_prompt": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index e57bd48b796f..6d7dcfd9489f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -1,6 +1,10 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", "enum": [ @@ -60,6 +64,39 @@ } ] }, + "AuthCredentialsStoreMode": { + "description": "Determines where Codex stores CLI authentication credentials.", + "oneOf": [ + { + "description": "Persist credentials in `CODEX_HOME/auth.json`.", + "enum": [ + "file" + ], + "type": "string" + }, + { + "description": "Persist credentials in the operating system keyring. Fail if unavailable.", + "enum": [ + "keyring" + ], + "type": "string" + }, + { + "description": "Use the operating system keyring when available; otherwise, fall back to `CODEX_HOME/auth.json`.", + "enum": [ + "auto" + ], + "type": "string" + }, + { + "description": "Store credentials in memory only for the current process.", + "enum": [ + "ephemeral" + ], + "type": "string" + } + ] + }, "ComputerUseRequirements": { "properties": { "allowLockedComputerUse": { @@ -79,6 +116,12 @@ "null" ] }, + "allowLoginShell": { + "type": [ + "boolean", + "null" + ] + }, "allowManagedHooksOnly": { "type": [ "boolean", @@ -100,6 +143,24 @@ "null" ] }, + "allowedChatgptWorkspaces": { + "additionalProperties": { + "type": "boolean" + }, + "type": [ + "object", + "null" + ] + }, + "allowedLoginMethods": { + "additionalProperties": { + "type": "boolean" + }, + "type": [ + "object", + "null" + ] + }, "allowedPermissionProfiles": { "additionalProperties": { "type": "boolean" @@ -136,6 +197,28 @@ "null" ] }, + "chatgptBaseUrl": { + "type": [ + "string", + "null" + ] + }, + "checkForUpdateOnStartup": { + "type": [ + "boolean", + "null" + ] + }, + "cliAuthCredentialsStore": { + "anyOf": [ + { + "$ref": "#/definitions/AuthCredentialsStoreMode" + }, + { + "type": "null" + } + ] + }, "computerUse": { "anyOf": [ { @@ -170,6 +253,66 @@ "object", "null" ] + }, + "feedback": { + "anyOf": [ + { + "$ref": "#/definitions/FeedbackRequirements" + }, + { + "type": "null" + } + ] + }, + "logDir": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "modelCatalogJson": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "otel": { + "description": "Config-native OTEL requirements. Exporter header names are preserved, while their values are redacted." + }, + "shellEnvironmentPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/ShellEnvironmentPolicyRequirements" + }, + { + "type": "null" + } + ], + "description": "Lossless config-native shell environment policy requirements." + }, + "sqliteHome": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "windowsSandboxPrivateDesktop": { + "type": [ + "boolean", + "null" + ] } }, "type": "object" @@ -274,6 +417,17 @@ ], "type": "object" }, + "FeedbackRequirements": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "ManagedHooksRequirements": { "properties": { "PermissionRequest": { @@ -499,6 +653,78 @@ ], "type": "string" }, + "ShellEnvironmentPolicyFilter": { + "description": "Assigns a shell environment variable pattern to the include-only or exclude set. Includes do not re-add variables removed by another exclude pattern.", + "enum": [ + "include", + "exclude" + ], + "type": "string" + }, + "ShellEnvironmentPolicyInherit": { + "oneOf": [ + { + "description": "\"Core\" environment variables for the platform. On UNIX, this would include HOME, LOGNAME, PATH, SHELL, and USER, among others.", + "enum": [ + "core" + ], + "type": "string" + }, + { + "description": "Inherits the full environment from the parent process.", + "enum": [ + "all" + ], + "type": "string" + }, + { + "description": "Do not inherit any environment variables from the parent process.", + "enum": [ + "none" + ], + "type": "string" + } + ] + }, + "ShellEnvironmentPolicyRequirements": { + "properties": { + "filters": { + "additionalProperties": { + "$ref": "#/definitions/ShellEnvironmentPolicyFilter" + }, + "type": [ + "object", + "null" + ] + }, + "ignore_default_excludes": { + "type": [ + "boolean", + "null" + ] + }, + "inherit": { + "anyOf": [ + { + "$ref": "#/definitions/ShellEnvironmentPolicyInherit" + }, + { + "type": "null" + } + ] + }, + "set": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, "WebSearchMode": { "enum": [ "disabled", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AuthCredentialsStoreMode.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AuthCredentialsStoreMode.ts new file mode 100644 index 000000000000..ecc556b9da29 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AuthCredentialsStoreMode.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * Determines where Codex stores CLI authentication credentials. + */ +export type AuthCredentialsStoreMode = "file" | "keyring" | "auto" | "ephemeral"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts index cc15fb4e720b..aa1ffc41a42e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts @@ -11,6 +11,7 @@ import type { JsonValue } from "../serde_json/JsonValue"; import type { AnalyticsConfig } from "./AnalyticsConfig"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { AuthCredentialsStoreMode } from "./AuthCredentialsStoreMode"; import type { ForcedChatgptWorkspaceIds } from "./ForcedChatgptWorkspaceIds"; import type { SandboxMode } from "./SandboxMode"; import type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; @@ -20,4 +21,4 @@ export type Config = {model: string | null, review_model: string | null, model_c * [UNSTABLE] Optional default for where approval requests are routed for * review. */ -approvals_reviewer: ApprovalsReviewer | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: ForcedChatgptWorkspaceIds | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, service_tier: string | null, analytics: AnalyticsConfig | null, desktop: { [key in string]?: JsonValue } | null} & ({ [key in string]?: number | string | boolean | Array | { [key in string]?: JsonValue } | null }); +approvals_reviewer: ApprovalsReviewer | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: ForcedChatgptWorkspaceIds | null, forced_login_method: ForcedLoginMethod | null, cli_auth_credentials_store: AuthCredentialsStoreMode | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, service_tier: string | null, analytics: AnalyticsConfig | null, desktop: { [key in string]?: JsonValue } | null} & ({ [key in string]?: number | string | boolean | Array | { [key in string]?: JsonValue } | null }); diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index 3eb9a65aa65d..dbc9e44749d3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -1,11 +1,23 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { WebSearchMode } from "../WebSearchMode"; +import type { JsonValue } from "../serde_json/JsonValue"; import type { AskForApproval } from "./AskForApproval"; +import type { AuthCredentialsStoreMode } from "./AuthCredentialsStoreMode"; import type { ComputerUseRequirements } from "./ComputerUseRequirements"; +import type { FeedbackRequirements } from "./FeedbackRequirements"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; +import type { ShellEnvironmentPolicyRequirements } from "./ShellEnvironmentPolicyRequirements"; import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, allowedLoginMethods: { [key in string]?: boolean } | null, allowedChatgptWorkspaces: { [key in string]?: boolean } | null, cliAuthCredentialsStore: AuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, sqliteHome: AbsolutePathBuf | null, logDir: AbsolutePathBuf | null, modelCatalogJson: AbsolutePathBuf | null, checkForUpdateOnStartup: boolean | null, /** + * Config-native OTEL requirements. Exporter header names are preserved, + * while their values are redacted. + */ +otel: JsonValue | null, allowLoginShell: boolean | null, /** + * Lossless config-native shell environment policy requirements. + */ +shellEnvironmentPolicy: ShellEnvironmentPolicyRequirements | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FeedbackRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FeedbackRequirements.ts new file mode 100644 index 000000000000..8d0a2002ee88 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FeedbackRequirements.ts @@ -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, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyFilter.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyFilter.ts new file mode 100644 index 000000000000..ea19b5b8f2dd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyFilter.ts @@ -0,0 +1,9 @@ +// 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. + +/** + * Assigns a shell environment variable pattern to the include-only or exclude + * set. Includes do not re-add variables removed by another exclude pattern. + */ +export type ShellEnvironmentPolicyFilter = "include" | "exclude"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyInherit.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyInherit.ts new file mode 100644 index 000000000000..4af9dfc5be8b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyInherit.ts @@ -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 ShellEnvironmentPolicyInherit = "core" | "all" | "none"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyRequirements.ts new file mode 100644 index 000000000000..3cac483c435f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ShellEnvironmentPolicyRequirements.ts @@ -0,0 +1,7 @@ +// 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 { ShellEnvironmentPolicyFilter } from "./ShellEnvironmentPolicyFilter"; +import type { ShellEnvironmentPolicyInherit } from "./ShellEnvironmentPolicyInherit"; + +export type ShellEnvironmentPolicyRequirements = { inherit: ShellEnvironmentPolicyInherit | null, ignore_default_excludes: boolean | null, set: { [key in string]?: string } | null, filters: { [key in string]?: ShellEnvironmentPolicyFilter } | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 5aa66a56b5f4..3f647b4a0d4c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -35,6 +35,7 @@ export type { AppsListResponse } from "./AppsListResponse"; export type { AskForApproval } from "./AskForApproval"; export type { AttestationGenerateParams } from "./AttestationGenerateParams"; export type { AttestationGenerateResponse } from "./AttestationGenerateResponse"; +export type { AuthCredentialsStoreMode } from "./AuthCredentialsStoreMode"; export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource"; export type { ByteRange } from "./ByteRange"; export type { CancelLoginAccountParams } from "./CancelLoginAccountParams"; @@ -117,6 +118,7 @@ export type { ExternalAgentConfigImportResponse } from "./ExternalAgentConfigImp export type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult"; export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; +export type { FeedbackRequirements } from "./FeedbackRequirements"; export type { FeedbackUploadParams } from "./FeedbackUploadParams"; export type { FeedbackUploadResponse } from "./FeedbackUploadResponse"; export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision"; @@ -353,6 +355,9 @@ export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmai export type { ServerRequestResolvedNotification } from "./ServerRequestResolvedNotification"; export type { SessionMigration } from "./SessionMigration"; export type { SessionSource } from "./SessionSource"; +export type { ShellEnvironmentPolicyFilter } from "./ShellEnvironmentPolicyFilter"; +export type { ShellEnvironmentPolicyInherit } from "./ShellEnvironmentPolicyInherit"; +export type { ShellEnvironmentPolicyRequirements } from "./ShellEnvironmentPolicyRequirements"; export type { SkillDependencies } from "./SkillDependencies"; export type { SkillErrorInfo } from "./SkillErrorInfo"; export type { SkillInterface } from "./SkillInterface"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index c4d113d1c75b..0035411b1dc2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -4,9 +4,12 @@ use super::SandboxMode; use super::WindowsSandboxSetupMode; use super::shared::default_enabled; use codex_experimental_api_macros::ExperimentalApi; +pub use codex_protocol::config_types::AuthCredentialsStoreMode; use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ShellEnvironmentPolicyFilter; +use codex_protocol::config_types::ShellEnvironmentPolicyInherit; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; @@ -257,6 +260,7 @@ pub struct Config { pub sandbox_workspace_write: Option, pub forced_chatgpt_workspace_id: Option, pub forced_login_method: Option, + pub cli_auth_credentials_store: Option, pub web_search: Option, pub tools: Option, pub instructions: Option, @@ -335,6 +339,7 @@ pub struct ConfigWriteResponse { #[ts(export_to = "v2/")] pub enum ConfigWriteErrorCode { ConfigLayerReadonly, + ConfigRequirementReadonly, ConfigVersionConflict, ConfigValidationError, ConfigPathNotFound, @@ -366,7 +371,9 @@ pub struct ConfigReadResponse { pub layers: Option>, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema, TS, ExperimentalApi, +)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ConfigRequirements { @@ -389,6 +396,39 @@ pub struct ConfigRequirements { pub enforce_residency: Option, #[experimental("configRequirements/read.network")] pub network: Option, + pub allowed_login_methods: Option>, + pub allowed_chatgpt_workspaces: Option>, + pub cli_auth_credentials_store: Option, + pub chatgpt_base_url: Option, + pub sqlite_home: Option, + pub log_dir: Option, + pub model_catalog_json: Option, + pub check_for_update_on_startup: Option, + /// Config-native OTEL requirements. Exporter header names are preserved, + /// while their values are redacted. + pub otel: Option, + pub allow_login_shell: Option, + /// Lossless config-native shell environment policy requirements. + pub shell_environment_policy: Option, + pub feedback: Option, + pub windows_sandbox_private_desktop: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FeedbackRequirements { + pub enabled: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct ShellEnvironmentPolicyRequirements { + pub inherit: Option, + pub ignore_default_excludes: Option, + pub r#set: Option>, + pub filters: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 2567e386c839..6bb3d0480ee7 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1643,6 +1643,7 @@ fn config_granular_approval_policy_is_marked_experimental() { sandbox_workspace_write: None, forced_chatgpt_workspace_id: None, forced_login_method: None, + cli_auth_credentials_store: None, web_search: None, tools: None, instructions: None, @@ -1676,6 +1677,7 @@ fn config_approvals_reviewer_is_marked_experimental() { sandbox_workspace_write: None, forced_chatgpt_workspace_id: None, forced_login_method: None, + cli_auth_credentials_store: None, web_search: None, tools: None, instructions: None, @@ -1705,20 +1707,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() request_permissions: false, mcp_elicitations: false, }]), - allowed_approvals_reviewers: None, - allowed_sandbox_modes: None, - allowed_windows_sandbox_implementations: None, - allowed_permission_profiles: None, - default_permissions: None, - allowed_web_search_modes: None, - allow_managed_hooks_only: None, - allow_appshots: None, - allow_remote_control: None, - computer_use: None, - feature_requirements: None, - hooks: None, - enforce_residency: None, - network: None, + ..Default::default() }); assert_eq!(reason, Some("askForApproval.granular")); diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 9188877384c1..ee96dbe44247 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -235,7 +235,7 @@ Example with notification opt-out: - `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. Returns an `importId` used to correlate the completion notification. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes with type-level `itemResults` containing each migrated type's success count, error count, successes, and raw errors (immediately after the response when everything completed synchronously, or after background imports finish). - `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. -- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), 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`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`, `allowedLoginMethods`, `allowedChatgptWorkspaces`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), 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`), `enforceResidency`, `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`, and pinned runtime settings such as credential storage, ChatGPT base URL, state/log/model-catalog paths, update checks, login-shell and environment policy, feedback, Windows private-desktop use, and OTEL configuration. Shell environment requirements expose pattern-to-action `filters` whose values are `include` or `exclude`; OTEL exporter responses preserve header names but redact their values. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/config_manager_service.rs b/codex-rs/app-server/src/config_manager_service.rs index 4b42c28aa904..f0fcd8e9cdde 100644 --- a/codex-rs/app-server/src/config_manager_service.rs +++ b/codex-rs/app-server/src/config_manager_service.rs @@ -17,7 +17,7 @@ use codex_config::ConfigLayerStack; use codex_config::ConfigLayerStackOrdering; use codex_config::ConfigRequirementsToml; use codex_config::config_toml::ConfigToml; -use codex_config::merge_toml_values; +use codex_config::merge_toml_values_at_path; use codex_core::config::deserialize_config_toml_with_base; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; @@ -237,6 +237,16 @@ impl ConfigManager { let segments = parse_key_path(&key_path).map_err(|message| { ConfigManagerError::write(ConfigWriteErrorCode::ConfigValidationError, message) })?; + if let Some(requirement) = + managed_requirement_for_path(layers.requirements_toml(), &segments) + { + return Err(ConfigManagerError::write( + ConfigWriteErrorCode::ConfigRequirementReadonly, + format!( + "Configuration path `{key_path}` is read-only because it is managed by `{requirement}`" + ), + )); + } if !value.is_null() { match segments.as_slice() { [segment] if segment == "profile" => { @@ -360,6 +370,132 @@ impl ConfigManager { } } +fn managed_requirement_for_path( + requirements: &ConfigRequirementsToml, + segments: &[String], +) -> Option<&'static str> { + let root = segments.first()?.as_str(); + let ConfigRequirementsToml { + allowed_login_methods, + allowed_chatgpt_workspaces, + cli_auth_credentials_store, + chatgpt_base_url, + sqlite_home, + log_dir, + model_catalog_json, + check_for_update_on_startup, + otel, + allow_login_shell, + shell_environment_policy, + feedback, + allowed_approval_policies, + allowed_approvals_reviewers, + allowed_sandbox_modes, + allowed_permission_profiles, + default_permissions, + remote_sandbox_config, + allowed_web_search_modes, + allow_managed_hooks_only, + allow_appshots: _, + allow_remote_control, + computer_use, + windows, + feature_requirements, + hooks, + mcp_servers, + plugins, + apps, + rules, + enforce_residency, + network, + permissions, + guardian_policy_config, + } = requirements; + + macro_rules! managed_root { + ($requirement:ident, $($root:literal),+ $(,)?) => { + if $requirement.is_some() && matches!(root, $($root)|+) { + return Some(stringify!($requirement)); + } + }; + } + + managed_root!(allowed_login_methods, "forced_login_method"); + managed_root!(allowed_chatgpt_workspaces, "forced_chatgpt_workspace_id"); + managed_root!(cli_auth_credentials_store, "cli_auth_credentials_store"); + managed_root!(chatgpt_base_url, "chatgpt_base_url"); + managed_root!(sqlite_home, "sqlite_home"); + managed_root!(log_dir, "log_dir"); + managed_root!(model_catalog_json, "model_catalog_json"); + managed_root!(check_for_update_on_startup, "check_for_update_on_startup"); + managed_root!(otel, "otel"); + managed_root!(allow_login_shell, "allow_login_shell"); + managed_root!(shell_environment_policy, "shell_environment_policy"); + managed_root!(feedback, "feedback"); + managed_root!(allowed_approval_policies, "approval_policy"); + managed_root!(allowed_approvals_reviewers, "approvals_reviewer"); + managed_root!( + allowed_sandbox_modes, + "sandbox_mode", + "sandbox_workspace_write", + "permissions" + ); + managed_root!(allowed_permission_profiles, "sandbox_mode", "permissions"); + managed_root!(default_permissions, "sandbox_mode", "permissions"); + managed_root!(remote_sandbox_config, "remote_sandbox_config"); + managed_root!(allowed_web_search_modes, "web_search"); + managed_root!(allow_managed_hooks_only, "hooks"); + if allow_remote_control.is_some() && is_feature_path(segments, "remote_control") { + return Some("allow_remote_control"); + } + if computer_use.is_some() && is_feature_path(segments, "computer_use") { + return Some("computer_use"); + } + managed_root!(windows, "windows"); + if let Some(requirements) = feature_requirements { + if matches!(segments, [root] if root == "features") { + return Some("feature_requirements"); + } + let key = match segments { + [root, key, ..] if root == "features" => key.as_str(), + [key, ..] => key.as_str(), + [] => return None, + }; + if let Some(feature) = codex_features::feature_for_key(key) + && requirements.entries.keys().any(|required| { + codex_features::feature_for_key(required).is_some_and(|value| value == feature) + }) + { + return Some("feature_requirements"); + } + } + managed_root!(hooks, "hooks"); + managed_root!(mcp_servers, "mcp_servers"); + managed_root!(plugins, "plugins"); + managed_root!(apps, "apps"); + managed_root!(rules, "rules"); + managed_root!(enforce_residency, "enforce_residency"); + managed_root!(network, "experimental_network", "network", "permissions"); + managed_root!(permissions, "permissions", "sandbox_mode"); + managed_root!(guardian_policy_config, "guardian_policy_config"); + None +} + +fn is_feature_path(segments: &[String], required_key: &str) -> bool { + match segments { + [root] if root == "features" => true, + [root, key, ..] if root == "features" => { + codex_features::feature_for_key(key) + == codex_features::canonical_feature_for_key(required_key) + } + [key, ..] => { + codex_features::feature_for_key(key) + == codex_features::canonical_feature_for_key(required_key) + } + [] => false, + } +} + async fn create_empty_user_layer( config_toml: &AbsolutePathBuf, ) -> Result { @@ -512,7 +648,7 @@ fn apply_merge( && matches!(existing, TomlValue::Table(_)) && matches!(value, TomlValue::Table(_)) { - merge_toml_values(existing, value); + merge_toml_values_at_path(existing, value, segments); return Ok(true); } diff --git a/codex-rs/app-server/src/config_manager_service_tests.rs b/codex-rs/app-server/src/config_manager_service_tests.rs index 8deae8b0658c..722037647b0e 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -458,7 +458,7 @@ writable_roots = ["~/code"] } #[tokio::test] -async fn write_value_reports_override() { +async fn write_value_rejects_managed_path_even_when_value_matches() { let tmp = tempdir().expect("tempdir"); std::fs::write( tmp.path().join(CONFIG_TOML_FILE), @@ -477,7 +477,7 @@ async fn write_value_reports_override() { CloudConfigBundleLoader::default(), ); - let result = service + let error = service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "approval_policy".to_string(), @@ -486,7 +486,12 @@ async fn write_value_reports_override() { expected_version: None, }) .await - .expect("result"); + .expect_err("managed path should be read-only"); + + assert_eq!( + error.write_error_code(), + Some(ConfigWriteErrorCode::ConfigRequirementReadonly) + ); let read_after = service .read(ConfigReadParams { @@ -509,8 +514,10 @@ async fn write_value_reports_override() { file: managed_file.clone() } ); - assert_eq!(result.status, WriteStatus::Ok); - assert!(result.overridden_metadata.is_none()); + assert_eq!( + std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).unwrap(), + "approval_policy = \"on-request\"" + ); } #[tokio::test] @@ -636,7 +643,7 @@ async fn load_default_config_preserves_selected_user_config_path_after_load_erro } #[tokio::test] -async fn invalid_user_value_rejected_even_if_overridden_by_managed() { +async fn managed_write_is_rejected_before_value_validation() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"").unwrap(); @@ -663,7 +670,7 @@ async fn invalid_user_value_rejected_even_if_overridden_by_managed() { assert_eq!( error.write_error_code(), - Some(ConfigWriteErrorCode::ConfigValidationError) + Some(ConfigWriteErrorCode::ConfigRequirementReadonly) ); let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"); @@ -728,12 +735,10 @@ personality = true assert_eq!( error.write_error_code(), - Some(ConfigWriteErrorCode::ConfigValidationError) + Some(ConfigWriteErrorCode::ConfigRequirementReadonly) ); assert!( - error - .to_string() - .contains("invalid value for `features`: `features.personality=false`"), + error.to_string().contains("feature_requirements"), "{error}" ); assert_eq!( @@ -742,6 +747,75 @@ personality = true ); } +#[tokio::test] +async fn write_value_allows_unmanaged_feature_next_to_requirement() { + let tmp = tempdir().expect("tempdir"); + std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").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#" +[features] +personality = true +"#, + ), + ); + + service + .write_value(ConfigValueWriteParams { + file_path: None, + key_path: "features.mentions_v2".to_string(), + value: serde_json::json!(true), + merge_strategy: MergeStrategy::Replace, + expected_version: None, + }) + .await + .expect("unmanaged feature should remain writable"); + + assert!( + std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)) + .unwrap() + .contains("mentions_v2 = true") + ); +} + +#[tokio::test] +async fn write_value_rejects_managed_exact_requirement_path() { + let tmp = tempdir().expect("tempdir"); + std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").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#"chatgpt_base_url = "https://managed.example""#, + ), + ); + + let error = service + .write_value(ConfigValueWriteParams { + file_path: None, + key_path: "chatgpt_base_url".to_string(), + value: serde_json::json!("https://user.example"), + merge_strategy: MergeStrategy::Replace, + expected_version: None, + }) + .await + .expect_err("managed config path should be read-only"); + + assert_eq!( + error.write_error_code(), + Some(ConfigWriteErrorCode::ConfigRequirementReadonly) + ); + assert!(error.to_string().contains("chatgpt_base_url")); + assert_eq!( + std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).unwrap(), + "" + ); +} + #[tokio::test] async fn read_reports_managed_overrides_user_and_session_flags() { let tmp = tempdir().expect("tempdir"); @@ -805,42 +879,6 @@ async fn read_reports_managed_overrides_user_and_session_flags() { ); } -#[tokio::test] -async fn write_value_reports_managed_override() { - let tmp = tempdir().expect("tempdir"); - std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap(); - - let managed_path = tmp.path().join("managed_config.toml"); - std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap(); - let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file"); - - let service = ConfigManager::new_for_tests( - tmp.path().to_path_buf(), - vec![], - LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()), - CloudConfigBundleLoader::default(), - ); - - let result = service - .write_value(ConfigValueWriteParams { - file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), - key_path: "approval_policy".to_string(), - value: serde_json::json!("on-request"), - merge_strategy: MergeStrategy::Replace, - expected_version: None, - }) - .await - .expect("result"); - - assert_eq!(result.status, WriteStatus::OkOverridden); - let overridden = result.overridden_metadata.expect("overridden metadata"); - assert_eq!( - overridden.overriding_layer.name, - ConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file } - ); - assert_eq!(overridden.effective_value, serde_json::json!("never")); -} - #[tokio::test] async fn upsert_merges_tables_replace_overwrites() -> Result<()> { let tmp = tempdir().expect("tempdir"); @@ -927,3 +965,44 @@ beta = "b" Ok(()) } + +#[tokio::test] +async fn upsert_shell_environment_policy_uses_its_full_key_path() -> Result<()> { + let tmp = tempdir()?; + let path = tmp.path().join(CONFIG_TOML_FILE); + std::fs::write( + &path, + r#" +[shell_environment_policy] +exclude = ["FLIP_*", "KEEP_*"] +"#, + )?; + let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); + + service + .write_value(ConfigValueWriteParams { + file_path: Some(path.display().to_string()), + key_path: "shell_environment_policy".to_string(), + value: serde_json::json!({ + "filters": { + "FLIP_*": "include", + }, + }), + merge_strategy: MergeStrategy::Upsert, + expected_version: None, + }) + .await?; + + let config: TomlValue = toml::from_str(&std::fs::read_to_string(path)?)?; + assert_eq!( + config, + toml::from_str( + r#" +[shell_environment_policy.filters] +"flip_*" = "include" +"keep_*" = "exclude" +"#, + )? + ); + Ok(()) +} diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index b9a3ad2c9adc..20d736f5afd0 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -22,6 +22,7 @@ use codex_app_server_protocol::ConfiguredHookHandler; use codex_app_server_protocol::ConfiguredHookMatcherGroup; use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams; use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse; +use codex_app_server_protocol::FeedbackRequirements; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ManagedHooksRequirements; use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse; @@ -29,6 +30,7 @@ use codex_app_server_protocol::NetworkDomainPermission; use codex_app_server_protocol::NetworkRequirements; use codex_app_server_protocol::NetworkUnixSocketPermission; use codex_app_server_protocol::SandboxMode; +use codex_app_server_protocol::ShellEnvironmentPolicyRequirements; use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::ConfigRequirementsToml; use codex_config::HookEventsToml; @@ -37,6 +39,9 @@ use codex_config::ManagedHooksRequirementsToml; use codex_config::MatcherGroup as CoreMatcherGroup; use codex_config::ResidencyRequirement as CoreResidencyRequirement; use codex_config::SandboxModeRequirement as CoreSandboxModeRequirement; +use codex_config::ShellEnvironmentPolicyRequirementsToml; +use codex_config::types::OtelConfigToml; +use codex_config::types::OtelExporterKind; use codex_core::ThreadManager; use codex_features::canonical_feature_for_key; use codex_features::feature_for_key; @@ -325,6 +330,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 @@ -387,7 +397,59 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .enforce_residency .map(map_residency_requirement_to_api), network: requirements.network.map(map_network_requirements_to_api), + allowed_login_methods: requirements.allowed_login_methods, + allowed_chatgpt_workspaces: requirements.allowed_chatgpt_workspaces, + cli_auth_credentials_store: requirements.cli_auth_credentials_store, + chatgpt_base_url: requirements.chatgpt_base_url, + sqlite_home: requirements.sqlite_home, + log_dir: requirements.log_dir, + model_catalog_json: requirements.model_catalog_json, + check_for_update_on_startup: requirements.check_for_update_on_startup, + otel: requirements.otel.map(redact_otel_requirements), + allow_login_shell: requirements.allow_login_shell, + shell_environment_policy: requirements + .shell_environment_policy + .map(map_shell_environment_policy_requirements_to_api), + feedback: requirements.feedback.map(|feedback| FeedbackRequirements { + enabled: feedback.enabled, + }), + windows_sandbox_private_desktop, + } +} + +fn map_shell_environment_policy_requirements_to_api( + policy: ShellEnvironmentPolicyRequirementsToml, +) -> ShellEnvironmentPolicyRequirements { + let ShellEnvironmentPolicyRequirementsToml { + inherit, + ignore_default_excludes, + r#set, + filters, + } = policy; + ShellEnvironmentPolicyRequirements { + inherit, + ignore_default_excludes, + r#set, + filters, + } +} + +fn redact_otel_requirements(mut otel: OtelConfigToml) -> serde_json::Value { + for exporter in [ + &mut otel.exporter, + &mut otel.trace_exporter, + &mut otel.metrics_exporter, + ] { + let headers = match exporter { + Some(OtelExporterKind::OtlpHttp { headers, .. }) + | Some(OtelExporterKind::OtlpGrpc { headers, .. }) => headers, + Some(OtelExporterKind::None | OtelExporterKind::Statsig) | None => continue, + }; + headers + .values_mut() + .for_each(|value| *value = "".to_string()); } + json!(otel) } fn map_computer_use_requirements_to_api( @@ -577,12 +639,17 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> #[cfg(test)] mod tests { use super::map_requirements_toml_to_api; + use codex_app_server_protocol::ShellEnvironmentPolicyRequirements; use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::ComputerUseRequirementsToml; use codex_config::ConfigRequirementsToml; use codex_config::WindowsRequirementsToml; + use codex_protocol::config_types::ShellEnvironmentPolicyFilter; + use codex_protocol::config_types::ShellEnvironmentPolicyInherit; use pretty_assertions::assert_eq; + use serde_json::json; use std::collections::BTreeMap; + use std::collections::HashMap; #[test] fn requirements_api_includes_allow_managed_hooks_only() { @@ -665,7 +732,7 @@ mod tests { codex_config::types::WindowsSandboxModeToml::Elevated, codex_config::types::WindowsSandboxModeToml::Unelevated, ]), - sandbox_private_desktop: None, + sandbox_private_desktop: Some(false), }), ..ConfigRequirementsToml::default() }); @@ -677,5 +744,61 @@ mod tests { WindowsSandboxSetupMode::Unelevated, ]) ); + assert_eq!(mapped.windows_sandbox_private_desktop, Some(false)); + } + + #[test] + fn requirements_api_redacts_otel_headers_and_preserves_shell_environment_policy() { + let requirements = toml::from_str( + r#" +[otel] +log_user_prompt = false +environment = "production" +exporter = { otlp-http = { endpoint = "https://otel.example.com/v1/logs", headers = { Authorization = "Bearer secret" }, protocol = "json" } } + +[otel.span_attributes] +team = "codex" + +[shell_environment_policy] +inherit = "core" +ignore_default_excludes = false +[shell_environment_policy.filters] +"*SECRET*" = "exclude" +"HOME" = "include" +"PATH" = "exclude" + +[shell_environment_policy.set] +MANAGED = "true" +"#, + ) + .expect("valid requirements TOML"); + let mapped = map_requirements_toml_to_api(requirements); + + assert_eq!( + mapped + .otel + .as_ref() + .and_then(|otel| otel.pointer("/exporter/otlp-http/headers")), + Some(&json!({"Authorization": ""})) + ); + assert_eq!( + mapped.shell_environment_policy, + Some(ShellEnvironmentPolicyRequirements { + inherit: Some(ShellEnvironmentPolicyInherit::Core), + ignore_default_excludes: Some(false), + r#set: Some(HashMap::from([ + ("MANAGED".to_string(), "true".to_string(),) + ])), + filters: Some(BTreeMap::from([ + ( + "*SECRET*".to_string(), + ShellEnvironmentPolicyFilter::Exclude, + ), + ("HOME".to_string(), ShellEnvironmentPolicyFilter::Include,), + ("PATH".to_string(), ShellEnvironmentPolicyFilter::Exclude,), + ])), + }) + ); + assert!(!mapped.otel.unwrap().to_string().contains("Bearer secret")); } } diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index bfb7d817204f..e7a3ac5edb97 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -9,6 +9,7 @@ use codex_app_server_protocol::ApprovalsReviewer; use codex_app_server_protocol::AppsConfig; use codex_app_server_protocol::AppsDefaultConfig; use codex_app_server_protocol::AskForApproval; +use codex_app_server_protocol::AuthCredentialsStoreMode; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigEdit; use codex_app_server_protocol::ConfigLayerSource; @@ -75,6 +76,48 @@ async fn config_requirements_read_includes_allow_remote_control() -> Result<()> Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_write_reports_managed_requirement_as_read_only() -> Result<()> { + let codex_home = TempDir::new()?; + write_config(&codex_home, "")?; + std::fs::write( + codex_home.path().join("requirements.toml"), + r#"chatgpt_base_url = "https://managed.example""#, + )?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_config_value_write_request(ConfigValueWriteParams { + file_path: None, + key_path: "chatgpt_base_url".to_string(), + value: json!("https://user.example"), + merge_strategy: MergeStrategy::Replace, + expected_version: None, + }) + .await?; + let error: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!( + error + .error + .data + .as_ref() + .and_then(|data| data.get("config_write_error_code")) + .and_then(serde_json::Value::as_str), + Some("configRequirementReadonly") + ); + assert_eq!( + std::fs::read_to_string(codex_home.path().join("config.toml"))?, + "" + ); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn config_read_returns_effective_and_layers() -> Result<()> { let codex_home = TempDir::new()?; @@ -83,6 +126,7 @@ async fn config_read_returns_effective_and_layers() -> Result<()> { r#" model = "gpt-user" sandbox_mode = "workspace-write" +cli_auth_credentials_store = "keyring" "#, )?; let codex_home_path = codex_home.path().canonicalize()?; @@ -109,6 +153,10 @@ sandbox_mode = "workspace-write" } = to_response(resp)?; assert_eq!(config.model.as_deref(), Some("gpt-user")); + assert_eq!( + config.cli_auth_credentials_store, + Some(AuthCredentialsStoreMode::Keyring) + ); assert_eq!( origins.get("model").expect("origin").name, ConfigLayerSource::User { diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index c2ef5ad785b3..e499eab6c81c 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -119,6 +119,7 @@ pub use mcp_types::McpServerToolConfig; pub use mcp_types::McpServerTransportConfig; pub use mcp_types::RawMcpServerConfig; pub use merge::merge_toml_values; +pub use merge::merge_toml_values_at_path; pub use overrides::build_cli_overrides_layer; pub use plugin_edit::PluginConfigEdit; pub use plugin_edit::apply_user_plugin_config_edits; diff --git a/codex-rs/config/src/merge.rs b/codex-rs/config/src/merge.rs index 373f2c3b7c58..8effc6f722fe 100644 --- a/codex-rs/config/src/merge.rs +++ b/codex-rs/config/src/merge.rs @@ -5,10 +5,14 @@ use toml::Value as TomlValue; /// Merge config `overlay` into `base`, giving `overlay` precedence. pub fn merge_toml_values(base: &mut TomlValue, overlay: &TomlValue) { - merge_toml_values_at_path(base, overlay, &mut Vec::new()); + merge_toml_values_at_path(base, overlay, &[]); } -fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &mut Vec) { +pub fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &[String]) { + merge_toml_values_recursive(base, overlay, &mut path.to_vec()); +} + +fn merge_toml_values_recursive(base: &mut TomlValue, overlay: &TomlValue, path: &mut Vec) { prepare_shell_environment_policy_merge(base, overlay, path); if let TomlValue::Table(overlay_table) = overlay @@ -31,7 +35,7 @@ fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &m for (key, value) in overlay_table { path.push(key.clone()); if let Some(existing) = base_table.get_mut(&key) { - merge_toml_values_at_path(existing, &value, path); + merge_toml_values_recursive(existing, &value, path); } else { base_table.insert(key, normalized_for_merge(&value, path)); } diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index c45bb3f3aa63..c820502311c5 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -201,8 +201,9 @@ impl JsonSchema for ApprovalsReviewer { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)] #[serde(rename_all = "kebab-case")] +#[ts(export_to = "v2/")] pub enum ShellEnvironmentPolicyInherit { /// "Core" environment variables for the platform. On UNIX, this would /// include HOME, LOGNAME, PATH, SHELL, and USER, among others.