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:
committed by
GitHub
Unverified
parent
e6a7368810
commit
8a5306ff88
@@ -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,
|
||||
|
||||
@@ -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::PermissionProfileModificationParams;
|
||||
use codex_app_server_protocol::PermissionProfileSelectionParams;
|
||||
use codex_app_server_protocol::RateLimitSnapshot;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -577,6 +576,12 @@ impl AppServerSession {
|
||||
responsesapi_client_metadata: None,
|
||||
environments: None,
|
||||
cwd: Some(cwd),
|
||||
runtime_workspace_roots: Some(
|
||||
workspace_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect(),
|
||||
),
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(approvals_reviewer.into()),
|
||||
sandbox_policy,
|
||||
@@ -1175,34 +1180,22 @@ fn sandbox_mode_from_permission_profile(
|
||||
|
||||
fn permissions_selection_from_active_profile(
|
||||
active: ActivePermissionProfile,
|
||||
cwd: &std::path::Path,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> PermissionProfileSelectionParams {
|
||||
let modifications = workspace_roots
|
||||
.iter()
|
||||
.filter(|root| root.as_path() != cwd)
|
||||
.cloned()
|
||||
.map(|path| PermissionProfileModificationParams::AdditionalWritableRoot { path })
|
||||
.collect::<Vec<_>>();
|
||||
PermissionProfileSelectionParams::Profile {
|
||||
id: active.id,
|
||||
modifications: (!modifications.is_empty()).then_some(modifications),
|
||||
}
|
||||
PermissionProfileSelectionParams::new(active.id)
|
||||
}
|
||||
|
||||
fn turn_permissions_overrides(
|
||||
permission_profile: &PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
cwd: &std::path::Path,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
_workspace_roots: &[AbsolutePathBuf],
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> (
|
||||
Option<codex_app_server_protocol::SandboxPolicy>,
|
||||
Option<PermissionProfileSelectionParams>,
|
||||
) {
|
||||
let permissions = if matches!(thread_params_mode, ThreadParamsMode::Embedded) {
|
||||
active_permission_profile
|
||||
.map(|active| permissions_selection_from_active_profile(active, cwd, workspace_roots))
|
||||
active_permission_profile.map(permissions_selection_from_active_profile)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1231,13 +1224,7 @@ fn permissions_selection_from_config(
|
||||
config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|active| {
|
||||
permissions_selection_from_active_profile(
|
||||
active,
|
||||
config.cwd.as_path(),
|
||||
config.permissions.user_visible_workspace_roots(),
|
||||
)
|
||||
})
|
||||
.map(permissions_selection_from_active_profile)
|
||||
}
|
||||
|
||||
fn thread_start_params_from_config(
|
||||
@@ -1261,6 +1248,13 @@ fn thread_start_params_from_config(
|
||||
model_provider: thread_params_mode.model_provider_from_config(config),
|
||||
service_tier: service_tier_override_from_config(config),
|
||||
cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override),
|
||||
runtime_workspace_roots: Some(
|
||||
config
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect(),
|
||||
),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(config),
|
||||
sandbox,
|
||||
@@ -1296,6 +1290,13 @@ fn thread_resume_params_from_config(
|
||||
model_provider: thread_params_mode.model_provider_from_config(&config),
|
||||
service_tier: service_tier_override_from_config(&config),
|
||||
cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),
|
||||
runtime_workspace_roots: Some(
|
||||
config
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect(),
|
||||
),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(&config),
|
||||
sandbox,
|
||||
@@ -1328,6 +1329,13 @@ fn thread_fork_params_from_config(
|
||||
model_provider: thread_params_mode.model_provider_from_config(&config),
|
||||
service_tier: service_tier_override_from_config(&config),
|
||||
cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),
|
||||
runtime_workspace_roots: Some(
|
||||
config
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect(),
|
||||
),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(&config),
|
||||
sandbox,
|
||||
@@ -1425,6 +1433,7 @@ async fn thread_session_state_from_thread_start_response(
|
||||
permission_profile,
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
@@ -1457,6 +1466,7 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
permission_profile,
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
@@ -1489,6 +1499,7 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
permission_profile,
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
@@ -1531,6 +1542,7 @@ async fn thread_session_state_from_thread_response(
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
cwd: AbsolutePathBuf,
|
||||
runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
config: &Config,
|
||||
@@ -1558,6 +1570,7 @@ async fn thread_session_state_from_thread_response(
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
cwd,
|
||||
runtime_workspace_roots,
|
||||
instruction_source_paths,
|
||||
reasoning_effort,
|
||||
message_history: Some(MessageHistoryMetadata {
|
||||
@@ -1637,19 +1650,23 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string()));
|
||||
assert_eq!(
|
||||
params.runtime_workspace_roots,
|
||||
Some(
|
||||
config
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect()
|
||||
)
|
||||
);
|
||||
assert_eq!(params.sandbox, None);
|
||||
assert_eq!(
|
||||
params.permissions,
|
||||
config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|active| {
|
||||
permissions_selection_from_active_profile(
|
||||
active,
|
||||
config.cwd.as_path(),
|
||||
config.permissions.user_visible_workspace_roots(),
|
||||
)
|
||||
})
|
||||
.map(permissions_selection_from_active_profile)
|
||||
);
|
||||
assert_eq!(params.model_provider, Some(config.model_provider_id));
|
||||
assert_eq!(params.thread_source, Some(ThreadSource::User));
|
||||
@@ -1676,11 +1693,8 @@ mod tests {
|
||||
let active_permission_profile =
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
|
||||
let workspace_roots = vec![cwd.clone()];
|
||||
let expected_permissions = permissions_selection_from_active_profile(
|
||||
active_permission_profile.clone(),
|
||||
cwd.as_path(),
|
||||
&workspace_roots,
|
||||
);
|
||||
let expected_permissions =
|
||||
permissions_selection_from_active_profile(active_permission_profile.clone());
|
||||
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&PermissionProfile::workspace_write(),
|
||||
@@ -1695,12 +1709,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_turn_permissions_include_extra_workspace_roots_as_modifications() {
|
||||
fn embedded_turn_permissions_select_profile_id_only() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
let extra_root = test_path_buf("/workspace/cache").abs();
|
||||
let active_permission_profile =
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
|
||||
let workspace_roots = vec![cwd.clone(), extra_root.clone()];
|
||||
let workspace_roots = vec![cwd.clone(), extra_root];
|
||||
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&PermissionProfile::workspace_write(),
|
||||
@@ -1713,14 +1727,9 @@ mod tests {
|
||||
assert_eq!(sandbox_policy, None);
|
||||
assert_eq!(
|
||||
permissions,
|
||||
Some(PermissionProfileSelectionParams::Profile {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
modifications: Some(vec![
|
||||
PermissionProfileModificationParams::AdditionalWritableRoot {
|
||||
path: extra_root
|
||||
}
|
||||
]),
|
||||
})
|
||||
Some(PermissionProfileSelectionParams::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1777,6 +1786,13 @@ mod tests {
|
||||
&config.permissions.effective_permission_profile(),
|
||||
config.cwd.as_path(),
|
||||
);
|
||||
let expected_runtime_workspace_roots = Some(
|
||||
config
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let start = thread_start_params_from_config(
|
||||
&config,
|
||||
@@ -1800,6 +1816,18 @@ mod tests {
|
||||
assert_eq!(start.cwd, None);
|
||||
assert_eq!(resume.cwd, None);
|
||||
assert_eq!(fork.cwd, None);
|
||||
assert_eq!(
|
||||
start.runtime_workspace_roots,
|
||||
expected_runtime_workspace_roots
|
||||
);
|
||||
assert_eq!(
|
||||
resume.runtime_workspace_roots,
|
||||
expected_runtime_workspace_roots
|
||||
);
|
||||
assert_eq!(
|
||||
fork.runtime_workspace_roots,
|
||||
expected_runtime_workspace_roots
|
||||
);
|
||||
assert_eq!(start.model_provider, None);
|
||||
assert_eq!(resume.model_provider, None);
|
||||
assert_eq!(fork.model_provider, None);
|
||||
@@ -2070,6 +2098,10 @@ mod tests {
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
runtime_workspace_roots: vec![
|
||||
test_path_buf("/tmp/project").abs(),
|
||||
test_path_buf("/tmp/project/extra").abs(),
|
||||
],
|
||||
instruction_sources: vec![test_path_buf("/tmp/project/AGENTS.md").abs()],
|
||||
approval_policy: codex_app_server_protocol::AskForApproval::Never,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,
|
||||
@@ -2090,6 +2122,10 @@ mod tests {
|
||||
.await
|
||||
.expect("resume response should map");
|
||||
assert_eq!(started.session.forked_from_id, Some(forked_from_id));
|
||||
assert_eq!(
|
||||
started.session.runtime_workspace_roots,
|
||||
response.runtime_workspace_roots
|
||||
);
|
||||
assert_eq!(
|
||||
started.session.instruction_source_paths,
|
||||
response.instruction_sources
|
||||
@@ -2097,6 +2133,17 @@ mod tests {
|
||||
assert_eq!(started.session.permission_profile, read_only_profile);
|
||||
assert_eq!(started.turns.len(), 1);
|
||||
assert_eq!(started.turns[0], response.thread.turns[0]);
|
||||
|
||||
let mut empty_roots_response = response;
|
||||
empty_roots_response.runtime_workspace_roots = Vec::new();
|
||||
let started = started_thread_from_resume_response(
|
||||
empty_roots_response,
|
||||
&config,
|
||||
ThreadParamsMode::Remote,
|
||||
)
|
||||
.await
|
||||
.expect("resume response should map");
|
||||
assert_eq!(started.session.runtime_workspace_roots, Vec::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2193,6 +2240,7 @@ mod tests {
|
||||
/*active_permission_profile*/ None,
|
||||
test_path_buf("/tmp/project").abs(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
/*reasoning_effort*/ None,
|
||||
&config,
|
||||
)
|
||||
@@ -2227,6 +2275,7 @@ mod tests {
|
||||
/*active_permission_profile*/ None,
|
||||
test_path_buf("/tmp/project").abs(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
/*reasoning_effort*/ None,
|
||||
&config,
|
||||
)
|
||||
|
||||
@@ -32,26 +32,12 @@ impl ChatWidget {
|
||||
self.forked_from = session.forked_from_id;
|
||||
self.current_rollout_path = session.rollout_path.clone();
|
||||
self.current_cwd = Some(session.cwd.to_path_buf());
|
||||
let previous_cwd = self.config.cwd.clone();
|
||||
let previous_workspace_roots = self.config.workspace_roots.clone();
|
||||
self.config.cwd = session.cwd.clone();
|
||||
if !self.config.workspace_roots_explicit {
|
||||
let mut workspace_roots = vec![session.cwd.clone()];
|
||||
if previous_workspace_roots
|
||||
.iter()
|
||||
.any(|root| root == &previous_cwd)
|
||||
{
|
||||
for root in previous_workspace_roots {
|
||||
if root != previous_cwd
|
||||
&& !workspace_roots.iter().any(|existing| existing == &root)
|
||||
{
|
||||
workspace_roots.push(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.config.workspace_roots = workspace_roots.clone();
|
||||
self.config.permissions.set_workspace_roots(workspace_roots);
|
||||
}
|
||||
let runtime_workspace_roots = session.runtime_workspace_roots.clone();
|
||||
self.config.workspace_roots = runtime_workspace_roots.clone();
|
||||
self.config
|
||||
.permissions
|
||||
.set_workspace_roots(runtime_workspace_roots);
|
||||
self.effective_service_tier = session.service_tier.clone();
|
||||
if let Err(err) = self
|
||||
.config
|
||||
|
||||
@@ -28,6 +28,7 @@ async fn submission_preserves_text_elements_and_local_images() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -131,6 +132,7 @@ async fn submission_includes_configured_permission_profile() {
|
||||
permission_profile: expected_permission_profile.clone(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -180,6 +182,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
|
||||
permission_profile: expected_permission_profile.clone(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -221,6 +224,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -314,6 +318,7 @@ async fn enter_with_only_remote_images_submits_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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -377,6 +382,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -415,6 +421,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -453,6 +460,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -494,6 +502,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
|
||||
@@ -957,6 +957,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
|
||||
@@ -29,6 +29,7 @@ async fn resumed_initial_messages_render_history() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -99,6 +100,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -167,6 +169,7 @@ async fn replayed_user_message_preserves_remote_image_urls() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -266,6 +269,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
|
||||
permission_profile: expected_permission_profile,
|
||||
active_permission_profile: None,
|
||||
cwd: expected_cwd.clone(),
|
||||
runtime_workspace_roots: vec![expected_cwd.clone()],
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -300,7 +304,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
|
||||
assert_eq!(
|
||||
chat.config_ref().permissions.effective_permission_profile(),
|
||||
updated_profile
|
||||
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&expected_cwd)),
|
||||
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&expected_cwd,)),
|
||||
"effective permissions should still use the current thread runtime workspace roots"
|
||||
);
|
||||
}
|
||||
@@ -319,9 +323,10 @@ async fn session_configured_preserves_profile_workspace_roots() {
|
||||
.set_workspace_roots(chat.config.workspace_roots.clone());
|
||||
|
||||
let session_cwd = test_path_buf("/home/user/sub-agent").abs();
|
||||
let session_workspace_roots = vec![session_cwd.clone(), profile_root];
|
||||
let session_runtime_workspace_roots = vec![session_cwd.clone()];
|
||||
let session_effective_workspace_roots = vec![session_cwd.clone(), profile_root];
|
||||
let session_permission_profile = PermissionProfile::workspace_write()
|
||||
.materialize_project_roots_with_workspace_roots(&session_workspace_roots);
|
||||
.materialize_project_roots_with_workspace_roots(&session_effective_workspace_roots);
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
@@ -335,6 +340,7 @@ async fn session_configured_preserves_profile_workspace_roots() {
|
||||
permission_profile: session_permission_profile.clone(),
|
||||
active_permission_profile: None,
|
||||
cwd: session_cwd.clone(),
|
||||
runtime_workspace_roots: session_runtime_workspace_roots.clone(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -347,7 +353,7 @@ async fn session_configured_preserves_profile_workspace_roots() {
|
||||
assert_eq!(&chat.config_ref().cwd, &session_cwd);
|
||||
assert_eq!(
|
||||
chat.config_ref().permissions.user_visible_workspace_roots(),
|
||||
session_workspace_roots.as_slice()
|
||||
session_runtime_workspace_roots.as_slice()
|
||||
);
|
||||
assert_eq!(
|
||||
chat.config_ref().permissions.effective_permission_profile(),
|
||||
@@ -380,6 +386,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
|
||||
permission_profile: expected_permission_profile,
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/external").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -420,6 +427,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -474,6 +482,7 @@ async fn replayed_user_message_with_only_local_images_renders_history_cell() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -744,6 +753,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_project_path().abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -789,6 +799,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_project_path().abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -584,6 +584,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_project_path().abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -631,6 +632,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
|
||||
permission_profile,
|
||||
active_permission_profile: None,
|
||||
cwd,
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -1217,6 +1217,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -1403,6 +1404,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
|
||||
@@ -2230,6 +2230,7 @@ async fn session_configured_clears_goal_status_footer() {
|
||||
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: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
|
||||
@@ -448,6 +448,7 @@ fn session_configured_event(model: &str) -> ThreadSessionState {
|
||||
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,
|
||||
|
||||
@@ -42,9 +42,30 @@ pub(crate) struct ThreadSessionState {
|
||||
/// when the server knows it.
|
||||
pub(crate) active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub(crate) instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
pub(crate) message_history: Option<MessageHistoryMetadata>,
|
||||
pub(crate) network_proxy: Option<SessionNetworkProxyRuntime>,
|
||||
pub(crate) rollout_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ThreadSessionState {
|
||||
pub(crate) fn set_cwd_retargeting_implicit_runtime_workspace_root(
|
||||
&mut self,
|
||||
cwd: AbsolutePathBuf,
|
||||
) {
|
||||
let previous_cwd = std::mem::replace(&mut self.cwd, cwd.clone());
|
||||
if !self.runtime_workspace_roots.contains(&previous_cwd) {
|
||||
return;
|
||||
}
|
||||
|
||||
let previous_roots = std::mem::take(&mut self.runtime_workspace_roots);
|
||||
self.runtime_workspace_roots.push(cwd);
|
||||
for root in previous_roots {
|
||||
if root != previous_cwd && !self.runtime_workspace_roots.contains(&root) {
|
||||
self.runtime_workspace_roots.push(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user