mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
[app-server] expose resolved permission presets
This commit is contained in:
Generated
+1
@@ -2011,6 +2011,7 @@ dependencies = [
|
||||
"codex-thread-store",
|
||||
"codex-tools",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-approval-presets",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
"codex-utils-json-to-toml",
|
||||
|
||||
+33
@@ -12990,6 +12990,39 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionPreset": {
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"$ref": "#/definitions/v2/AskForApproval"
|
||||
},
|
||||
"approvalsReviewer": {
|
||||
"$ref": "#/definitions/v2/ApprovalsReviewer"
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"permissionProfileId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"approvalPolicy",
|
||||
"approvalsReviewer",
|
||||
"id",
|
||||
"isDefault",
|
||||
"permissionProfileId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
+33
@@ -9394,6 +9394,39 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionPreset": {
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"$ref": "#/definitions/AskForApproval"
|
||||
},
|
||||
"approvalsReviewer": {
|
||||
"$ref": "#/definitions/ApprovalsReviewer"
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"permissionProfileId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"approvalPolicy",
|
||||
"approvalsReviewer",
|
||||
"id",
|
||||
"isDefault",
|
||||
"permissionProfileId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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 { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
|
||||
export type PermissionPreset = { id: string, permissionProfileId: string, description: string | null, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, isDefault: boolean, };
|
||||
@@ -279,6 +279,7 @@ export type { OverriddenMetadata } from "./OverriddenMetadata";
|
||||
export type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
export type { PatchChangeKind } from "./PatchChangeKind";
|
||||
export type { PermissionGrantScope } from "./PermissionGrantScope";
|
||||
export type { PermissionPreset } from "./PermissionPreset";
|
||||
export type { PermissionProfileListParams } from "./PermissionProfileListParams";
|
||||
export type { PermissionProfileListResponse } from "./PermissionProfileListResponse";
|
||||
export type { PermissionProfileSummary } from "./PermissionProfileSummary";
|
||||
|
||||
@@ -875,6 +875,12 @@ client_request_definitions! {
|
||||
serialization: global_shared_read("config"),
|
||||
response: v2::PermissionProfileListResponse,
|
||||
},
|
||||
#[experimental("permissionPreset/list")]
|
||||
PermissionPresetList => "permissionPreset/list" {
|
||||
params: v2::PermissionPresetListParams,
|
||||
serialization: global_shared_read("config"),
|
||||
response: v2::PermissionPresetListResponse,
|
||||
},
|
||||
ExperimentalFeatureEnablementSet => "experimentalFeature/enablement/set" {
|
||||
params: v2::ExperimentalFeatureEnablementSetParams,
|
||||
serialization: global("config"),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use super::ApprovalsReviewer;
|
||||
use super::AskForApproval;
|
||||
use super::shared::v2_enum_from_core;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;
|
||||
@@ -392,6 +394,38 @@ pub struct PermissionProfileListResponse {
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionPresetListParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub cursor: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub limit: Option<u32>,
|
||||
#[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 PermissionPreset {
|
||||
pub id: String,
|
||||
pub permission_profile_id: String,
|
||||
pub description: Option<String>,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionPresetListResponse {
|
||||
pub data: Vec<PermissionPreset>,
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -75,6 +75,7 @@ codex-state = { workspace = true }
|
||||
codex-thread-store = { workspace = true }
|
||||
codex-tools = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-approval-presets = { workspace = true }
|
||||
codex-utils-json-to-toml = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
||||
@@ -203,6 +203,7 @@ Example with notification opt-out:
|
||||
- `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 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.
|
||||
- `permissionPreset/list` — experimental; list resolved, selectable permission presets for an optional `cwd`, using cursor pagination. Each preset includes an opaque `id`, `permissionProfileId`, `approvalPolicy`, `approvalsReviewer`, optional custom-profile `description`, and `isDefault`. The server applies managed requirements, omits unavailable profiles, and removes presets that resolve to the same settings; clients should preserve response order and apply the returned settings without reconstructing preset policy.
|
||||
- `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.
|
||||
|
||||
@@ -102,6 +102,7 @@ mod message_processor;
|
||||
mod models;
|
||||
mod models_refresh_worker;
|
||||
mod outgoing_message;
|
||||
mod permission_presets;
|
||||
mod request_processors;
|
||||
mod request_serialization;
|
||||
mod server_request_error;
|
||||
|
||||
@@ -1309,6 +1309,9 @@ impl MessageProcessor {
|
||||
ClientRequest::PermissionProfileList { params, .. } => {
|
||||
self.catalog_processor.permission_profile_list(params).await
|
||||
}
|
||||
ClientRequest::PermissionPresetList { params, .. } => {
|
||||
self.catalog_processor.permission_preset_list(params).await
|
||||
}
|
||||
ClientRequest::CollaborationModeList { params, .. } => {
|
||||
self.catalog_processor.collaboration_mode_list(params).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
use crate::config_manager::ConfigManager;
|
||||
use codex_app_server_protocol::PermissionPreset;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_utils_approval_presets::builtin_approval_presets;
|
||||
use futures::StreamExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const PRESET_RESOLUTION_CONCURRENCY: usize = 5;
|
||||
|
||||
struct PermissionPresetCandidate {
|
||||
id: String,
|
||||
permission_profile_id: String,
|
||||
description: Option<String>,
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: ApprovalsReviewer,
|
||||
}
|
||||
|
||||
pub(crate) async fn permission_presets(
|
||||
config_manager: &ConfigManager,
|
||||
cwd: Option<PathBuf>,
|
||||
) -> std::io::Result<Vec<PermissionPreset>> {
|
||||
let config = config_manager
|
||||
.load_for_cwd(None, ConfigOverrides::default(), cwd.clone())
|
||||
.await?;
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
for preset in builtin_approval_presets() {
|
||||
candidates.push(PermissionPresetCandidate {
|
||||
id: preset.id.to_string(),
|
||||
permission_profile_id: preset.active_permission_profile.id.clone(),
|
||||
description: None,
|
||||
approval_policy: preset.approval,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
});
|
||||
if preset.id == "auto" {
|
||||
candidates.push(PermissionPresetCandidate {
|
||||
id: "granular".to_string(),
|
||||
permission_profile_id: preset.active_permission_profile.id.clone(),
|
||||
description: None,
|
||||
approval_policy: AskForApproval::Granular(GranularApprovalConfig {
|
||||
sandbox_approval: false,
|
||||
rules: false,
|
||||
skill_approval: false,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
});
|
||||
if config.features.enabled(Feature::GuardianApproval) {
|
||||
candidates.push(PermissionPresetCandidate {
|
||||
id: "guardian-approvals".to_string(),
|
||||
permission_profile_id: preset.active_permission_profile.id,
|
||||
description: None,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::AutoReview,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut custom_profiles = config.custom_permission_profiles.clone();
|
||||
custom_profiles.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
candidates.extend(
|
||||
custom_profiles
|
||||
.into_iter()
|
||||
.map(|profile| PermissionPresetCandidate {
|
||||
id: format!("permission-profile:{}", profile.id),
|
||||
permission_profile_id: profile.id,
|
||||
description: profile.description,
|
||||
approval_policy: config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
}),
|
||||
);
|
||||
|
||||
let resolved_presets = futures::stream::iter(candidates)
|
||||
.map(|candidate| resolve_candidate(config_manager, cwd.clone(), &config, candidate))
|
||||
.buffered(PRESET_RESOLUTION_CONCURRENCY)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let mut presets = Vec::new();
|
||||
for preset in resolved_presets {
|
||||
let Some(preset) = preset else {
|
||||
continue;
|
||||
};
|
||||
if presets.iter().any(|existing: &PermissionPreset| {
|
||||
existing.permission_profile_id == preset.permission_profile_id
|
||||
&& existing.approval_policy == preset.approval_policy
|
||||
&& existing.approvals_reviewer == preset.approvals_reviewer
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
presets.push(preset);
|
||||
}
|
||||
|
||||
Ok(presets)
|
||||
}
|
||||
|
||||
async fn resolve_candidate(
|
||||
config_manager: &ConfigManager,
|
||||
cwd: Option<PathBuf>,
|
||||
default_config: &Config,
|
||||
candidate: PermissionPresetCandidate,
|
||||
) -> Option<PermissionPreset> {
|
||||
let config = config_manager
|
||||
.load_for_cwd(
|
||||
None,
|
||||
ConfigOverrides {
|
||||
approval_policy: Some(candidate.approval_policy),
|
||||
approvals_reviewer: Some(candidate.approvals_reviewer),
|
||||
default_permissions: Some(candidate.permission_profile_id.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
cwd,
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
let active_profile = config.permissions.active_permission_profile()?;
|
||||
if active_profile.id != candidate.permission_profile_id {
|
||||
return None;
|
||||
}
|
||||
let approval_policy = config.permissions.approval_policy.value();
|
||||
let approvals_reviewer = config.approvals_reviewer;
|
||||
let is_default = default_config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.is_some_and(|profile| profile.id == candidate.permission_profile_id)
|
||||
&& default_config.permissions.approval_policy.value() == approval_policy
|
||||
&& default_config.approvals_reviewer == approvals_reviewer;
|
||||
|
||||
Some(PermissionPreset {
|
||||
id: candidate.id,
|
||||
permission_profile_id: candidate.permission_profile_id,
|
||||
description: candidate.description,
|
||||
approval_policy: approval_policy.into(),
|
||||
approvals_reviewer: approvals_reviewer.into(),
|
||||
is_default,
|
||||
})
|
||||
}
|
||||
@@ -115,6 +115,8 @@ 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::PermissionPresetListParams;
|
||||
use codex_app_server_protocol::PermissionPresetListResponse;
|
||||
use codex_app_server_protocol::PermissionProfileListParams;
|
||||
use codex_app_server_protocol::PermissionProfileListResponse;
|
||||
use codex_app_server_protocol::PermissionProfileSummary;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::permission_presets::permission_presets;
|
||||
use codex_core::config::permission_profile_catalog;
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -180,6 +181,15 @@ impl CatalogRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn permission_preset_list(
|
||||
&self,
|
||||
params: PermissionPresetListParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.permission_preset_list_response(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn collaboration_mode_list(
|
||||
&self,
|
||||
params: CollaborationModeListParams,
|
||||
@@ -466,6 +476,39 @@ impl CatalogRequestProcessor {
|
||||
Ok(PermissionProfileListResponse { data, next_cursor })
|
||||
}
|
||||
|
||||
async fn permission_preset_list_response(
|
||||
&self,
|
||||
params: PermissionPresetListParams,
|
||||
) -> Result<PermissionPresetListResponse, JSONRPCErrorError> {
|
||||
let PermissionPresetListParams { cursor, limit, cwd } = params;
|
||||
let presets = permission_presets(&self.config_manager, cwd.map(PathBuf::from))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to resolve permission presets: {err}"))
|
||||
})?;
|
||||
let total = presets.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 presets {total}"
|
||||
)));
|
||||
}
|
||||
|
||||
let end = start.saturating_add(effective_limit).min(total);
|
||||
let data = presets[start..end].to_vec();
|
||||
let next_cursor = (end < total).then_some(end.to_string());
|
||||
|
||||
Ok(PermissionPresetListResponse { data, next_cursor })
|
||||
}
|
||||
|
||||
async fn mock_experimental_method_inner(
|
||||
&self,
|
||||
params: MockExperimentalMethodParams,
|
||||
|
||||
@@ -58,6 +58,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::PermissionPresetListParams;
|
||||
use codex_app_server_protocol::PermissionProfileListParams;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstalledParams;
|
||||
@@ -648,6 +649,15 @@ impl TestAppServer {
|
||||
self.send_request("permissionProfile/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `permissionPreset/list` JSON-RPC request.
|
||||
pub async fn send_permission_preset_list_request(
|
||||
&mut self,
|
||||
params: PermissionPresetListParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("permissionPreset/list", params).await
|
||||
}
|
||||
|
||||
/// Send an `experimentalFeature/enablement/set` JSON-RPC request.
|
||||
pub async fn send_experimental_feature_enablement_set_request(
|
||||
&mut self,
|
||||
|
||||
@@ -35,6 +35,7 @@ mod memory_reset;
|
||||
mod model_list;
|
||||
mod model_provider_capabilities_read;
|
||||
mod output_schema;
|
||||
mod permission_preset_list;
|
||||
mod permission_profile_list;
|
||||
mod plan_item;
|
||||
mod plugin_install;
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ApprovalsReviewer;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PermissionPreset;
|
||||
use codex_app_server_protocol::PermissionPresetListParams;
|
||||
use codex_app_server_protocol::PermissionPresetListResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
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_preset_list_returns_resolved_builtin_presets() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"
|
||||
default_permissions = ":workspace"
|
||||
approvals_reviewer = "auto_review"
|
||||
|
||||
[features]
|
||||
guardian_approval = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_permission_preset_list_request(PermissionPresetListParams {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
cwd: None,
|
||||
})
|
||||
.await?;
|
||||
let actual = read_response::<PermissionPresetListResponse>(&mut mcp, request_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
PermissionPresetListResponse {
|
||||
data: vec![
|
||||
preset(
|
||||
"read-only",
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
AskForApproval::OnRequest,
|
||||
ApprovalsReviewer::User,
|
||||
false,
|
||||
),
|
||||
preset(
|
||||
"auto",
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
AskForApproval::OnRequest,
|
||||
ApprovalsReviewer::User,
|
||||
false,
|
||||
),
|
||||
preset(
|
||||
"granular",
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
AskForApproval::Granular {
|
||||
sandbox_approval: false,
|
||||
rules: false,
|
||||
skill_approval: false,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
},
|
||||
ApprovalsReviewer::User,
|
||||
false,
|
||||
),
|
||||
preset(
|
||||
"guardian-approvals",
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
AskForApproval::OnRequest,
|
||||
ApprovalsReviewer::AutoReview,
|
||||
true,
|
||||
),
|
||||
preset(
|
||||
"full-access",
|
||||
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS,
|
||||
AskForApproval::Never,
|
||||
ApprovalsReviewer::User,
|
||||
false,
|
||||
),
|
||||
],
|
||||
next_cursor: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_preset_list_applies_managed_profile_and_policy_fallbacks() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
approval_policy = "never"
|
||||
approvals_reviewer = "auto_review"
|
||||
|
||||
[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"
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("requirements.toml"),
|
||||
r#"
|
||||
allowed_approval_policies = ["on-request"]
|
||||
allowed_approvals_reviewers = ["user"]
|
||||
default_permissions = "audit"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
":read-only" = true
|
||||
audit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_permission_preset_list_request(PermissionPresetListParams {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
cwd: None,
|
||||
})
|
||||
.await?;
|
||||
let actual = read_response::<PermissionPresetListResponse>(&mut mcp, request_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
PermissionPresetListResponse {
|
||||
data: vec![
|
||||
preset(
|
||||
"read-only",
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
AskForApproval::OnRequest,
|
||||
ApprovalsReviewer::User,
|
||||
false,
|
||||
),
|
||||
PermissionPreset {
|
||||
id: "permission-profile:audit".to_string(),
|
||||
permission_profile_id: "audit".to_string(),
|
||||
description: Some("Inspect without writes.".to_string()),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
is_default: true,
|
||||
},
|
||||
],
|
||||
next_cursor: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_preset_list_removes_presets_with_identical_resolved_settings() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"
|
||||
default_permissions = ":workspace"
|
||||
approval_policy = "never"
|
||||
approvals_reviewer = "auto_review"
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("requirements.toml"),
|
||||
r#"
|
||||
allowed_approval_policies = ["on-request"]
|
||||
allowed_approvals_reviewers = ["user"]
|
||||
default_permissions = ":workspace"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
":workspace" = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_permission_preset_list_request(PermissionPresetListParams {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
cwd: None,
|
||||
})
|
||||
.await?;
|
||||
let actual = read_response::<PermissionPresetListResponse>(&mut mcp, request_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
PermissionPresetListResponse {
|
||||
data: vec![preset(
|
||||
"auto",
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
AskForApproval::OnRequest,
|
||||
ApprovalsReviewer::User,
|
||||
true,
|
||||
)],
|
||||
next_cursor: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn preset(
|
||||
id: &str,
|
||||
permission_profile_id: &str,
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: ApprovalsReviewer,
|
||||
is_default: bool,
|
||||
) -> PermissionPreset {
|
||||
PermissionPreset {
|
||||
id: id.to_string(),
|
||||
permission_profile_id: permission_profile_id.to_string(),
|
||||
description: None,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
is_default,
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_response<T: serde::de::DeserializeOwned>(
|
||||
mcp: &mut TestAppServer,
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user