mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
permission profiles: expose availability to clients (#26678)
## Why `permissionProfile/list` currently advertises every built-in and configured profile even when effective enterprise requirements prevent selecting it. That forces each client to reconstruct policy from lower-level requirement fields, which is easy to miss and difficult to keep consistent. The catalog should remain complete so clients can explain that an option was disabled by an administrator, while also reporting whether each profile is selectable. ## What - Add an `allowed` field to each permission profile summary. - Build a shared catalog from the effective config and current requirements, including `allowed_sandbox_modes`, `allowed_permissions`, and filesystem restrictions. - Use the shared catalog in app-server and the TUI so disallowed profiles remain visible but cannot be selected. - Use the canonical `:danger-full-access` profile ID in the TUI. - Update the app-server schemas, API documentation, behavioral tests, and TUI snapshots. ## Scope This PR targets `main` directly and is independent of #24852. It preserves the current behavior where built-in profiles are constrained by sandbox-mode requirements and `allowed_permissions` applies to configured profiles. ## Testing - `just test -p codex-core permission_profile_catalog_marks_profiles_disallowed_by_requirements` - `just test -p codex-app-server permission_profile_list` - `just test -p codex-app-server-protocol` - `just test -p codex-tui profile_permissions` - `just fix -p codex-core` - `just fix -p codex-app-server-protocol` - `just fix -p codex-app-server` - `just fix -p codex-tui` - `just fmt` --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Joey Trasatti <joey.trasatti@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
bd5bd953fb
commit
ced3e4b9a7
+5
@@ -13045,6 +13045,10 @@
|
||||
},
|
||||
"PermissionProfileSummary": {
|
||||
"properties": {
|
||||
"allowed": {
|
||||
"description": "Whether the effective requirements allow selecting this profile.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"description": "Optional user-facing description for display in clients.",
|
||||
"type": [
|
||||
@@ -13058,6 +13062,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"allowed",
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
+5
@@ -9449,6 +9449,10 @@
|
||||
},
|
||||
"PermissionProfileSummary": {
|
||||
"properties": {
|
||||
"allowed": {
|
||||
"description": "Whether the effective requirements allow selecting this profile.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"description": "Optional user-facing description for display in clients.",
|
||||
"type": [
|
||||
@@ -9462,6 +9466,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"allowed",
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
+5
@@ -3,6 +3,10 @@
|
||||
"definitions": {
|
||||
"PermissionProfileSummary": {
|
||||
"properties": {
|
||||
"allowed": {
|
||||
"description": "Whether the effective requirements allow selecting this profile.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"description": "Optional user-facing description for display in clients.",
|
||||
"type": [
|
||||
@@ -16,6 +20,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"allowed",
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
+5
-1
@@ -10,4 +10,8 @@ id: string,
|
||||
/**
|
||||
* Optional user-facing description for display in clients.
|
||||
*/
|
||||
description: string | null, };
|
||||
description: string | null,
|
||||
/**
|
||||
* Whether the effective requirements allow selecting this profile.
|
||||
*/
|
||||
allowed: boolean, };
|
||||
|
||||
@@ -378,6 +378,8 @@ pub struct PermissionProfileSummary {
|
||||
pub id: String,
|
||||
/// Optional user-facing description for display in clients.
|
||||
pub description: Option<String>,
|
||||
/// Whether the effective requirements allow selecting this profile.
|
||||
pub allowed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
|
||||
@@ -202,7 +202,7 @@ Example with notification opt-out:
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with model-advertised string reasoning effort options in the catalog's intended progression order, `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. Clients should preserve the `supportedReasoningEfforts` array order rather than deriving order from the effort names.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
|
||||
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text and an `allowed` flag reflecting effective requirements, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for currently supported feature keys. For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default. Invalid keys will be ignored.
|
||||
- `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; optional `connectTimeoutMs` overrides the WebSocket connection timeout; returns `{}` and does not change the default environment.
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
|
||||
|
||||
@@ -396,9 +396,6 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_core::config::permission_profile_catalog;
|
||||
use futures::StreamExt;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -434,35 +434,15 @@ impl CatalogRequestProcessor {
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?,
|
||||
};
|
||||
let effective_config: ConfigToml = config_layer_stack
|
||||
.effective_config()
|
||||
.try_into()
|
||||
.map_err(|err| internal_error(format!("failed to read effective config: {err}")))?;
|
||||
let mut profiles = vec![
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(),
|
||||
description: None,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
description: None,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
|
||||
description: None,
|
||||
},
|
||||
];
|
||||
let mut configured_profiles = effective_config
|
||||
.permissions
|
||||
let profiles = permission_profile_catalog(&config_layer_stack)
|
||||
.map_err(|err| internal_error(format!("failed to resolve permission profiles: {err}")))?
|
||||
.into_iter()
|
||||
.flat_map(|permissions| permissions.entries)
|
||||
.map(|(id, profile)| PermissionProfileSummary {
|
||||
id,
|
||||
.map(|profile| PermissionProfileSummary {
|
||||
id: profile.id,
|
||||
description: profile.description,
|
||||
allowed: profile.allowed,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
configured_profiles.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
profiles.extend(configured_profiles);
|
||||
let total = profiles.len();
|
||||
let effective_limit = limit.unwrap_or(total as u32).max(1) as usize;
|
||||
let effective_limit = effective_limit.min(total);
|
||||
|
||||
@@ -60,22 +60,27 @@ description = "Inspect without writes."
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: "audit".to_string(),
|
||||
description: Some("Inspect without writes.".to_string()),
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: "dev".to_string(),
|
||||
description: Some("Day-to-day coding work.".to_string()),
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
next_cursor: None,
|
||||
@@ -126,14 +131,17 @@ description = "Project-scoped profile."
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
next_cursor: Some("3".to_string()),
|
||||
@@ -155,6 +163,7 @@ description = "Project-scoped profile."
|
||||
data: vec![PermissionProfileSummary {
|
||||
id: "project".to_string(),
|
||||
description: Some("Project-scoped profile.".to_string()),
|
||||
allowed: true,
|
||||
}],
|
||||
next_cursor: None,
|
||||
}
|
||||
@@ -200,18 +209,22 @@ description = "Project-scoped profile."
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: "project".to_string(),
|
||||
description: Some("Project-scoped profile.".to_string()),
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
next_cursor: None,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::ConstraintError;
|
||||
use crate::config::PermissionProfileCatalogEntry;
|
||||
use crate::config::permission_profile_catalog;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::CloudConfigBundleLoadError;
|
||||
@@ -1733,6 +1735,74 @@ managed-standard = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_catalog_marks_profiles_disallowed_by_requirements() -> 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_sandbox_modes = ["read-only", "workspace-write"]
|
||||
default_permissions = "managed-standard"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-standard = true
|
||||
|
||||
[permissions.managed-standard]
|
||||
extends = ":workspace"
|
||||
|
||||
[permissions.managed-disabled]
|
||||
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!(
|
||||
permission_profile_catalog(&config.config_layer_stack)?,
|
||||
vec![
|
||||
PermissionProfileCatalogEntry {
|
||||
id: ":read-only".to_string(),
|
||||
description: None,
|
||||
allowed: false,
|
||||
},
|
||||
PermissionProfileCatalogEntry {
|
||||
id: ":workspace".to_string(),
|
||||
description: None,
|
||||
allowed: false,
|
||||
},
|
||||
PermissionProfileCatalogEntry {
|
||||
id: ":danger-full-access".to_string(),
|
||||
description: None,
|
||||
allowed: false,
|
||||
},
|
||||
PermissionProfileCatalogEntry {
|
||||
id: "managed-disabled".to_string(),
|
||||
description: None,
|
||||
allowed: false,
|
||||
},
|
||||
PermissionProfileCatalogEntry {
|
||||
id: "managed-standard".to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn system_requirements_preserve_allowed_configured_permission_default() -> anyhow::Result<()>
|
||||
{
|
||||
|
||||
@@ -3192,9 +3192,10 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
|
||||
|
||||
assert_eq!(
|
||||
config.custom_permission_profiles,
|
||||
vec![CustomPermissionProfileSummary {
|
||||
vec![PermissionProfileCatalogEntry {
|
||||
id: "dev".to_string(),
|
||||
description: Some("Workspace access.".to_string()),
|
||||
allowed: true,
|
||||
}]
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -148,6 +148,7 @@ pub mod edit;
|
||||
mod managed_features;
|
||||
mod network_proxy_spec;
|
||||
mod otel;
|
||||
mod permission_profile_catalog;
|
||||
mod permissions;
|
||||
mod resolved_permission_profile;
|
||||
#[cfg(test)]
|
||||
@@ -164,6 +165,11 @@ pub use codex_sandboxing::system_bwrap_warning;
|
||||
pub use managed_features::ManagedFeatures;
|
||||
pub use network_proxy_spec::NetworkProxySpec;
|
||||
pub use network_proxy_spec::StartedNetworkProxy;
|
||||
pub use permission_profile_catalog::PermissionProfileCatalogEntry;
|
||||
pub use permission_profile_catalog::permission_profile_catalog;
|
||||
use permission_profile_catalog::permission_profile_catalog_from_permissions;
|
||||
use permission_profile_catalog::permission_profile_is_allowed;
|
||||
use permission_profile_catalog::validate_permission_profile_for_deny_read;
|
||||
pub(crate) use permissions::is_builtin_permission_profile_name;
|
||||
pub(crate) use permissions::reject_unknown_builtin_permission_profile;
|
||||
pub(crate) use permissions::resolve_permission_profile;
|
||||
@@ -645,7 +651,7 @@ pub struct Config {
|
||||
pub explicit_permission_profile_mode: bool,
|
||||
|
||||
/// User-defined permission profiles available from effective config.
|
||||
pub custom_permission_profiles: Vec<CustomPermissionProfileSummary>,
|
||||
pub custom_permission_profiles: Vec<PermissionProfileCatalogEntry>,
|
||||
|
||||
/// Configures who approval requests are routed to for review once they have
|
||||
/// been escalated. This does not disable separate safety checks such as
|
||||
@@ -2150,12 +2156,6 @@ pub struct AgentRoleConfig {
|
||||
pub nickname_candidates: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CustomPermissionProfileSummary {
|
||||
pub id: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
fn resolve_tool_suggest_config(
|
||||
config_toml: &ConfigToml,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
@@ -3132,19 +3132,13 @@ impl Config {
|
||||
permission_config_syntax,
|
||||
Some(PermissionConfigSyntax::Profiles)
|
||||
);
|
||||
let custom_permission_profiles = cfg
|
||||
.permissions
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |permissions| {
|
||||
permissions
|
||||
.entries
|
||||
.iter()
|
||||
.map(|(id, profile)| CustomPermissionProfileSummary {
|
||||
id: id.clone(),
|
||||
description: profile.description.clone(),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
let custom_permission_profiles = permission_profile_catalog_from_permissions(
|
||||
&config_layer_stack,
|
||||
effective_permission_selection.profiles.as_ref(),
|
||||
)?
|
||||
.into_iter()
|
||||
.filter(|profile| !is_builtin_permission_profile_name(&profile.id))
|
||||
.collect();
|
||||
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
|
||||
@@ -3656,20 +3650,10 @@ impl Config {
|
||||
constrained_permission_profile
|
||||
.value
|
||||
.add_validator(move |permission_profile| {
|
||||
let mode = sandbox_mode_requirement_for_permission_profile(permission_profile);
|
||||
match mode {
|
||||
SandboxModeRequirement::ReadOnly
|
||||
| SandboxModeRequirement::WorkspaceWrite => Ok(()),
|
||||
SandboxModeRequirement::DangerFullAccess
|
||||
| SandboxModeRequirement::ExternalSandbox => {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{mode:?}"),
|
||||
allowed: "[read-only, workspace-write]".to_string(),
|
||||
requirement_source: requirement_source.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
validate_permission_profile_for_deny_read(
|
||||
permission_profile,
|
||||
&requirement_source,
|
||||
)
|
||||
})
|
||||
.map_err(std::io::Error::from)?;
|
||||
}
|
||||
@@ -4122,6 +4106,15 @@ impl Config {
|
||||
pub fn bundled_skills_enabled(&self) -> bool {
|
||||
crate::skills::service::bundled_skills_enabled_from_stack(&self.config_layer_stack)
|
||||
}
|
||||
|
||||
/// Returns whether effective requirements allow selecting a concrete profile.
|
||||
pub fn permission_profile_allowed(
|
||||
&self,
|
||||
profile_id: &str,
|
||||
permission_profile: &PermissionProfile,
|
||||
) -> bool {
|
||||
permission_profile_is_allowed(&self.config_layer_stack, profile_id, permission_profile)
|
||||
}
|
||||
}
|
||||
|
||||
fn guardian_policy_config_from_requirements(
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::SandboxModeRequirement;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::sandbox_mode_requirement_for_permission_profile;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
|
||||
use super::ConstraintError;
|
||||
use super::ConstraintResult;
|
||||
use super::is_permission_allowed;
|
||||
use super::merge_managed_permission_profiles;
|
||||
use super::permissions::BUILT_IN_DANGER_FULL_ACCESS_PROFILE;
|
||||
use super::permissions::BUILT_IN_READ_ONLY_PROFILE;
|
||||
use super::permissions::BUILT_IN_WORKSPACE_PROFILE;
|
||||
use super::permissions::compile_permission_profile_selection;
|
||||
use super::permissions::validate_user_permission_profile_names;
|
||||
use super::validate_required_permission_profile_catalog;
|
||||
|
||||
/// A permission profile exposed to clients together with its effective availability.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PermissionProfileCatalogEntry {
|
||||
pub id: String,
|
||||
pub description: Option<String>,
|
||||
pub allowed: bool,
|
||||
}
|
||||
|
||||
/// Builds the effective permission profile catalog for a config layer stack.
|
||||
pub fn permission_profile_catalog(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> std::io::Result<Vec<PermissionProfileCatalogEntry>> {
|
||||
let permissions = config_layer_stack
|
||||
.effective_config()
|
||||
.get("permissions")
|
||||
.cloned()
|
||||
.map(toml::Value::try_into::<PermissionsToml>)
|
||||
.transpose()
|
||||
.map_err(std::io::Error::other)?;
|
||||
let requirements_toml = config_layer_stack.requirements_toml();
|
||||
let permissions = merge_managed_permission_profiles(permissions.as_ref(), requirements_toml)?;
|
||||
|
||||
permission_profile_catalog_from_permissions(config_layer_stack, permissions.as_ref())
|
||||
}
|
||||
|
||||
pub(super) fn permission_profile_catalog_from_permissions(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
permissions: Option<&PermissionsToml>,
|
||||
) -> std::io::Result<Vec<PermissionProfileCatalogEntry>> {
|
||||
let requirements_toml = config_layer_stack.requirements_toml();
|
||||
validate_user_permission_profile_names(permissions)?;
|
||||
validate_required_permission_profile_catalog(requirements_toml, permissions)?;
|
||||
|
||||
let mut catalog = [
|
||||
(BUILT_IN_READ_ONLY_PROFILE, PermissionProfile::read_only()),
|
||||
(
|
||||
BUILT_IN_WORKSPACE_PROFILE,
|
||||
PermissionProfile::workspace_write(),
|
||||
),
|
||||
(
|
||||
BUILT_IN_DANGER_FULL_ACCESS_PROFILE,
|
||||
PermissionProfile::Disabled,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(id, permission_profile)| PermissionProfileCatalogEntry {
|
||||
id: id.to_string(),
|
||||
description: None,
|
||||
allowed: permission_profile_is_allowed(config_layer_stack, id, &permission_profile),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(permissions) = permissions {
|
||||
catalog.extend(permissions.entries.iter().map(|(id, profile)| {
|
||||
let mut warnings = Vec::new();
|
||||
let allowed = compile_permission_profile_selection(
|
||||
Some(permissions),
|
||||
id,
|
||||
/*workspace_write*/ None,
|
||||
&mut warnings,
|
||||
)
|
||||
.map(|(file_system, network)| {
|
||||
PermissionProfile::from_runtime_permissions(&file_system, network)
|
||||
})
|
||||
.is_ok_and(|permission_profile| {
|
||||
permission_profile_is_allowed(config_layer_stack, id, &permission_profile)
|
||||
});
|
||||
PermissionProfileCatalogEntry {
|
||||
id: id.clone(),
|
||||
description: profile.description.clone(),
|
||||
allowed,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(catalog)
|
||||
}
|
||||
|
||||
pub(super) fn permission_profile_is_allowed(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
profile_id: &str,
|
||||
permission_profile: &PermissionProfile,
|
||||
) -> bool {
|
||||
let allowed_by_id = config_layer_stack
|
||||
.requirements_toml()
|
||||
.allowed_permission_profiles
|
||||
.as_ref()
|
||||
.is_none_or(|allowed| is_permission_allowed(allowed, profile_id));
|
||||
let allowed_by_sandbox_mode = config_layer_stack
|
||||
.requirements()
|
||||
.permission_profile
|
||||
.can_set(permission_profile)
|
||||
.is_ok();
|
||||
let allowed_by_filesystem = config_layer_stack
|
||||
.requirements()
|
||||
.filesystem
|
||||
.as_ref()
|
||||
.is_none_or(|Sourced { value, source }| {
|
||||
value.deny_read.is_empty()
|
||||
|| validate_permission_profile_for_deny_read(permission_profile, source).is_ok()
|
||||
});
|
||||
allowed_by_id && allowed_by_sandbox_mode && allowed_by_filesystem
|
||||
}
|
||||
|
||||
pub(super) fn validate_permission_profile_for_deny_read(
|
||||
permission_profile: &PermissionProfile,
|
||||
requirement_source: &RequirementSource,
|
||||
) -> ConstraintResult<()> {
|
||||
let mode = sandbox_mode_requirement_for_permission_profile(permission_profile);
|
||||
match mode {
|
||||
SandboxModeRequirement::ReadOnly | SandboxModeRequirement::WorkspaceWrite => Ok(()),
|
||||
SandboxModeRequirement::DangerFullAccess | SandboxModeRequirement::ExternalSandbox => {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{mode:?}"),
|
||||
allowed: "[read-only, workspace-write]".to_string(),
|
||||
requirement_source: requirement_source.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ impl ChatWidget {
|
||||
}
|
||||
items.push(self.builtin_permission_mode_selection_item(
|
||||
full_access,
|
||||
":danger-no-sandbox",
|
||||
":danger-full-access",
|
||||
full_access.description.to_string(),
|
||||
AskForApproval::from(full_access.approval),
|
||||
ApprovalsReviewer::User,
|
||||
@@ -73,6 +73,7 @@ impl ChatWidget {
|
||||
.as_deref()
|
||||
.unwrap_or("Configured permission profile."),
|
||||
active_profile_id.as_deref(),
|
||||
profile.allowed,
|
||||
)
|
||||
}),
|
||||
);
|
||||
@@ -141,6 +142,12 @@ impl ChatWidget {
|
||||
.can_set_permission_profile(&preset.permission_profile)
|
||||
.err()
|
||||
.map(|err| err.to_string())
|
||||
})
|
||||
.or_else(|| {
|
||||
(!self
|
||||
.config
|
||||
.permission_profile_allowed(id, &preset.permission_profile))
|
||||
.then(|| "Disabled by requirements.".to_string())
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -151,6 +158,7 @@ impl ChatWidget {
|
||||
id: &str,
|
||||
description: &str,
|
||||
active_profile_id: Option<&str>,
|
||||
allowed: bool,
|
||||
) -> SelectionItem {
|
||||
let id_for_action = id.to_string();
|
||||
let selection = PermissionProfileSelection {
|
||||
@@ -165,6 +173,7 @@ impl ChatWidget {
|
||||
is_current: active_profile_id == Some(id),
|
||||
actions: Self::permission_profile_selection_actions(selection),
|
||||
dismiss_on_select: true,
|
||||
disabled_reason: (!allowed).then(|| "Disabled by requirements.".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
+14
-13
@@ -4,18 +4,19 @@ expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
Update Model Permissions
|
||||
|
||||
1. Ask for approval Codex can read and edit files in the current
|
||||
workspace, and run commands. Approval is required
|
||||
to access the internet or edit other files.
|
||||
2. Approve for me Only ask for actions detected as potentially
|
||||
unsafe.
|
||||
3. Full Access Codex can edit files outside this workspace and
|
||||
access the internet without asking for approval.
|
||||
Exercise caution when using.
|
||||
4. Read Only Codex can read files in the current workspace.
|
||||
Approval is required to edit files or access the
|
||||
internet.
|
||||
› 5. locked-down (current) Inspect and patch only approved workspace files.
|
||||
6. web-enabled Workspace profile with network access.
|
||||
1. Ask for approval Codex can read and edit files in the current
|
||||
workspace, and run commands. Approval is required
|
||||
to access the internet or edit other files.
|
||||
2. Approve for me Only ask for actions detected as potentially
|
||||
unsafe.
|
||||
3. Full Access Codex can edit files outside this workspace and
|
||||
access the internet without asking for approval.
|
||||
Exercise caution when using.
|
||||
4. Read Only Codex can read files in the current workspace.
|
||||
Approval is required to edit files or access the
|
||||
internet.
|
||||
› 5. locked-down (current) Inspect and patch only approved workspace files.
|
||||
web-enabled (disabled) Workspace profile with network access. (disabled:
|
||||
Disabled by requirements.)
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/permissions.rs
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
Update Model Permissions
|
||||
|
||||
1. Ask for approval Codex can read and edit files in the current
|
||||
workspace, and run commands. Approval is required
|
||||
to access the internet or edit other files.
|
||||
2. Approve for me Only ask for actions detected as potentially
|
||||
unsafe.
|
||||
Full Access (disabled) Codex can edit files outside this workspace and
|
||||
access the internet without asking for approval.
|
||||
Exercise caution when using. (disabled: Disabled
|
||||
by requirements.)
|
||||
› 3. Read Only (current) Codex can read files in the current workspace.
|
||||
Approval is required to edit files or access the
|
||||
internet.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::legacy_core::config::CustomPermissionProfileSummary;
|
||||
use crate::legacy_core::config::PermissionProfileCatalogEntry;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
@@ -57,6 +57,10 @@ fn windows_sandbox_requirements_stack(
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
requirements_stack(requirements_toml)
|
||||
}
|
||||
|
||||
fn requirements_stack(requirements_toml: codex_config::ConfigRequirementsToml) -> ConfigLayerStack {
|
||||
let mut requirements_with_sources = codex_config::ConfigRequirementsWithSources::default();
|
||||
requirements_with_sources
|
||||
.merge_unset_fields(RequirementSource::Unknown, requirements_toml.clone());
|
||||
@@ -104,18 +108,40 @@ async fn profile_permissions_selection_popup_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_permissions_selection_popup_with_disallowed_full_access_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.explicit_permission_profile_mode = true;
|
||||
chat.config.config_layer_stack = requirements_stack(codex_config::ConfigRequirementsToml {
|
||||
allowed_sandbox_modes: Some(vec![
|
||||
codex_config::SandboxModeRequirement::ReadOnly,
|
||||
codex_config::SandboxModeRequirement::WorkspaceWrite,
|
||||
]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
chat.open_permissions_popup();
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"profile_permissions_selection_popup_with_disallowed_full_access",
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_permissions_selection_popup_with_custom_profiles_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.explicit_permission_profile_mode = true;
|
||||
chat.config.custom_permission_profiles = vec![
|
||||
CustomPermissionProfileSummary {
|
||||
PermissionProfileCatalogEntry {
|
||||
id: "locked-down".to_string(),
|
||||
description: Some("Inspect and patch only approved workspace files.".to_string()),
|
||||
allowed: true,
|
||||
},
|
||||
CustomPermissionProfileSummary {
|
||||
PermissionProfileCatalogEntry {
|
||||
id: "web-enabled".to_string(),
|
||||
description: Some("Workspace profile with network access.".to_string()),
|
||||
allowed: false,
|
||||
},
|
||||
];
|
||||
chat.config
|
||||
@@ -170,9 +196,10 @@ async fn profile_permissions_selection_emits_named_profile_event_only() {
|
||||
async fn profile_permissions_selection_emits_active_custom_profile() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.explicit_permission_profile_mode = true;
|
||||
chat.config.custom_permission_profiles = vec![CustomPermissionProfileSummary {
|
||||
chat.config.custom_permission_profiles = vec![PermissionProfileCatalogEntry {
|
||||
id: "locked-down".to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
}];
|
||||
chat.config
|
||||
.permissions
|
||||
@@ -256,7 +283,7 @@ async fn profile_permissions_full_access_opens_confirmation() {
|
||||
display_label,
|
||||
}),
|
||||
} if preset.id == "full-access"
|
||||
&& profile_id == ":danger-no-sandbox"
|
||||
&& profile_id == ":danger-full-access"
|
||||
&& display_label == "Full Access"
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user