From 5f5b4fabbdadd449570de37ff945d0ac6e4baaf1 Mon Sep 17 00:00:00 2001 From: Leo Shimonaka Date: Thu, 30 Apr 2026 12:37:52 -0700 Subject: [PATCH] SAI-13224 add Computer Use requirements --- .../codex_app_server_protocol.schemas.json | 109 +++++++++ .../codex_app_server_protocol.v2.schemas.json | 109 +++++++++ .../schema/json/v2/ConfigReadResponse.json | 55 +++++ .../v2/ConfigRequirementsReadResponse.json | 54 +++++ .../schema/typescript/v2/ComputerUseConfig.ts | 6 + .../typescript/v2/ComputerUseMacosConfig.ts | 5 + .../v2/ComputerUseMacosRequirements.ts | 5 + .../typescript/v2/ComputerUseRequirements.ts | 6 + .../schema/typescript/v2/Config.ts | 3 +- .../typescript/v2/ConfigRequirements.ts | 3 +- .../schema/typescript/v2/index.ts | 4 + .../app-server-protocol/src/protocol/v2.rs | 41 ++++ codex-rs/app-server/README.md | 2 +- .../src/config_manager_service_tests.rs | 40 +++ .../request_processors/config_processor.rs | 151 ++++++++++++ codex-rs/cloud-requirements/src/lib.rs | 16 ++ codex-rs/config/src/config_requirements.rs | 228 +++++++++++++++++- codex-rs/config/src/config_toml.rs | 5 + codex-rs/config/src/lib.rs | 2 + codex-rs/config/src/types.rs | 26 ++ codex-rs/core/config.schema.json | 51 +++- .../core/src/config/config_loader_tests.rs | 3 + codex-rs/core/src/config/config_tests.rs | 2 + codex-rs/tui/src/debug_config.rs | 2 + 24 files changed, 923 insertions(+), 5 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseConfig.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseMacosConfig.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseMacosRequirements.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseRequirements.ts 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 6c2ad69e1b..2e31399d23 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 @@ -6971,6 +6971,94 @@ ], "type": "object" }, + "ComputerUseConfig": { + "properties": { + "allow_persistent_approval": { + "type": [ + "boolean", + "null" + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ComputerUseMacosConfig" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ComputerUseMacosConfig": { + "properties": { + "allowed_bundle_ids": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "denied_bundle_ids": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "ComputerUseMacosRequirements": { + "properties": { + "allowedBundleIds": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "deniedBundleIds": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "ComputerUseRequirements": { + "properties": { + "allowPersistentApproval": { + "type": [ + "boolean", + "null" + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ComputerUseMacosRequirements" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "Config": { "additionalProperties": true, "properties": { @@ -7011,6 +7099,17 @@ "null" ] }, + "computer_use": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ComputerUseConfig" + }, + { + "type": "null" + } + ], + "default": null + }, "developer_instructions": { "type": [ "string", @@ -7487,6 +7586,16 @@ "null" ] }, + "computerUse": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ComputerUseRequirements" + }, + { + "type": "null" + } + ] + }, "enforceResidency": { "anyOf": [ { 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 150b221adf..9a38f86b88 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 @@ -3469,6 +3469,94 @@ ], "type": "object" }, + "ComputerUseConfig": { + "properties": { + "allow_persistent_approval": { + "type": [ + "boolean", + "null" + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseMacosConfig" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ComputerUseMacosConfig": { + "properties": { + "allowed_bundle_ids": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "denied_bundle_ids": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "ComputerUseMacosRequirements": { + "properties": { + "allowedBundleIds": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "deniedBundleIds": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "ComputerUseRequirements": { + "properties": { + "allowPersistentApproval": { + "type": [ + "boolean", + "null" + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseMacosRequirements" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "Config": { "additionalProperties": true, "properties": { @@ -3509,6 +3597,17 @@ "null" ] }, + "computer_use": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseConfig" + }, + { + "type": "null" + } + ], + "default": null + }, "developer_instructions": { "type": [ "string", @@ -3985,6 +4084,16 @@ "null" ] }, + "computerUse": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseRequirements" + }, + { + "type": "null" + } + ] + }, "enforceResidency": { "anyOf": [ { 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 c348e7d955..13951d04b0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -188,6 +188,50 @@ } ] }, + "ComputerUseConfig": { + "properties": { + "allow_persistent_approval": { + "type": [ + "boolean", + "null" + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseMacosConfig" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ComputerUseMacosConfig": { + "properties": { + "allowed_bundle_ids": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "denied_bundle_ids": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, "Config": { "additionalProperties": true, "properties": { @@ -228,6 +272,17 @@ "null" ] }, + "computer_use": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseConfig" + }, + { + "type": "null" + } + ], + "default": null + }, "developer_instructions": { "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 545d8dc9b4..0ba6f98933 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -60,6 +60,50 @@ } ] }, + "ComputerUseMacosRequirements": { + "properties": { + "allowedBundleIds": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "deniedBundleIds": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "ComputerUseRequirements": { + "properties": { + "allowPersistentApproval": { + "type": [ + "boolean", + "null" + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseMacosRequirements" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "ConfigRequirements": { "properties": { "allowedApprovalPolicies": { @@ -89,6 +133,16 @@ "null" ] }, + "computerUse": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseRequirements" + }, + { + "type": "null" + } + ] + }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseConfig.ts new file mode 100644 index 0000000000..596413e223 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseConfig.ts @@ -0,0 +1,6 @@ +// 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 { ComputerUseMacosConfig } from "./ComputerUseMacosConfig"; + +export type ComputerUseConfig = { allow_persistent_approval: boolean | null, macos: ComputerUseMacosConfig | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseMacosConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseMacosConfig.ts new file mode 100644 index 0000000000..f4b460bdf4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseMacosConfig.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 ComputerUseMacosConfig = { denied_bundle_ids: Array | null, allowed_bundle_ids: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseMacosRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseMacosRequirements.ts new file mode 100644 index 0000000000..0c761ec983 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseMacosRequirements.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 ComputerUseMacosRequirements = { deniedBundleIds: Array | null, allowedBundleIds: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseRequirements.ts new file mode 100644 index 0000000000..c55f527b34 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseRequirements.ts @@ -0,0 +1,6 @@ +// 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 { ComputerUseMacosRequirements } from "./ComputerUseMacosRequirements"; + +export type ComputerUseRequirements = { allowPersistentApproval: boolean | null, macos: ComputerUseMacosRequirements | null, }; 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 508fe84e92..65e70676d3 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 { ComputerUseConfig } from "./ComputerUseConfig"; import type { ProfileV2 } from "./ProfileV2"; 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: string | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, profile: string | null, profiles: { [key in string]?: ProfileV2 }, 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: ServiceTier | null, analytics: AnalyticsConfig | 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: string | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, profile: string | null, profiles: { [key in string]?: ProfileV2 }, 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: ServiceTier | null, analytics: AnalyticsConfig | null, computer_use: ComputerUseConfig | 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 47a99453fe..b0a3ede40a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -3,7 +3,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { WebSearchMode } from "../WebSearchMode"; import type { AskForApproval } from "./AskForApproval"; +import type { ComputerUseRequirements } from "./ComputerUseRequirements"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, computerUse: ComputerUseRequirements | 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 d369ba3423..50cb391b36 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -61,6 +61,10 @@ export type { CommandExecutionRequestApprovalResponse } from "./CommandExecution export type { CommandExecutionSource } from "./CommandExecutionSource"; export type { CommandExecutionStatus } from "./CommandExecutionStatus"; export type { CommandMigration } from "./CommandMigration"; +export type { ComputerUseConfig } from "./ComputerUseConfig"; +export type { ComputerUseMacosConfig } from "./ComputerUseMacosConfig"; +export type { ComputerUseMacosRequirements } from "./ComputerUseMacosRequirements"; +export type { ComputerUseRequirements } from "./ComputerUseRequirements"; export type { Config } from "./Config"; export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams"; export type { ConfigEdit } from "./ConfigEdit"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 4eb33fa850..091b846b3f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -851,10 +851,29 @@ pub struct Config { #[experimental("config/read.apps")] #[serde(default)] pub apps: Option, + #[experimental("config/read.computer_use")] + #[serde(default)] + pub computer_use: Option, #[serde(default, flatten)] pub additional: HashMap, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct ComputerUseMacosConfig { + pub denied_bundle_ids: Option>, + pub allowed_bundle_ids: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct ComputerUseConfig { + pub allow_persistent_approval: Option, + pub macos: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -946,6 +965,22 @@ pub struct ConfigReadResponse { pub layers: Option>, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ComputerUseMacosRequirements { + pub denied_bundle_ids: Option>, + pub allowed_bundle_ids: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ComputerUseRequirements { + pub allow_persistent_approval: Option, + pub macos: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -962,6 +997,7 @@ pub struct ConfigRequirements { pub enforce_residency: Option, #[experimental("configRequirements/read.network")] pub network: Option, + pub computer_use: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] @@ -9561,6 +9597,7 @@ mod tests { service_tier: None, analytics: None, apps: None, + computer_use: None, additional: HashMap::new(), }); @@ -9594,6 +9631,7 @@ mod tests { service_tier: None, analytics: None, apps: None, + computer_use: None, additional: HashMap::new(), }); @@ -9649,6 +9687,7 @@ mod tests { service_tier: None, analytics: None, apps: None, + computer_use: None, additional: HashMap::new(), }); @@ -9698,6 +9737,7 @@ mod tests { service_tier: None, analytics: None, apps: None, + computer_use: None, additional: HashMap::new(), }); @@ -9722,6 +9762,7 @@ mod tests { hooks: None, enforce_residency: None, network: None, + computer_use: None, }); assert_eq!(reason, Some("askForApproval.granular")); diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index dab47ec3a2..9ed4773d2a 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -226,7 +226,7 @@ Example with notification opt-out: - `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish). - `config/value/write` — write a single config key/value to the user's config.toml on disk. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads. -- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), 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`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, Computer Use policy (`computerUse`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread 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 e9b0b3c769..df3747c72f 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -4,6 +4,8 @@ use codex_app_server_protocol::AppConfig; use codex_app_server_protocol::AppToolApproval; use codex_app_server_protocol::AppsConfig; use codex_app_server_protocol::AskForApproval; +use codex_app_server_protocol::ComputerUseConfig; +use codex_app_server_protocol::ComputerUseMacosConfig; use codex_config::CloudRequirementsLoader; use codex_config::FeatureRequirementsToml; use codex_config::LoaderOverrides; @@ -191,6 +193,44 @@ async fn write_value_supports_nested_app_paths() -> Result<()> { Ok(()) } +#[tokio::test] +async fn read_returns_computer_use_config() -> Result<()> { + let tmp = tempdir().expect("tempdir"); + std::fs::write( + tmp.path().join(CONFIG_TOML_FILE), + r#" +[computer_use] +allow_persistent_approval = false + +[computer_use.macos] +denied_bundle_ids = ["com.apple.Terminal"] +allowed_bundle_ids = ["com.apple.Safari"] +"#, + )?; + + let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); + let read = service + .read(ConfigReadParams { + include_layers: false, + cwd: None, + }) + .await + .expect("config read succeeds"); + + assert_eq!( + read.config.computer_use, + Some(ComputerUseConfig { + allow_persistent_approval: Some(false), + macos: Some(ComputerUseMacosConfig { + denied_bundle_ids: Some(vec!["com.apple.Terminal".to_string()]), + allowed_bundle_ids: Some(vec!["com.apple.Safari".to_string()]), + }), + }) + ); + + Ok(()) +} + #[tokio::test] async fn write_value_supports_custom_mcp_server_default_tool_approval_mode() -> Result<()> { let tmp = tempdir().expect("tempdir"); 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 8043675e0d..8fe828f188 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -11,6 +11,11 @@ use crate::transport::RemoteControlHandle; use codex_analytics::AnalyticsEventsClient; use codex_app_server_protocol::AppListUpdatedNotification; use codex_app_server_protocol::ClientResponsePayload; +use codex_app_server_protocol::ComputerUseConfig; +use codex_app_server_protocol::ComputerUseMacosConfig; +use codex_app_server_protocol::ComputerUseMacosRequirements; +use codex_app_server_protocol::ComputerUseRequirements; +use codex_app_server_protocol::Config as ApiConfig; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigReadParams; use codex_app_server_protocol::ConfigReadResponse; @@ -97,6 +102,10 @@ impl ConfigRequestProcessor { let fallback_cwd = params.cwd.as_ref().map(PathBuf::from); let mut response = self.config_manager.read(params).await.map_err(map_error)?; let config = self.load_latest_config(fallback_cwd).await?; + apply_computer_use_requirements( + &mut response.config, + config.config_layer_stack.requirements_toml(), + ); for feature_key in SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT { let Some(feature) = feature_for_key(feature_key) else { continue; @@ -412,6 +421,48 @@ impl ConfigRequestProcessor { } } +fn apply_computer_use_requirements(config: &mut ApiConfig, requirements: &ConfigRequirementsToml) { + let Some(requirements) = requirements.computer_use.as_ref() else { + return; + }; + + let computer_use = config.computer_use.get_or_insert(ComputerUseConfig { + allow_persistent_approval: None, + macos: None, + }); + if requirements.allow_persistent_approval == Some(false) + || computer_use.allow_persistent_approval.is_none() + { + computer_use.allow_persistent_approval = requirements.allow_persistent_approval; + } + + let Some(required_macos) = requirements.macos.as_ref() else { + return; + }; + + let macos = computer_use.macos.get_or_insert(ComputerUseMacosConfig { + denied_bundle_ids: None, + allowed_bundle_ids: None, + }); + if let Some(denied_bundle_ids) = required_macos.denied_bundle_ids.as_ref() + && !denied_bundle_ids.is_empty() + { + let config_denied_bundle_ids = macos.denied_bundle_ids.get_or_insert_with(Vec::new); + for bundle_id in denied_bundle_ids { + if !config_denied_bundle_ids.contains(bundle_id) { + config_denied_bundle_ids.push(bundle_id.clone()); + } + } + } + if let Some(allowed_bundle_ids) = required_macos.allowed_bundle_ids.as_ref() { + if let Some(config_allowed_bundle_ids) = macos.allowed_bundle_ids.as_mut() { + config_allowed_bundle_ids.retain(|bundle_id| allowed_bundle_ids.contains(bundle_id)); + } else { + macos.allowed_bundle_ids = Some(allowed_bundle_ids.clone()); + } + } +} + fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigRequirements { ConfigRequirements { allowed_approval_policies: requirements.allowed_approval_policies.map(|policies| { @@ -450,6 +501,17 @@ 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), + computer_use: requirements + .computer_use + .map(|requirements| ComputerUseRequirements { + allow_persistent_approval: requirements.allow_persistent_approval, + macos: requirements + .macos + .map(|requirements| ComputerUseMacosRequirements { + denied_bundle_ids: requirements.denied_bundle_ids, + allowed_bundle_ids: requirements.allowed_bundle_ids, + }), + }), } } @@ -620,3 +682,92 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> })), } } + +#[cfg(test)] +mod tests { + use super::*; + use codex_config::ComputerUseMacosRequirementsToml as CoreComputerUseMacosRequirementsToml; + use codex_config::ComputerUseRequirementsToml as CoreComputerUseRequirementsToml; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[test] + fn map_requirements_toml_to_api_converts_computer_use() { + let requirements = ConfigRequirementsToml { + computer_use: Some(CoreComputerUseRequirementsToml { + allow_persistent_approval: Some(false), + macos: Some(CoreComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec!["com.apple.Terminal".to_string()]), + allowed_bundle_ids: Some(vec!["com.apple.Safari".to_string()]), + }), + }), + ..Default::default() + }; + + let mapped = map_requirements_toml_to_api(requirements); + + assert_eq!( + mapped.computer_use, + Some(ComputerUseRequirements { + allow_persistent_approval: Some(false), + macos: Some(ComputerUseMacosRequirements { + denied_bundle_ids: Some(vec!["com.apple.Terminal".to_string()]), + allowed_bundle_ids: Some(vec!["com.apple.Safari".to_string()]), + }), + }), + ); + } + + #[test] + fn apply_computer_use_requirements_to_config_is_restrictive() { + let mut config: ApiConfig = serde_json::from_value(json!({ + "computer_use": { + "allow_persistent_approval": true, + "macos": { + "denied_bundle_ids": [ + "com.example.ConfigDenied", + "com.example.SharedDenied" + ], + "allowed_bundle_ids": [ + "com.example.SharedAllowed", + "com.example.ConfigAllowed" + ] + } + } + })) + .expect("valid config"); + let requirements = ConfigRequirementsToml { + computer_use: Some(CoreComputerUseRequirementsToml { + allow_persistent_approval: Some(false), + macos: Some(CoreComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec![ + "com.example.RequiredDenied".to_string(), + "com.example.SharedDenied".to_string(), + ]), + allowed_bundle_ids: Some(vec![ + "com.example.SharedAllowed".to_string(), + "com.example.RequiredAllowed".to_string(), + ]), + }), + }), + ..Default::default() + }; + + apply_computer_use_requirements(&mut config, &requirements); + + assert_eq!( + config.computer_use, + Some(ComputerUseConfig { + allow_persistent_approval: Some(false), + macos: Some(ComputerUseMacosConfig { + denied_bundle_ids: Some(vec![ + "com.example.ConfigDenied".to_string(), + "com.example.SharedDenied".to_string(), + "com.example.RequiredDenied".to_string(), + ]), + allowed_bundle_ids: Some(vec!["com.example.SharedAllowed".to_string()]), + }), + }), + ); + } +} diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 6f283b43d0..cb4efcea45 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -1209,6 +1209,7 @@ mod tests { mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1291,6 +1292,7 @@ mod tests { mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1324,6 +1326,7 @@ mod tests { mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1374,6 +1377,7 @@ mod tests { mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1490,6 +1494,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1570,6 +1575,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1648,6 +1654,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1854,6 +1861,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1894,6 +1902,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1954,6 +1963,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -2010,6 +2020,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -2068,6 +2079,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -2127,6 +2139,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -2186,6 +2199,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -2275,6 +2289,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -2306,6 +2321,7 @@ command = "sample-mcp" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 59d1cde9ea..5f1305fb00 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -614,6 +614,77 @@ impl AppsRequirementsToml { } } +#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct ComputerUseMacosRequirementsToml { + pub denied_bundle_ids: Option>, + pub allowed_bundle_ids: Option>, +} + +impl ComputerUseMacosRequirementsToml { + pub fn is_empty(&self) -> bool { + self.denied_bundle_ids.as_ref().is_none_or(Vec::is_empty) + && self.allowed_bundle_ids.is_none() + } +} + +#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct ComputerUseRequirementsToml { + pub allow_persistent_approval: Option, + pub macos: Option, +} + +impl ComputerUseRequirementsToml { + pub fn is_empty(&self) -> bool { + self.allow_persistent_approval.is_none() + && self + .macos + .as_ref() + .is_none_or(ComputerUseMacosRequirementsToml::is_empty) + } +} + +pub(crate) fn merge_computer_use_requirements_descending( + base: &mut ComputerUseRequirementsToml, + incoming: ComputerUseRequirementsToml, +) { + if base.allow_persistent_approval == Some(false) + || incoming.allow_persistent_approval == Some(false) + { + base.allow_persistent_approval = Some(false); + } else { + base.allow_persistent_approval = base + .allow_persistent_approval + .or(incoming.allow_persistent_approval); + } + + let Some(incoming_macos) = incoming.macos else { + return; + }; + + let base_macos = base.macos.get_or_insert_with(Default::default); + if let Some(incoming_denied_bundle_ids) = incoming_macos.denied_bundle_ids + && !incoming_denied_bundle_ids.is_empty() + { + let base_denied_bundle_ids = base_macos + .denied_bundle_ids + .get_or_insert_with(Default::default); + for bundle_id in incoming_denied_bundle_ids { + if !base_denied_bundle_ids.contains(&bundle_id) { + base_denied_bundle_ids.push(bundle_id); + } + } + } + + if let Some(incoming_allowed_bundle_ids) = incoming_macos.allowed_bundle_ids { + if let Some(base_allowed_bundle_ids) = base_macos.allowed_bundle_ids.as_mut() { + base_allowed_bundle_ids + .retain(|bundle_id| incoming_allowed_bundle_ids.contains(bundle_id)); + } else { + base_macos.allowed_bundle_ids = Some(incoming_allowed_bundle_ids); + } + } +} + /// Merge `enabled` configs from a lower-precedence source into an existing higher-precedence set. /// This lets managed sources (for example Cloud/MDM) enforce setting disablement across layers. /// Implemented with AppsRequirementsToml for now, could be abstracted if we have more enablement-style configs in the future. @@ -648,6 +719,7 @@ pub struct ConfigRequirementsToml { pub mcp_servers: Option>, pub plugins: Option>, pub apps: Option, + pub computer_use: Option, pub rules: Option, pub enforce_residency: Option, #[serde(rename = "experimental_network")] @@ -695,6 +767,7 @@ pub struct ConfigRequirementsWithSources { pub mcp_servers: Option>>, pub plugins: Option>>, pub apps: Option>, + pub computer_use: Option>, pub rules: Option>, pub enforce_residency: Option>, pub network: Option>, @@ -731,6 +804,7 @@ impl ConfigRequirementsWithSources { mcp_servers: _, plugins: _, apps: _, + computer_use: _, rules: _, enforce_residency: _, network: _, @@ -771,7 +845,18 @@ impl ConfigRequirementsWithSources { if let Some(existing_apps) = self.apps.as_mut() { merge_enablement_settings_descending(&mut existing_apps.value, incoming_apps); } else { - self.apps = Some(Sourced::new(incoming_apps, source)); + self.apps = Some(Sourced::new(incoming_apps, source.clone())); + } + } + + if let Some(incoming_computer_use) = other.computer_use.take() { + if let Some(existing_computer_use) = self.computer_use.as_mut() { + merge_computer_use_requirements_descending( + &mut existing_computer_use.value, + incoming_computer_use, + ); + } else { + self.computer_use = Some(Sourced::new(incoming_computer_use, source)); } } } @@ -787,6 +872,7 @@ impl ConfigRequirementsWithSources { mcp_servers, plugins, apps, + computer_use, rules, enforce_residency, network, @@ -804,6 +890,7 @@ impl ConfigRequirementsWithSources { mcp_servers: mcp_servers.map(|sourced| sourced.value), plugins: plugins.map(|sourced| sourced.value), apps: apps.map(|sourced| sourced.value), + computer_use: computer_use.map(|sourced| sourced.value), rules: rules.map(|sourced| sourced.value), enforce_residency: enforce_residency.map(|sourced| sourced.value), network: network.map(|sourced| sourced.value), @@ -899,6 +986,10 @@ impl ConfigRequirementsToml { .apps .as_ref() .is_none_or(AppsRequirementsToml::is_empty) + && self + .computer_use + .as_ref() + .is_none_or(ComputerUseRequirementsToml::is_empty) && self.rules.is_none() && self.enforce_residency.is_none() && self.network.is_none() @@ -924,6 +1015,7 @@ impl TryFrom for ConfigRequirements { mcp_servers, plugins, apps: _apps, + computer_use: _computer_use, rules, enforce_residency, network, @@ -1231,6 +1323,7 @@ mod tests { mcp_servers, plugins, apps, + computer_use, rules, enforce_residency, network, @@ -1252,6 +1345,7 @@ mod tests { mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)), plugins: plugins.map(|value| Sourced::new(value, RequirementSource::Unknown)), apps: apps.map(|value| Sourced::new(value, RequirementSource::Unknown)), + computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)), rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)), enforce_residency: enforce_residency .map(|value| Sourced::new(value, RequirementSource::Unknown)), @@ -1298,6 +1392,7 @@ mod tests { mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: Some(enforce_residency), network: None, @@ -1331,6 +1426,7 @@ mod tests { mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)), network: None, @@ -1370,6 +1466,7 @@ mod tests { mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1417,6 +1514,7 @@ mod tests { mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1590,6 +1688,32 @@ allowed_approvals_reviewers = ["user"] Ok(()) } + #[test] + fn deserialize_computer_use_requirements() -> Result<()> { + let toml_str = r#" + [computer_use] + allow_persistent_approval = false + + [computer_use.macos] + denied_bundle_ids = ["com.apple.Terminal"] + allowed_bundle_ids = ["com.apple.Safari"] + "#; + let requirements: ConfigRequirementsToml = from_str(toml_str)?; + + assert_eq!( + requirements.computer_use, + Some(ComputerUseRequirementsToml { + allow_persistent_approval: Some(false), + macos: Some(ComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec!["com.apple.Terminal".to_string()]), + allowed_bundle_ids: Some(vec!["com.apple.Safari".to_string()]), + }), + }) + ); + assert!(!requirements.is_empty()); + Ok(()) + } + fn apps_requirements(entries: &[(&str, Option)]) -> AppsRequirementsToml { AppsRequirementsToml { apps: entries @@ -1736,6 +1860,108 @@ allowed_approvals_reviewers = ["user"] ); } + #[test] + fn merge_computer_use_requirements_descending_is_restrictive() { + let mut merged = ComputerUseRequirementsToml { + allow_persistent_approval: Some(true), + macos: Some(ComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec![ + "com.example.High".to_string(), + "com.example.Shared".to_string(), + ]), + allowed_bundle_ids: Some(vec![ + "com.example.Shared".to_string(), + "com.example.HighOnly".to_string(), + ]), + }), + }; + let lower = ComputerUseRequirementsToml { + allow_persistent_approval: Some(false), + macos: Some(ComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec![ + "com.example.Low".to_string(), + "com.example.Shared".to_string(), + ]), + allowed_bundle_ids: Some(vec![ + "com.example.Shared".to_string(), + "com.example.LowOnly".to_string(), + ]), + }), + }; + + merge_computer_use_requirements_descending(&mut merged, lower); + + assert_eq!( + merged, + ComputerUseRequirementsToml { + allow_persistent_approval: Some(false), + macos: Some(ComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec![ + "com.example.High".to_string(), + "com.example.Shared".to_string(), + "com.example.Low".to_string(), + ]), + allowed_bundle_ids: Some(vec!["com.example.Shared".to_string()]), + }), + } + ); + } + + #[test] + fn merge_unset_fields_merges_computer_use_across_sources() { + let higher_source = RequirementSource::CloudRequirements; + let mut target = ConfigRequirementsWithSources::default(); + + target.merge_unset_fields( + higher_source.clone(), + ConfigRequirementsToml { + computer_use: Some(ComputerUseRequirementsToml { + allow_persistent_approval: Some(true), + macos: Some(ComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec!["com.example.High".to_string()]), + allowed_bundle_ids: Some(vec![ + "com.example.Shared".to_string(), + "com.example.HighOnly".to_string(), + ]), + }), + }), + ..Default::default() + }, + ); + target.merge_unset_fields( + RequirementSource::LegacyManagedConfigTomlFromMdm, + ConfigRequirementsToml { + computer_use: Some(ComputerUseRequirementsToml { + allow_persistent_approval: Some(false), + macos: Some(ComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec!["com.example.Low".to_string()]), + allowed_bundle_ids: Some(vec![ + "com.example.Shared".to_string(), + "com.example.LowOnly".to_string(), + ]), + }), + }), + ..Default::default() + }, + ); + + let computer_use = target.computer_use.expect("computer_use should be present"); + assert_eq!( + computer_use.value, + ComputerUseRequirementsToml { + allow_persistent_approval: Some(false), + macos: Some(ComputerUseMacosRequirementsToml { + denied_bundle_ids: Some(vec![ + "com.example.High".to_string(), + "com.example.Low".to_string(), + ]), + allowed_bundle_ids: Some(vec!["com.example.Shared".to_string()]), + }), + } + ); + assert_eq!(computer_use.source, higher_source); + } + #[test] fn constraint_error_includes_requirement_source() -> Result<()> { let source: ConfigRequirementsToml = from_str( diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 6406f9e1ff..50e26e3297 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -11,6 +11,7 @@ use crate::types::AnalyticsConfigToml; use crate::types::ApprovalsReviewer; use crate::types::AppsConfigToml; use crate::types::AuthCredentialsStoreMode; +use crate::types::ComputerUseConfigToml; use crate::types::FeedbackConfigToml; use crate::types::History; use crate::types::MarketplaceConfig; @@ -424,6 +425,10 @@ pub struct ConfigToml { #[serde(default)] pub apps: Option, + /// Settings for Computer Use controls. + #[serde(default)] + pub computer_use: Option, + /// OTEL configuration. pub otel: Option, diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index e88c736db0..08b793d891 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -34,6 +34,8 @@ pub use codex_app_server_protocol::ConfigLayerSource; pub use codex_utils_absolute_path::AbsolutePathBuf; pub use config_requirements::AppRequirementToml; pub use config_requirements::AppsRequirementsToml; +pub use config_requirements::ComputerUseMacosRequirementsToml; +pub use config_requirements::ComputerUseRequirementsToml; pub use config_requirements::ConfigRequirements; pub use config_requirements::ConfigRequirementsToml; pub use config_requirements::ConfigRequirementsWithSources; diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index b856367a66..0cae27ee70 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -428,6 +428,32 @@ pub struct AppsConfigToml { pub apps: HashMap, } +/// macOS-specific Computer Use controls loaded from `config.toml`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct ComputerUseMacosConfigToml { + /// Bundle identifiers that Computer Use is not allowed to access. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub denied_bundle_ids: Option>, + + /// Bundle identifiers that Computer Use is allowed to access. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub allowed_bundle_ids: Option>, +} + +/// Computer Use controls loaded from `config.toml`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct ComputerUseConfigToml { + /// Whether approval requests can offer persistent approval. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub allow_persistent_approval: Option, + + /// macOS-specific Computer Use controls. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub macos: Option, +} + // ===== OTEL configuration ===== #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index a75e287134..f37e5b910a 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -328,6 +328,46 @@ }, "type": "object" }, + "ComputerUseConfigToml": { + "additionalProperties": false, + "description": "Computer Use controls loaded from `config.toml`.", + "properties": { + "allow_persistent_approval": { + "description": "Whether approval requests can offer persistent approval.", + "type": "boolean" + }, + "macos": { + "allOf": [ + { + "$ref": "#/definitions/ComputerUseMacosConfigToml" + } + ], + "description": "macOS-specific Computer Use controls." + } + }, + "type": "object" + }, + "ComputerUseMacosConfigToml": { + "additionalProperties": false, + "description": "macOS-specific Computer Use controls loaded from `config.toml`.", + "properties": { + "allowed_bundle_ids": { + "description": "Bundle identifiers that Computer Use is allowed to access.", + "items": { + "type": "string" + }, + "type": "array" + }, + "denied_bundle_ids": { + "description": "Bundle identifiers that Computer Use is not allowed to access.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, "ConfigProfile": { "additionalProperties": false, "description": "Collection of common configuration options that a user can define as a unit in `config.toml`.", @@ -3756,6 +3796,15 @@ "description": "Compact prompt used for history compaction.", "type": "string" }, + "computer_use": { + "allOf": [ + { + "$ref": "#/definitions/ComputerUseConfigToml" + } + ], + "default": null, + "description": "Settings for Computer Use controls." + }, "debug": { "allOf": [ { @@ -4503,4 +4552,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 6fcd5f872d..d7d1ce14c6 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -782,6 +782,7 @@ allowed_approval_policies = ["on-request"] mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -839,6 +840,7 @@ allowed_approval_policies = ["on-request"] mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -1047,6 +1049,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index b48cf020f2..e5c8753442 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -6984,6 +6984,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, @@ -7696,6 +7697,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None, diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 1d4bf2b858..95d65393b6 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -699,6 +699,7 @@ mod tests { )])), plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: Some(ResidencyRequirement::Us), network: None, @@ -899,6 +900,7 @@ approval_policy = "never" mcp_servers: None, plugins: None, apps: None, + computer_use: None, rules: None, enforce_residency: None, network: None,