mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
app-server: use permission ids and runtime workspace roots (#22611)
## Why This PR builds on [#22610](https://github.com/openai/codex/pull/22610) and is the app-server side of the migration from mutable per-turn `SandboxPolicy` replacement toward selecting immutable permission profiles by id plus mutable runtime workspace roots. Once permission profiles can carry their own immutable `workspace_roots`, app-server no longer needs to mutate the selected `PermissionProfile` just to represent thread-specific filesystem context. The mutable part now lives on the thread as explicit `runtimeWorkspaceRoots`, while `:workspace_roots` remains symbolic until the sandbox is realized for a turn. ## What Changed - Replaced the v2 permission-selection wrapper surface with plain profile ids for `thread/start`, `thread/resume`, `thread/fork`, and `turn/start`. - Removed the API surface for profile modifications (`PermissionProfileSelectionParams`, `PermissionProfileModificationParams`, `ActivePermissionProfileModification`). - Added experimental `runtimeWorkspaceRoots` fields to the thread lifecycle and turn-start APIs. - Threaded runtime workspace roots through core session/thread snapshots, turn overrides, app-server request handling, and command execution permission resolution. - Kept session permission state symbolic so later runtime root updates and cwd-only implicit-root retargeting rebind `:workspace_roots` correctly. - Updated the embedded clients just enough to send and restore the new thread state. - Refreshed the generated schema/TypeScript artifacts and the app-server README to match the new contract. ## Verification Targeted coverage for this layer lives in: - `codex-rs/app-server-protocol/src/protocol/v2/tests.rs` - `codex-rs/app-server/tests/suite/v2/thread_start.rs` - `codex-rs/app-server/tests/suite/v2/thread_resume.rs` - `codex-rs/app-server/tests/suite/v2/turn_start.rs` - `codex-rs/core/src/session/tests.rs` The key regression checks exercise that: - `runtimeWorkspaceRoots` resolve against the effective cwd on thread start. - Profile-declared workspace roots are excluded from the runtime workspace roots returned by app-server. - A turn-level runtime workspace-root update persists onto the thread and is returned by `thread/resume`. - A named permission profile selected on one turn remains symbolic so a later runtime-root-only turn update changes the actual sandbox writes. - A cwd-only turn update retargets the implicit runtime cwd root while preserving additional runtime roots. - The protocol fixtures and generated client artifacts stay in sync with the string-based permission selection contract. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22611). * #22612 * __->__ #22611
This commit is contained in:
@@ -661,6 +661,7 @@ mod tests {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: next_cwd.clone().abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -2796,10 +2796,13 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
||||
ThreadId::from_string("00000000-0000-0000-0000-000000000101").expect("valid thread");
|
||||
let agent_thread_id =
|
||||
ThreadId::from_string("00000000-0000-0000-0000-000000000202").expect("valid thread");
|
||||
let primary_cwd = test_path_buf("/tmp/main").abs();
|
||||
let shared_root = test_path_buf("/tmp/shared").abs();
|
||||
let primary_session = ThreadSessionState {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
..test_thread_session(main_thread_id, test_path_buf("/tmp/main"))
|
||||
runtime_workspace_roots: vec![primary_cwd.clone(), shared_root.clone()],
|
||||
..test_thread_session(main_thread_id, primary_cwd.to_path_buf())
|
||||
};
|
||||
|
||||
app.primary_thread_id = Some(main_thread_id);
|
||||
@@ -2871,6 +2874,10 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
||||
assert_eq!(session.model_provider_id, "agent-provider");
|
||||
assert_eq!(session.approval_policy, primary_session.approval_policy);
|
||||
assert_eq!(session.cwd.as_path(), test_path_buf("/tmp/agent").as_path());
|
||||
assert_eq!(
|
||||
session.runtime_workspace_roots,
|
||||
vec![test_path_buf("/tmp/agent").abs(), shared_root]
|
||||
);
|
||||
assert_eq!(session.rollout_path, Some(rollout_path));
|
||||
assert_eq!(
|
||||
app.agent_navigation.get(&agent_thread_id),
|
||||
@@ -2892,10 +2899,12 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_
|
||||
ThreadId::from_string("00000000-0000-0000-0000-000000000301").expect("valid thread");
|
||||
let agent_thread_id =
|
||||
ThreadId::from_string("00000000-0000-0000-0000-000000000302").expect("valid thread");
|
||||
let primary_cwd = test_path_buf("/tmp/main").abs();
|
||||
let primary_session = ThreadSessionState {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
..test_thread_session(main_thread_id, test_path_buf("/tmp/main"))
|
||||
runtime_workspace_roots: vec![primary_cwd.clone()],
|
||||
..test_thread_session(main_thread_id, primary_cwd.to_path_buf())
|
||||
};
|
||||
|
||||
app.primary_thread_id = Some(main_thread_id);
|
||||
@@ -2962,10 +2971,12 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() {
|
||||
ThreadId::from_string("00000000-0000-0000-0000-000000000401").expect("valid thread");
|
||||
let read_thread_id =
|
||||
ThreadId::from_string("00000000-0000-0000-0000-000000000402").expect("valid thread");
|
||||
let primary_cwd = test_path_buf("/tmp/main").abs();
|
||||
let primary_session = ThreadSessionState {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
..test_thread_session(main_thread_id, test_path_buf("/tmp/main"))
|
||||
runtime_workspace_roots: vec![primary_cwd.clone()],
|
||||
..test_thread_session(main_thread_id, primary_cwd.to_path_buf())
|
||||
};
|
||||
app.primary_session_configured = Some(primary_session);
|
||||
|
||||
@@ -2997,6 +3008,10 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() {
|
||||
|
||||
assert_eq!(session.thread_id, read_thread_id);
|
||||
assert_eq!(session.cwd.as_path(), test_path_buf("/tmp/read").as_path());
|
||||
assert_eq!(
|
||||
session.runtime_workspace_roots,
|
||||
vec![test_path_buf("/tmp/read").abs()]
|
||||
);
|
||||
let expected_permission_profile = app
|
||||
.chat_widget
|
||||
.config_ref()
|
||||
@@ -3688,6 +3703,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::High),
|
||||
message_history: None,
|
||||
@@ -3936,6 +3952,7 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: cwd.abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -4511,6 +4528,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -4574,6 +4592,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -4666,6 +4685,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -4901,6 +4921,7 @@ async fn refreshed_snapshot_session_persists_resumed_turns() {
|
||||
)];
|
||||
let resumed_session = ThreadSessionState {
|
||||
cwd: test_path_buf("/tmp/refreshed").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
..initial_session.clone()
|
||||
};
|
||||
@@ -5065,6 +5086,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -5186,6 +5208,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -352,6 +352,7 @@ mod tests {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: cwd.abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -895,7 +895,8 @@ impl App {
|
||||
session.thread_id = thread_id;
|
||||
session.thread_name = notification.thread.name.clone();
|
||||
session.model_provider_id = notification.thread.model_provider.clone();
|
||||
session.cwd = notification.thread.cwd.clone();
|
||||
session
|
||||
.set_cwd_retargeting_implicit_runtime_workspace_root(notification.thread.cwd.clone());
|
||||
let rollout_path = notification.thread.path.clone();
|
||||
if let Some(model) =
|
||||
read_session_model(self.state_db.as_deref(), thread_id, rollout_path.as_deref()).await
|
||||
|
||||
@@ -72,6 +72,7 @@ impl App {
|
||||
permission_profile: permission_profile.clone(),
|
||||
active_permission_profile: active_permission_profile.clone(),
|
||||
cwd: thread.cwd.clone(),
|
||||
runtime_workspace_roots: self.config.workspace_roots.clone(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: self.chat_widget.current_reasoning_effort(),
|
||||
message_history: None,
|
||||
@@ -81,7 +82,7 @@ impl App {
|
||||
session.thread_id = thread_id;
|
||||
session.thread_name = thread.name.clone();
|
||||
session.model_provider_id = thread.model_provider.clone();
|
||||
session.cwd = thread.cwd.clone();
|
||||
session.set_cwd_retargeting_implicit_runtime_workspace_root(thread.cwd.clone());
|
||||
session.permission_profile = permission_profile;
|
||||
session.active_permission_profile = active_permission_profile;
|
||||
session.instruction_source_paths = Vec::new();
|
||||
@@ -148,6 +149,7 @@ mod tests {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: cwd.abs(),
|
||||
runtime_workspace_roots: vec![cwd.abs()],
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
Reference in New Issue
Block a user