mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
5696167fe8
commit
3fd79b7986
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user