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-19 19:42:56 -07:00
committed by GitHub
Unverified
parent 1495302347
commit c3faea0b09
23 changed files with 769 additions and 1 deletions
@@ -1541,6 +1541,34 @@
],
"type": "string"
},
"PermissionProfileListParams": {
"properties": {
"cursor": {
"description": "Opaque pagination cursor returned by a previous call.",
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Optional working directory to resolve project config layers.",
"type": [
"string",
"null"
]
},
"limit": {
"description": "Optional page size; defaults to the full result set.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"type": "object"
},
"Personality": {
"enum": [
"none",
@@ -5324,6 +5352,30 @@
"title": "ExperimentalFeature/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"permissionProfile/list"
],
"title": "PermissionProfile/listRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/PermissionProfileListParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "PermissionProfile/listRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -1429,6 +1429,30 @@
"title": "ExperimentalFeature/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"permissionProfile/list"
],
"title": "PermissionProfile/listRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/PermissionProfileListParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "PermissionProfile/listRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -11708,6 +11732,78 @@
}
]
},
"PermissionProfileListParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cursor": {
"description": "Opaque pagination cursor returned by a previous call.",
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Optional working directory to resolve project config layers.",
"type": [
"string",
"null"
]
},
"limit": {
"description": "Optional page size; defaults to the full result set.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"title": "PermissionProfileListParams",
"type": "object"
},
"PermissionProfileListResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"data": {
"items": {
"$ref": "#/definitions/v2/PermissionProfileSummary"
},
"type": "array"
},
"nextCursor": {
"description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.",
"type": [
"string",
"null"
]
}
},
"required": [
"data"
],
"title": "PermissionProfileListResponse",
"type": "object"
},
"PermissionProfileSummary": {
"properties": {
"description": {
"description": "Optional user-facing description for display in clients.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Available permission profile identifier.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"Personality": {
"enum": [
"none",
@@ -2155,6 +2155,30 @@
"title": "ExperimentalFeature/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"permissionProfile/list"
],
"title": "PermissionProfile/listRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/PermissionProfileListParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "PermissionProfile/listRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -8257,6 +8281,78 @@
}
]
},
"PermissionProfileListParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cursor": {
"description": "Opaque pagination cursor returned by a previous call.",
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Optional working directory to resolve project config layers.",
"type": [
"string",
"null"
]
},
"limit": {
"description": "Optional page size; defaults to the full result set.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"title": "PermissionProfileListParams",
"type": "object"
},
"PermissionProfileListResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"data": {
"items": {
"$ref": "#/definitions/PermissionProfileSummary"
},
"type": "array"
},
"nextCursor": {
"description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.",
"type": [
"string",
"null"
]
}
},
"required": [
"data"
],
"title": "PermissionProfileListResponse",
"type": "object"
},
"PermissionProfileSummary": {
"properties": {
"description": {
"description": "Optional user-facing description for display in clients.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Available permission profile identifier.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"Personality": {
"enum": [
"none",
@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cursor": {
"description": "Opaque pagination cursor returned by a previous call.",
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Optional working directory to resolve project config layers.",
"type": [
"string",
"null"
]
},
"limit": {
"description": "Optional page size; defaults to the full result set.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"title": "PermissionProfileListParams",
"type": "object"
}
@@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"PermissionProfileSummary": {
"properties": {
"description": {
"description": "Optional user-facing description for display in clients.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Available permission profile identifier.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
}
},
"properties": {
"data": {
"items": {
"$ref": "#/definitions/PermissionProfileSummary"
},
"type": "array"
},
"nextCursor": {
"description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.",
"type": [
"string",
"null"
]
}
},
"required": [
"data"
],
"title": "PermissionProfileListResponse",
"type": "object"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PermissionProfileListParams = {
/**
* Opaque pagination cursor returned by a previous call.
*/
cursor?: string | null,
/**
* Optional page size; defaults to the full result set.
*/
limit?: number | null,
/**
* Optional working directory to resolve project config layers.
*/
cwd?: string | null, };
@@ -0,0 +1,11 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PermissionProfileSummary } from "./PermissionProfileSummary";
export type PermissionProfileListResponse = { data: Array<PermissionProfileSummary>,
/**
* Opaque cursor to pass to the next call to continue after the last item.
* If None, there are no more items to return.
*/
nextCursor: string | null, };
@@ -0,0 +1,13 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PermissionProfileSummary = {
/**
* Available permission profile identifier.
*/
id: string,
/**
* Optional user-facing description for display in clients.
*/
description: string | null, };
@@ -256,6 +256,9 @@ export type { OverriddenMetadata } from "./OverriddenMetadata";
export type { PatchApplyStatus } from "./PatchApplyStatus";
export type { PatchChangeKind } from "./PatchChangeKind";
export type { PermissionGrantScope } from "./PermissionGrantScope";
export type { PermissionProfileListParams } from "./PermissionProfileListParams";
export type { PermissionProfileListResponse } from "./PermissionProfileListResponse";
export type { PermissionProfileSummary } from "./PermissionProfileSummary";
export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams";
export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse";
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
@@ -800,6 +800,11 @@ client_request_definitions! {
serialization: global("config"),
response: v2::ExperimentalFeatureListResponse,
},
PermissionProfileList => "permissionProfile/list" {
params: v2::PermissionProfileListParams,
serialization: global_shared_read("config"),
response: v2::PermissionProfileListResponse,
},
ExperimentalFeatureEnablementSet => "experimentalFeature/enablement/set" {
params: v2::ExperimentalFeatureEnablementSetParams,
serialization: global("config"),
@@ -287,6 +287,41 @@ impl From<FileSystemSandboxEntry> for CoreFileSystemSandboxEntry {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PermissionProfileListParams {
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional page size; defaults to the full result set.
#[ts(optional = nullable)]
pub limit: Option<u32>,
/// Optional working directory to resolve project config layers.
#[ts(optional = nullable)]
pub cwd: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PermissionProfileSummary {
/// Available permission profile identifier.
pub id: String,
/// Optional user-facing description for display in clients.
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PermissionProfileListResponse {
pub data: Vec<PermissionProfileSummary>,
/// Opaque cursor to pass to the next call to continue after the last item.
/// If None, there are no more items to return.
pub next_cursor: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
+1
View File
@@ -189,6 +189,7 @@ Example with notification opt-out:
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
- `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.
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
- `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; 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.
@@ -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,
@@ -56,6 +56,7 @@ use codex_app_server_protocol::McpServerToolCallParams;
use codex_app_server_protocol::MockExperimentalMethodParams;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelProviderCapabilitiesReadParams;
use codex_app_server_protocol::PermissionProfileListParams;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginInstalledParams;
use codex_app_server_protocol::PluginListParams;
@@ -561,6 +562,15 @@ impl McpProcess {
self.send_request("experimentalFeature/list", params).await
}
/// Send a `permissionProfile/list` JSON-RPC request.
pub async fn send_permission_profile_list_request(
&mut self,
params: PermissionProfileListParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("permissionProfile/list", params).await
}
/// Send an `experimentalFeature/enablement/set` JSON-RPC request.
pub async fn send_experimental_feature_enablement_set_request(
&mut self,
@@ -29,6 +29,7 @@ mod memory_reset;
mod model_list;
mod model_provider_capabilities_read;
mod output_schema;
mod permission_profile_list;
mod plan_item;
mod plugin_install;
mod plugin_list;
@@ -0,0 +1,233 @@
use std::time::Duration;
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PermissionProfileListParams;
use codex_app_server_protocol::PermissionProfileListResponse;
use codex_app_server_protocol::PermissionProfileSummary;
use codex_app_server_protocol::RequestId;
use codex_core::config::set_project_trust_level;
use codex_protocol::config_types::TrustLevel;
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 pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[tokio::test]
async fn permission_profile_list_returns_builtin_and_configured_profiles() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"
default_permissions = "dev"
[permissions.dev]
description = "Day-to-day coding work."
[permissions.dev.filesystem]
":workspace_roots" = "write"
[permissions.audit]
description = "Inspect without writes."
[permissions.audit.filesystem]
":workspace_roots" = "read"
"#,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_permission_profile_list_request(PermissionProfileListParams {
cursor: None,
limit: None,
cwd: None,
})
.await?;
let actual = read_response::<PermissionProfileListResponse>(&mut mcp, request_id).await?;
assert_eq!(
actual,
PermissionProfileListResponse {
data: 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,
},
PermissionProfileSummary {
id: "audit".to_string(),
description: Some("Inspect without writes.".to_string()),
},
PermissionProfileSummary {
id: "dev".to_string(),
description: Some("Day-to-day coding work.".to_string()),
},
],
next_cursor: None,
}
);
Ok(())
}
#[tokio::test]
async fn permission_profile_list_resolves_project_profiles_and_paginates() -> Result<()> {
let codex_home = TempDir::new()?;
let workspace = TempDir::new()?;
let project_config_dir = workspace.path().join(".codex");
std::fs::create_dir_all(&project_config_dir)?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"
default_permissions = ":workspace"
"#,
)?;
std::fs::write(
project_config_dir.join("config.toml"),
r#"
[permissions.project]
description = "Project-scoped profile."
[permissions.project.filesystem]
":workspace_roots" = "write"
"#,
)?;
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let first_request_id = mcp
.send_permission_profile_list_request(PermissionProfileListParams {
cursor: None,
limit: Some(3),
cwd: Some(workspace.path().to_string_lossy().into_owned()),
})
.await?;
let first = read_response::<PermissionProfileListResponse>(&mut mcp, first_request_id).await?;
assert_eq!(
first,
PermissionProfileListResponse {
data: 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,
},
],
next_cursor: Some("3".to_string()),
}
);
let second_request_id = mcp
.send_permission_profile_list_request(PermissionProfileListParams {
cursor: first.next_cursor,
limit: Some(3),
cwd: Some(workspace.path().to_string_lossy().into_owned()),
})
.await?;
let second =
read_response::<PermissionProfileListResponse>(&mut mcp, second_request_id).await?;
assert_eq!(
second,
PermissionProfileListResponse {
data: vec![PermissionProfileSummary {
id: "project".to_string(),
description: Some("Project-scoped profile.".to_string()),
}],
next_cursor: None,
}
);
Ok(())
}
#[tokio::test]
async fn permission_profile_list_discovers_project_profiles_without_default_selection() -> Result<()>
{
let codex_home = TempDir::new()?;
let workspace = TempDir::new()?;
let project_config_dir = workspace.path().join(".codex");
std::fs::create_dir_all(&project_config_dir)?;
std::fs::write(
project_config_dir.join("config.toml"),
r#"
[permissions.project]
description = "Project-scoped profile."
[permissions.project.filesystem]
":workspace_roots" = "write"
"#,
)?;
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_permission_profile_list_request(PermissionProfileListParams {
cursor: None,
limit: None,
cwd: Some(workspace.path().to_string_lossy().into_owned()),
})
.await?;
let actual = read_response::<PermissionProfileListResponse>(&mut mcp, request_id).await?;
assert_eq!(
actual,
PermissionProfileListResponse {
data: 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,
},
PermissionProfileSummary {
id: "project".to_string(),
description: Some("Project-scoped profile.".to_string()),
},
],
next_cursor: None,
}
);
Ok(())
}
async fn read_response<T: serde::de::DeserializeOwned>(
mcp: &mut McpProcess,
request_id: i64,
) -> Result<T> {
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
to_response(response)
}
+1
View File
@@ -25,6 +25,7 @@ impl PermissionsToml {
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct PermissionProfileToml {
pub description: Option<String>,
pub workspace_roots: Option<WorkspaceRootsToml>,
pub filesystem: Option<FilesystemPermissionsToml>,
pub network: Option<NetworkToml>,
+3
View File
@@ -2002,6 +2002,9 @@
"PermissionProfileToml": {
"additionalProperties": false,
"properties": {
"description": {
"type": "string"
},
"filesystem": {
"$ref": "#/definitions/FilesystemPermissionsToml"
},
+22
View File
@@ -734,6 +734,9 @@ fn config_toml_deserializes_permission_profiles() {
let toml = r#"
default_permissions = "workspace"
[permissions.workspace]
description = "Day-to-day workspace access."
[permissions.workspace.workspace_roots]
"~/code/openai" = true
"~/code/ignored" = false
@@ -765,6 +768,7 @@ allow_upstream_proxy = false
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: Some("Day-to-day workspace access.".to_string()),
workspace_roots: Some(WorkspaceRootsToml {
entries: BTreeMap::from([
("~/code/ignored".to_string(), false),
@@ -830,6 +834,7 @@ async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -984,6 +989,7 @@ async fn network_proxy_feature_matrix_preserves_sandbox_network_semantics() -> s
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -1135,6 +1141,7 @@ async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io:
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -1238,6 +1245,7 @@ enabled = false
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -1287,6 +1295,7 @@ async fn permissions_profiles_network_disabled_by_default_does_not_start_proxy()
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -1334,6 +1343,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -1638,6 +1648,7 @@ async fn permission_profile_override_preserves_configured_network_policy_without
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -1698,6 +1709,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: Some(2),
@@ -1777,6 +1789,7 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<(
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -1904,6 +1917,7 @@ async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots(
entries: BTreeMap::from([(
"dev".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: Some(WorkspaceRootsToml {
entries: BTreeMap::from([(
profile_root.to_string_lossy().into_owned(),
@@ -2362,6 +2376,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -2423,6 +2438,7 @@ async fn permissions_profiles_reject_nested_entries_for_non_workspace_roots() ->
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -2484,6 +2500,7 @@ async fn load_workspace_permission_profile(
#[tokio::test]
async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -2528,6 +2545,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<(
async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
-> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -2565,6 +2583,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
#[tokio::test]
async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: None,
network: None,
@@ -2594,6 +2613,7 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io
#[tokio::test]
async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -2630,6 +2650,7 @@ async fn permissions_profiles_reject_workspace_root_parent_traversal() -> std::i
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -2677,6 +2698,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()>
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -67,6 +67,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
@@ -285,6 +286,7 @@ fn compile_permission_profile_workspace_roots_resolves_enabled_entries() -> std:
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: Some(WorkspaceRootsToml {
entries: BTreeMap::from([
("backend".to_string(), true),
@@ -394,6 +396,7 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()>
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
description: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,