SAI-13224 add Computer Use requirements

This commit is contained in:
Leo Shimonaka
2026-05-04 10:51:44 -07:00
parent 4fd7dfe223
commit 5f5b4fabbd
24 changed files with 923 additions and 5 deletions
@@ -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": [
{
@@ -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": [
{
@@ -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",
@@ -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": [
{
@@ -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, };
@@ -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<string> | null, allowed_bundle_ids: Array<string> | null, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ComputerUseMacosRequirements = { deniedBundleIds: Array<string> | null, allowedBundleIds: Array<string> | null, };
@@ -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, };
+2 -1
View File
@@ -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<JsonValue> | { [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<JsonValue> | { [key in string]?: JsonValue } | null });
@@ -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<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, computerUse: ComputerUseRequirements | null};
@@ -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";
@@ -851,10 +851,29 @@ pub struct Config {
#[experimental("config/read.apps")]
#[serde(default)]
pub apps: Option<AppsConfig>,
#[experimental("config/read.computer_use")]
#[serde(default)]
pub computer_use: Option<ComputerUseConfig>,
#[serde(default, flatten)]
pub additional: HashMap<String, JsonValue>,
}
#[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<Vec<String>>,
pub allowed_bundle_ids: Option<Vec<String>>,
}
#[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<bool>,
pub macos: Option<ComputerUseMacosConfig>,
}
#[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<Vec<ConfigLayer>>,
}
#[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<Vec<String>>,
pub allowed_bundle_ids: Option<Vec<String>>,
}
#[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<bool>,
pub macos: Option<ComputerUseMacosRequirements>,
}
#[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<ResidencyRequirement>,
#[experimental("configRequirements/read.network")]
pub network: Option<NetworkRequirements>,
pub computer_use: Option<ComputerUseRequirements>,
}
#[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"));
+1 -1
View File
@@ -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
@@ -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");
@@ -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<String>) ->
})),
}
}
#[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()]),
}),
}),
);
}
}
+16
View File
@@ -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,
+227 -1
View File
@@ -614,6 +614,77 @@ impl AppsRequirementsToml {
}
}
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct ComputerUseMacosRequirementsToml {
pub denied_bundle_ids: Option<Vec<String>>,
pub allowed_bundle_ids: Option<Vec<String>>,
}
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<bool>,
pub macos: Option<ComputerUseMacosRequirementsToml>,
}
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<BTreeMap<String, McpServerRequirement>>,
pub plugins: Option<BTreeMap<String, PluginRequirementsToml>>,
pub apps: Option<AppsRequirementsToml>,
pub computer_use: Option<ComputerUseRequirementsToml>,
pub rules: Option<RequirementsExecPolicyToml>,
pub enforce_residency: Option<ResidencyRequirement>,
#[serde(rename = "experimental_network")]
@@ -695,6 +767,7 @@ pub struct ConfigRequirementsWithSources {
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub plugins: Option<Sourced<BTreeMap<String, PluginRequirementsToml>>>,
pub apps: Option<Sourced<AppsRequirementsToml>>,
pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
pub network: Option<Sourced<NetworkRequirementsToml>>,
@@ -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<ConfigRequirementsWithSources> 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<bool>)]) -> 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(
+5
View File
@@ -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<AppsConfigToml>,
/// Settings for Computer Use controls.
#[serde(default)]
pub computer_use: Option<ComputerUseConfigToml>,
/// OTEL configuration.
pub otel: Option<OtelConfigToml>,
+2
View File
@@ -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;
+26
View File
@@ -428,6 +428,32 @@ pub struct AppsConfigToml {
pub apps: HashMap<String, AppConfig>,
}
/// 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<Vec<String>>,
/// Bundle identifiers that Computer Use is allowed to access.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_bundle_ids: Option<Vec<String>>,
}
/// 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<bool>,
/// macOS-specific Computer Use controls.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub macos: Option<ComputerUseMacosConfigToml>,
}
// ===== OTEL configuration =====
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
+50 -1
View File
@@ -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"
}
}
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,