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

+33 -7
View File
@@ -22,6 +22,7 @@ use crate::shell_snapshot::ShellSnapshot;
pub(crate) fn default_thread_environment_selections(
environment_manager: &EnvironmentManager,
cwd: &AbsolutePathBuf,
workspace_roots: &[AbsolutePathBuf],
) -> Vec<TurnEnvironmentSelection> {
environment_manager
.default_environment_ids()
@@ -29,6 +30,7 @@ pub(crate) fn default_thread_environment_selections(
.map(|environment_id| TurnEnvironmentSelection {
environment_id,
cwd: PathUri::from_abs_path(cwd),
workspace_roots: workspace_roots.iter().map(PathUri::from_abs_path).collect(),
})
.collect()
}
@@ -175,8 +177,13 @@ impl ThreadEnvironments {
} else {
Some(local_shell)
};
let mut turn_environment =
TurnEnvironment::new(selection.environment_id, environment, selection.cwd, shell);
let mut turn_environment = TurnEnvironment::new(
selection.environment_id,
environment,
selection.cwd,
selection.workspace_roots,
shell,
);
let task = shell_snapshot
.build(turn_environment.clone())
.boxed()
@@ -406,10 +413,11 @@ mod tests {
.await;
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri],
}]
);
}
@@ -434,15 +442,17 @@ url = "ws://127.0.0.1:8765"
.expect("environment manager");
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
vec![
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri.clone()],
},
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri],
},
]
);
@@ -454,7 +464,7 @@ url = "ws://127.0.0.1:8765"
let manager = EnvironmentManager::without_environments();
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
Vec::<TurnEnvironmentSelection>::new()
);
}
@@ -476,6 +486,7 @@ url = "ws://127.0.0.1:8765"
turn_environments.update_selections(&[TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
}]);
let snapshot = turn_environments.snapshot().await;
@@ -496,6 +507,7 @@ url = "ws://127.0.0.1:8765"
let first = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
};
let resolved = resolve_turn_environments(
@@ -505,6 +517,7 @@ url = "ws://127.0.0.1:8765"
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.join("other").expect("other cwd URI"),
workspace_roots: Vec::new(),
},
],
)
@@ -525,6 +538,7 @@ url = "ws://127.0.0.1:8765"
&[TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: selected_cwd_uri,
workspace_roots: Vec::new(),
}],
)
.await;
@@ -562,6 +576,7 @@ url = "ws://127.0.0.1:8765"
let local = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
};
let resolved = resolve_turn_environments(
@@ -570,6 +585,7 @@ url = "ws://127.0.0.1:8765"
TurnEnvironmentSelection {
environment_id: "missing".to_string(),
cwd: cwd_uri,
workspace_roots: Vec::new(),
},
local.clone(),
],
@@ -597,6 +613,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
workspace_roots: Vec::new(),
};
let environments = Arc::new(ThreadEnvironments::new(
manager,
@@ -644,10 +661,12 @@ url = "ws://127.0.0.1:8765"
let remote = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd.clone(),
workspace_roots: Vec::new(),
};
let local = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd,
workspace_roots: Vec::new(),
};
let turn_environments = ThreadEnvironments::new(
manager,
@@ -706,6 +725,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
workspace_roots: Vec::new(),
};
let environments = ThreadEnvironments::new(
Arc::clone(&manager),
@@ -757,6 +777,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
};
let environments = ThreadEnvironments::new(
Arc::clone(&manager),
@@ -813,6 +834,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
};
let inherited_environment = Arc::new(
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
@@ -822,6 +844,7 @@ url = "ws://127.0.0.1:8765"
selection.environment_id.clone(),
Arc::clone(&inherited_environment),
selection.cwd.clone(),
Vec::new(),
/*shell*/ None,
);
let manager = Arc::new(EnvironmentManager::without_environments());
@@ -865,6 +888,7 @@ url = "ws://127.0.0.1:8765"
&[TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
}],
)
.await;
@@ -878,6 +902,7 @@ url = "ws://127.0.0.1:8765"
REMOTE_ENVIRONMENT_ID.to_string(),
remote_environment.clone(),
cwd_uri.clone(),
Vec::new(),
/*shell*/ None,
)],
starting: Vec::new(),
@@ -889,6 +914,7 @@ url = "ws://127.0.0.1:8765"
REMOTE_ENVIRONMENT_ID.to_string(),
remote_environment,
cwd_uri,
Vec::new(),
/*shell*/ None,
),
],