feat: add permission profile list api (#23412)

## Why

Clients need a typed permission-profile catalog instead of
reconstructing that state from config internals.

## What changed

- Added `permissionProfile/list` to the app-server v2 protocol with
cursor pagination and optional `cwd`.
- The list response includes built-in permission profiles plus
config-defined `[permissions.<id>]` profiles from the effective config
for the request context.
- Permission profiles keep optional `description` metadata for display
purposes.
- App-server docs and schema fixtures are updated for the new RPC.
This commit is contained in:
viyatb-oai
2026-05-20 02:42:56 +00:00
committed by GitHub
parent 1495302347
commit c3faea0b09
23 changed files with 769 additions and 1 deletions
@@ -1149,6 +1149,9 @@ impl MessageProcessor {
.experimental_feature_list(params)
.await
}
ClientRequest::PermissionProfileList { params, .. } => {
self.catalog_processor.permission_profile_list(params).await
}
ClientRequest::CollaborationModeList { params, .. } => {
self.catalog_processor.collaboration_mode_list(params).await
}
@@ -103,6 +103,9 @@ use codex_app_server_protocol::MockExperimentalMethodParams;
use codex_app_server_protocol::MockExperimentalMethodResponse;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::PermissionProfileListParams;
use codex_app_server_protocol::PermissionProfileListResponse;
use codex_app_server_protocol::PermissionProfileSummary;
use codex_app_server_protocol::PluginDetail;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginInstallResponse;
@@ -357,6 +360,9 @@ 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;
#[cfg(test)]
use codex_protocol::permissions::FileSystemSandboxPolicy;
@@ -1,4 +1,5 @@
use super::*;
use codex_config::config_toml::ConfigToml;
use futures::StreamExt;
#[derive(Clone)]
@@ -155,6 +156,15 @@ impl CatalogRequestProcessor {
.map(|response| Some(response.into()))
}
pub(crate) async fn permission_profile_list(
&self,
params: PermissionProfileListParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.permission_profile_list_response(params)
.await
.map(|response| Some(response.into()))
}
pub(crate) async fn collaboration_mode_list(
&self,
params: CollaborationModeListParams,
@@ -389,6 +399,78 @@ impl CatalogRequestProcessor {
Ok(ExperimentalFeatureListResponse { data, next_cursor })
}
async fn permission_profile_list_response(
&self,
params: PermissionProfileListParams,
) -> Result<PermissionProfileListResponse, JSONRPCErrorError> {
let PermissionProfileListParams { cursor, limit, cwd } = params;
let config_layer_stack = match cwd {
Some(cwd) => {
let cwd = PathBuf::from(cwd);
let (_, config_layer_stack) = self
.resolve_cwd_config(&cwd)
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?;
config_layer_stack
}
None => self
.config_manager
.load_config_layers(/*cwd*/ None)
.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
.into_iter()
.flat_map(|permissions| permissions.entries)
.map(|(id, profile)| PermissionProfileSummary {
id,
description: profile.description,
})
.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);
let start = match cursor {
Some(cursor) => cursor
.parse::<usize>()
.map_err(|_| invalid_request(format!("invalid cursor: {cursor}")))?,
None => 0,
};
if start > total {
return Err(invalid_request(format!(
"cursor {start} exceeds total permission profiles {total}"
)));
}
let end = start.saturating_add(effective_limit).min(total);
let data = profiles[start..end].to_vec();
let next_cursor = (end < total).then_some(end.to_string());
Ok(PermissionProfileListResponse { data, next_cursor })
}
async fn mock_experimental_method_inner(
&self,
params: MockExperimentalMethodParams,