app-server: use profile ids in v2 permission params (#23360)

## Why

The v2 app-server permission profile fields are experimental, but the
previous migration kept a legacy object payload for profile selection.
That made clients aware of server-owned `activePermissionProfile`
metadata such as `extends`, and it kept a
`legacy_additional_writable_roots` path even though
`runtimeWorkspaceRoots` now owns runtime workspace-root selection.

This PR makes the client contract match the intended model: clients
select a permission profile by id, and the server resolves and reports
active profile provenance in response payloads.

Follow-up to #22611.

## What Changed

- Changed `thread/start`, `thread/resume`, `thread/fork`, and
`turn/start` permission profile selection to plain profile id strings.
- Changed `command/exec.permissionProfile` to a plain profile id string
for the same client/server ownership split.
- Removed `PermissionProfileSelectionParams` and the legacy `{ type:
"profile", modifications: [...] }` compatibility deserializer.
- Updated app-server, TUI, and `codex exec` call sites to send only ids,
while keeping `activePermissionProfile` as server response metadata.
- Updated app-server docs and schema fixtures for the revised
`command/exec.permissionProfile` shape.

## Verification

- `cargo test -p codex-app-server-protocol`
- `RUST_MIN_STACK=8388608 cargo test -p codex-app-server`
- `cargo test -p codex-exec`
- `RUST_MIN_STACK=8388608 cargo test -p codex-tui`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23360).
* #23368
* __->__ #23360
This commit is contained in:
Michael Bolin
2026-05-18 17:28:50 -07:00
committed by GitHub
Unverified
parent 5696167fe8
commit 3fd79b7986
21 changed files with 79 additions and 337 deletions
@@ -5,26 +5,6 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"AddCreditsNudgeCreditType": {
"enum": [
"credits",
@@ -5,26 +5,6 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"CommandExecTerminalSize": {
"description": "PTY size in character cells for `command/exec` PTY sessions.",
"properties": {
+3 -13
View File
@@ -2747,7 +2747,6 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k
let _guard = TempDirGuard(output_dir.clone());
let path = output_dir.join("CommandExecParams.ts");
let content = r#"import type { CommandExecTerminalSize } from "./CommandExecTerminalSize";
import type { ActivePermissionProfile } from "./ActivePermissionProfile";
import type { SandboxPolicy } from "./SandboxPolicy";
export type CommandExecParams = {/**
@@ -2770,12 +2769,12 @@ size?: CommandExecTerminalSize | null, /**
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Optional active permissions profile for this command.
* Optional active permissions profile id for this command.
*
* Defaults to the user's configured permissions when omitted. Cannot be
* combined with `sandboxPolicy`.
*/
permissionProfile?: ActivePermissionProfile | null};
permissionProfile?: string | null};
"#;
fs::write(&path, content)?;
@@ -2788,16 +2787,7 @@ permissionProfile?: ActivePermissionProfile | null};
filter_experimental_type_fields_ts(&output_dir, &[&CUSTOM_FIELD])?;
let filtered = fs::read_to_string(&path)?;
assert_eq!(
filtered.contains("permissionProfile?: ActivePermissionProfile"),
false
);
assert_eq!(
filtered.contains(
r#"import type { ActivePermissionProfile } from "./ActivePermissionProfile";"#
),
false
);
assert_eq!(filtered.contains("permissionProfile?: string"), false);
assert_eq!(filtered.contains("sandboxPolicy?: SandboxPolicy"), true);
assert_eq!(
filtered.contains(r#"import type { SandboxPolicy } from "./SandboxPolicy";"#),
@@ -1555,6 +1555,7 @@ mod tests {
use anyhow::Result;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::RealtimeConversationVersion;
use codex_protocol::protocol::RealtimeOutputModality;
@@ -2994,7 +2995,7 @@ mod tests {
env: None,
size: None,
sandbox_policy: None,
permission_profile: Some(v2::ActivePermissionProfile::read_only()),
permission_profile: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
},
};
@@ -1,4 +1,3 @@
use super::ActivePermissionProfile;
use super::SandboxPolicy;
use codex_experimental_api_macros::ExperimentalApi;
use schemars::JsonSchema;
@@ -100,13 +99,13 @@ pub struct CommandExecParams {
/// combined with `permissionProfile`.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Optional active permissions profile for this command.
/// Optional active permissions profile id for this command.
///
/// Defaults to the user's configured permissions when omitted. Cannot be
/// combined with `sandboxPolicy`.
#[experimental("command/exec.permissionProfile")]
#[ts(optional = nullable)]
pub permission_profile: Option<ActivePermissionProfile>,
pub permission_profile: Option<String>,
}
/// Final buffered result for `command/exec`.
@@ -18,9 +18,7 @@ use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequest
use codex_utils_absolute_path::AbsolutePathBuf;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use ts_rs::TS;
@@ -333,102 +331,6 @@ impl From<ActivePermissionProfile> for CoreActivePermissionProfile {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionProfileSelectionParams {
id: String,
legacy_additional_writable_roots: Vec<AbsolutePathBuf>,
}
impl PermissionProfileSelectionParams {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
legacy_additional_writable_roots: Vec::new(),
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn into_id(self) -> String {
self.id
}
pub fn legacy_additional_writable_roots(&self) -> &[AbsolutePathBuf] {
&self.legacy_additional_writable_roots
}
}
impl From<String> for PermissionProfileSelectionParams {
fn from(id: String) -> Self {
Self::new(id)
}
}
impl Serialize for PermissionProfileSelectionParams {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.id)
}
}
impl<'de> Deserialize<'de> for PermissionProfileSelectionParams {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Wire {
Id(String),
LegacyProfile {
#[serde(rename = "type")]
_type: LegacyPermissionProfileSelectionType,
id: String,
#[serde(default)]
modifications: Option<Vec<LegacyPermissionProfileModificationParams>>,
},
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
enum LegacyPermissionProfileSelectionType {
Profile,
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
enum LegacyPermissionProfileModificationParams {
#[serde(rename_all = "camelCase")]
AdditionalWritableRoot { path: AbsolutePathBuf },
}
match Wire::deserialize(deserializer)? {
Wire::Id(id) => Ok(Self::new(id)),
Wire::LegacyProfile {
id, modifications, ..
} => {
let legacy_additional_writable_roots = modifications
.unwrap_or_default()
.into_iter()
.map(|modification| match modification {
LegacyPermissionProfileModificationParams::AdditionalWritableRoot {
path,
} => path,
})
.collect();
Ok(Self {
id,
legacy_additional_writable_roots,
})
}
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -14,6 +14,7 @@ use codex_protocol::mcp::CallToolResult;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::MessagePhase;
@@ -614,57 +615,49 @@ fn permissions_request_approval_response_accepts_strict_auto_review() {
}
#[test]
fn permission_profile_selection_accepts_legacy_object_shape() {
let additional_root = absolute_path("additional-root");
let params = json!({
"permissions": {
"type": "profile",
"id": ":workspace",
"modifications": [
{
"type": "additionalWritableRoot",
"path": additional_root,
}
],
},
});
let start: ThreadStartParams =
serde_json::from_value(params.clone()).expect("thread/start params deserialize");
assert_legacy_permission_profile_selection(start.permissions, &additional_root);
let resume: ThreadResumeParams = serde_json::from_value(json!({
"threadId": "thread-1",
"permissions": params["permissions"].clone(),
fn permission_profile_selection_uses_id_string() {
let start: ThreadStartParams = serde_json::from_value(json!({
"permissions": BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
}))
.expect("thread/resume params deserialize");
assert_legacy_permission_profile_selection(resume.permissions, &additional_root);
let fork: ThreadForkParams = serde_json::from_value(json!({
"threadId": "thread-1",
"permissions": params["permissions"].clone(),
}))
.expect("thread/fork params deserialize");
assert_legacy_permission_profile_selection(fork.permissions, &additional_root);
.expect("thread/start params deserialize");
assert_eq!(
start.permissions,
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
);
let turn: TurnStartParams = serde_json::from_value(json!({
"threadId": "thread-1",
"input": [],
"permissions": params["permissions"].clone(),
"permissions": "dev",
}))
.expect("turn/start params deserialize");
assert_legacy_permission_profile_selection(turn.permissions, &additional_root);
}
assert_eq!(turn.permissions, Some("dev".to_string()));
fn assert_legacy_permission_profile_selection(
selection: Option<PermissionProfileSelectionParams>,
additional_root: &AbsolutePathBuf,
) {
let selection = selection.expect("permissions should be present");
assert_eq!(selection.id(), ":workspace");
let command: CommandExecParams = serde_json::from_value(json!({
"command": ["echo", "hello"],
"permissionProfile": "dev",
}))
.expect("command/exec params deserialize");
assert_eq!(command.permission_profile, Some("dev".to_string()));
let resume: ThreadResumeParams = serde_json::from_value(json!({
"threadId": "thread-1",
"permissions": BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
}))
.expect("thread/resume params deserialize");
assert_eq!(
selection.legacy_additional_writable_roots(),
std::slice::from_ref(additional_root)
resume.permissions,
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
);
let fork: ThreadForkParams = serde_json::from_value(json!({
"threadId": "thread-1",
"permissions": BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
}))
.expect("thread/fork params deserialize");
assert_eq!(
fork.permissions,
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
);
}
@@ -1,7 +1,6 @@
use super::ActivePermissionProfile;
use super::ApprovalsReviewer;
use super::AskForApproval;
use super::PermissionProfileSelectionParams;
use super::SandboxMode;
use super::SandboxPolicy;
use super::Thread;
@@ -122,10 +121,8 @@ pub struct ThreadStartParams {
pub sandbox: Option<SandboxMode>,
/// Named profile id for this thread. Cannot be combined with `sandbox`.
#[experimental("thread/start.permissions")]
#[schemars(with = "Option<String>")]
#[ts(type = "string | null")]
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
pub permissions: Option<String>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, JsonValue>>,
#[ts(optional = nullable)]
@@ -284,10 +281,8 @@ pub struct ThreadResumeParams {
/// Named profile id for the resumed thread. Cannot be combined with
/// `sandbox`.
#[experimental("thread/resume.permissions")]
#[schemars(with = "Option<String>")]
#[ts(type = "string | null")]
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
pub permissions: Option<String>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
@@ -395,10 +390,8 @@ pub struct ThreadForkParams {
/// Named profile id for the forked thread. Cannot be combined with
/// `sandbox`.
#[experimental("thread/fork.permissions")]
#[schemars(with = "Option<String>")]
#[ts(type = "string | null")]
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
pub permissions: Option<String>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
@@ -1,6 +1,5 @@
use super::ApprovalsReviewer;
use super::AskForApproval;
use super::PermissionProfileSelectionParams;
use super::SandboxPolicy;
use super::Turn;
use codex_experimental_api_macros::ExperimentalApi;
@@ -85,10 +84,8 @@ pub struct TurnStartParams {
/// Select a named permissions profile id for this turn and subsequent
/// turns. Cannot be combined with `sandboxPolicy`.
#[experimental("turn/start.permissions")]
#[schemars(with = "Option<String>")]
#[ts(type = "string | null")]
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
pub permissions: Option<String>,
/// Override the model for this turn and subsequent turns.
#[ts(optional = nullable)]
pub model: Option<String>,
+1 -1
View File
@@ -927,7 +927,7 @@ Run a standalone command (argv vector) in the servers sandbox without creatin
"cwd": "/Users/me/project", // optional; defaults to server cwd
"env": { "FOO": "override" }, // optional; merges into the server env and overrides matching names
"size": { "rows": 40, "cols": 120 }, // optional; PTY size in character cells, only valid with tty=true
"permissionProfile": { "id": ":workspace", "extends": null }, // optional; defaults to user config
"permissionProfile": ":workspace", // optional profile id; defaults to user config
"outputBytesCap": 1048576, // optional; per-stream capture cap
"disableOutputCap": false, // optional; cannot be combined with outputBytesCap
"timeoutMs": 10000, // optional; ms timeout; defaults to server timeout
@@ -103,7 +103,6 @@ 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::PermissionProfileSelectionParams;
use codex_app_server_protocol::PluginDetail;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginInstallResponse;
@@ -117,13 +117,6 @@ impl CommandExecRequestProcessor {
"`permissionProfile` cannot be combined with `sandboxPolicy`",
));
}
let permission_profile = if let Some(active_permission_profile) = permission_profile {
Some(PermissionProfileSelectionParams::new(
active_permission_profile.id,
))
} else {
None
};
if size.is_some() && !tty {
return Err(invalid_params("command/exec size requires tty: true"));
@@ -199,14 +192,11 @@ impl CommandExecRequestProcessor {
network_proxy_permission_profile,
managed_network_requirements_enabled,
) = if let Some(permission_profile) = permission_profile {
let mut overrides = ConfigOverrides {
let overrides = ConfigOverrides {
cwd: Some(cwd.to_path_buf()),
default_permissions: Some(permission_profile),
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(
&mut overrides,
Some(permission_profile),
);
let config = self
.config_manager
.load_for_cwd(
@@ -1239,17 +1239,18 @@ impl ThreadRequestProcessor {
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
sandbox: Option<SandboxMode>,
permissions: Option<PermissionProfileSelectionParams>,
permissions: Option<String>,
base_instructions: Option<String>,
developer_instructions: Option<String>,
personality: Option<Personality>,
) -> ConfigOverrides {
let mut overrides = ConfigOverrides {
ConfigOverrides {
model,
model_provider,
service_tier,
cwd: cwd.map(PathBuf::from),
workspace_roots: runtime_workspace_roots,
default_permissions: permissions,
approval_policy: approval_policy
.map(codex_app_server_protocol::AskForApproval::to_core),
approvals_reviewer: approvals_reviewer
@@ -1261,9 +1262,7 @@ impl ThreadRequestProcessor {
developer_instructions,
personality,
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(&mut overrides, permissions);
overrides
}
}
fn parse_environment_selections(
@@ -175,29 +175,6 @@ pub(super) fn thread_response_active_permission_profile(
active_permission_profile.map(Into::into)
}
pub(super) fn apply_permission_profile_selection_to_config_overrides(
overrides: &mut ConfigOverrides,
permissions: Option<PermissionProfileSelectionParams>,
) {
let Some(selection) = permissions else {
return;
};
overrides.default_permissions = Some(selection.id().to_string());
if selection.legacy_additional_writable_roots().is_empty() {
return;
}
let legacy_roots = selection
.legacy_additional_writable_roots()
.iter()
.map(AbsolutePathBuf::to_path_buf);
if let Some(workspace_roots) = overrides.workspace_roots.as_mut() {
workspace_roots.extend(legacy_roots);
} else {
overrides.additional_writable_roots.extend(legacy_roots);
}
}
pub(super) fn thread_response_sandbox_policy(
permission_profile: &codex_protocol::models::PermissionProfile,
cwd: &Path,
@@ -66,43 +66,3 @@ fn extract_conversation_summary_prefers_plain_user_messages() -> Result<()> {
assert_eq!(summary, expected);
Ok(())
}
#[test]
fn legacy_permission_profile_modifications_extend_runtime_roots() -> Result<()> {
let root = if cfg!(windows) {
AbsolutePathBuf::try_from("C:\\workspace-extra")?
} else {
AbsolutePathBuf::try_from("/workspace-extra")?
};
let selection = serde_json::from_value::<PermissionProfileSelectionParams>(json!({
"type": "profile",
"id": ":workspace",
"modifications": [
{
"type": "additionalWritableRoot",
"path": root,
}
],
}))?;
let mut overrides = ConfigOverrides::default();
apply_permission_profile_selection_to_config_overrides(&mut overrides, Some(selection.clone()));
assert_eq!(
overrides.default_permissions,
Some(":workspace".to_string())
);
assert_eq!(
overrides.additional_writable_roots,
vec![root.to_path_buf()]
);
let mut overrides = ConfigOverrides {
workspace_roots: Some(Vec::new()),
..ConfigOverrides::default()
};
apply_permission_profile_selection_to_config_overrides(&mut overrides, Some(selection));
assert_eq!(overrides.additional_writable_roots, Vec::<PathBuf>::new());
assert_eq!(overrides.workspace_roots, Some(vec![root.to_path_buf()]));
Ok(())
}
@@ -425,7 +425,7 @@ impl TurnRequestProcessor {
"turn/start permission selection missing thread snapshot",
));
};
let mut overrides = ConfigOverrides {
let overrides = ConfigOverrides {
cwd: cwd.clone(),
workspace_roots: Some(runtime_workspace_roots_request.clone().unwrap_or_else(
|| {
@@ -436,14 +436,11 @@ impl TurnRequestProcessor {
.collect()
},
)),
default_permissions: Some(permissions),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(
&mut overrides,
Some(permissions),
);
let config = self
.config_manager
.load_for_cwd(
@@ -5,7 +5,6 @@ use app_test_support::create_mock_responses_server_sequence_unchecked;
use app_test_support::to_response;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::ActivePermissionProfile;
use codex_app_server_protocol::CommandExecOutputDeltaNotification;
use codex_app_server_protocol::CommandExecOutputStream;
use codex_app_server_protocol::CommandExecParams;
@@ -18,6 +17,7 @@ use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxPolicy;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::Path;
@@ -219,7 +219,7 @@ async fn command_exec_accepts_permission_profile() -> Result<()> {
env: None,
size: None,
sandbox_policy: None,
permission_profile: Some(ActivePermissionProfile::read_only()),
permission_profile: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
})
.await?;
@@ -270,7 +270,7 @@ async fn command_exec_permission_profile_starts_selected_network_proxy() -> Resu
env: None,
size: None,
sandbox_policy: None,
permission_profile: Some(ActivePermissionProfile::new("networked")),
permission_profile: Some("networked".to_string()),
})
.await?;
@@ -318,7 +318,7 @@ async fn command_exec_permission_profile_does_not_reuse_default_network_proxy()
env: None,
size: None,
sandbox_policy: None,
permission_profile: Some(ActivePermissionProfile::read_only()),
permission_profile: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
})
.await?;
@@ -376,7 +376,7 @@ async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Resu
env: None,
size: None,
sandbox_policy: None,
permission_profile: Some(ActivePermissionProfile::new("command-cwd")),
permission_profile: Some("command-cwd".to_string()),
})
.await?;
@@ -423,7 +423,7 @@ async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result
env: None,
size: None,
sandbox_policy: Some(SandboxPolicy::DangerFullAccess),
permission_profile: Some(ActivePermissionProfile::read_only()),
permission_profile: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
})
.await?;
@@ -889,11 +889,7 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn()
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
permissions: Some(
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS
.to_string()
.into(),
),
permissions: Some(BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string()),
..Default::default()
})
.await?;
@@ -2147,7 +2143,7 @@ stream_max_retries = 0
text_elements: Vec::new(),
}],
runtime_workspace_roots: Some(vec![old_root]),
permissions: Some("dev".to_string().into()),
permissions: Some("dev".to_string()),
..Default::default()
})
.await?;
+4 -7
View File
@@ -24,7 +24,6 @@ use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::McpServerElicitationRequestResponse;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ReviewStartParams;
use codex_app_server_protocol::ReviewStartResponse;
@@ -1009,17 +1008,15 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa
}
}
fn permissions_selection_from_config(config: &Config) -> Option<PermissionProfileSelectionParams> {
fn permissions_selection_from_config(config: &Config) -> Option<String> {
config
.permissions
.active_permission_profile()
.map(permissions_selection_from_active_profile)
.map(permission_profile_id_from_active_profile)
}
fn permissions_selection_from_active_profile(
active: ActivePermissionProfile,
) -> PermissionProfileSelectionParams {
PermissionProfileSelectionParams::new(active.id)
fn permission_profile_id_from_active_profile(active: ActivePermissionProfile) -> String {
active.id
}
fn sandbox_mode_from_permission_profile(
+2 -5
View File
@@ -479,14 +479,11 @@ async fn thread_start_params_include_user_thread_source() {
#[test]
fn active_profile_selection_uses_profile_id_only() {
let selection = permissions_selection_from_active_profile(ActivePermissionProfile::new(
let selection = permission_profile_id_from_active_profile(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
));
assert_eq!(
selection,
PermissionProfileSelectionParams::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)
);
assert_eq!(selection, BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string());
}
#[tokio::test]
+10 -15
View File
@@ -34,7 +34,6 @@ use codex_app_server_protocol::MemoryResetResponse;
use codex_app_server_protocol::Model as ApiModel;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ReviewDelivery;
@@ -1182,10 +1181,8 @@ fn sandbox_mode_from_permission_profile(
}
}
fn permissions_selection_from_active_profile(
active: ActivePermissionProfile,
) -> PermissionProfileSelectionParams {
PermissionProfileSelectionParams::new(active.id)
fn permission_profile_id_from_active_profile(active: ActivePermissionProfile) -> String {
active.id
}
fn turn_permissions_overrides(
@@ -1193,13 +1190,13 @@ fn turn_permissions_overrides(
cwd: &std::path::Path,
) -> (
Option<codex_app_server_protocol::SandboxPolicy>,
Option<PermissionProfileSelectionParams>,
Option<String>,
) {
match permissions_override {
TurnPermissionsOverride::Preserve => (None, None),
TurnPermissionsOverride::ActiveProfile(active_permission_profile) => (
None,
Some(permissions_selection_from_active_profile(
Some(permission_profile_id_from_active_profile(
active_permission_profile,
)),
),
@@ -1220,7 +1217,7 @@ fn turn_permissions_overrides(
fn permissions_selection_from_config(
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> Option<PermissionProfileSelectionParams> {
) -> Option<String> {
if matches!(thread_params_mode, ThreadParamsMode::Remote) {
return None;
}
@@ -1228,7 +1225,7 @@ fn permissions_selection_from_config(
config
.permissions
.active_permission_profile()
.map(permissions_selection_from_active_profile)
.map(permission_profile_id_from_active_profile)
}
fn thread_start_params_from_config(
@@ -1671,7 +1668,7 @@ mod tests {
config
.permissions
.active_permission_profile()
.map(permissions_selection_from_active_profile)
.map(permission_profile_id_from_active_profile)
);
assert_eq!(params.model_provider, Some(config.model_provider_id));
assert_eq!(params.thread_source, Some(ThreadSource::User));
@@ -1698,7 +1695,7 @@ mod tests {
let active_permission_profile =
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
let expected_permissions =
permissions_selection_from_active_profile(active_permission_profile.clone());
permission_profile_id_from_active_profile(active_permission_profile.clone());
let (sandbox_policy, permissions) = turn_permissions_overrides(
TurnPermissionsOverride::ActiveProfile(active_permission_profile),
@@ -1723,9 +1720,7 @@ mod tests {
assert_eq!(sandbox_policy, None);
assert_eq!(
permissions,
Some(PermissionProfileSelectionParams::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE
))
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
);
}
@@ -1763,7 +1758,7 @@ mod tests {
let cwd = test_path_buf("/workspace/project").abs();
let active_permission_profile = ActivePermissionProfile::new("strict");
let expected_permissions =
permissions_selection_from_active_profile(active_permission_profile.clone());
permission_profile_id_from_active_profile(active_permission_profile.clone());
let (sandbox_policy, permissions) = turn_permissions_overrides(
TurnPermissionsOverride::ActiveProfile(active_permission_profile),