Compare commits

...

1 Commits

Author SHA1 Message Date
Abhinav Vedmala
219d524c04 Allow any granular approval requirement
Add a requirements-only approval policy matcher so managed requirements can allow any granular approval-policy shape with { granular = "any" }. This keeps runtime AskForApproval concrete while avoiding enumerating every granular flag permutation.

Co-authored-by: Codex <noreply@openai.com>
2026-04-23 15:18:15 -07:00
19 changed files with 635 additions and 87 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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";

View File

@@ -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<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
export type ConfigRequirements = {allowedApprovalPolicies: Array<ApprovalPolicyConstraint> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};

View File

@@ -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, };

View File

@@ -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;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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<Vec<AskForApproval>>,
pub allowed_approval_policies: Option<Vec<ApprovalPolicyConstraint>>,
#[experimental("configRequirements/read.allowedApprovalsReviewers")]
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
@@ -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(

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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<Vec<AskForApproval>>,
pub allowed_approval_policies: Option<Vec<ApprovalPolicyConstraint>>,
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
pub remote_sandbox_config: Option<Vec<RemoteSandboxConfigToml>>,
@@ -642,6 +643,118 @@ pub struct ConfigRequirementsToml {
pub guardian_policy_config: Option<String>,
}
#[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<AskForApproval> 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<D>(deserializer: D) -> Result<Self, D::Error>
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<String>,
@@ -672,7 +785,7 @@ impl<T> std::ops::Deref for Sourced<T> {
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsWithSources {
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
pub allowed_approval_policies: Option<Sourced<Vec<ApprovalPolicyConstraint>>>,
pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
@@ -912,13 +1025,19 @@ impl TryFrom<ConfigRequirementsWithSources> 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#"

View File

@@ -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;

View File

@@ -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()
}))
}))

View File

@@ -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<LegacyManagedConfigToml> 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];

View File

@@ -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,

View File

@@ -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,