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
@@ -59,6 +59,8 @@ pub struct ThreadConfigSnapshot {
|
||||
pub permission_profile: PermissionProfile,
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub profile_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub ephemeral: bool,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub personality: Option<Personality>,
|
||||
@@ -82,6 +84,8 @@ impl ThreadConfigSnapshot {
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CodexThreadTurnContextOverrides {
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
@@ -258,6 +262,8 @@ impl CodexThread {
|
||||
) -> ConstraintResult<()> {
|
||||
let CodexThreadTurnContextOverrides {
|
||||
cwd,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
@@ -283,6 +289,8 @@ impl CodexThread {
|
||||
|
||||
let updates = SessionSettingsUpdate {
|
||||
cwd,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
|
||||
@@ -1905,9 +1905,28 @@ async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cwd_abs = cwd.abs();
|
||||
let runtime_root_abs = runtime_root.abs();
|
||||
let profile_root_abs = profile_root.abs();
|
||||
assert_eq!(
|
||||
config.workspace_roots,
|
||||
vec![cwd_abs.clone(), runtime_root_abs.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.workspace_roots(),
|
||||
&[cwd_abs.clone(), runtime_root_abs.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
config.effective_workspace_roots(),
|
||||
vec![
|
||||
cwd_abs.clone(),
|
||||
runtime_root_abs.clone(),
|
||||
profile_root_abs.clone()
|
||||
]
|
||||
);
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
for root in [cwd.abs(), runtime_root.abs(), profile_root_abs.clone()] {
|
||||
for root in [cwd_abs, runtime_root_abs, profile_root_abs.clone()] {
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(root.as_path(), cwd.as_path()),
|
||||
"expected workspace root to be writable, policy: {policy:?}"
|
||||
|
||||
@@ -1231,6 +1231,13 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn effective_workspace_roots(&self) -> Vec<AbsolutePathBuf> {
|
||||
let mut workspace_roots = self.workspace_roots.clone();
|
||||
workspace_roots.extend(self.permissions.profile_workspace_roots().iter().cloned());
|
||||
dedupe_absolute_paths(&mut workspace_roots);
|
||||
workspace_roots
|
||||
}
|
||||
|
||||
pub fn to_models_manager_config(&self) -> ModelsManagerConfig {
|
||||
ModelsManagerConfig {
|
||||
model_context_window: self.model_context_window,
|
||||
@@ -2671,8 +2678,6 @@ impl Config {
|
||||
configured_workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
|
||||
}
|
||||
dedupe_absolute_paths(&mut configured_workspace_roots);
|
||||
workspace_roots.extend(configured_workspace_roots.iter().cloned());
|
||||
dedupe_absolute_paths(&mut workspace_roots);
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_materialized_project_roots_for_workspace_roots(&configured_workspace_roots);
|
||||
let mut permission_profile = if let Some(permission_profile) =
|
||||
|
||||
@@ -101,24 +101,6 @@ impl ResolvedPermissionProfile {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_permission_profile(&self, permission_profile: PermissionProfile) -> Self {
|
||||
match self {
|
||||
Self::Legacy(_) => Self::legacy(permission_profile),
|
||||
Self::BuiltIn(profile) => Self::BuiltIn(BuiltInPermissionProfile {
|
||||
id: profile.id,
|
||||
extends: profile.extends.clone(),
|
||||
permission_profile,
|
||||
profile_workspace_roots: profile.profile_workspace_roots.clone(),
|
||||
}),
|
||||
Self::Named(profile) => Self::Named(NamedPermissionProfile {
|
||||
id: profile.id.clone(),
|
||||
extends: profile.extends.clone(),
|
||||
permission_profile,
|
||||
profile_workspace_roots: profile.profile_workspace_roots.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
|
||||
match self {
|
||||
Self::Legacy(_) => None,
|
||||
@@ -189,19 +171,6 @@ impl PermissionProfileState {
|
||||
self.resolved_permission_profile.get().permission_profile()
|
||||
}
|
||||
|
||||
pub(crate) fn clone_with_permission_profile(
|
||||
&self,
|
||||
permission_profile: PermissionProfile,
|
||||
) -> ConstraintResult<Self> {
|
||||
let candidate = self
|
||||
.resolved_permission_profile
|
||||
.get()
|
||||
.with_permission_profile(permission_profile);
|
||||
let mut state = self.clone();
|
||||
state.resolved_permission_profile.set(candidate)?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub(crate) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
|
||||
self.resolved_permission_profile
|
||||
.get()
|
||||
|
||||
@@ -146,6 +146,8 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer,
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
workspace_roots: None,
|
||||
profile_workspace_roots: None,
|
||||
permission_profile,
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: None,
|
||||
@@ -163,6 +165,8 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
}
|
||||
Op::UserInputWithTurnContext {
|
||||
cwd,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
@@ -195,6 +199,8 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
items,
|
||||
SessionSettingsUpdate {
|
||||
cwd,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
|
||||
@@ -621,6 +621,7 @@ impl Codex {
|
||||
permission_profile_state: session_permission_profile_state_from_config(&config)?,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: environment_selections.to_selections(),
|
||||
@@ -821,15 +822,7 @@ fn get_service_tier(
|
||||
fn session_permission_profile_state_from_config(
|
||||
config: &Config,
|
||||
) -> CodexResult<PermissionProfileState> {
|
||||
config
|
||||
.permissions
|
||||
.permission_profile_state()
|
||||
.clone_with_permission_profile(config.permissions.effective_permission_profile())
|
||||
.map_err(|err| {
|
||||
CodexErr::Fatal(format!(
|
||||
"failed to materialize workspace roots for session permissions: {err}"
|
||||
))
|
||||
})
|
||||
Ok(config.permissions.permission_profile_state().clone())
|
||||
}
|
||||
|
||||
fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool {
|
||||
|
||||
@@ -63,9 +63,9 @@ pub(crate) struct SessionConfiguration {
|
||||
/// When to escalate for approval for execution
|
||||
pub(super) approval_policy: Constrained<AskForApproval>,
|
||||
pub(super) approvals_reviewer: ApprovalsReviewer,
|
||||
/// Permission profile state for the session. Keep the constrained profile
|
||||
/// and selected profile id in sync by using the methods below instead of
|
||||
/// mutating the fields independently.
|
||||
/// Permission profile state for the session. Keep the constrained profile,
|
||||
/// active profile id, and profile-defined workspace roots in sync by using
|
||||
/// the methods below instead of mutating the fields independently.
|
||||
pub(super) permission_profile_state: PermissionProfileState,
|
||||
pub(super) windows_sandbox_level: WindowsSandboxLevel,
|
||||
|
||||
@@ -74,6 +74,9 @@ pub(crate) struct SessionConfiguration {
|
||||
/// execution sandbox are resolved against this directory **instead** of
|
||||
/// the process-wide current working directory.
|
||||
pub(super) cwd: AbsolutePathBuf,
|
||||
/// Thread-scoped runtime workspace roots for materializing symbolic
|
||||
/// workspace permissions at session runtime.
|
||||
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
|
||||
/// Directory containing all Codex state for this session.
|
||||
pub(super) codex_home: AbsolutePathBuf,
|
||||
/// Optional user-facing name for the thread, updated during the session.
|
||||
@@ -107,13 +110,20 @@ impl SessionConfiguration {
|
||||
}
|
||||
|
||||
pub(super) fn permission_profile(&self) -> PermissionProfile {
|
||||
self.permission_profile_state.permission_profile().clone()
|
||||
self.permission_profile_state
|
||||
.permission_profile()
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
|
||||
}
|
||||
|
||||
pub(super) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
|
||||
self.permission_profile_state.active_permission_profile()
|
||||
}
|
||||
|
||||
pub(super) fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] {
|
||||
self.permission_profile_state.profile_workspace_roots()
|
||||
}
|
||||
|
||||
pub(super) fn apply_permission_profile_to_permissions(
|
||||
&self,
|
||||
permissions: &mut crate::config::Permissions,
|
||||
@@ -164,6 +174,8 @@ impl SessionConfiguration {
|
||||
permission_profile: self.permission_profile(),
|
||||
active_permission_profile: self.active_permission_profile(),
|
||||
cwd: self.cwd.clone(),
|
||||
workspace_roots: self.workspace_roots.clone(),
|
||||
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
|
||||
ephemeral: self.original_config_do_not_use.ephemeral,
|
||||
reasoning_effort: self.collaboration_mode.reasoning_effort(),
|
||||
personality: self.personality,
|
||||
@@ -243,6 +255,23 @@ impl SessionConfiguration {
|
||||
|
||||
let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path();
|
||||
next_configuration.cwd = absolute_cwd;
|
||||
if let Some(workspace_roots) = updates.workspace_roots.clone() {
|
||||
next_configuration.workspace_roots = workspace_roots;
|
||||
} else if cwd_changed && self.workspace_roots.contains(&self.cwd) {
|
||||
let mut retargeted_workspace_roots =
|
||||
Vec::with_capacity(next_configuration.workspace_roots.len());
|
||||
for root in &self.workspace_roots {
|
||||
let root = if root == &self.cwd {
|
||||
next_configuration.cwd.clone()
|
||||
} else {
|
||||
root.clone()
|
||||
};
|
||||
if !retargeted_workspace_roots.contains(&root) {
|
||||
retargeted_workspace_roots.push(root);
|
||||
}
|
||||
}
|
||||
next_configuration.workspace_roots = retargeted_workspace_roots;
|
||||
}
|
||||
|
||||
if let Some(permission_profile) = updates.permission_profile.clone() {
|
||||
let active_permission_profile =
|
||||
@@ -256,6 +285,7 @@ impl SessionConfiguration {
|
||||
next_configuration.set_permission_profile_projection(
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
updates.profile_workspace_roots.clone().unwrap_or_default(),
|
||||
Some(¤t_file_system_sandbox_policy),
|
||||
)?;
|
||||
} else if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
|
||||
@@ -311,6 +341,7 @@ impl SessionConfiguration {
|
||||
&mut self,
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
profile_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
preserve_deny_reads_from: Option<&FileSystemSandboxPolicy>,
|
||||
) -> ConstraintResult<()> {
|
||||
let enforcement = permission_profile.enforcement();
|
||||
@@ -329,7 +360,7 @@ impl SessionConfiguration {
|
||||
self.permission_profile_state.set_active_permission_profile(
|
||||
effective_permission_profile,
|
||||
active_permission_profile,
|
||||
Vec::new(),
|
||||
profile_workspace_roots,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -337,6 +368,8 @@ impl SessionConfiguration {
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) cwd: Option<PathBuf>,
|
||||
pub(crate) workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub(crate) profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub(crate) approval_policy: Option<AskForApproval>,
|
||||
pub(crate) approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
pub(crate) sandbox_policy: Option<SandboxPolicy>,
|
||||
|
||||
@@ -36,6 +36,7 @@ use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
@@ -2153,29 +2154,47 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn session_permission_profile_materializes_runtime_workspace_roots() -> anyhow::Result<()> {
|
||||
async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow::Result<()> {
|
||||
let codex_home = tempfile::TempDir::new()?;
|
||||
let cwd = tempfile::TempDir::new()?;
|
||||
let extra_root = tempfile::TempDir::new()?;
|
||||
let old_root = test_path_buf("/workspace/old").abs();
|
||||
let new_root = test_path_buf("/workspace/new").abs();
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(crate::config::ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
|
||||
additional_writable_roots: vec![extra_root.path().to_path_buf()],
|
||||
additional_writable_roots: vec![old_root.to_path_buf()],
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let session_permission_profile_state = session_permission_profile_state_from_config(&config)?;
|
||||
let file_system_policy = session_permission_profile_state
|
||||
let stored_file_system_policy = session_permission_profile_state
|
||||
.permission_profile()
|
||||
.file_system_sandbox_policy();
|
||||
|
||||
assert!(
|
||||
file_system_policy.can_write_path_with_cwd(extra_root.path(), config.cwd.as_path()),
|
||||
"session permission profile should carry materialized runtime workspace roots"
|
||||
!stored_file_system_policy
|
||||
.can_write_path_with_cwd(old_root.as_path(), config.cwd.as_path()),
|
||||
"session permission profile state should keep runtime workspace roots symbolic"
|
||||
);
|
||||
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
session_configuration.cwd = config.cwd.clone();
|
||||
session_configuration.workspace_roots = config.workspace_roots.clone();
|
||||
session_configuration.permission_profile_state = session_permission_profile_state;
|
||||
|
||||
let initial_policy = session_configuration.file_system_sandbox_policy();
|
||||
assert!(initial_policy.can_write_path_with_cwd(old_root.as_path(), config.cwd.as_path()));
|
||||
|
||||
let updated = session_configuration.apply(&SessionSettingsUpdate {
|
||||
workspace_roots: Some(vec![new_root.clone()]),
|
||||
..Default::default()
|
||||
})?;
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2917,6 +2936,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
@@ -3020,6 +3040,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
@@ -3492,6 +3513,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
@@ -3561,6 +3583,8 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
|
||||
),
|
||||
)
|
||||
.expect("set permission profile");
|
||||
let expected_file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.materialize_project_roots_with_workspace_roots(&session_configuration.workspace_roots);
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
@@ -3571,7 +3595,7 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
|
||||
|
||||
assert_eq!(
|
||||
updated.file_system_sandbox_policy(),
|
||||
file_system_sandbox_policy
|
||||
expected_file_system_sandbox_policy
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3620,7 +3644,8 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
|
||||
})
|
||||
.expect("permission profile update should succeed");
|
||||
|
||||
let mut expected_file_system_policy = requested_file_system_policy;
|
||||
let mut expected_file_system_policy = requested_file_system_policy
|
||||
.materialize_project_roots_with_workspace_roots(&session_configuration.workspace_roots);
|
||||
expected_file_system_policy.glob_scan_max_depth = Some(2);
|
||||
expected_file_system_policy.entries.push(deny_entry);
|
||||
assert_eq!(
|
||||
@@ -3675,6 +3700,93 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configuration_apply_rebinds_symbolic_profile_to_updated_workspace_roots() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let old_root = tempfile::tempdir().expect("create old root");
|
||||
let new_root = tempfile::tempdir().expect("create new root");
|
||||
let profile_root = tempfile::tempdir().expect("create profile root");
|
||||
let old_root = old_root.path().abs();
|
||||
let new_root = new_root.path().abs();
|
||||
let profile_root = profile_root.path().abs();
|
||||
session_configuration.workspace_roots = vec![old_root.clone()];
|
||||
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
workspace_roots: Some(vec![new_root.clone()]),
|
||||
permission_profile: Some(permission_profile),
|
||||
active_permission_profile: Some(ActivePermissionProfile::new("dev")),
|
||||
profile_workspace_roots: Some(vec![profile_root.clone()]),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("permission profile update should succeed");
|
||||
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
assert_eq!(
|
||||
updated.active_permission_profile(),
|
||||
Some(ActivePermissionProfile::new("dev"))
|
||||
);
|
||||
assert_eq!(updated.profile_workspace_roots(), &[profile_root]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_update() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let old_root = tempfile::tempdir().expect("create old root");
|
||||
let new_root = tempfile::tempdir().expect("create new root");
|
||||
let extra_root = tempfile::tempdir().expect("create extra root");
|
||||
let old_root = old_root.path().abs();
|
||||
let new_root = new_root.path().abs();
|
||||
let extra_root = extra_root.path().abs();
|
||||
session_configuration.cwd = old_root.clone();
|
||||
session_configuration.workspace_roots = vec![old_root.clone(), extra_root.clone()];
|
||||
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
session_configuration
|
||||
.set_permission_profile_for_tests(permission_profile)
|
||||
.expect("set permission profile");
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(new_root.to_path_buf()),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
|
||||
assert_eq!(
|
||||
updated.workspace_roots,
|
||||
vec![new_root.clone(), extra_root.clone()]
|
||||
);
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, ignore)]
|
||||
#[tokio::test]
|
||||
async fn new_default_turn_uses_config_aware_skills_for_role_overrides() {
|
||||
@@ -3762,12 +3874,13 @@ enabled = false
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_update() {
|
||||
async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_update() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let project_root = workspace.path().join("project");
|
||||
let original_cwd = project_root.join("subdir");
|
||||
session_configuration.cwd = original_cwd.abs();
|
||||
let original_cwd = workspace.path().join("repo-a").abs();
|
||||
let project_root = workspace.path().join("repo-b").abs();
|
||||
session_configuration.cwd = original_cwd.clone();
|
||||
session_configuration.workspace_roots = vec![session_configuration.cwd.clone()];
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
@@ -3790,20 +3903,23 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(project_root.clone()),
|
||||
cwd: Some(project_root.to_path_buf()),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
|
||||
let expected_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&updated.sandbox_policy(),
|
||||
&project_root,
|
||||
);
|
||||
assert_eq!(updated.workspace_roots, vec![project_root.clone()]);
|
||||
assert!(
|
||||
updated
|
||||
.file_system_sandbox_policy()
|
||||
.is_semantically_equivalent_to(&expected_file_system_policy, &project_root),
|
||||
"cwd-only update should rederive the legacy filesystem policy for the new cwd"
|
||||
.can_write_path_with_cwd(project_root.as_path(), updated.cwd.as_path()),
|
||||
"cwd-only update should keep the new cwd writable"
|
||||
);
|
||||
assert!(
|
||||
!updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd.as_path()),
|
||||
"cwd-only update should not keep the old implicit cwd writable"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4032,6 +4148,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
@@ -4140,6 +4257,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
@@ -4373,6 +4491,7 @@ async fn make_session_with_config_and_rx(
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
@@ -4475,6 +4594,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
@@ -5124,6 +5244,8 @@ fn op_kind_distinguishes_turn_ops() {
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
cwd: None,
|
||||
workspace_roots: None,
|
||||
profile_workspace_roots: None,
|
||||
approval_policy: None,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: None,
|
||||
@@ -5991,6 +6113,7 @@ where
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
|
||||
@@ -432,6 +432,10 @@ impl Session {
|
||||
let config = session_configuration.original_config_do_not_use.clone();
|
||||
let mut per_turn_config = (*config).clone();
|
||||
per_turn_config.cwd = cwd;
|
||||
per_turn_config.workspace_roots = session_configuration.workspace_roots.clone();
|
||||
per_turn_config
|
||||
.permissions
|
||||
.set_workspace_roots(session_configuration.workspace_roots.clone());
|
||||
per_turn_config.model_reasoning_effort =
|
||||
session_configuration.collaboration_mode.reasoning_effort();
|
||||
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
|
||||
@@ -466,6 +470,10 @@ impl Session {
|
||||
Self::build_per_turn_config(session_configuration, session_configuration.cwd.clone());
|
||||
config.model = Some(session_configuration.collaboration_mode.model().to_string());
|
||||
config.permissions.approval_policy = session_configuration.approval_policy.clone();
|
||||
config.workspace_roots = session_configuration.workspace_roots.clone();
|
||||
config
|
||||
.permissions
|
||||
.set_workspace_roots(session_configuration.workspace_roots.clone());
|
||||
config
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user