mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
permissions: enforce managed permission profile allowlists (#24852)
## Why Permission profile allowlists are an enterprise security boundary, but they also need to compose across the managed requirements layers added in #24620. A map representation lets each requirements layer add, allow, or revoke individual profiles without replacing an entire array. ## Managed Contract Administrators configure the mergeable allow map with `allowed_permission_profiles`. A recommended enterprise configuration explicitly lists every built-in and custom profile users should be able to select: ```toml default_permissions = "review_only" [allowed_permission_profiles] ":read-only" = true ":workspace" = true review_only = true # ":danger-full-access" is intentionally omitted, so it is denied. [permissions.review_only] extends = ":read-only" ``` - Profiles whose effective merged value is `true` are allowed. - Missing profiles and profiles set to `false` are denied. - This is a closed allowlist: built-in profiles and profiles introduced in future versions are denied unless explicitly allowed. - Explicitly list each built-in profile the enterprise wants to make available. Omit built-ins such as `:danger-full-access` when they should remain unavailable. - Set `default_permissions` explicitly to the allowed profile users should receive when they have no local selection. - Higher-precedence layers override only the profile keys they define. - `false` is only needed when a higher-precedence layer must revoke a `true` inherited from a lower layer. - Explicit keys must refer to known built-in or managed profiles. A custom or narrowed allowlist requires an allowed `default_permissions`. For compatibility, if both `:workspace` and `:read-only` are explicitly allowed, an omitted default resolves to `:workspace`; customer configurations should still set the intended default explicitly. When `allowed_permission_profiles` is absent, existing implicit permission and legacy `sandbox_mode` behavior is unchanged. ## What Changed - Add `allowed_permission_profiles` as a `BTreeMap<String, bool>` that merges per profile across requirements layers. - Enforce managed defaults, strict denial of omitted profiles, and the explicitly allowed standard-pair fallback. - Expose `allowedPermissionProfiles` through `configRequirements/read` and regenerate its schemas. - Add regression coverage for map composition and revocation, managed defaults, strict denial of omitted built-ins, and API output. ## Verification - Focused `codex-config` coverage for layered map composition and revocation - Focused `codex-core` coverage for managed defaults, invalid defaults, strict denial of omitted built-ins, and the standard built-in pair - Focused `codex-app-server` coverage for requirements API output - Scoped Clippy for `codex-config`, `codex-core`, `codex-app-server-protocol`, and `codex-app-server` ## Documentation The managed `requirements.toml` documentation should introduce `allowed_permission_profiles` as a closed permission-profile allowlist before this setting is published on developers.openai.com. --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
954e2878bb
commit
2f108f9fd9
+10
-4
@@ -8020,12 +8020,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissions": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
"allowedPermissionProfiles": {
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
@@ -8066,6 +8066,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaultPermissions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enforceResidency": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+10
-4
@@ -4362,12 +4362,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissions": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
"allowedPermissionProfiles": {
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
@@ -4408,6 +4408,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaultPermissions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enforceResidency": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+10
-4
@@ -94,12 +94,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissions": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
"allowedPermissionProfiles": {
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
@@ -140,6 +140,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaultPermissions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enforceResidency": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -8,4 +8,4 @@ import type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
|
||||
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||
|
||||
@@ -375,7 +375,8 @@ pub struct ConfigRequirements {
|
||||
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
||||
pub allowed_windows_sandbox_implementations: Option<Vec<WindowsSandboxSetupMode>>,
|
||||
pub allowed_permissions: Option<Vec<String>>,
|
||||
pub allowed_permission_profiles: Option<BTreeMap<String, bool>>,
|
||||
pub default_permissions: Option<String>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||
pub allow_managed_hooks_only: Option<bool>,
|
||||
pub allow_appshots: Option<bool>,
|
||||
|
||||
@@ -1674,7 +1674,8 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_windows_sandbox_implementations: None,
|
||||
allowed_permissions: None,
|
||||
allowed_permission_profiles: None,
|
||||
default_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
|
||||
@@ -231,7 +231,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; dotted paths such as `desktop.someKey` use the same generic write surface.
|
||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`, `allowedPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||
|
||||
### Example: Start or resume a thread
|
||||
|
||||
|
||||
@@ -350,7 +350,8 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
||||
.collect()
|
||||
})
|
||||
}),
|
||||
allowed_permissions: requirements.allowed_permissions,
|
||||
allowed_permission_profiles: requirements.allowed_permission_profiles,
|
||||
default_permissions: requirements.default_permissions,
|
||||
allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| {
|
||||
let mut normalized = modes
|
||||
.into_iter()
|
||||
@@ -569,29 +570,43 @@ mod tests {
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::WindowsRequirementsToml;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_allow_managed_hooks_only() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
allowed_permissions: Some(vec![
|
||||
"managed-standard".to_string(),
|
||||
"managed-build".to_string(),
|
||||
]),
|
||||
allow_managed_hooks_only: Some(true),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
mapped.allowed_permissions,
|
||||
Some(vec![
|
||||
"managed-standard".to_string(),
|
||||
"managed-build".to_string(),
|
||||
])
|
||||
);
|
||||
assert_eq!(mapped.allow_managed_hooks_only, Some(true));
|
||||
assert_eq!(mapped.hooks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_permission_default_and_allowlist() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
allowed_permission_profiles: Some(BTreeMap::from([
|
||||
("managed-build".to_string(), false),
|
||||
("managed-standard".to_string(), true),
|
||||
])),
|
||||
default_permissions: Some("managed-standard".to_string()),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
mapped.allowed_permission_profiles,
|
||||
Some(BTreeMap::from([
|
||||
("managed-build".to_string(), false),
|
||||
("managed-standard".to_string(), true),
|
||||
]))
|
||||
);
|
||||
assert_eq!(
|
||||
mapped.default_permissions,
|
||||
Some("managed-standard".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_allow_appshots() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
|
||||
@@ -822,7 +822,8 @@ pub struct ConfigRequirementsToml {
|
||||
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
|
||||
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
|
||||
pub allowed_permissions: Option<Vec<String>>,
|
||||
pub allowed_permission_profiles: Option<BTreeMap<String, bool>>,
|
||||
pub default_permissions: Option<String>,
|
||||
pub remote_sandbox_config: Option<Vec<RemoteSandboxConfigToml>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
|
||||
pub allow_managed_hooks_only: Option<bool>,
|
||||
@@ -876,7 +877,8 @@ pub struct ConfigRequirementsWithSources {
|
||||
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
|
||||
pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
|
||||
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
|
||||
pub allowed_permissions: Option<Sourced<Vec<String>>>,
|
||||
pub allowed_permission_profiles: Option<Sourced<BTreeMap<String, bool>>>,
|
||||
pub default_permissions: Option<Sourced<String>>,
|
||||
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
|
||||
pub allow_managed_hooks_only: Option<Sourced<bool>>,
|
||||
pub allow_appshots: Option<Sourced<bool>>,
|
||||
@@ -916,7 +918,8 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies: _,
|
||||
allowed_approvals_reviewers: _,
|
||||
allowed_sandbox_modes: _,
|
||||
allowed_permissions: _,
|
||||
allowed_permission_profiles: _,
|
||||
default_permissions: _,
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes: _,
|
||||
allow_managed_hooks_only: _,
|
||||
@@ -951,7 +954,8 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions,
|
||||
allowed_permission_profiles,
|
||||
default_permissions,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
@@ -983,7 +987,8 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions,
|
||||
allowed_permission_profiles,
|
||||
default_permissions,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
@@ -1004,7 +1009,8 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
|
||||
allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value),
|
||||
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
|
||||
allowed_permissions: allowed_permissions.map(|sourced| sourced.value),
|
||||
allowed_permission_profiles: allowed_permission_profiles.map(|sourced| sourced.value),
|
||||
default_permissions: default_permissions.map(|sourced| sourced.value),
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
|
||||
allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value),
|
||||
@@ -1092,7 +1098,8 @@ impl ConfigRequirementsToml {
|
||||
self.allowed_approval_policies.is_none()
|
||||
&& self.allowed_approvals_reviewers.is_none()
|
||||
&& self.allowed_sandbox_modes.is_none()
|
||||
&& self.allowed_permissions.is_none()
|
||||
&& self.allowed_permission_profiles.is_none()
|
||||
&& self.default_permissions.is_none()
|
||||
&& self.remote_sandbox_config.is_none()
|
||||
&& self.allowed_web_search_modes.is_none()
|
||||
&& self.allow_managed_hooks_only.is_none()
|
||||
@@ -1144,7 +1151,8 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions: _,
|
||||
allowed_permission_profiles: _,
|
||||
default_permissions: _,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
@@ -1511,7 +1519,8 @@ mod tests {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions,
|
||||
allowed_permission_profiles,
|
||||
default_permissions,
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
@@ -1536,7 +1545,9 @@ mod tests {
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allowed_sandbox_modes: allowed_sandbox_modes
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allowed_permissions: allowed_permissions
|
||||
allowed_permission_profiles: allowed_permission_profiles
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
default_permissions: default_permissions
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allowed_web_search_modes: allowed_web_search_modes
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
@@ -1592,7 +1603,11 @@ mod tests {
|
||||
fn deserialize_managed_permission_profiles() -> Result<()> {
|
||||
let requirements: ConfigRequirementsToml = from_str(
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard", "managed-build"]
|
||||
default_permissions = "managed-standard"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-standard = true
|
||||
managed-build = true
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":workspace"
|
||||
@@ -1603,11 +1618,15 @@ mod tests {
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
requirements.allowed_permissions,
|
||||
Some(vec![
|
||||
"managed-standard".to_string(),
|
||||
"managed-build".to_string(),
|
||||
])
|
||||
requirements.allowed_permission_profiles,
|
||||
Some(BTreeMap::from([
|
||||
("managed-build".to_string(), true),
|
||||
("managed-standard".to_string(), true),
|
||||
]))
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.default_permissions,
|
||||
Some("managed-standard".to_string())
|
||||
);
|
||||
let permissions = requirements
|
||||
.permissions
|
||||
@@ -1720,7 +1739,8 @@ mod tests {
|
||||
allowed_approval_policies: Some(allowed_approval_policies.clone()),
|
||||
allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()),
|
||||
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
|
||||
allowed_permissions: Some(vec!["managed".to_string()]),
|
||||
allowed_permission_profiles: Some(BTreeMap::from([("managed".to_string(), true)])),
|
||||
default_permissions: Some("managed".to_string()),
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
|
||||
allow_managed_hooks_only: Some(true),
|
||||
@@ -1753,10 +1773,11 @@ mod tests {
|
||||
source.clone(),
|
||||
)),
|
||||
allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)),
|
||||
allowed_permissions: Some(Sourced::new(
|
||||
vec!["managed".to_string()],
|
||||
allowed_permission_profiles: Some(Sourced::new(
|
||||
BTreeMap::from([("managed".to_string(), true)]),
|
||||
source.clone(),
|
||||
)),
|
||||
default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)),
|
||||
allowed_web_search_modes: Some(Sourced::new(
|
||||
allowed_web_search_modes,
|
||||
enforce_source.clone(),
|
||||
@@ -1809,7 +1830,8 @@ mod tests {
|
||||
)),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
allowed_permission_profiles: None,
|
||||
default_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
@@ -1861,7 +1883,8 @@ mod tests {
|
||||
)),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
allowed_permission_profiles: None,
|
||||
default_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
|
||||
@@ -176,7 +176,8 @@ fn populate_merged_regular_fields_with_sources(
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions,
|
||||
allowed_permission_profiles,
|
||||
default_permissions,
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
@@ -201,7 +202,11 @@ fn populate_merged_regular_fields_with_sources(
|
||||
&["allowed_approvals_reviewers"]
|
||||
);
|
||||
set_sourced!(allowed_sandbox_modes, &["allowed_sandbox_modes"]);
|
||||
set_sourced!(allowed_permissions, &["allowed_permissions"]);
|
||||
set_sourced!(
|
||||
allowed_permission_profiles,
|
||||
&["allowed_permission_profiles"]
|
||||
);
|
||||
set_sourced!(default_permissions, &["default_permissions"]);
|
||||
set_sourced!(allowed_web_search_modes, &["allowed_web_search_modes"]);
|
||||
set_sourced!(allow_managed_hooks_only, &["allow_managed_hooks_only"]);
|
||||
set_sourced!(allow_appshots, &["allow_appshots"]);
|
||||
|
||||
@@ -62,6 +62,11 @@ fn top_level_values_use_toml_priority() {
|
||||
r#"
|
||||
allowed_approval_policies = ["on-request"]
|
||||
allowed_sandbox_modes = ["workspace-write"]
|
||||
default_permissions = ":workspace"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
":read-only" = true
|
||||
":workspace" = true
|
||||
"#,
|
||||
),
|
||||
layer(
|
||||
@@ -70,6 +75,11 @@ allowed_sandbox_modes = ["workspace-write"]
|
||||
r#"
|
||||
allowed_approval_policies = ["never"]
|
||||
allowed_sandbox_modes = ["read-only"]
|
||||
default_permissions = ":read-only"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
":danger-full-access" = false
|
||||
":workspace" = false
|
||||
"#,
|
||||
),
|
||||
])
|
||||
@@ -82,6 +92,12 @@ allowed_sandbox_modes = ["read-only"]
|
||||
r#"
|
||||
allowed_approval_policies = ["never"]
|
||||
allowed_sandbox_modes = ["read-only"]
|
||||
default_permissions = ":read-only"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
":danger-full-access" = false
|
||||
":read-only" = true
|
||||
":workspace" = false
|
||||
"#
|
||||
)
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -1375,7 +1375,10 @@ default_permissions = "managed-standard"
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard"]
|
||||
default_permissions = "managed-standard"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-standard = true
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":workspace"
|
||||
@@ -1397,8 +1400,8 @@ extends = ":workspace"
|
||||
config
|
||||
.config_layer_stack
|
||||
.requirements_toml()
|
||||
.allowed_permissions,
|
||||
Some(vec!["managed-standard".to_string()])
|
||||
.allowed_permission_profiles,
|
||||
Some(BTreeMap::from([("managed-standard".to_string(), true)]))
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
@@ -1411,26 +1414,9 @@ extends = ":workspace"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_allowed_permissions_keep_builtin_permission_fallbacks() -> anyhow::Result<()> {
|
||||
for (trust_level, expected_profile) in [
|
||||
(
|
||||
Some(TrustLevel::Trusted),
|
||||
if cfg!(target_os = "windows") {
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY
|
||||
} else {
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE
|
||||
},
|
||||
),
|
||||
(
|
||||
Some(TrustLevel::Untrusted),
|
||||
if cfg!(target_os = "windows") {
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY
|
||||
} else {
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE
|
||||
},
|
||||
),
|
||||
(None, BUILT_IN_PERMISSION_PROFILE_READ_ONLY),
|
||||
] {
|
||||
async fn system_allowed_permission_profiles_select_managed_default_without_local_default()
|
||||
-> anyhow::Result<()> {
|
||||
for trust_level in [Some(TrustLevel::Trusted), Some(TrustLevel::Untrusted), None] {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
@@ -1447,10 +1433,17 @@ async fn system_allowed_permissions_keep_builtin_permission_fallbacks() -> anyho
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard"]
|
||||
default_permissions = "managed-standard"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-build = true
|
||||
managed-standard = true
|
||||
|
||||
[permissions.managed-standard.filesystem]
|
||||
":workspace_roots" = "read"
|
||||
|
||||
[permissions.managed-build]
|
||||
extends = ":workspace"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
@@ -1470,30 +1463,189 @@ allowed_permissions = ["managed-standard"]
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
Some(expected_profile.to_string()),
|
||||
Some("managed-standard".to_string()),
|
||||
"trust level {trust_level:?}",
|
||||
);
|
||||
assert!(
|
||||
!config.startup_warnings.iter().any(|warning| warning
|
||||
.contains("Configured value for `permission_profile` is disallowed")),
|
||||
"{:?}",
|
||||
config.startup_warnings
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_allowed_permissions_keep_explicit_builtin_defaults() -> anyhow::Result<()> {
|
||||
async fn system_allowed_permission_profiles_require_managed_default() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
[permissions.managed-standard]
|
||||
extends = ":read-only"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-standard = true
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
overrides.system_requirements_path = Some(requirements_path);
|
||||
let err = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.fallback_cwd(Some(tmp.path().to_path_buf()))
|
||||
.loader_overrides(overrides)
|
||||
.build()
|
||||
.await
|
||||
.expect_err("allowed_permission_profiles without default_permissions should fail");
|
||||
|
||||
assert!(
|
||||
err.to_string().contains(
|
||||
"default_permissions must be set unless allowed_permission_profiles allows both"
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_allowed_permission_profiles_standard_pair_defaults_to_workspace()
|
||||
-> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
[allowed_permission_profiles]
|
||||
":read-only" = true
|
||||
":workspace" = true
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
overrides.system_requirements_path = Some(requirements_path);
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.fallback_cwd(Some(tmp.path().to_path_buf()))
|
||||
.loader_overrides(overrides)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_managed_default_must_be_allowed() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
default_permissions = "managed-build"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-standard = true
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":read-only"
|
||||
|
||||
[permissions.managed-build]
|
||||
extends = ":workspace"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
overrides.system_requirements_path = Some(requirements_path);
|
||||
let err = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.fallback_cwd(Some(tmp.path().to_path_buf()))
|
||||
.loader_overrides(overrides)
|
||||
.build()
|
||||
.await
|
||||
.expect_err("managed default outside allowed_permission_profiles should fail");
|
||||
|
||||
assert!(
|
||||
err.to_string().contains(
|
||||
"default_permissions `managed-build` must be allowed by allowed_permission_profiles"
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_managed_default_requires_allowed_permission_profiles() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
default_permissions = ":read-only"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
overrides.system_requirements_path = Some(requirements_path);
|
||||
let err = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.fallback_cwd(Some(tmp.path().to_path_buf()))
|
||||
.loader_overrides(overrides)
|
||||
.build()
|
||||
.await
|
||||
.expect_err("managed default without allowed_permission_profiles should fail");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("default_permissions requires allowed_permission_profiles"),
|
||||
"{err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_allowed_permission_profiles_fall_back_from_disallowed_danger_full_access()
|
||||
-> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
tokio::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
default_permissions = ":workspace"
|
||||
"#,
|
||||
format!(
|
||||
r#"
|
||||
default_permissions = "{BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS}"
|
||||
"#
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard"]
|
||||
default_permissions = "managed-standard"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-standard = true
|
||||
|
||||
[permissions.managed-standard.filesystem]
|
||||
":workspace_roots" = "read"
|
||||
@@ -1516,7 +1668,67 @@ allowed_permissions = ["managed-standard"]
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
|
||||
Some("managed-standard".to_string())
|
||||
);
|
||||
assert!(
|
||||
config.startup_warnings.iter().any(|warning| warning
|
||||
.contains("Configured value for `permission_profile` is disallowed by requirements")),
|
||||
"{:?}",
|
||||
config.startup_warnings
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_allowed_permission_profiles_fall_back_from_disallowed_workspace()
|
||||
-> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
tokio::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
default_permissions = ":workspace"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
default_permissions = "managed-standard"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-standard = true
|
||||
|
||||
[permissions.managed-standard.filesystem]
|
||||
":workspace_roots" = "read"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?;
|
||||
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
overrides.system_requirements_path = Some(requirements_path);
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.fallback_cwd(Some(cwd.to_path_buf()))
|
||||
.loader_overrides(overrides)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
Some("managed-standard".to_string())
|
||||
);
|
||||
assert!(
|
||||
config.startup_warnings.iter().any(|warning| warning
|
||||
.contains("Configured value for `permission_profile` is disallowed by requirements")),
|
||||
"{:?}",
|
||||
config.startup_warnings
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1538,7 +1750,11 @@ default_permissions = "managed-build"
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard", "managed-build"]
|
||||
default_permissions = "managed-standard"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-build = true
|
||||
managed-standard = true
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":read-only"
|
||||
@@ -1579,7 +1795,10 @@ async fn system_requirements_warn_for_disallowed_explicit_permission_override()
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard"]
|
||||
default_permissions = "managed-standard"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-standard = true
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":workspace"
|
||||
|
||||
@@ -8351,7 +8351,8 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
allowed_approval_policies: None,
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
allowed_permission_profiles: None,
|
||||
default_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||
allow_managed_hooks_only: None,
|
||||
|
||||
@@ -119,6 +119,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::permissions::BUILT_IN_READ_ONLY_PROFILE;
|
||||
use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE;
|
||||
use crate::config::permissions::apply_network_proxy_feature_config;
|
||||
use crate::config::permissions::builtin_permission_profile;
|
||||
@@ -3835,7 +3836,9 @@ fn resolve_effective_permission_selection<'a>(
|
||||
Ok(EffectivePermissionSelection {
|
||||
profiles,
|
||||
selected_profile_id,
|
||||
requirements_force_profile_selection: requirements_toml.allowed_permissions.is_some(),
|
||||
requirements_force_profile_selection: requirements_toml
|
||||
.allowed_permission_profiles
|
||||
.is_some(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3845,28 +3848,36 @@ fn resolve_default_permissions<'a>(
|
||||
requirements_toml: &'a ConfigRequirementsToml,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> std::io::Result<Option<&'a str>> {
|
||||
let allowed_permissions = requirements_toml.allowed_permissions.as_ref();
|
||||
let mut default_permissions = default_permissions_override.or(configured_default_permissions);
|
||||
if let (Some(selected_permissions), Some(allowed_permissions)) =
|
||||
(default_permissions, allowed_permissions)
|
||||
&& !is_builtin_permission_profile_name(selected_permissions)
|
||||
&& !allowed_permissions
|
||||
.iter()
|
||||
.any(|allowed_permission| allowed_permission == selected_permissions)
|
||||
{
|
||||
let Some(fallback_permissions) = allowed_permissions.first().map(String::as_str) else {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"requirements.toml allowed_permissions must include at least one profile",
|
||||
));
|
||||
};
|
||||
startup_warnings.push(format!(
|
||||
"Configured value for `permission_profile` is disallowed by requirements; falling back from `{selected_permissions}` to required value `{fallback_permissions}`."
|
||||
let selected_permissions = default_permissions_override.or(configured_default_permissions);
|
||||
let Some(allowed_permission_profiles) = requirements_toml.allowed_permission_profiles.as_ref()
|
||||
else {
|
||||
return Ok(selected_permissions);
|
||||
};
|
||||
let Some(fallback_permissions) = requirements_toml
|
||||
.default_permissions
|
||||
.as_deref()
|
||||
.or_else(|| implicit_default_permissions(allowed_permission_profiles))
|
||||
else {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"requirements.toml default_permissions must be set unless allowed_permission_profiles allows both `:workspace` and `:read-only`",
|
||||
));
|
||||
default_permissions = Some(fallback_permissions);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(default_permissions)
|
||||
match selected_permissions {
|
||||
None => Ok(Some(fallback_permissions)),
|
||||
Some(selected_permissions)
|
||||
if is_permission_allowed(allowed_permission_profiles, selected_permissions) =>
|
||||
{
|
||||
Ok(Some(selected_permissions))
|
||||
}
|
||||
Some(selected_permissions) => {
|
||||
startup_warnings.push(format!(
|
||||
"Configured value for `permission_profile` is disallowed by requirements; falling back from `{selected_permissions}` to required value `{fallback_permissions}`."
|
||||
));
|
||||
Ok(Some(fallback_permissions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_required_permission_profile_catalog(
|
||||
@@ -3880,30 +3891,67 @@ fn validate_required_permission_profile_catalog(
|
||||
.is_some_and(|permissions| permissions.entries.contains_key(profile_id))
|
||||
};
|
||||
|
||||
let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else {
|
||||
let Some(allowed_permission_profiles) = requirements_toml.allowed_permission_profiles.as_ref()
|
||||
else {
|
||||
if requirements_toml.default_permissions.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"requirements.toml default_permissions requires allowed_permission_profiles",
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
if allowed_permissions.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"requirements.toml allowed_permissions must include at least one profile",
|
||||
));
|
||||
}
|
||||
|
||||
for profile_id in allowed_permissions {
|
||||
for profile_id in allowed_permission_profiles.keys() {
|
||||
if !is_known_profile(profile_id) {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"requirements.toml allowed_permissions refers to undefined profile `{profile_id}`"
|
||||
"requirements.toml allowed_permission_profiles refers to undefined profile `{profile_id}`"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let Some(default_permissions) = requirements_toml
|
||||
.default_permissions
|
||||
.as_deref()
|
||||
.or_else(|| implicit_default_permissions(allowed_permission_profiles))
|
||||
else {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"requirements.toml default_permissions must be set unless allowed_permission_profiles allows both `:workspace` and `:read-only`",
|
||||
));
|
||||
};
|
||||
if !is_permission_allowed(allowed_permission_profiles, default_permissions) {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"requirements.toml default_permissions `{default_permissions}` must be allowed by allowed_permission_profiles"
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn implicit_default_permissions(
|
||||
allowed_permission_profiles: &BTreeMap<String, bool>,
|
||||
) -> Option<&'static str> {
|
||||
(is_permission_allowed(allowed_permission_profiles, BUILT_IN_WORKSPACE_PROFILE)
|
||||
&& is_permission_allowed(allowed_permission_profiles, BUILT_IN_READ_ONLY_PROFILE))
|
||||
.then_some(BUILT_IN_WORKSPACE_PROFILE)
|
||||
}
|
||||
|
||||
fn is_permission_allowed(
|
||||
allowed_permission_profiles: &BTreeMap<String, bool>,
|
||||
profile_id: &str,
|
||||
) -> bool {
|
||||
allowed_permission_profiles
|
||||
.get(profile_id)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn normalize_guardian_policy_config(value: Option<&str>) -> Option<String> {
|
||||
value.and_then(|value| {
|
||||
let trimmed = value.trim();
|
||||
|
||||
@@ -702,7 +702,8 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]),
|
||||
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
|
||||
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
|
||||
allowed_permissions: None,
|
||||
allowed_permission_profiles: None,
|
||||
default_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
|
||||
allow_managed_hooks_only: Some(true),
|
||||
@@ -973,7 +974,8 @@ approval_policy = "never"
|
||||
allowed_approval_policies: None,
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
allowed_permission_profiles: None,
|
||||
default_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(Vec::new()),
|
||||
allow_managed_hooks_only: None,
|
||||
|
||||
Reference in New Issue
Block a user