Scope runtime workspace roots to execution environments (#32952)

## Why

Multi-environment turns can run against different filesystems, so a single
thread-wide workspace-root list cannot accurately describe every selected
environment.

## What changed

- Add optional `runtimeWorkspaceRoots` to each app-server environment selection.
  Omitted roots default to that environment's `cwd`, while an empty list selects
  no roots.
- Carry environment-native roots through turn selection, model-visible context,
  permission materialization, and tool sandboxing.
- Keep the top-level `runtimeWorkspaceRoots` field as a compatibility input when
  constructing default environment selections; explicit selections use their
  own roots.

## Testing

- Cover thread and turn API defaults, overrides, empty roots, and schema
  round-tripping.
- Verify foreign roots remain model-visible and remote execution applies the
  target environment's roots before sandbox selection.

GitOrigin-RevId: c6e7a62883c45c676c153055f46a55ee15a323a4
This commit is contained in:
pakrym-oai authored and copyberry committed 2026-07-14 04:41:08 +00:00
1 parent b019a5577c
commit 393f64565a
62 files changed
+769 -446

No files matched your search

+22 -19
View File
@@ -46,6 +46,7 @@ pub(crate) struct TurnEnvironment {
pub(crate) environment_id: String,
pub(crate) environment: Arc<Environment>,
cwd: PathUri,
workspace_roots: Vec<PathUri>,
pub(crate) shell: Option<shell::Shell>,
pub(crate) shell_snapshot: ShellSnapshotTask,
}
@@ -55,12 +56,14 @@ impl TurnEnvironment {
environment_id: String,
environment: Arc<Environment>,
cwd: PathUri,
workspace_roots: Vec<PathUri>,
shell: Option<shell::Shell>,
) -> Self {
Self {
environment_id,
environment,
cwd,
workspace_roots,
shell,
shell_snapshot: futures::future::ready(None).boxed().shared(),
}
@@ -80,10 +83,15 @@ impl TurnEnvironment {
&self.cwd
}
pub(crate) fn workspace_roots(&self) -> &[PathUri] {
&self.workspace_roots
}
pub(crate) fn selection(&self) -> TurnEnvironmentSelection {
TurnEnvironmentSelection {
environment_id: self.environment_id.clone(),
cwd: self.cwd.clone(),
workspace_roots: self.workspace_roots.clone(),
}
}
}
@@ -94,6 +102,7 @@ impl std::fmt::Debug for TurnEnvironment {
.field("environment_id", &self.environment_id)
.field("environment", &self.environment)
.field("cwd", &self.cwd)
.field("workspace_roots", &self.workspace_roots)
.field("shell", &self.shell)
.finish_non_exhaustive()
}
@@ -317,10 +326,11 @@ impl TurnContext {
pub(crate) fn file_system_sandbox_context(
&self,
additional_permissions: Option<AdditionalPermissionProfile>,
cwd: &PathUri,
environment: &TurnEnvironment,
) -> FileSystemSandboxContext {
let permission_profile = self.config.permissions.permission_profile();
let (base_file_system_sandbox_policy, base_network_sandbox_policy) =
self.permission_profile.to_runtime_permissions();
permission_profile.to_runtime_permissions();
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&base_file_system_sandbox_policy,
additional_permissions.as_ref(),
@@ -330,19 +340,14 @@ impl TurnContext {
additional_permissions.as_ref(),
);
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
self.permission_profile.enforcement(),
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
);
FileSystemSandboxContext {
permissions: permissions.into(),
cwd: Some(cwd.clone()),
workspace_roots: self
.config
.effective_workspace_roots()
.iter()
.map(PathUri::from_abs_path)
.collect(),
cwd: Some(environment.cwd().clone()),
workspace_roots: environment.workspace_roots().to_vec(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self
.config
@@ -438,10 +443,11 @@ 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();
let workspace_roots = session_configuration.primary_workspace_roots();
per_turn_config.workspace_roots = workspace_roots.clone();
per_turn_config
.permissions
.set_workspace_roots(session_configuration.workspace_roots.clone());
.set_workspace_roots(workspace_roots);
per_turn_config.model_reasoning_effort =
session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
@@ -476,10 +482,6 @@ 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
}
@@ -531,6 +533,7 @@ impl Session {
per_turn_config.features.enabled(Feature::FastMode),
&model_info,
);
let permission_profile = per_turn_config.permissions.effective_permission_profile();
let per_turn_config = Arc::new(per_turn_config);
let turn_metadata_state = Arc::new(TurnMetadataState::new(
session_id.to_string(),
@@ -541,7 +544,7 @@ impl Session {
session_configuration.thread_source.clone(),
sub_id.clone(),
cwd.clone(),
&session_configuration.permission_profile(),
&permission_profile,
session_configuration.windows_sandbox_level,
network.is_some(),
));
@@ -578,7 +581,7 @@ impl Session {
multi_agent_version,
personality: session_configuration.personality,
approval_policy: session_configuration.approval_policy.clone(),
permission_profile: session_configuration.permission_profile(),
permission_profile,
network,
windows_sandbox_level: session_configuration.windows_sandbox_level,
available_models,
@@ -702,7 +705,7 @@ impl Session {
multi_agent_runtime: TurnMultiAgentRuntime,
) -> Arc<TurnContext> {
let turn_environments = self.services.turn_environments.snapshot().await;
let primary_turn_environment = turn_environments.primary().cloned();
let primary_turn_environment = turn_environments.primary();
// TODO(anp): Migrate per-turn config and legacy TurnContext cwd consumers to PathUri so
// a foreign primary environment does not fall back to the session's host cwd.
let cwd = primary_turn_environment