Preserve executor path URIs in permission profile workspace roots (#45863)

## Why

Executor profile roots can use path conventions that are not native to the current host. Converting them to host paths during configuration or turn reconstruction can reject or drop those roots, while case-insensitive comparison can hide Windows path spelling changes.

## What changed

- Store profile roots as URI-backed `ProfileWorkspaceRoot` values throughout permission snapshots and thread settings, preserving spelling in equality and deduplication.
- Keep effective workspace roots as `PathUri` values for permission materialization and status summaries. Convert Windows sandbox root hints to native paths only at native Windows sandbox boundaries, rejecting incompatible roots.
- Omit the legacy rollout `workspace_roots` field when profile roots cannot be represented as host paths, retaining the compiled permission profile.

## Testing

Add regression coverage for Windows and UNC root spelling changes, settings restoration and turn recording with foreign roots, executor-root status display, and backend-specific Windows root conversion.

GitOrigin-RevId: 903c068fd74959bdd10e7cb1141aa42b953a59a4
This commit is contained in:
Sean Huang
2026-09-16 04:23:46 +00:00
committed by copyberry
parent 04581f9604
commit 83dc7d11e8
27 changed files with 408 additions and 105 deletions
+1
View File
@@ -39,6 +39,7 @@ mod item_metadata;
pub use crate::local_media::MAX_PROMPT_AUDIO_INPUT_BYTES;
pub use crate::local_media::snapshot_local_user_input;
pub use crate::permission_profile_snapshot::PermissionProfileSnapshot;
pub use crate::permission_profile_snapshot::ProfileWorkspaceRoot;
pub use configuration_update::ConfigurationReasoning;
pub use executed_tool_calls::ExecutedToolCall;
pub use executed_tool_calls::ExecutedToolCallArguments;
@@ -1,8 +1,44 @@
//! Trusted permission state keeps local and remote profile roots as executor path URIs.
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use crate::models::ActivePermissionProfile;
use crate::models::PermissionProfile;
/// A profile-declared root, separate from turn-scoped runtime workspace roots.
///
/// Local and remote roots share the same URI representation. Snapshot equality
/// compares the stored URI spelling without folding Windows case, so a spelling
/// change still propagates through settings.
#[derive(Debug, Clone, Eq)]
pub struct ProfileWorkspaceRoot(PathUri);
impl PartialEq for ProfileWorkspaceRoot {
fn eq(&self, other: &Self) -> bool {
self.0.to_string() == other.0.to_string()
}
}
impl ProfileWorkspaceRoot {
/// Borrow the resolved URI without projecting it onto the current host.
pub fn as_uri(&self) -> &PathUri {
&self.0
}
}
impl From<AbsolutePathBuf> for ProfileWorkspaceRoot {
fn from(path: AbsolutePathBuf) -> Self {
Self(PathUri::from_abs_path(&path))
}
}
impl From<PathUri> for ProfileWorkspaceRoot {
fn from(path: PathUri) -> Self {
Self(path)
}
}
/// Trusted snapshot of a resolved permission profile.
///
/// Keeps the concrete permissions, optional active profile identity, and
@@ -13,7 +49,7 @@ use crate::models::PermissionProfile;
pub struct PermissionProfileSnapshot {
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
profile_workspace_roots: Vec<AbsolutePathBuf>,
profile_workspace_roots: Vec<ProfileWorkspaceRoot>,
}
impl PermissionProfileSnapshot {
@@ -44,10 +80,12 @@ impl PermissionProfileSnapshot {
/// Create a snapshot for an active profile and its declared roots.
///
/// Profile roots remain distinct from turn-scoped runtime workspace roots.
/// The supplied profile must already contain their compiled permissions;
/// storing roots here does not itself grant access or materialize rules.
pub fn active_with_profile_workspace_roots(
permission_profile: PermissionProfile,
active_permission_profile: ActivePermissionProfile,
profile_workspace_roots: Vec<AbsolutePathBuf>,
profile_workspace_roots: Vec<ProfileWorkspaceRoot>,
) -> Self {
Self {
permission_profile,
@@ -80,7 +118,11 @@ impl PermissionProfileSnapshot {
}
/// Borrow profile-declared workspace roots captured in this snapshot.
pub fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] {
pub fn profile_workspace_roots(&self) -> &[ProfileWorkspaceRoot] {
&self.profile_workspace_roots
}
}
#[cfg(test)]
#[path = "permission_profile_snapshot_tests.rs"]
mod tests;
@@ -0,0 +1,20 @@
//! Profile-root spelling changes must propagate through settings snapshots.
use super::*;
#[test]
fn executor_profile_root_case_changes_update_snapshot() {
for (first, second) in [
("file:///C:/Work/Project", "file:///C:/work/project"),
("file://server/share/Project", "file://server/share/project"),
] {
let snapshot = |root| {
PermissionProfileSnapshot::active_with_profile_workspace_roots(
PermissionProfile::read_only(),
ActivePermissionProfile::new("executor"),
vec![PathUri::parse(root).unwrap().into()],
)
};
assert_ne!(snapshot(first), snapshot(second));
}
}
+2 -1
View File
@@ -47,6 +47,7 @@ use crate::models::ImageDetail;
use crate::models::InternalChatMessageMetadataPassthrough;
use crate::models::MessagePhase;
use crate::models::PermissionProfile;
use crate::models::ProfileWorkspaceRoot;
use crate::models::ResponseInputItem;
use crate::models::ResponseItem;
use crate::models::SandboxEnforcement;
@@ -528,7 +529,7 @@ pub struct ThreadSettingsOverrides {
/// Updated profile-defined workspace roots for status summaries and
/// per-turn config reconstruction.
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub profile_workspace_roots: Option<Vec<ProfileWorkspaceRoot>>,
/// Updated command approval policy.
pub approval_policy: Option<AskForApproval>,