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 6904ef7165..2222a66519 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 @@ -5780,6 +5780,32 @@ "AppToolsConfig": { "type": "object" }, + "ApprovalPolicyConstraint": { + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "granular": { + "$ref": "#/definitions/v2/GranularApprovalConstraint" + } + }, + "required": [ + "granular" + ], + "title": "GranularApprovalPolicyConstraint", + "type": "object" + } + ] + }, "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": [ @@ -7211,7 +7237,7 @@ "properties": { "allowedApprovalPolicies": { "items": { - "$ref": "#/definitions/v2/AskForApproval" + "$ref": "#/definitions/v2/ApprovalPolicyConstraint" }, "type": [ "array", @@ -9196,6 +9222,49 @@ }, "type": "object" }, + "GranularApprovalConfig": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, + "GranularApprovalConstraint": { + "anyOf": [ + { + "$ref": "#/definitions/v2/GranularApprovalWildcard" + }, + { + "$ref": "#/definitions/v2/GranularApprovalConfig" + } + ] + }, + "GranularApprovalWildcard": { + "enum": [ + "any" + ], + "type": "string" + }, "GuardianApprovalReview": { "description": "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", "properties": { 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 bc00828e68..48cf5bd991 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 @@ -606,6 +606,32 @@ "AppToolsConfig": { "type": "object" }, + "ApprovalPolicyConstraint": { + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "granular": { + "$ref": "#/definitions/GranularApprovalConstraint" + } + }, + "required": [ + "granular" + ], + "title": "GranularApprovalPolicyConstraint", + "type": "object" + } + ] + }, "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": [ @@ -3771,7 +3797,7 @@ "properties": { "allowedApprovalPolicies": { "items": { - "$ref": "#/definitions/AskForApproval" + "$ref": "#/definitions/ApprovalPolicyConstraint" }, "type": [ "array", @@ -5867,6 +5893,49 @@ }, "type": "object" }, + "GranularApprovalConfig": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, + "GranularApprovalConstraint": { + "anyOf": [ + { + "$ref": "#/definitions/GranularApprovalWildcard" + }, + { + "$ref": "#/definitions/GranularApprovalConfig" + } + ] + }, + "GranularApprovalWildcard": { + "enum": [ + "any" + ], + "type": "string" + }, "GuardianApprovalReview": { "description": "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", "properties": { 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..3718171480 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -1,16 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "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": [ - "user", - "auto_review", - "guardian_subagent" - ], - "type": "string" - }, - "AskForApproval": { + "ApprovalPolicyConstraint": { "oneOf": [ { "enum": [ @@ -25,46 +16,31 @@ "additionalProperties": false, "properties": { "granular": { - "properties": { - "mcp_elicitations": { - "type": "boolean" - }, - "request_permissions": { - "default": false, - "type": "boolean" - }, - "rules": { - "type": "boolean" - }, - "sandbox_approval": { - "type": "boolean" - }, - "skill_approval": { - "default": false, - "type": "boolean" - } - }, - "required": [ - "mcp_elicitations", - "rules", - "sandbox_approval" - ], - "type": "object" + "$ref": "#/definitions/GranularApprovalConstraint" } }, "required": [ "granular" ], - "title": "GranularAskForApproval", + "title": "GranularApprovalPolicyConstraint", "type": "object" } ] }, + "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": [ + "user", + "auto_review", + "guardian_subagent" + ], + "type": "string" + }, "ConfigRequirements": { "properties": { "allowedApprovalPolicies": { "items": { - "$ref": "#/definitions/AskForApproval" + "$ref": "#/definitions/ApprovalPolicyConstraint" }, "type": [ "array", @@ -205,6 +181,49 @@ ], "type": "object" }, + "GranularApprovalConfig": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, + "GranularApprovalConstraint": { + "anyOf": [ + { + "$ref": "#/definitions/GranularApprovalWildcard" + }, + { + "$ref": "#/definitions/GranularApprovalConfig" + } + ] + }, + "GranularApprovalWildcard": { + "enum": [ + "any" + ], + "type": "string" + }, "ManagedHooksRequirements": { "properties": { "PermissionRequest": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ApprovalPolicyConstraint.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ApprovalPolicyConstraint.ts new file mode 100644 index 0000000000..b1f0d365c8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ApprovalPolicyConstraint.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 { GranularApprovalConstraint } from "./GranularApprovalConstraint"; + +export type ApprovalPolicyConstraint = "untrusted" | "on-failure" | "on-request" | { "granular": GranularApprovalConstraint } | "never"; 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..a37a154749 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -2,8 +2,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 { ApprovalPolicyConstraint } from "./ApprovalPolicyConstraint"; 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}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalConfig.ts new file mode 100644 index 0000000000..2e1f9550ce --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalConfig.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 GranularApprovalConfig = { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalConstraint.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalConstraint.ts new file mode 100644 index 0000000000..32a049f67a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalConstraint.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 { GranularApprovalConfig } from "./GranularApprovalConfig"; +import type { GranularApprovalWildcard } from "./GranularApprovalWildcard"; + +export type GranularApprovalConstraint = GranularApprovalWildcard | GranularApprovalConfig; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalWildcard.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalWildcard.ts new file mode 100644 index 0000000000..a01f1f2de8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GranularApprovalWildcard.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 GranularApprovalWildcard = "any"; 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 be747508ac..6c01d71e52 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -20,6 +20,7 @@ export type { AppScreenshot } from "./AppScreenshot"; export type { AppSummary } from "./AppSummary"; export type { AppToolApproval } from "./AppToolApproval"; export type { AppToolsConfig } from "./AppToolsConfig"; +export type { ApprovalPolicyConstraint } from "./ApprovalPolicyConstraint"; export type { ApprovalsReviewer } from "./ApprovalsReviewer"; export type { AppsConfig } from "./AppsConfig"; export type { AppsDefaultConfig } from "./AppsDefaultConfig"; @@ -143,6 +144,9 @@ export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsRespons export type { GetAccountResponse } from "./GetAccountResponse"; export type { GitInfo } from "./GitInfo"; export type { GrantedPermissionProfile } from "./GrantedPermissionProfile"; +export type { GranularApprovalConfig } from "./GranularApprovalConfig"; +export type { GranularApprovalConstraint } from "./GranularApprovalConstraint"; +export type { GranularApprovalWildcard } from "./GranularApprovalWildcard"; export type { GuardianApprovalReview } from "./GuardianApprovalReview"; export type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction"; export type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index b6726679fb..70f2b3b84c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -270,6 +270,50 @@ pub enum AskForApproval { Never, } +#[derive( + Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case", export_to = "v2/")] +pub enum ApprovalPolicyConstraint { + #[serde(rename = "untrusted")] + #[ts(rename = "untrusted")] + UnlessTrusted, + OnFailure, + OnRequest, + #[experimental("askForApproval.granular")] + Granular(GranularApprovalConstraint), + Never, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(untagged)] +#[ts(export_to = "v2/")] +pub enum GranularApprovalConstraint { + Any(GranularApprovalWildcard), + Exact(GranularApprovalConfig), +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case", export_to = "v2/")] +pub enum GranularApprovalWildcard { + Any, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case", export_to = "v2/")] +pub struct GranularApprovalConfig { + pub sandbox_approval: bool, + pub rules: bool, + #[serde(default)] + pub skill_approval: bool, + #[serde(default)] + pub request_permissions: bool, + pub mcp_elicitations: bool, +} + impl AskForApproval { pub fn to_core(self) -> CoreAskForApproval { match self { @@ -944,7 +988,7 @@ pub struct ConfigReadResponse { #[ts(export_to = "v2/")] pub struct ConfigRequirements { #[experimental(nested)] - pub allowed_approval_policies: Option>, + pub allowed_approval_policies: Option>, #[experimental("configRequirements/read.allowedApprovalsReviewers")] pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, @@ -9011,13 +9055,15 @@ mod tests { fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements { - allowed_approval_policies: Some(vec![AskForApproval::Granular { - sandbox_approval: true, - rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, - }]), + allowed_approval_policies: Some(vec![ApprovalPolicyConstraint::Granular( + GranularApprovalConstraint::Exact(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: false, + request_permissions: false, + mcp_elicitations: false, + }), + )]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_web_search_modes: None, @@ -9030,6 +9076,31 @@ mod tests { assert_eq!(reason, Some("askForApproval.granular")); } + #[test] + fn approval_policy_constraint_any_granular_uses_granular_any_wire_shape() { + let value = serde_json::to_value(ApprovalPolicyConstraint::Granular( + GranularApprovalConstraint::Any(GranularApprovalWildcard::Any), + )) + .expect("approval policy constraint should serialize"); + + assert_eq!( + value, + serde_json::json!({ + "granular": "any", + }) + ); + + let decoded: ApprovalPolicyConstraint = + serde_json::from_value(value).expect("approval policy constraint should deserialize"); + + assert_eq!( + decoded, + ApprovalPolicyConstraint::Granular(GranularApprovalConstraint::Any( + GranularApprovalWildcard::Any + )) + ); + } + #[test] fn client_request_thread_start_granular_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason( diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index e46d785b3a..4528834fca 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -216,7 +216,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 `details` returned by detect. When a request includes plugin imports, the server emits `externalAgentConfig/import/completed` after the full import finishes (immediately after the response when everything completed synchronously, or after background remote 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`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. `allowedApprovalPolicies` entries mirror concrete approval policies and may also include `{ "granular": "any" }` to allow any granular approval-policy shape. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index ce0ea34069..4234f4af18 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -16,6 +16,9 @@ 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::GranularApprovalConfig; +use codex_app_server_protocol::GranularApprovalConstraint; +use codex_app_server_protocol::GranularApprovalWildcard; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ManagedHooksRequirements; use codex_app_server_protocol::NetworkDomainPermission; @@ -24,7 +27,9 @@ use codex_app_server_protocol::NetworkUnixSocketPermission; use codex_app_server_protocol::SandboxMode; use codex_core::ThreadManager; use codex_core::config::Config; +use codex_core::config_loader::ApprovalPolicyConstraint as CoreApprovalPolicyConstraint; use codex_core::config_loader::ConfigRequirementsToml; +use codex_core::config_loader::GranularApprovalConstraint as CoreGranularApprovalConstraint; use codex_core::config_loader::HookEventsToml; use codex_core::config_loader::HookHandlerConfig as CoreHookHandlerConfig; use codex_core::config_loader::ManagedHooksRequirementsToml; @@ -269,7 +274,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR allowed_approval_policies: requirements.allowed_approval_policies.map(|policies| { policies .into_iter() - .map(codex_app_server_protocol::AskForApproval::from) + .map(map_approval_policy_constraint_to_api) .collect() }), allowed_approvals_reviewers: requirements.allowed_approvals_reviewers.map(|reviewers| { @@ -305,6 +310,49 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR } } +fn map_approval_policy_constraint_to_api( + constraint: CoreApprovalPolicyConstraint, +) -> codex_app_server_protocol::ApprovalPolicyConstraint { + match constraint { + CoreApprovalPolicyConstraint::UnlessTrusted => { + codex_app_server_protocol::ApprovalPolicyConstraint::UnlessTrusted + } + CoreApprovalPolicyConstraint::OnFailure => { + codex_app_server_protocol::ApprovalPolicyConstraint::OnFailure + } + CoreApprovalPolicyConstraint::OnRequest => { + codex_app_server_protocol::ApprovalPolicyConstraint::OnRequest + } + CoreApprovalPolicyConstraint::Granular(granular) => { + codex_app_server_protocol::ApprovalPolicyConstraint::Granular( + map_granular_approval_constraint_to_api(granular), + ) + } + CoreApprovalPolicyConstraint::Never => { + codex_app_server_protocol::ApprovalPolicyConstraint::Never + } + } +} + +fn map_granular_approval_constraint_to_api( + constraint: CoreGranularApprovalConstraint, +) -> GranularApprovalConstraint { + match constraint { + CoreGranularApprovalConstraint::Any => { + GranularApprovalConstraint::Any(GranularApprovalWildcard::Any) + } + CoreGranularApprovalConstraint::Exact(config) => { + GranularApprovalConstraint::Exact(GranularApprovalConfig { + sandbox_approval: config.sandbox_approval, + rules: config.rules, + skill_approval: config.skill_approval, + request_permissions: config.request_permissions, + mcp_elicitations: config.mcp_elicitations, + }) + } + } +} + fn map_hooks_requirements_to_api(hooks: ManagedHooksRequirementsToml) -> ManagedHooksRequirements { let ManagedHooksRequirementsToml { managed_dir, @@ -527,8 +575,8 @@ mod tests { fn map_requirements_toml_to_api_converts_core_enums() { let requirements = ConfigRequirementsToml { allowed_approval_policies: Some(vec![ - CoreAskForApproval::Never, - CoreAskForApproval::OnRequest, + CoreAskForApproval::Never.into(), + CoreAskForApproval::OnRequest.into(), ]), allowed_approvals_reviewers: Some(vec![ CoreApprovalsReviewer::User, @@ -605,8 +653,8 @@ mod tests { assert_eq!( mapped.allowed_approval_policies, Some(vec![ - codex_app_server_protocol::AskForApproval::Never, - codex_app_server_protocol::AskForApproval::OnRequest, + codex_app_server_protocol::ApprovalPolicyConstraint::Never, + codex_app_server_protocol::ApprovalPolicyConstraint::OnRequest, ]) ); assert_eq!( @@ -682,6 +730,28 @@ mod tests { ); } + #[test] + fn map_requirements_toml_to_api_preserves_any_granular_approval_constraint() { + let requirements: ConfigRequirementsToml = toml::from_str( + r#" + allowed_approval_policies = ["on-request", { granular = "any" }] + "#, + ) + .expect("requirements should parse"); + + let mapped = map_requirements_toml_to_api(requirements); + + assert_eq!( + mapped.allowed_approval_policies, + Some(vec![ + codex_app_server_protocol::ApprovalPolicyConstraint::OnRequest, + codex_app_server_protocol::ApprovalPolicyConstraint::Granular( + GranularApprovalConstraint::Any(GranularApprovalWildcard::Any) + ), + ]) + ); + } + #[test] fn map_requirements_toml_to_api_omits_unix_socket_none_entries_from_legacy_network_fields() { let requirements = ConfigRequirementsToml { diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index ca9ec56fe9..c1693de1b9 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -1172,7 +1172,7 @@ mod tests { assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1204,7 +1204,7 @@ mod tests { assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1236,7 +1236,7 @@ mod tests { assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1285,7 +1285,7 @@ mod tests { assert_eq!( result, Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1370,7 +1370,7 @@ enabled = false assert_eq!( handle.await.expect("cloud requirements task"), Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1446,7 +1446,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1520,7 +1520,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1721,7 +1721,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1759,7 +1759,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1817,7 +1817,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), + allowed_approval_policies: Some(vec![AskForApproval::OnRequest.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1870,7 +1870,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), + allowed_approval_policies: Some(vec![AskForApproval::OnRequest.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1927,7 +1927,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -1985,7 +1985,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -2043,7 +2043,7 @@ enabled = false .as_deref() .and_then(|contents| parse_cloud_requirements(contents).ok().flatten()), Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -2131,7 +2131,7 @@ enabled = false assert_eq!( service.fetch().await, Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -2161,7 +2161,7 @@ enabled = false .as_deref() .and_then(|contents| parse_cloud_requirements(contents).ok().flatten()), Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), + allowed_approval_policies: Some(vec![AskForApproval::OnRequest.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 56ff26f907..b702b8f1e7 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -2,6 +2,7 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::WebSearchMode; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; @@ -624,7 +625,7 @@ pub(crate) fn merge_enablement_settings_descending( /// Base config deserialized from system `requirements.toml` or MDM. #[derive(Deserialize, Debug, Clone, Default, PartialEq)] pub struct ConfigRequirementsToml { - pub allowed_approval_policies: Option>, + pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, pub remote_sandbox_config: Option>, @@ -642,6 +643,118 @@ pub struct ConfigRequirementsToml { pub guardian_policy_config: Option, } +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum ApprovalPolicyConstraint { + #[serde(rename = "untrusted")] + UnlessTrusted, + OnFailure, + OnRequest, + Granular(GranularApprovalConstraint), + Never, +} + +impl ApprovalPolicyConstraint { + pub fn initial_policy(self) -> AskForApproval { + match self { + Self::UnlessTrusted => AskForApproval::UnlessTrusted, + Self::OnFailure => AskForApproval::OnFailure, + Self::OnRequest => AskForApproval::OnRequest, + Self::Granular(GranularApprovalConstraint::Any) => { + AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, + }) + } + Self::Granular(GranularApprovalConstraint::Exact(config)) => { + AskForApproval::Granular(config) + } + Self::Never => AskForApproval::Never, + } + } + + fn matches(self, candidate: AskForApproval) -> bool { + match self { + Self::Granular(GranularApprovalConstraint::Any) => { + matches!(candidate, AskForApproval::Granular(_)) + } + Self::Granular(GranularApprovalConstraint::Exact(config)) => { + matches!(candidate, AskForApproval::Granular(candidate_config) if candidate_config == config) + } + _ => candidate == self.initial_policy(), + } + } +} + +impl From for ApprovalPolicyConstraint { + fn from(value: AskForApproval) -> Self { + match value { + AskForApproval::UnlessTrusted => Self::UnlessTrusted, + AskForApproval::OnFailure => Self::OnFailure, + AskForApproval::OnRequest => Self::OnRequest, + AskForApproval::Granular(config) => { + Self::Granular(GranularApprovalConstraint::Exact(config)) + } + AskForApproval::Never => Self::Never, + } + } +} + +impl fmt::Display for ApprovalPolicyConstraint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnlessTrusted => write!(f, "untrusted"), + Self::OnFailure => write!(f, "on-failure"), + Self::OnRequest => write!(f, "on-request"), + Self::Granular(granular) => write!(f, "granular:{granular}"), + Self::Never => write!(f, "never"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GranularApprovalConstraint { + Any, + Exact(GranularApprovalConfig), +} + +impl fmt::Display for GranularApprovalConstraint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Any => write!(f, "any"), + Self::Exact(_) => write!(f, "exact"), + } + } +} + +impl<'de> Deserialize<'de> for GranularApprovalConstraint { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Wire { + Any(GranularApprovalWildcard), + Exact(GranularApprovalConfig), + } + + match Wire::deserialize(deserializer)? { + Wire::Any(GranularApprovalWildcard::Any) => Ok(Self::Any), + Wire::Exact(config) => Ok(Self::Exact(config)), + } + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +enum GranularApprovalWildcard { + Any, +} + #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct RemoteSandboxConfigToml { pub hostname_patterns: Vec, @@ -672,7 +785,7 @@ impl std::ops::Deref for Sourced { #[derive(Debug, Clone, Default, PartialEq)] pub struct ConfigRequirementsWithSources { - pub allowed_approval_policies: Option>>, + pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, pub allowed_web_search_modes: Option>>, @@ -912,13 +1025,19 @@ impl TryFrom for ConfigRequirements { value: policies, source: requirement_source, }) => { - let Some(initial_value) = policies.first().copied() else { + let Some(initial_value) = policies + .first() + .map(|requirement| requirement.initial_policy()) + else { return Err(ConstraintError::empty_field("allowed_approval_policies")); }; let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(initial_value, move |candidate| { - if policies.contains(candidate) { + if policies + .iter() + .any(|requirement| requirement.matches(*candidate)) + { Ok(()) } else { Err(ConstraintError::InvalidValue { @@ -1229,7 +1348,10 @@ mod tests { let mut target = ConfigRequirementsWithSources::default(); let source = RequirementSource::LegacyManagedConfigTomlFromMdm; - let allowed_approval_policies = vec![AskForApproval::UnlessTrusted, AskForApproval::Never]; + let allowed_approval_policies = vec![ + ApprovalPolicyConstraint::from(AskForApproval::UnlessTrusted), + ApprovalPolicyConstraint::from(AskForApproval::Never), + ]; let allowed_approvals_reviewers = vec![ApprovalsReviewer::AutoReview, ApprovalsReviewer::User]; let allowed_sandbox_modes = vec![ @@ -1319,7 +1441,7 @@ mod tests { empty_target, ConfigRequirementsWithSources { allowed_approval_policies: Some(Sourced::new( - vec![AskForApproval::OnRequest], + vec![ApprovalPolicyConstraint::from(AskForApproval::OnRequest)], source_location, )), allowed_approvals_reviewers: None, @@ -1365,7 +1487,7 @@ mod tests { populated_target, ConfigRequirementsWithSources { allowed_approval_policies: Some(Sourced::new( - vec![AskForApproval::Never], + vec![ApprovalPolicyConstraint::from(AskForApproval::Never)], existing_source, )), allowed_approvals_reviewers: None, @@ -1877,6 +1999,100 @@ allowed_approvals_reviewers = ["user"] Ok(()) } + #[test] + fn allowed_approval_policies_support_any_granular_requirement() -> Result<()> { + let toml_str = r#" + allowed_approval_policies = ["on-request", { granular = "any" }] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; + + assert!( + requirements + .approval_policy + .can_set(&AskForApproval::Granular( + codex_protocol::protocol::GranularApprovalConfig { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + } + )) + .is_ok() + ); + assert!( + requirements + .approval_policy + .can_set(&AskForApproval::Granular( + codex_protocol::protocol::GranularApprovalConfig { + sandbox_approval: false, + rules: true, + skill_approval: true, + request_permissions: false, + mcp_elicitations: true, + } + )) + .is_ok() + ); + assert_eq!( + requirements.approval_policy.can_set(&AskForApproval::Never), + Err(ConstraintError::InvalidValue { + field_name: "approval_policy", + candidate: "Never".into(), + allowed: "[OnRequest, Granular(Any)]".into(), + requirement_source: RequirementSource::Unknown, + }) + ); + + Ok(()) + } + + #[test] + fn allowed_approval_policies_keep_exact_granular_requirement() -> Result<()> { + let toml_str = r#" + allowed_approval_policies = [ + { granular = { sandbox_approval = true, rules = false, skill_approval = false, request_permissions = true, mcp_elicitations = false } } + ] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; + + assert!( + requirements + .approval_policy + .can_set(&AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + })) + .is_ok() + ); + assert_eq!( + requirements.approval_policy.can_set(&AskForApproval::Granular( + GranularApprovalConfig { + sandbox_approval: false, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + } + )), + Err(ConstraintError::InvalidValue { + field_name: "approval_policy", + candidate: "Granular(GranularApprovalConfig { sandbox_approval: false, rules: false, skill_approval: false, request_permissions: true, mcp_elicitations: false })" + .into(), + allowed: "[Granular(Exact(GranularApprovalConfig { sandbox_approval: true, rules: false, skill_approval: false, request_permissions: true, mcp_elicitations: false }))]" + .into(), + requirement_source: RequirementSource::Unknown, + }) + ); + + Ok(()) + } + #[test] fn deserialize_allowed_approvals_reviewers() -> Result<()> { let toml_str = r#" diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index e3d95acb86..d1c7d35412 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -31,6 +31,7 @@ pub use cloud_requirements::CloudRequirementsLoader; pub use codex_app_server_protocol::ConfigLayerSource; pub use codex_utils_absolute_path::AbsolutePathBuf; pub use config_requirements::AppRequirementToml; +pub use config_requirements::ApprovalPolicyConstraint; pub use config_requirements::AppsRequirementsToml; pub use config_requirements::ConfigRequirements; pub use config_requirements::ConfigRequirementsToml; @@ -39,6 +40,7 @@ pub use config_requirements::ConstrainedWithSource; pub use config_requirements::FeatureRequirementsToml; pub use config_requirements::FilesystemConstraints; pub use config_requirements::FilesystemDenyReadPattern; +pub use config_requirements::GranularApprovalConstraint; pub use config_requirements::McpServerIdentity; pub use config_requirements::McpServerRequirement; pub use config_requirements::NetworkConstraints; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index d46d1a316d..f8e8ef1036 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -6538,7 +6538,7 @@ trust_level = "untrusted" .fallback_cwd(Some(workspace.path().to_path_buf())) .cloud_requirements(CloudRequirementsLoader::new(async { Ok(Some(crate::config_loader::ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), + allowed_approval_policies: Some(vec![AskForApproval::OnRequest.into()]), ..Default::default() })) })) @@ -6567,7 +6567,7 @@ async fn explicit_approval_policy_falls_back_when_disallowed_by_requirements() - .fallback_cwd(Some(codex_home.path().to_path_buf())) .cloud_requirements(CloudRequirementsLoader::new(async { Ok(Some(crate::config_loader::ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), + allowed_approval_policies: Some(vec![AskForApproval::OnRequest.into()]), ..Default::default() })) })) diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 4681aa0753..fec1600e2a 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -30,6 +30,7 @@ use std::path::PathBuf; use toml::Value as TomlValue; pub use codex_config::AppRequirementToml; +pub use codex_config::ApprovalPolicyConstraint; pub use codex_config::AppsRequirementsToml; pub use codex_config::CloudRequirementsLoadError; pub use codex_config::CloudRequirementsLoadErrorCode; @@ -45,6 +46,7 @@ pub use codex_config::ConstrainedWithSource; pub use codex_config::FeatureRequirementsToml; pub use codex_config::FilesystemConstraints; pub use codex_config::FilesystemDenyReadPattern; +pub use codex_config::GranularApprovalConstraint; pub use codex_config::HookEventsToml; pub use codex_config::HookHandlerConfig; pub use codex_config::LoaderOverrides; @@ -1048,7 +1050,7 @@ impl From for ConfigRequirementsToml { sandbox_mode, } = legacy; if let Some(approval_policy) = approval_policy { - config_requirements_toml.allowed_approval_policies = Some(vec![approval_policy]); + config_requirements_toml.allowed_approval_policies = Some(vec![approval_policy.into()]); } if let Some(approvals_reviewer) = approvals_reviewer { let mut allowed_reviewers = vec![approvals_reviewer]; diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 7f61952690..cde0fde0c1 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -681,7 +681,10 @@ personality = true .allowed_approval_policies .as_deref() .cloned(), - Some(vec![AskForApproval::Never, AskForApproval::OnRequest]) + Some(vec![ + AskForApproval::Never.into(), + AskForApproval::OnRequest.into(), + ]) ); assert_eq!( config_requirements_toml @@ -771,7 +774,7 @@ allowed_approval_policies = ["on-request"] loader_overrides, CloudRequirementsLoader::new(async { Ok(Some(ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -828,7 +831,7 @@ allowed_approval_policies = ["on-request"] config_requirements_toml.merge_unset_fields( RequirementSource::CloudRequirements, ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, @@ -857,7 +860,7 @@ allowed_approval_policies = ["on-request"] .allowed_approval_policies .as_ref() .map(|sourced| sourced.value.clone()), - Some(vec![AskForApproval::Never]) + Some(vec![AskForApproval::Never.into()]) ); assert_eq!( config_requirements_toml @@ -1037,7 +1040,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; let requirements = ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_approval_policies: Some(vec![AskForApproval::Never.into()]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, remote_sandbox_config: None, diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 1c48c44918..9631efe64a 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -679,7 +679,7 @@ mod tests { }; let requirements_toml = ConfigRequirementsToml { - allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), + allowed_approval_policies: Some(vec![AskForApproval::OnRequest.into()]), allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]), allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), remote_sandbox_config: None,