mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: support managed permission profiles in requirements.toml (#23433)
## Why Cloud-managed `requirements.toml` should be able to define the managed permission profiles a client may select and constrain that selectable set without requiring local user config to recreate the profile catalog. This keeps requirements focused on restrictions. The selected default remains a config or session choice, while requirements contribute the managed profile bodies and `allowed_permissions` allowlist that the config-loading boundary validates before a resolved runtime `PermissionProfile` is installed. ## What changed - Add `requirements.toml` support for a managed permission-profile catalog plus its allowlist: ```toml allowed_permissions = ["review", "build"] [permissions.review] extends = ":read-only" [permissions.build] extends = ":workspace" ``` - Merge requirements-defined profile bodies into the effective permission catalog and reject profile ids that collide with config-defined profiles. - Validate that every `allowed_permissions` entry resolves to a built-in or catalog profile before selection uses it. - Preserve allowed configured named-profile selections. When a configured named profile is disallowed, fall back to the first allowed requirements profile with a startup warning. - Keep built-in selections and the stock trust-based `:read-only` / `:workspace` fallback path intact when no permission profile is explicitly selected. - Centralize the managed catalog and allowlist selection path in `EffectivePermissionSelection` so the requirements boundary is visible in config loading. - Surface `allowedPermissions` through `configRequirements/read`, and update the generated app-server schema fixtures plus the app-server README. ## Validation - `cargo test -p codex-config` - `cargo test -p codex-core system_requirements_` - `cargo test -p codex-core system_allowed_permissions_` - `cargo test -p codex-app-server-protocol` - `just write-app-server-schema` ## Related work - Uses merged permission-profile inheritance support from #22270 and #23705. - Kept separate from the in-flight permission profile listing API in #23412.
This commit is contained in:
committed by
GitHub
Unverified
parent
c9ff067e31
commit
713a5b1b00
+9
@@ -7744,6 +7744,15 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissions": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedSandboxModes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/SandboxMode"
|
||||
|
||||
+9
@@ -4113,6 +4113,15 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissions": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedSandboxModes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/SandboxMode"
|
||||
|
||||
+9
@@ -88,6 +88,15 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissions": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedSandboxModes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/SandboxMode"
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { ComputerUseRequirements } from "./ComputerUseRequirements";
|
||||
import type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: 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, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||
|
||||
@@ -386,6 +386,7 @@ pub struct ConfigRequirements {
|
||||
#[experimental("configRequirements/read.allowedApprovalsReviewers")]
|
||||
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
||||
pub allowed_permissions: Option<Vec<String>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||
pub allow_managed_hooks_only: Option<bool>,
|
||||
pub computer_use: Option<ComputerUseRequirements>,
|
||||
|
||||
@@ -1728,6 +1728,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
|
||||
}]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
computer_use: None,
|
||||
|
||||
@@ -226,7 +226,7 @@ Example with notification opt-out:
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish).
|
||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk; 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`), 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`, `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`.
|
||||
|
||||
### Example: Start or resume a thread
|
||||
|
||||
|
||||
@@ -419,6 +419,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
||||
.filter_map(map_sandbox_mode_requirement_to_api)
|
||||
.collect()
|
||||
}),
|
||||
allowed_permissions: requirements.allowed_permissions,
|
||||
allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| {
|
||||
let mut normalized = modes
|
||||
.into_iter()
|
||||
@@ -638,10 +639,21 @@ mod tests {
|
||||
#[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);
|
||||
}
|
||||
|
||||
@@ -1216,6 +1216,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1300,6 +1301,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1335,6 +1337,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1387,6 +1390,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1568,6 +1572,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1650,6 +1655,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1730,6 +1736,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1938,6 +1945,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1980,6 +1988,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -2042,6 +2051,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -2100,6 +2110,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -2160,6 +2171,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -2221,6 +2233,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -2286,6 +2299,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -2377,6 +2391,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -2414,6 +2429,7 @@ command = "sample-mcp"
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::Constrained;
|
||||
use crate::ConstraintError;
|
||||
use crate::ManagedHooksRequirementsToml;
|
||||
use crate::mcp_types::AppToolApproval;
|
||||
use crate::permissions_toml::PermissionProfileToml;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RequirementSource {
|
||||
@@ -432,14 +433,58 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct FilesystemRequirementsToml {
|
||||
pub deny_read: Option<Vec<FilesystemDenyReadPattern>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawFilesystemRequirementsToml {
|
||||
deny_read: Option<Vec<FilesystemDenyReadPattern>>,
|
||||
description: Option<serde::de::IgnoredAny>,
|
||||
extends: Option<serde::de::IgnoredAny>,
|
||||
workspace_roots: Option<serde::de::IgnoredAny>,
|
||||
filesystem: Option<serde::de::IgnoredAny>,
|
||||
network: Option<serde::de::IgnoredAny>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FilesystemRequirementsToml {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw = RawFilesystemRequirementsToml::deserialize(deserializer)?;
|
||||
let RawFilesystemRequirementsToml {
|
||||
deny_read,
|
||||
description,
|
||||
extends,
|
||||
workspace_roots,
|
||||
filesystem,
|
||||
network,
|
||||
} = raw;
|
||||
|
||||
if description.is_some()
|
||||
|| extends.is_some()
|
||||
|| workspace_roots.is_some()
|
||||
|| filesystem.is_some()
|
||||
|| network.is_some()
|
||||
{
|
||||
return Err(D::Error::custom(
|
||||
"`permissions.filesystem` is reserved for requirements-level filesystem constraints and cannot define a profile",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self { deny_read })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct PermissionsRequirementsToml {
|
||||
pub filesystem: Option<FilesystemRequirementsToml>,
|
||||
// For legacy reasons, `filesystem` stays reserved for requirements-level
|
||||
// filesystem constraints and cannot name a profile.
|
||||
#[serde(default, flatten)]
|
||||
pub profiles: BTreeMap<String, PermissionProfileToml>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -701,6 +746,7 @@ 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 remote_sandbox_config: Option<Vec<RemoteSandboxConfigToml>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
|
||||
pub allow_managed_hooks_only: Option<bool>,
|
||||
@@ -752,6 +798,7 @@ 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_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
|
||||
pub allow_managed_hooks_only: Option<Sourced<bool>>,
|
||||
pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
|
||||
@@ -789,6 +836,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies: _,
|
||||
allowed_approvals_reviewers: _,
|
||||
allowed_sandbox_modes: _,
|
||||
allowed_permissions: _,
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes: _,
|
||||
allow_managed_hooks_only: _,
|
||||
@@ -821,6 +869,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
computer_use,
|
||||
@@ -850,6 +899,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
computer_use,
|
||||
@@ -868,6 +918,7 @@ 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),
|
||||
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),
|
||||
@@ -953,6 +1004,7 @@ 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.remote_sandbox_config.is_none()
|
||||
&& self.allowed_web_search_modes.is_none()
|
||||
&& self.allow_managed_hooks_only.is_none()
|
||||
@@ -992,10 +1044,14 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
type Error = ConstraintError;
|
||||
|
||||
fn try_from(toml: ConfigRequirementsWithSources) -> Result<Self, Self::Error> {
|
||||
// Profile catalog selection remains on ConfigRequirementsToml for
|
||||
// config loading and requirements API projection. The normalized
|
||||
// constraints below only need the compiled PermissionProfile envelope.
|
||||
let ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions: _,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
computer_use,
|
||||
@@ -1301,6 +1357,7 @@ mod tests {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
allowed_sandbox_modes,
|
||||
allowed_permissions,
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
@@ -1323,6 +1380,8 @@ 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
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allowed_web_search_modes: allowed_web_search_modes
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allow_managed_hooks_only: allow_managed_hooks_only
|
||||
@@ -1370,6 +1429,61 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_managed_permission_profiles() -> Result<()> {
|
||||
let requirements: ConfigRequirementsToml = from_str(
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard", "managed-build"]
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":workspace"
|
||||
|
||||
[permissions.managed-build]
|
||||
extends = "managed-standard"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
requirements.allowed_permissions,
|
||||
Some(vec![
|
||||
"managed-standard".to_string(),
|
||||
"managed-build".to_string(),
|
||||
])
|
||||
);
|
||||
let permissions = requirements
|
||||
.permissions
|
||||
.as_ref()
|
||||
.expect("managed permission profiles");
|
||||
assert!(permissions.profiles.contains_key("managed-standard"));
|
||||
assert!(
|
||||
permissions
|
||||
.profiles
|
||||
.get("managed-build")
|
||||
.and_then(|profile| profile.extends.as_deref())
|
||||
.is_some()
|
||||
);
|
||||
assert!(!requirements.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filesystem_requirements_table_cannot_define_a_permission_profile() {
|
||||
let err = from_str::<ConfigRequirementsToml>(
|
||||
r#"
|
||||
[permissions.filesystem]
|
||||
extends = ":workspace"
|
||||
"#,
|
||||
)
|
||||
.expect_err("filesystem requirements cannot define a permission profile");
|
||||
|
||||
assert!(
|
||||
err.to_string().contains(
|
||||
"`permissions.filesystem` is reserved for requirements-level filesystem constraints and cannot define a profile"
|
||||
),
|
||||
"unexpected error: {err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_computer_use_requirements() -> Result<()> {
|
||||
let requirements: ConfigRequirementsToml = from_str(
|
||||
@@ -1421,6 +1535,7 @@ 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()]),
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
|
||||
allow_managed_hooks_only: Some(true),
|
||||
@@ -1451,6 +1566,10 @@ mod tests {
|
||||
source.clone(),
|
||||
)),
|
||||
allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)),
|
||||
allowed_permissions: Some(Sourced::new(
|
||||
vec!["managed".to_string()],
|
||||
source.clone(),
|
||||
)),
|
||||
allowed_web_search_modes: Some(Sourced::new(
|
||||
allowed_web_search_modes,
|
||||
enforce_source.clone(),
|
||||
@@ -1501,6 +1620,7 @@ mod tests {
|
||||
)),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
computer_use: None,
|
||||
@@ -1550,6 +1670,7 @@ mod tests {
|
||||
)),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
computer_use: None,
|
||||
|
||||
@@ -27,6 +27,8 @@ use codex_config::loader::load_requirements_toml;
|
||||
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_WORKSPACE;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -1095,6 +1097,7 @@ allowed_approval_policies = ["on-request"]
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1154,6 +1157,7 @@ allowed_approval_policies = ["on-request"]
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1362,6 +1366,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -1411,6 +1416,264 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_requirements_define_managed_permission_profiles() -> 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 = "managed-standard"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard"]
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":workspace"
|
||||
"#,
|
||||
)
|
||||
.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
|
||||
.config_layer_stack
|
||||
.requirements_toml()
|
||||
.allowed_permissions,
|
||||
Some(vec!["managed-standard".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
Some("managed-standard".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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),
|
||||
] {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
if let Some(trust_level) = trust_level {
|
||||
make_config_for_test(
|
||||
&codex_home,
|
||||
tmp.path(),
|
||||
trust_level,
|
||||
/*project_root_markers*/ None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard"]
|
||||
|
||||
[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(expected_profile.to_string()),
|
||||
"trust level {trust_level:?}",
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_allowed_permissions_keep_explicit_builtin_defaults() -> 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#"
|
||||
allowed_permissions = ["managed-standard"]
|
||||
|
||||
[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(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_requirements_preserve_allowed_configured_permission_default() -> 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 = "managed-build"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
allowed_permissions = ["managed-standard", "managed-build"]
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":read-only"
|
||||
|
||||
[permissions.managed-build]
|
||||
extends = ":workspace"
|
||||
"#,
|
||||
)
|
||||
.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-build".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_requirements_warn_for_disallowed_explicit_permission_override() -> 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_permissions = ["managed-standard"]
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":workspace"
|
||||
"#,
|
||||
)
|
||||
.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()))
|
||||
.harness_overrides(ConfigOverrides {
|
||||
default_permissions: Some("managed-build".to_string()),
|
||||
..ConfigOverrides::default()
|
||||
})
|
||||
.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(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_can_ignore_managed_requirements() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
|
||||
@@ -9091,6 +9091,7 @@ 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,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||
allow_managed_hooks_only: None,
|
||||
@@ -9875,6 +9876,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
||||
allowed_approval_policies: None,
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
||||
allowed_permissions: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
|
||||
+190
-32
@@ -33,6 +33,7 @@ use codex_config::config_toml::ThreadStoreToml;
|
||||
use codex_config::config_toml::validate_model_providers;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::profile_toml::ConfigProfile;
|
||||
use codex_config::sandbox_mode_requirement_for_permission_profile;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
@@ -1997,6 +1998,38 @@ struct PermissionSelectionToml {
|
||||
sandbox_mode: Option<SandboxMode>,
|
||||
}
|
||||
|
||||
// Resolve the named-profile catalog and selected profile id together. Runtime
|
||||
// profile constraints are applied later after this selection compiles into a
|
||||
// concrete `PermissionProfile`.
|
||||
#[derive(Debug)]
|
||||
struct EffectivePermissionSelection<'a> {
|
||||
profiles: Option<PermissionsToml>,
|
||||
selected_profile_id: Option<&'a str>,
|
||||
requirements_force_profile_selection: bool,
|
||||
}
|
||||
|
||||
impl EffectivePermissionSelection<'_> {
|
||||
fn has_profiles(&self) -> bool {
|
||||
self.profiles
|
||||
.as_ref()
|
||||
.is_some_and(|profiles| !profiles.is_empty())
|
||||
}
|
||||
|
||||
fn profiles_are_active(
|
||||
&self,
|
||||
default_permissions_override: Option<&str>,
|
||||
permission_config_syntax: Option<PermissionConfigSyntax>,
|
||||
) -> bool {
|
||||
self.requirements_force_profile_selection
|
||||
|| default_permissions_override.is_some()
|
||||
|| matches!(
|
||||
permission_config_syntax,
|
||||
Some(PermissionConfigSyntax::Profiles)
|
||||
)
|
||||
|| permission_config_syntax.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_permission_config_syntax(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
cfg: &ConfigToml,
|
||||
@@ -2621,20 +2654,21 @@ impl Config {
|
||||
sandbox_mode,
|
||||
config_profile.sandbox_mode,
|
||||
);
|
||||
let has_permission_profiles = cfg
|
||||
.permissions
|
||||
.as_ref()
|
||||
.is_some_and(|profiles| !profiles.is_empty());
|
||||
let default_permissions = default_permissions_override
|
||||
.as_deref()
|
||||
.or(cfg.default_permissions.as_deref());
|
||||
validate_user_permission_profile_names(cfg.permissions.as_ref())?;
|
||||
if has_permission_profiles
|
||||
let requirements_toml = config_layer_stack.requirements_toml();
|
||||
let effective_permission_selection = resolve_effective_permission_selection(
|
||||
cfg.permissions.as_ref(),
|
||||
default_permissions_override.as_deref(),
|
||||
cfg.default_permissions.as_deref(),
|
||||
requirements_toml,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
if effective_permission_selection.has_profiles()
|
||||
&& !matches!(
|
||||
permission_config_syntax,
|
||||
Some(PermissionConfigSyntax::Legacy)
|
||||
)
|
||||
&& default_permissions.is_none()
|
||||
&& effective_permission_selection.selected_profile_id.is_none()
|
||||
&& !effective_permission_selection.requirements_force_profile_selection
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
@@ -2651,12 +2685,10 @@ impl Config {
|
||||
std::fs::create_dir_all(&memories_root)?;
|
||||
let internal_writable_roots = vec![memories_root];
|
||||
|
||||
let profiles_are_active = default_permissions_override.is_some()
|
||||
|| matches!(
|
||||
permission_config_syntax,
|
||||
Some(PermissionConfigSyntax::Profiles)
|
||||
)
|
||||
|| permission_config_syntax.is_none();
|
||||
let profiles_are_active = effective_permission_selection.profiles_are_active(
|
||||
default_permissions_override.as_deref(),
|
||||
permission_config_syntax,
|
||||
);
|
||||
let explicit_permission_profile_mode = default_permissions_override.is_some()
|
||||
|| matches!(
|
||||
permission_config_syntax,
|
||||
@@ -2668,9 +2700,11 @@ impl Config {
|
||||
.map_or_else(Vec::new, |permissions| {
|
||||
permissions.entries.keys().cloned().collect()
|
||||
});
|
||||
let using_implicit_builtin_profile =
|
||||
permission_config_syntax.is_none() && default_permissions.is_none();
|
||||
let should_seed_legacy_workspace_roots = default_permissions.is_none()
|
||||
let using_implicit_builtin_profile = permission_config_syntax.is_none()
|
||||
&& effective_permission_selection.selected_profile_id.is_none();
|
||||
let should_seed_legacy_workspace_roots = effective_permission_selection
|
||||
.selected_profile_id
|
||||
.is_none()
|
||||
&& matches!(
|
||||
permission_config_syntax,
|
||||
None | Some(PermissionConfigSyntax::Legacy)
|
||||
@@ -2720,14 +2754,16 @@ impl Config {
|
||||
// PermissionProfile carries the active network sandbox bit, not the configured
|
||||
// proxy/allowlist policy. Keep that config so active profiles can round-trip
|
||||
// without broadening network behavior.
|
||||
let default_permissions = default_permissions.unwrap_or_else(|| {
|
||||
default_builtin_permission_profile_name(
|
||||
&active_project,
|
||||
windows_sandbox_level,
|
||||
)
|
||||
});
|
||||
let default_permissions = effective_permission_selection
|
||||
.selected_profile_id
|
||||
.unwrap_or_else(|| {
|
||||
default_builtin_permission_profile_name(
|
||||
&active_project,
|
||||
windows_sandbox_level,
|
||||
)
|
||||
});
|
||||
network_proxy_config_for_profile_selection(
|
||||
cfg.permissions.as_ref(),
|
||||
effective_permission_selection.profiles.as_ref(),
|
||||
default_permissions,
|
||||
)?
|
||||
} else {
|
||||
@@ -2765,28 +2801,30 @@ impl Config {
|
||||
Vec::new(),
|
||||
)
|
||||
} else if profiles_are_active {
|
||||
let default_permissions = default_permissions.unwrap_or_else(|| {
|
||||
default_builtin_permission_profile_name(&active_project, windows_sandbox_level)
|
||||
});
|
||||
let default_permissions = effective_permission_selection
|
||||
.selected_profile_id
|
||||
.unwrap_or_else(|| {
|
||||
default_builtin_permission_profile_name(&active_project, windows_sandbox_level)
|
||||
});
|
||||
let builtin_workspace_write_settings = if using_implicit_builtin_profile {
|
||||
cfg.sandbox_workspace_write.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let configured_network_proxy_config = network_proxy_config_for_profile_selection(
|
||||
cfg.permissions.as_ref(),
|
||||
effective_permission_selection.profiles.as_ref(),
|
||||
default_permissions,
|
||||
)?;
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
compile_permission_profile_selection(
|
||||
cfg.permissions.as_ref(),
|
||||
effective_permission_selection.profiles.as_ref(),
|
||||
default_permissions,
|
||||
builtin_workspace_write_settings,
|
||||
resolved_cwd.as_path(),
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let mut configured_workspace_roots = compile_permission_profile_workspace_roots(
|
||||
cfg.permissions.as_ref(),
|
||||
effective_permission_selection.profiles.as_ref(),
|
||||
default_permissions,
|
||||
resolved_cwd.as_path(),
|
||||
)?;
|
||||
@@ -3779,6 +3817,126 @@ fn guardian_policy_config_from_requirements(
|
||||
normalize_guardian_policy_config(requirements_toml.guardian_policy_config.as_deref())
|
||||
}
|
||||
|
||||
fn merge_managed_permission_profiles(
|
||||
configured_permissions: Option<&PermissionsToml>,
|
||||
requirements_toml: &ConfigRequirementsToml,
|
||||
) -> std::io::Result<Option<PermissionsToml>> {
|
||||
let managed_profiles = requirements_toml
|
||||
.permissions
|
||||
.as_ref()
|
||||
.map(|permissions| &permissions.profiles)
|
||||
.filter(|profiles| !profiles.is_empty());
|
||||
let Some(managed_profiles) = managed_profiles else {
|
||||
return Ok(configured_permissions.cloned());
|
||||
};
|
||||
|
||||
let mut merged_permissions = configured_permissions.cloned().unwrap_or_default();
|
||||
for (profile_id, managed_profile) in managed_profiles {
|
||||
if merged_permissions.entries.contains_key(profile_id) {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"requirements.toml permissions profile `{profile_id}` conflicts with a config-defined profile of the same name"
|
||||
),
|
||||
));
|
||||
}
|
||||
merged_permissions
|
||||
.entries
|
||||
.insert(profile_id.clone(), managed_profile.clone());
|
||||
}
|
||||
|
||||
Ok(Some(merged_permissions))
|
||||
}
|
||||
|
||||
fn resolve_effective_permission_selection<'a>(
|
||||
configured_permissions: Option<&PermissionsToml>,
|
||||
default_permissions_override: Option<&'a str>,
|
||||
configured_default_permissions: Option<&'a str>,
|
||||
requirements_toml: &'a ConfigRequirementsToml,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> std::io::Result<EffectivePermissionSelection<'a>> {
|
||||
let profiles = merge_managed_permission_profiles(configured_permissions, requirements_toml)?;
|
||||
validate_user_permission_profile_names(profiles.as_ref())?;
|
||||
validate_required_permission_profile_catalog(requirements_toml, profiles.as_ref())?;
|
||||
let selected_profile_id = resolve_default_permissions(
|
||||
default_permissions_override,
|
||||
configured_default_permissions,
|
||||
requirements_toml,
|
||||
startup_warnings,
|
||||
)?;
|
||||
|
||||
Ok(EffectivePermissionSelection {
|
||||
profiles,
|
||||
selected_profile_id,
|
||||
requirements_force_profile_selection: requirements_toml.allowed_permissions.is_some(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_default_permissions<'a>(
|
||||
default_permissions_override: Option<&'a str>,
|
||||
configured_default_permissions: Option<&'a str>,
|
||||
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}`."
|
||||
));
|
||||
default_permissions = Some(fallback_permissions);
|
||||
}
|
||||
|
||||
Ok(default_permissions)
|
||||
}
|
||||
|
||||
fn validate_required_permission_profile_catalog(
|
||||
requirements_toml: &ConfigRequirementsToml,
|
||||
available_permissions: Option<&PermissionsToml>,
|
||||
) -> std::io::Result<()> {
|
||||
let is_known_profile = |profile_id: &str| {
|
||||
is_builtin_permission_profile_name(profile_id)
|
||||
|| available_permissions
|
||||
.as_ref()
|
||||
.is_some_and(|permissions| permissions.entries.contains_key(profile_id))
|
||||
};
|
||||
|
||||
let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else {
|
||||
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 {
|
||||
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}`"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_guardian_policy_config(value: Option<&str>) -> Option<String> {
|
||||
value.and_then(|value| {
|
||||
let trimmed = value.trim();
|
||||
|
||||
@@ -697,6 +697,7 @@ 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,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
|
||||
allow_managed_hooks_only: Some(true),
|
||||
@@ -912,6 +913,7 @@ approval_policy = "never"
|
||||
allowed_approval_policies: None,
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_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