fix: align Workdir session cwd and model environment

Project alias, cwd, scope and command ceilings from actual sessions. Bind initial attachment cwd once in the shared broker/router while keeping root-relative delegation and detach generations. Preserve caller coordinates across reads, searches, checked checkout and commands; keep provider authorization and symlink boundaries.

Connect real SubWorkers, all filesystem Tools, scoped aliases, HTTP providers and limited Host grants in permanent regressions. Validated related normal suites (workdir http-client, tools, worker, worker-runtime, server-api typescript, fs-operation), workspace-server normal suite (1272 passed, 3 existing ignores), root check, product binary builds, rustdoc/doctests, generated contract checks, format and state-contract names. Test roots are checkout-local; Git parent discovery requires GIT_CEILING_DIRECTORIES at TMPDIR, confirmed against exact b860 baseline.
This commit is contained in:
2026-10-22 20:46:10 +09:00
parent b860c67ea2
commit 8b6e3070dd
39 changed files with 2562 additions and 160 deletions
+42 -30
View File
@@ -206,26 +206,36 @@ impl AuthorizedWorkdirSession {
.map(CheckoutSearchResult::Grep),
};
};
scoped
.authorize_scope_path(WorkdirScopeAuthorizationRequest {
rules: vec![WorkdirToolScopeRule {
target: WorkdirPath::root(),
..rule.clone()
}],
path: relative_path(&root, operation.path())?,
permission: WorkdirToolScopePermission::Read,
})
.await?;
// Lease cwd rebases paths. Use the same provider scoped-traversal contract
// with attachment output_root instead, retaining typed paths, cursors and
// Grep's rendered filenames without parsing or post-filtering output.
let mut request = CheckoutSearchRequest::new(operation);
request.scope_layers.push(vec![rule]);
self.session.checkout_search(request).await
self.search_with_scope(scoped, root, rule, CheckoutSearchRequest::new(operation))
.await
})
.await
}
async fn search_with_scope(
&self,
scoped: WorkdirSessionHandle,
root: WorkdirPath,
rule: WorkdirToolScopeRule,
mut request: CheckoutSearchRequest,
) -> Result<CheckoutSearchResult, WorkdirError> {
scoped
.authorize_scope_path(WorkdirScopeAuthorizationRequest {
rules: vec![WorkdirToolScopeRule {
target: WorkdirPath::root(),
..rule.clone()
}],
path: relative_path(&root, request.operation.path())?,
permission: WorkdirToolScopePermission::Read,
})
.await?;
// Preserve the caller's coordinate root, cursors and all existing
// attenuation layers. The provider applies the Host ceiling during
// traversal, not by filtering already-rendered filenames afterward.
request.scope_layers.push(vec![rule]);
self.session.checkout_search(request).await
}
fn authorize_command(&self) -> Result<(), WorkdirError> {
// cwd is not a sandbox: arbitrary shell can access the entire attachment.
self.authorize(COMMAND, &WorkdirPath::root(), true)
@@ -261,6 +271,12 @@ impl WorkdirSession for AuthorizedWorkdirSession {
self.session.capabilities()
}
fn environment(&self) -> workdir::WorkdirSessionEnvironment {
let mut environment = self.session.environment();
environment.capabilities = environment.capabilities.intersection(self.capabilities());
environment
}
async fn authorize_scope_path(
&self,
mut request: WorkdirScopeAuthorizationRequest,
@@ -296,8 +312,14 @@ impl WorkdirSession for AuthorizedWorkdirSession {
&self,
request: CheckoutSearchRequest,
) -> Result<CheckoutSearchResult, WorkdirError> {
self.require_legacy(READ)?;
self.session.checkout_search(request).await
let path = request.operation.path().clone();
self.with_authorized(READ, &path, true, |scoped, root, rule| async move {
match rule {
Some(rule) => self.search_with_scope(scoped, root, rule, request).await,
None => self.session.checkout_search(request).await,
}
})
.await
}
async fn checkout_observe(
@@ -490,6 +512,8 @@ mod tests {
//! Component tests connect this adapter to the real LocalWorkdirSession.
//! Only the Host authorization transport/policy is replaced; these are not E2E tests.
mod environment;
use super::*;
use crate::{WorkspaceClientError, WorkspaceRequest, WorkspaceResponse};
use server_api::authorization::ScopedPermission;
@@ -842,18 +866,6 @@ mod tests {
.await,
Err(WorkdirError::Denied(_))
));
assert!(matches!(
session
.checkout_search(CheckoutSearchRequest::new(CheckoutSearchOperation::List(
ListRequest {
path: WorkdirPath::root(),
after: None,
limit: 10,
}
)))
.await,
Err(WorkdirError::Denied(_))
));
let rule = WorkdirToolScopeRule {
target: WorkdirPath::root(),
permission: WorkdirToolScopePermission::Read,
@@ -0,0 +1,198 @@
//! Normal lib tests connecting live Host-grant checks to real provider traversal
//! and router cwd projection. Only the Host transport is replaced by the parent
//! module's grant fixture; no commands, network or ambient Workspace state are used.
use super::*;
#[tokio::test]
async fn authorization_wrapper_preserves_inner_environment_and_capability_ceiling() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir(root.path().join("src")).unwrap();
let provider = Arc::new(LocalWorkdirSession::materialized(
root.path().to_path_buf(),
root.path().to_path_buf(),
manifest::SharedScope::new(manifest::Scope::writable(root.path()).unwrap()),
WorkdirSessionCapabilities::READ_ONLY,
));
let broker = WorkdirToolBroker::at_cwd(provider, WorkdirPath::new("src").unwrap()).unwrap();
let inner = broker.tool_session();
let expected = inner.environment();
let client = AuthorizationClient::new(vec![permission(READ, "src", true)]);
let wrapped = authorized_workdir_session(inner, client.clone(), "attachment-id".into());
assert_eq!(wrapped.environment(), expected);
assert_eq!(wrapped.environment().cwd.as_str(), "src");
assert_eq!(
wrapped.environment().capabilities,
WorkdirSessionCapabilities::READ_ONLY
);
assert!(
client.requests.lock().unwrap().is_empty(),
"environment is metadata, not a cached grant decision"
);
wrapped.close().await.unwrap();
}
#[tokio::test]
async fn nonroot_routed_search_retains_host_grants_output_coordinates_and_cursors() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir(root.path().join("src")).unwrap();
std::fs::create_dir(root.path().join("private")).unwrap();
for name in ["a.txt", "b.txt"] {
std::fs::write(root.path().join("src").join(name), "needle public marker").unwrap();
}
std::fs::write(
root.path().join("private/secret.txt"),
"needle private marker",
)
.unwrap();
let client = AuthorizationClient::new(vec![permission(READ, "src", true)]);
let authorized = session(&root, client.clone());
let router = workdir::WorkdirSessionRouter::new();
let alias = workdir::WorkdirAttachmentAlias::new("checkout").unwrap();
router
.attach_at_cwd(alias.clone(), authorized, WorkdirPath::new("src").unwrap())
.unwrap();
let routed = router.resolve(Some("checkout")).unwrap().session;
assert_eq!(routed.environment().cwd.as_str(), "src");
let paths = routed
.glob(GlobRequest {
path: WorkdirPath::root(),
pattern: "**/*.txt".into(),
limit: 100,
})
.await
.unwrap()
.paths;
assert_eq!(
paths,
vec![
WorkdirPath::new("a.txt").unwrap(),
WorkdirPath::new("b.txt").unwrap()
]
);
// Search output is reusable as a path without adding src/ at the caller.
assert_eq!(
routed.read(read(paths[0].as_str())).await.unwrap().bytes,
b"needle public marker"
);
for output_mode in [
workdir::GrepOutputMode::Content,
workdir::GrepOutputMode::FilesWithMatches,
workdir::GrepOutputMode::Count,
] {
let result = routed
.grep(GrepRequest {
path: WorkdirPath::root(),
pattern: "needle".into(),
glob: None,
file_type: None,
case_insensitive: false,
before_context: 0,
after_context: 0,
multiline: false,
output_mode,
limit: 100,
offset: 0,
})
.await
.unwrap();
let mut grep_paths = result.paths;
grep_paths.sort_by(|left, right| left.as_str().cmp(right.as_str()));
assert_eq!(grep_paths, paths);
assert!(result.output.contains("a.txt"));
assert!(!result.output.contains("src/"));
assert!(!result.output.contains("secret"));
}
let first = routed
.list(ListRequest {
path: WorkdirPath::root(),
after: None,
limit: 1,
})
.await
.unwrap();
assert_eq!(first.entries[0].path.as_str(), "a.txt");
assert_eq!(first.next_after.as_ref().unwrap().path.as_str(), "a.txt");
let second = routed
.list(ListRequest {
path: WorkdirPath::root(),
after: first.next_after,
limit: 1,
})
.await
.unwrap();
assert_eq!(second.entries[0].path.as_str(), "b.txt");
assert!(second.next_after.is_none());
// An existing caller ceiling must survive the extra Host layer. An empty
// layer is invalid and cannot silently turn into the broader Host grant.
let mut denied = CheckoutSearchRequest::new(CheckoutSearchOperation::Glob(GlobRequest {
path: WorkdirPath::root(),
pattern: "**/*.txt".into(),
limit: 100,
}));
denied.scope_layers.push(Vec::new());
assert!(routed.checkout_search(denied).await.is_err());
assert!(client.requests.lock().unwrap().iter().all(|request| {
matches!(&request.resource, PermissionResource::Workdir { path, .. } if path == "src" || path == "src/a.txt")
}));
client.revoked.store(true, Ordering::SeqCst);
assert!(
routed
.glob(GlobRequest {
path: WorkdirPath::root(),
pattern: "**/*.txt".into(),
limit: 100,
})
.await
.is_err()
);
router.detach(&alias).await.unwrap();
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn scoped_checkout_search_preserves_caller_layer_and_rejects_resolved_escape() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir_all(root.path().join("src/nested")).unwrap();
std::fs::create_dir(root.path().join("private")).unwrap();
std::fs::write(root.path().join("src/public.txt"), "needle public").unwrap();
std::fs::write(root.path().join("src/nested/hidden.txt"), "needle hidden").unwrap();
std::fs::write(root.path().join("private/secret.txt"), "needle secret").unwrap();
std::os::unix::fs::symlink("../private", root.path().join("src/escape")).unwrap();
std::os::unix::fs::symlink("../private/secret.txt", root.path().join("src/alias.txt")).unwrap();
let client = AuthorizationClient::new(vec![permission(READ, "src", true)]);
let authorized = session(&root, client);
let request = CheckoutSearchRequest {
operation: CheckoutSearchOperation::Glob(GlobRequest {
path: WorkdirPath::new("src").unwrap(),
pattern: "**/*.txt".into(),
limit: 100,
}),
output_root: WorkdirPath::new("src").unwrap(),
scope_layers: vec![vec![WorkdirToolScopeRule {
target: WorkdirPath::new("src").unwrap(),
recursive: false,
permission: WorkdirToolScopePermission::Read,
symlink_policy: manifest::SymlinkPolicy::Resolved,
}]],
};
let result = authorized.checkout_search(request).await.unwrap();
let CheckoutSearchResult::Glob(result) = result else {
panic!("expected Glob result")
};
assert_eq!(result.paths, vec![WorkdirPath::new("public.txt").unwrap()]);
assert!(
authorized
.checkout_search(CheckoutSearchRequest::new(CheckoutSearchOperation::Glob(
GlobRequest {
path: WorkdirPath::new("private").unwrap(),
pattern: "**/*.txt".into(),
limit: 100,
}
)))
.await
.is_err()
);
authorized.close().await.unwrap();
}
+37
View File
@@ -121,6 +121,7 @@ async fn open_session(
session_id: WorkdirSessionId::new(SESSION).unwrap(),
workdir_id: state.session.workdir().id().clone(),
capabilities: state.session.capabilities(),
environment: state.session.environment(),
})
.into_response()
}
@@ -251,6 +252,10 @@ impl Drop for HttpFixture {
impl HttpFixture {
async fn new(capabilities: WorkdirSessionCapabilities) -> Self {
Self::at_cwd(capabilities, "").await
}
async fn at_cwd(capabilities: WorkdirSessionCapabilities, cwd: &str) -> Self {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("src/deep")).unwrap();
std::fs::write(
@@ -264,6 +269,13 @@ impl HttpFixture {
manifest::SharedScope::new(manifest::Scope::writable(dir.path()).unwrap()),
capabilities,
));
let session: WorkdirSessionHandle = if cwd.is_empty() {
session
} else {
workdir::WorkdirToolBroker::at_cwd(session, WorkdirPath::new(cwd).unwrap())
.unwrap()
.tool_session()
};
let state = Arc::new(ProviderState {
session,
requests: Mutex::new(Vec::new()),
@@ -349,6 +361,31 @@ impl HttpFixture {
}
}
#[tokio::test]
async fn remote_open_keeps_provider_environment_and_nonroot_operation_coordinates() {
let fixture = HttpFixture::at_cwd(WorkdirSessionCapabilities::READ_ONLY, "src/deep").await;
let remote = fixture.open(TOKEN).await.unwrap();
assert_eq!(remote.environment(), fixture.state.session.environment());
assert_eq!(remote.environment().cwd.as_str(), "src/deep");
assert_eq!(
remote.environment().capabilities,
WorkdirSessionCapabilities::READ_ONLY
);
let result = remote
.read(workdir::ReadRequest {
path: WorkdirPath::new("a.txt").unwrap(),
offset: 0,
limit: 10,
max_bytes: 4096,
})
.await
.unwrap();
assert_eq!(result.bytes, b"first\nneedle needle\nlast\n");
fixture.public_output(&serde_json::to_string(&remote.environment()).unwrap());
remote.close().await.unwrap();
fixture.assert_endpoints();
}
fn native_runtime(remote: Arc<RemoteWorkdirSession>) -> (WipRuntime, Arc<WorkdirSessionRouter>) {
let router = Arc::new(WorkdirSessionRouter::new());
router
+9 -4
View File
@@ -78,6 +78,12 @@ impl WorkdirSession for HookedSession {
self.local.capabilities()
}
fn environment(&self) -> workdir::WorkdirSessionEnvironment {
let mut environment = self.local.environment();
environment.capabilities = environment.capabilities.intersection(self.capabilities());
environment
}
async fn authorize_scope_path(
&self,
request: WorkdirScopeAuthorizationRequest,
@@ -201,11 +207,10 @@ async fn checkout_wip_file_operations_share_core_and_keep_prior_read_boundary()
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("text.txt"), "旧 旧\n").unwrap();
let router = Arc::new(WorkdirSessionRouter::new());
let hooked = HookedSession::new(&dir);
assert_eq!(hooked.environment(), hooked.local.environment());
router
.attach(
WorkdirAttachmentAlias::new("main").unwrap(),
HookedSession::new(&dir),
)
.attach(WorkdirAttachmentAlias::new("main").unwrap(), hooked)
.unwrap();
let provider = provider(router);
let route = format!("{}/text.txt", checkout_root("main"));
@@ -226,6 +226,7 @@ pub struct WorkspaceAttachedWorkdirSession {
target_workdir: String,
workdir: Workdir,
capabilities: WorkdirSessionCapabilities,
environment: workdir::WorkdirSessionEnvironment,
}
impl WorkspaceAttachedWorkdirSession {
@@ -249,17 +250,38 @@ impl WorkspaceAttachedWorkdirSession {
)
}
/// Bind a known root-coordinate attachment. Opened provider sessions must
/// instead pass their metadata to [`Self::handle_for_workdir_with_environment`].
pub fn handle_for_workdir_with_capabilities(
client: Arc<dyn WorkspaceClient>,
target_workdir: impl Into<String>,
workdir_id: impl Into<String>,
capabilities: WorkdirSessionCapabilities,
) -> WorkdirSessionHandle {
Self::handle_for_workdir_with_environment(
client,
target_workdir,
workdir_id,
capabilities,
workdir::WorkdirSessionEnvironment::root(capabilities),
)
}
/// Bind provider-authored coordinates without allowing metadata to expand
/// the Backend attachment capability ceiling.
pub fn handle_for_workdir_with_environment(
client: Arc<dyn WorkspaceClient>,
target_workdir: impl Into<String>,
workdir_id: impl Into<String>,
capabilities: WorkdirSessionCapabilities,
environment: workdir::WorkdirSessionEnvironment,
) -> WorkdirSessionHandle {
Arc::new(Self {
client,
target_workdir: target_workdir.into(),
workdir: Workdir::new(workdir_id),
capabilities,
capabilities: capabilities.intersection(environment.capabilities),
environment,
})
}
@@ -362,6 +384,12 @@ impl WorkdirSession for WorkspaceAttachedWorkdirSession {
self.capabilities
}
fn environment(&self) -> workdir::WorkdirSessionEnvironment {
let mut environment = self.environment.clone();
environment.capabilities = environment.capabilities.intersection(self.capabilities());
environment
}
async fn authorize_scope_path(
&self,
request: WorkdirScopeAuthorizationRequest,
@@ -684,11 +712,12 @@ impl WorkspaceHttpWorkdirBackend {
let response = self.attach_response(alias, working_directory_id)?;
if let Err(error) = self.session_router.attach(
alias_key.clone(),
WorkspaceAttachedWorkdirSession::handle_for_workdir_with_capabilities(
WorkspaceAttachedWorkdirSession::handle_for_workdir_with_environment(
self.client.clone(),
alias,
working_directory_id,
response.capabilities,
response.attachment.capabilities,
response.environment.clone(),
),
) {
let _ = self.detach(alias);
@@ -706,9 +735,9 @@ impl WorkspaceHttpWorkdirBackend {
&self,
alias: &str,
working_directory_id: &str,
) -> Result<WorkdirAttachmentResponse, ToolError> {
) -> Result<WorkdirSessionAttachmentResponse, ToolError> {
let workspace_id = encode_path_segment(self.workspace_id()?);
self.execute_json::<WorkdirAttachmentResponse>(WorkspaceRequest::json(
self.execute_json::<WorkdirSessionAttachmentResponse>(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
format!("/api/w/{workspace_id}/workers/self/workdir-attachments"),
serde_json::to_string(&WorkdirAttachRequest {
@@ -1192,6 +1221,13 @@ struct WorkdirAttachmentResponse {
attached: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct WorkdirSessionAttachmentResponse {
#[serde(flatten)]
attachment: WorkdirAttachmentResponse,
environment: workdir::WorkdirSessionEnvironment,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct WorkdirDeleteInput {
@@ -1502,6 +1538,7 @@ mod tests {
"alias": "checkout",
"working_directory_id": "wd-created",
"capabilities": {"bits": 63},
"environment": workdir::WorkdirSessionEnvironment::root(WorkdirSessionCapabilities::ALL),
"attached": true
})),
response(json!({
@@ -2004,6 +2041,7 @@ mod tests {
"alias": "checkout",
"working_directory_id": "wd-attached",
"capabilities": {"bits": 63},
"environment": workdir::WorkdirSessionEnvironment::root(WorkdirSessionCapabilities::ALL),
"attached": true
})),
response(json!({
@@ -2063,16 +2101,29 @@ mod tests {
);
}
#[test]
fn attach_propagates_provider_read_only_capabilities_to_the_alias_session() {
#[tokio::test]
async fn attach_projects_provider_environment_without_expanding_capability_ceiling() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("src")).unwrap();
let provider = Arc::new(workdir::LocalWorkdirSession::materialized(
dir.path().to_path_buf(),
dir.path().to_path_buf(),
manifest::SharedScope::new(manifest::Scope::writable(dir.path()).unwrap()),
WorkdirSessionCapabilities::ALL,
));
let broker =
workdir::WorkdirToolBroker::at_cwd(provider, workdir::WorkdirPath::new("src").unwrap())
.unwrap();
let mut environment = broker.tool_session().environment();
let client = Arc::new(RecordingWorkspaceClient::new(vec![response(json!({
"workspace_id": "workspace/test",
"alias": "external",
"working_directory_id": "external-workdir",
"capabilities": {"bits": 25},
"environment": environment,
"attached": true
}))]));
let backend = WorkspaceHttpWorkdirBackend::new(client);
let backend = WorkspaceHttpWorkdirBackend::new(client.clone());
backend
.attach(WorkdirAttachInput {
alias: "external".to_string(),
@@ -2084,10 +2135,33 @@ mod tests {
.session_router
.session(&workdir::WorkdirAttachmentAlias::new("external").unwrap())
.unwrap();
environment.capabilities = WorkdirSessionCapabilities::READ_ONLY;
assert_eq!(session.environment(), environment);
assert_eq!(
session.capabilities(),
WorkdirSessionCapabilities::READ_ONLY
);
assert!(matches!(
session
.checkout_execute(workdir::CheckoutRequest {
target: workdir::WorkdirPath::root(),
validator: vec![1],
operation: workdir::CheckoutOperation::Create {
path: workdir::WorkdirPath::new("new.txt").unwrap(),
content: b"denied".to_vec(),
},
})
.await,
Err(WorkdirError::Unsupported(
workdir::WorkdirSessionCapability::Write
))
));
assert_eq!(
client.requests().len(),
1,
"denied mutation must not reach the Backend"
);
broker.tool_session().close().await.unwrap();
}
#[tokio::test]
@@ -2097,6 +2171,7 @@ mod tests {
"alias": "checkout",
"working_directory_id": "wd-attached",
"capabilities": {"bits": 63},
"environment": workdir::WorkdirSessionEnvironment::root(WorkdirSessionCapabilities::ALL),
"attached": true
}))]));
let reopen_calls = Arc::new(AtomicUsize::new(0));
@@ -1253,7 +1253,7 @@ pub(crate) mod tests {
state.generation += 1;
let raw = json!({"alias":body["alias"], "working_directory_id":body["working_directory_id"], "connection_id":format!("connection-{}", state.generation), "capabilities":{"bits":63}});
state.attachments.push(raw);
json!({"workspace_id":"test-workspace", "alias":body["alias"], "working_directory_id":body["working_directory_id"], "capabilities":{"bits":63}, "attached":true})
json!({"workspace_id":"test-workspace", "alias":body["alias"], "working_directory_id":body["working_directory_id"], "capabilities":{"bits":63}, "environment":workdir::WorkdirSessionEnvironment::root(workdir::WorkdirSessionCapabilities::ALL), "attached":true})
}
(WorkspaceRequestMethod::Delete, p)
if p.starts_with("workers/self/workdir-attachments/") =>
+1 -2
View File
@@ -13,7 +13,6 @@
mod result;
pub use result::{JobResultFeature, JobResultFinalizer, JobResultSink, SUBMIT_JOB_RESULT_TOOL};
use std::borrow::Cow;
use std::path::Path;
use std::sync::Arc;
@@ -154,7 +153,7 @@ fn prepare_job_from_registry_with_grant(
)?;
let mut system_prompt = template.render(&SystemPromptContext {
now: chrono::Utc::now(),
cwd: Cow::Borrowed("(no filesystem)"),
workdir_sessions: &workdir::WorkdirSessionRouter::new(),
language: &manifest.engine.language,
scope: &scope,
tool_names,
+85 -10
View File
@@ -7,13 +7,12 @@
//! eagerly syntax-checks it at Worker construction. The final system
//! prompt is materialised exactly once just before the first LLM turn:
//! the rendered body is appended with a fixed trailing section carrying
//! the Worker's `Scope` summary, (if present) the project's `AGENTS.md`
//! contents, resident memory sections, and conditional Worker-orchestration
//! guidance, then the whole string is handed to the Engine via
//! `set_system_prompt`. Subsequent turns and compactions reuse that
//! the Worker's `Scope` summary, actual Workdir session environments, (if
//! present) the project's `AGENTS.md` contents, resident memory sections, and
//! conditional Worker-orchestration guidance, then the whole string is handed
//! to the Engine via `set_system_prompt`. Subsequent turns and compactions reuse that
//! materialised string verbatim.
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::Arc;
@@ -105,7 +104,8 @@ impl std::fmt::Debug for SystemPromptTemplate {
/// templates cannot drop them on the floor.
pub struct SystemPromptContext<'a> {
pub now: DateTime<Utc>,
pub cwd: Cow<'a, str>,
/// The same session router used by the Worker's filesystem tools.
pub workdir_sessions: &'a workdir::WorkdirSessionRouter,
/// Language policy exposed to instruction templates as `{{ language }}`.
pub language: &'a str,
pub scope: &'a Scope,
@@ -140,7 +140,14 @@ impl<'a> SystemPromptContext<'a> {
"datetime".into(),
Value::from(self.now.to_rfc3339_opts(SecondsFormat::Secs, true)),
);
root.insert("cwd".into(), Value::from(self.cwd.as_ref()));
// A singular template cwd is meaningful only with one attachment. Never
// substitute the local Profile/Memory discovery directory for a session cwd.
let cwd = self
.workdir_sessions
.only_session()
.map(|session| display_workdir_path(&session.environment().cwd).to_owned())
.unwrap_or_else(|| "no single Workdir session cwd; see execution environment".into());
root.insert("cwd".into(), Value::from(cwd));
root.insert("language".into(), Value::from(self.language));
root.insert(
"tools".into(),
@@ -255,6 +262,70 @@ fn exact_prompt_name(reference: &str) -> Option<String> {
}
}
fn display_workdir_path(path: &workdir::WorkdirPath) -> &str {
if path.as_str().is_empty() {
"."
} else {
path.as_str()
}
}
/// Project provider-owned session metadata, rather than reconstructing authority
/// from a manifest Scope or the directory used to discover Profiles and Memory.
pub(crate) fn workdir_environment_section(
router: &workdir::WorkdirSessionRouter,
) -> Result<String, SystemPromptError> {
use workdir::WorkdirSessionCapability::{Command, Edit, Glob, Grep, Read, Write};
let mut environments = Vec::new();
for alias in router.aliases() {
let resolved = router
.resolve(Some(alias.as_str()))
.map_err(|error| SystemPromptError::Render(error.to_string()))?;
let environment = resolved.session.environment();
let capabilities: Vec<_> = [
(Read, "read"),
(Write, "write"),
(Edit, "edit"),
(Glob, "glob"),
(Grep, "grep"),
(Command, "command"),
]
.into_iter()
.filter_map(|(capability, name)| {
environment
.capabilities
.supports(capability)
.then_some(name)
})
.collect();
environments.push(serde_json::json!({
"alias": alias.as_str(),
"cwd": display_workdir_path(&environment.cwd),
"scope": environment.scope,
"restrictions": environment.restrictions,
"capabilities": capabilities,
"command": environment.capabilities.supports(Command),
}));
}
let json = serde_json::to_string_pretty(&environments)
.map_err(|error| SystemPromptError::Render(error.to_string()))?;
Ok(format!(
"## Workdir execution environment\n\n\
Select a Workdir by its alias (`target_workdir`); omission is allowed only with one attachment. \
Relative filesystem operation paths are resolved from that selected session's cwd, not its Workdir root. \
Paths returned by Glob/Grep use the same cwd-relative coordinates and can be passed unchanged to other filesystem tools. \
Bash starts at the session cwd; an explicit relative cwd is resolved from it, and shell cd does not change the session cwd.\n\n\
In the metadata below, cwd and relative scope targets are Workdir-root-relative (`.` or an empty path denotes the root); explicit absolute provider scope targets stay absolute. \
A null scope means the provider's full available scope, not unrestricted host access; an empty scope grants no paths. \
Restrictions retain additional provider or ancestor ceilings, including deny rules. \
Capabilities and command availability are session ceilings, not new grants. \
Resolved targets remain subject to scope, symlink and command authorization. \
SubWorker scope declarations remain Workdir-root-relative; other Scope declarations and Profile/Memory roots retain their own meaning and are not rebased by session cwd.\n\n\
```json\n{json}\n```"
))
}
/// Build the final system prompt by appending the fixed trailing
/// section to `body`. The Rust side owns the layout (blank-line
/// separators, trailing-whitespace trim); each section's header + body
@@ -276,8 +347,12 @@ fn append_trailing_section(
}
out.push('\n');
let boundaries = prompts.working_boundaries_section(&scope.summary())?;
out.push_str(boundaries.trim_end_matches(&['\n', ' '][..]));
if ctx.workdir_sessions.is_empty() {
let boundaries = prompts.working_boundaries_section(&scope.summary())?;
out.push_str(boundaries.trim_end_matches(&['\n', ' '][..]));
out.push('\n');
}
out.push_str(&workdir_environment_section(ctx.workdir_sessions)?);
out.push('\n');
if let Some(agents) = agents_md {
out.push('\n');
@@ -392,7 +467,7 @@ mod tests {
now: DateTime::parse_from_rfc3339("2026-10-08T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
cwd: Cow::Borrowed("."),
workdir_sessions: &workdir::WorkdirSessionRouter::new(),
language: "en",
scope: &scope,
tool_names: metas.iter().map(|meta| meta.name.clone()).collect(),
+2
View File
@@ -1107,6 +1107,8 @@ fn sub_worker_spawn_tool_impl(
#[cfg(test)]
mod tests {
mod environment;
use super::*;
use manifest::{DelegationScope, Permission, Scope, SharedScope};
use std::pin::Pin;
@@ -0,0 +1,629 @@
//! Connection tests for model-visible environments and actual routed file operations.
//! Runs the production SubWorker spawn/broker/Worker/prompt/tool path with temporary
//! LocalWorkdirSession providers; only the external LLM is scripted. Selected by
//! the normal worker lib suite, without network credentials or live Workspace data.
use super::*;
use agen::Item;
use serde_json::{Value, json};
use workdir::{WorkdirSessionCapability, WorkdirSessionRouter};
const TASK: &str = "Read marker.txt from each attached Workdir.";
fn prompt_environments(prompt: &str) -> Vec<Value> {
let section = prompt
.split_once("## Workdir execution environment\n")
.expect("Worker must supply the execution environment independently of task text")
.1;
let json = section.split_once("```json\n").unwrap().1;
serde_json::from_str(json.split_once("\n```").unwrap().0).unwrap()
}
#[derive(Clone, Default)]
struct EnvironmentClient {
requests: Arc<Mutex<Vec<Request>>>,
read_issued: Arc<AtomicBool>,
}
#[async_trait]
impl LlmClient for EnvironmentClient {
fn clone_boxed(&self) -> Box<dyn LlmClient> {
Box::new(self.clone())
}
async fn stream(
&self,
request: Request,
) -> Result<Pin<Box<dyn Stream<Item = Result<LlmEvent, ClientError>> + Send>>, ClientError>
{
let environments = prompt_environments(request.system_prompt.as_deref().unwrap());
self.requests.lock().unwrap().push(request);
let mut events = Vec::new();
if !self.read_issued.swap(true, Ordering::SeqCst) {
// Use only the model-visible aliases and the same relative path in
// every session. No test-side cwd prefix is passed to the Read tool.
for (index, environment) in environments.iter().enumerate() {
let alias = environment["alias"].as_str().unwrap();
events.extend([
LlmEvent::tool_use_start(index, format!("read-{alias}"), "Read"),
LlmEvent::tool_input_delta(
index,
json!({"target_workdir": alias, "file_path": "marker.txt"}).to_string(),
),
LlmEvent::tool_use_stop(index),
]);
}
} else {
events.extend([
LlmEvent::text_block_start(0),
LlmEvent::text_delta(0, "read complete"),
LlmEvent::text_block_stop(0, None),
]);
}
events.push(LlmEvent::Status(StatusEvent {
status: ResponseStatus::Completed,
}));
Ok(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
}
}
struct EnvironmentFixture {
runtime: TempDir,
root: PathBuf,
manifest: WorkerManifest,
profiles: AvailableProfiles,
registry: Arc<SpawnedWorkerRegistry>,
sessions: Arc<WorkdirSessionRouter>,
}
impl EnvironmentFixture {
fn new(aliases: &[(&str, &str)]) -> Self {
let runtime = TempDir::new().unwrap();
let root = runtime.path().join("profile-root");
// Profile discovery stays at this root, deliberately outside every
// attached Workdir. Session cwd must never redirect it into work/nested.
let profiles = write_project_profile_registry(&root, None, &[]);
let mut manifest = parent_manifest(&root, None);
manifest.feature.sub_worker.enabled = true;
let registry = SpawnedWorkerRegistry::new_internal(
"parent".into(),
SharedScope::new(Scope::from_config(&manifest.scope).unwrap()),
);
let sessions = Arc::new(WorkdirSessionRouter::new());
for (alias, cwd) in aliases {
let checkout = runtime.path().join(alias);
for path in ["", "work", "work/nested", "work/nested/deep", "other"] {
std::fs::create_dir_all(checkout.join(path)).unwrap();
std::fs::write(checkout.join(path).join("marker.txt"), marker(alias, path))
.unwrap();
}
let scope = SharedScope::new(Scope::writable(&checkout).unwrap());
let session = Arc::new(workdir::LocalWorkdirSession::materialized_bound(
workdir::Workdir::new(format!("fixture-{alias}")),
checkout.clone(),
checkout.join(cwd),
scope,
workdir::WorkdirSessionCapabilities::ALL,
));
sessions
.attach_at_cwd(
workdir::WorkdirAttachmentAlias::new(*alias).unwrap(),
session,
workdir::WorkdirPath::new(cwd).unwrap(),
)
.unwrap();
}
Self {
runtime,
root,
manifest,
profiles,
registry,
sessions,
}
}
async fn spawn(
&self,
spawner: &str,
registry: Arc<SpawnedWorkerRegistry>,
broker: workdir::WorkdirToolBrokerRouter,
input: Value,
) -> (
crate::spawn::registry::InternalSpawnedWorkerRecord,
EnvironmentClient,
) {
assert_eq!(input["task"], TASK);
let client = EnvironmentClient::default();
let mut manifest = self.manifest.clone();
manifest.worker.name = spawner.into();
let tool = SubWorkerSpawnTool::new(
spawner.into(),
crate::worker::WorkerWorkspaceContext::local_filesystem(None),
ParentNotificationTarget::Durable(Arc::new(|_| {})),
self.runtime.path().to_path_buf(),
self.root.clone(),
Some(broker),
registry.clone(),
manifest,
PromptCatalogSource::builtins_only(),
self.profiles.clone(),
)
.with_internal_client(Box::new(client.clone()));
tool.execute(
&input.to_string(),
agen::tool::ToolExecutionContext::direct(),
)
.await
.expect("spawn through production delegation and Worker construction");
let record = registry
.get_internal(input["name"].as_str().unwrap())
.unwrap();
assert_eq!(
tokio::time::timeout(Duration::from_secs(5), record.session.wait_until_idle())
.await
.expect("scripted read turn must terminate"),
InternalWorkerSessionStatus::Idle,
);
(record, client)
}
}
fn marker(alias: &str, cwd: &str) -> String {
format!(
"{alias}:{}:session-marker",
if cwd.is_empty() { "." } else { cwd }
)
}
fn assert_environment_and_reads(
router: &WorkdirSessionRouter,
client: &EnvironmentClient,
expected: &[(&str, &str, bool)],
) -> String {
let requests = client.requests.lock().unwrap();
assert!(
requests.len() >= 2,
"Read must execute and return to the model"
);
let prompt = requests[0].system_prompt.as_deref().unwrap();
assert!(
!prompt.contains("## Working boundaries"),
"legacy Scope must not override the connected session environment"
);
let environments = prompt_environments(prompt);
assert_eq!(environments.len(), expected.len());
assert_eq!(router.len(), expected.len());
for (alias, cwd, command) in expected {
let environment = router.resolve(Some(alias)).unwrap().session.environment();
assert_eq!(environment.cwd.as_str(), *cwd);
assert_eq!(
environment
.capabilities
.supports(WorkdirSessionCapability::Command),
*command
);
let projected = environments
.iter()
.find(|env| env["alias"] == *alias)
.unwrap();
assert_eq!(projected["cwd"], if cwd.is_empty() { "." } else { *cwd });
assert_eq!(
projected["scope"],
serde_json::to_value(&environment.scope).unwrap()
);
assert_eq!(
projected["restrictions"],
serde_json::to_value(&environment.restrictions).unwrap()
);
assert_eq!(projected["command"], *command);
for (capability, name) in [
(WorkdirSessionCapability::Read, "read"),
(WorkdirSessionCapability::Write, "write"),
(WorkdirSessionCapability::Edit, "edit"),
(WorkdirSessionCapability::Glob, "glob"),
(WorkdirSessionCapability::Grep, "grep"),
(WorkdirSessionCapability::Command, "command"),
] {
assert_eq!(
projected["capabilities"]
.as_array()
.unwrap()
.iter()
.any(|value| value == name),
environment.capabilities.supports(capability),
);
}
let expected_marker = marker(alias, cwd);
let observed_read = requests
.iter()
.flat_map(|request| &request.items)
.any(|item| {
matches!(item, Item::ToolResult { call_id, content: Some(content), .. }
if call_id == &format!("read-{alias}") && content.contains(&expected_marker))
});
assert!(
observed_read,
"{alias}: relative Read did not return {expected_marker}"
);
}
prompt.to_owned()
}
fn assert_retained_environment(
record: &crate::spawn::registry::InternalSpawnedWorkerRecord,
prompt: &str,
) {
let retained = record.session.entries().iter().any(|entry| {
matches!(entry, session_store::LogEntry::AnnotatedSegmentStart { system_prompt: Some(saved), .. }
if saved == prompt)
});
assert!(
retained,
"the model-visible environment must also be retained in session history"
);
}
#[tokio::test]
async fn spawn_root_and_nonroot_cwd_are_projected_without_task_annotations() {
for (name, cwd, command, permission) in [
("root-child", "", false, "read"),
("cwd-child", "work", true, "write"),
] {
let fixture = EnvironmentFixture::new(&[("checkout", "")]);
let (record, client) = fixture
.spawn(
"parent",
fixture.registry.clone(),
workdir::WorkdirToolBrokerRouter::new(fixture.sessions.clone()),
json!({
"name": name, "task": TASK, "cwd": if cwd.is_empty() { "." } else { cwd },
"scope": [{"target": ".", "permission": permission}],
"command": command,
}),
)
.await;
let router = record.workdir_tool_scope.session_router();
let prompt = assert_environment_and_reads(&router, &client, &[("checkout", cwd, command)]);
assert_retained_environment(&record, &prompt);
assert_eq!(
record.installed_tools.iter().any(|tool| tool == "Bash"),
command
);
fixture.registry.remove_internal(name).await.unwrap();
}
}
#[tokio::test]
async fn spawn_multiple_aliases_and_nested_child_keep_real_session_environment() {
let fixture = EnvironmentFixture::new(&[("alpha", ""), ("beta", "")]);
let (child, child_client) = fixture
.spawn(
"parent",
fixture.registry.clone(),
workdir::WorkdirToolBrokerRouter::new(fixture.sessions.clone()),
json!({
"name": "child", "task": TASK, "cwd": "work", "command": true,
"scope": [
{"target_workdir": "alpha", "target": ".", "permission": "write"},
{"target_workdir": "beta", "target": ".", "permission": "write"},
],
}),
)
.await;
let child_router = child.workdir_tool_scope.session_router();
let child_prompt = assert_environment_and_reads(
&child_router,
&child_client,
&[("alpha", "work", true), ("beta", "work", true)],
);
assert_retained_environment(&child, &child_prompt);
assert!(
child
.installed_tools
.iter()
.any(|tool| tool == "SubWorkerSpawn")
);
// Reuse the actual child's registry and attenuated broker. The nested tool
// receives the scripted LLM at the same boundary as the first spawn; session
// construction, routing, delegation and prompt materialization remain real.
let (nested, nested_client) = fixture
.spawn(
"child",
child.child_registry.clone(),
child.workdir_tool_scope.broker_router(),
json!({
"name": "nested", "task": TASK, "cwd": "work/nested",
"scope": [
{"target_workdir": "alpha", "target": "work/nested", "permission": "read"},
{"target_workdir": "beta", "target": "work/nested", "permission": "read"},
],
}),
)
.await;
let nested_router = nested.workdir_tool_scope.session_router();
let nested_prompt = assert_environment_and_reads(
&nested_router,
&nested_client,
&[
("alpha", "work/nested", false),
("beta", "work/nested", false),
],
);
assert_retained_environment(&nested, &nested_prompt);
assert!(!nested.installed_tools.iter().any(|tool| tool == "Bash"));
for alias in ["alpha", "beta"] {
let session = nested_router.resolve(Some(alias)).unwrap().session;
let environment = session.environment();
let scope = environment
.scope
.expect("nested session must remain scoped");
assert_eq!(scope.len(), 1);
assert_eq!(scope[0].target.as_str(), "work/nested");
assert_eq!(
scope[0].permission,
workdir::WorkdirToolScopePermission::Read
);
assert!(
session
.read(workdir::ReadRequest {
path: workdir::WorkdirPath::new_scoped(
fixture
.runtime
.path()
.join(alias)
.join("marker.txt")
.display()
.to_string(),
)
.unwrap(),
offset: 0,
limit: 10,
max_bytes: 1024,
})
.await
.is_err()
);
}
// Grandchild cwd and reduced command authority must not rewrite its parent.
assert_environment_and_reads(
&child_router,
&child_client,
&[("alpha", "work", true), ("beta", "work", true)],
);
child
.child_registry
.remove_internal("nested")
.await
.unwrap();
fixture.registry.remove_internal("child").await.unwrap();
}
#[tokio::test]
async fn spawn_multiple_aliases_keep_root_scope_independent_of_parent_session_cwds() {
let fixture = EnvironmentFixture::new(&[("alpha", "work"), ("beta", "other")]);
let (record, client) = fixture
.spawn(
"parent",
fixture.registry.clone(),
workdir::WorkdirToolBrokerRouter::new(fixture.sessions.clone()),
json!({
"name": "child", "task": TASK, "cwd": "work/nested",
"scope": [
{"target_workdir": "alpha", "target": "work/nested", "permission": "read"},
{"target_workdir": "beta", "target": "work/nested", "permission": "read"},
],
}),
)
.await;
let prompt = assert_environment_and_reads(
&record.workdir_tool_scope.session_router(),
&client,
&[
("alpha", "work/nested", false),
("beta", "work/nested", false),
],
);
assert_retained_environment(&record, &prompt);
for (alias, cwd) in [("alpha", "work"), ("beta", "other")] {
assert_eq!(
fixture
.sessions
.resolve(Some(alias))
.unwrap()
.session
.environment()
.cwd
.as_str(),
cwd
);
}
fixture.registry.remove_internal("child").await.unwrap();
}
#[tokio::test]
async fn nonroot_startup_then_spawn_and_nested_child_keep_root_relative_scope() {
let fixture = EnvironmentFixture::new(&[("workdir", "")]);
let checkout = fixture.runtime.path().join("workdir");
let client = EnvironmentClient::default();
let mut manifest = fixture.manifest.clone();
manifest.scope.allow = vec![abs_rule(&checkout, Permission::Write)];
let mut worker =
Worker::<Box<dyn LlmClient>, EphemeralSessionStore>::from_internal_manifest_with_context(
manifest,
EphemeralSessionStore::default(),
PromptCatalogSource::builtins_only(),
crate::worker::WorkerWorkspaceContext::local_filesystem(None),
WorkerFilesystemAuthority::local(checkout.clone(), checkout.join("work")),
Some(Box::new(client.clone())),
)
.await
.unwrap();
// Do not bind a test router: exercise workdir_sessions_from_authority via
// the real constructor and the normal controller's broker selection.
let startup = worker.workdir_sessions();
register_worker_tools(
&mut worker,
fixture.runtime.path().to_path_buf(),
fixture.registry.clone(),
None,
None,
)
.await
.unwrap();
tokio::time::timeout(Duration::from_secs(5), worker.run_text(TASK))
.await
.expect("startup read turn must terminate")
.unwrap();
assert_environment_and_reads(&startup, &client, &[("workdir", "work", true)]);
let (child, child_client) = fixture
.spawn(
"parent",
fixture.registry.clone(),
workdir::WorkdirToolBrokerRouter::new(startup.clone()),
json!({
"name": "child", "task": TASK, "cwd": "work/nested",
"scope": [{"target": "work/nested", "permission": "read"}],
}),
)
.await;
let child_router = child.workdir_tool_scope.session_router();
let child_prompt = assert_environment_and_reads(
&child_router,
&child_client,
&[("workdir", "work/nested", false)],
);
assert_retained_environment(&child, &child_prompt);
let (nested, nested_client) = fixture
.spawn(
"child",
child.child_registry.clone(),
child.workdir_tool_scope.broker_router(),
json!({
"name": "nested", "task": TASK, "cwd": "work/nested/deep",
"scope": [{"target": "work/nested/deep", "permission": "read"}],
}),
)
.await;
let nested_router = nested.workdir_tool_scope.session_router();
let nested_prompt = assert_environment_and_reads(
&nested_router,
&nested_client,
&[("workdir", "work/nested/deep", false)],
);
assert_retained_environment(&nested, &nested_prompt);
for (router, target) in [
(&child_router, "work/nested"),
(&nested_router, "work/nested/deep"),
] {
let environment = router.resolve(None).unwrap().session.environment();
let rules = environment.scope.unwrap();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].target.as_str(), target);
}
assert_eq!(
startup
.resolve(None)
.unwrap()
.session
.environment()
.cwd
.as_str(),
"work"
);
let alias = workdir::WorkdirAttachmentAlias::new("workdir").unwrap();
startup.begin_detach(&alias).await.unwrap();
// Both the parent tool view and descendant root-coordinate broker source
// must share this fence, rather than bypassing it through a cached broker.
for router in [&startup, &child_router, &nested_router] {
let session = router.resolve(None).unwrap().session;
assert!(
session
.read(workdir::ReadRequest {
path: workdir::WorkdirPath::new("marker.txt").unwrap(),
offset: 0,
limit: 10,
max_bytes: 1024,
})
.await
.is_err()
);
}
startup.cancel_detach(&alias);
child
.child_registry
.remove_internal("nested")
.await
.unwrap();
fixture.registry.remove_internal("child").await.unwrap();
}
#[tokio::test]
async fn startup_rejects_cwd_outside_workdir_root_without_running_the_model() {
let fixture = EnvironmentFixture::new(&[("workdir", "")]);
let client = EnvironmentClient::default();
let mut manifest = fixture.manifest.clone();
manifest.scope.allow = vec![abs_rule(fixture.runtime.path(), Permission::Write)];
let error =
Worker::<Box<dyn LlmClient>, EphemeralSessionStore>::from_internal_manifest_with_context(
manifest,
EphemeralSessionStore::default(),
PromptCatalogSource::builtins_only(),
crate::worker::WorkerWorkspaceContext::local_filesystem(None),
WorkerFilesystemAuthority::local(
fixture.runtime.path().join("workdir"),
fixture.root.clone(),
),
Some(Box::new(client.clone())),
)
.await
.err()
.expect("a session cwd must be representable within its Workdir root");
assert!(matches!(error, crate::WorkerError::InvalidCwd { .. }));
assert!(client.requests.lock().unwrap().is_empty());
}
#[tokio::test]
async fn worker_multiple_aliases_project_distinct_session_cwds_not_discovery_root() {
for prompt_override in [None, Some("Feature-owned instruction body")] {
let fixture = EnvironmentFixture::new(&[("alpha", "work"), ("beta", "other")]);
let client = EnvironmentClient::default();
let mut worker = Worker::<Box<dyn LlmClient>, EphemeralSessionStore>::from_internal_manifest_with_context(
fixture.manifest.clone(),
EphemeralSessionStore::default(),
PromptCatalogSource::builtins_only(),
crate::worker::WorkerWorkspaceContext::local_filesystem(None),
WorkerFilesystemAuthority::None,
Some(Box::new(client.clone())),
)
.await
.unwrap();
worker.bind_workdir_sessions(fixture.sessions.clone());
register_worker_tools(
&mut worker,
fixture.runtime.path().to_path_buf(),
fixture.registry.clone(),
None,
Some(workdir::WorkdirToolBrokerRouter::new(
fixture.sessions.clone(),
)),
)
.await
.unwrap();
worker.install_system_prompt_contribution(None, prompt_override.map(str::to_owned));
tokio::time::timeout(Duration::from_secs(5), worker.run_text(TASK))
.await
.expect("scripted worker turn must terminate")
.unwrap();
let prompt = assert_environment_and_reads(
&fixture.sessions,
&client,
&[("alpha", "work", true), ("beta", "other", true)],
);
if let Some(body) = prompt_override {
assert!(prompt.starts_with(body));
}
}
}
+31 -15
View File
@@ -3700,7 +3700,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
let prompts = Arc::new(ArcSwap::from(PromptCatalog::builtins_only()?));
DelegationScope::from_config(&manifest.delegation_scope).map_err(WorkerError::Scope)?;
let scope = SharedScope::new(scope);
let workdir_sessions = workdir_sessions_from_authority(&filesystem_authority, &scope);
let workdir_sessions = workdir_sessions_from_authority(&filesystem_authority, &scope)?;
let mut worker = Self {
backend_job: std::sync::OnceLock::new(),
manifest,
@@ -4980,11 +4980,14 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
let Some(template) = self.system_prompt_template.as_ref() else {
return Ok(());
};
if let Some(rendered) = self.feature_system_prompt_override.as_ref() {
if let Some(body) = self.feature_system_prompt_override.as_ref() {
let environment =
crate::prompt::system::workdir_environment_section(&self.workdir_sessions)
.map_err(|source| WorkerError::SystemPromptRender { source })?;
self.engine
.as_mut()
.expect("worker present")
.set_system_prompt(rendered.clone());
.set_system_prompt(format!("{body}\n\n{environment}"));
return Ok(());
}
let alerter = self.alerter.clone();
@@ -5029,14 +5032,10 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
};
let worker_language = worker_language(&self.manifest.engine);
let scope_snapshot = self.scope.snapshot();
let cwd_for_prompt = self
.local_working_directory()
.map(|local| local.cwd.display().to_string())
.unwrap_or_else(|| "no local working directory".to_string());
let prompt_catalog = self.prompts.load_full();
let ctx = SystemPromptContext {
now: chrono::Utc::now(),
cwd: cwd_for_prompt.into(),
workdir_sessions: &self.workdir_sessions,
language: worker_language,
scope: &scope_snapshot,
tool_names,
@@ -8306,7 +8305,7 @@ where
let worker_metadata_segment_cas = Some(worker_metadata_segment_cas_for_store(&store));
let scope = SharedScope::new(common.scope);
let workdir_sessions =
workdir_sessions_from_authority(&common.filesystem_authority, &scope);
workdir_sessions_from_authority(&common.filesystem_authority, &scope)?;
let mut worker = Self {
backend_job: std::sync::OnceLock::new(),
@@ -8707,7 +8706,7 @@ where
engine.set_cache_key(Some(segment_id.to_string()));
let scope = SharedScope::new(common.scope);
let workdir_sessions =
workdir_sessions_from_authority(&common.filesystem_authority, &scope);
workdir_sessions_from_authority(&common.filesystem_authority, &scope)?;
let mut worker = Self {
backend_job: std::sync::OnceLock::new(),
manifest,
@@ -9109,7 +9108,7 @@ where
let worker_metadata_segment_cas = Some(worker_metadata_segment_cas_for_store(&store));
let scope = SharedScope::new(common.scope);
let workdir_sessions =
workdir_sessions_from_authority(&common.filesystem_authority, &scope);
workdir_sessions_from_authority(&common.filesystem_authority, &scope)?;
let mut worker = Self {
backend_job: std::sync::OnceLock::new(),
@@ -10220,9 +10219,25 @@ pub enum WorkerError {
fn workdir_sessions_from_authority(
authority: &WorkerFilesystemAuthority,
scope: &SharedScope,
) -> Arc<WorkdirSessionRouter> {
) -> Result<Arc<WorkdirSessionRouter>, WorkerError> {
let router = Arc::new(WorkdirSessionRouter::new());
if let Some(local) = authority.as_local() {
let invalid_cwd = |error: String| WorkerError::InvalidCwd {
cwd: local.cwd.clone(),
source: std::io::Error::new(std::io::ErrorKind::InvalidInput, error),
};
let relative_cwd = local.cwd.strip_prefix(&local.root).map_err(|error| {
invalid_cwd(format!(
"session cwd must be within its Workdir root: {error}"
))
})?;
let relative_cwd = relative_cwd
.to_str()
.ok_or_else(|| invalid_cwd("session cwd must be UTF-8".into()))?;
let relative_cwd = workdir::WorkdirPath::new(relative_cwd)
.map_err(|error| invalid_cwd(error.to_string()))?;
// The provider keeps root-coordinate operations. Only the routed tool
// view uses cwd; delegation reuses the same root-coordinate broker.
let session = Arc::new(LocalWorkdirSession::materialized(
local.root.clone(),
local.cwd.clone(),
@@ -10230,13 +10245,14 @@ fn workdir_sessions_from_authority(
WorkdirSessionCapabilities::ALL,
)) as WorkdirSessionHandle;
router
.attach(
.attach_at_cwd(
WorkdirAttachmentAlias::new("workdir").expect("static alias is valid"),
session,
relative_cwd,
)
.expect("fresh Workdir router accepts its only attachment");
.map_err(|error| invalid_cwd(error.to_string()))?;
}
router
Ok(router)
}
/// Bundle of resources that every high-level Worker constructor needs:
@@ -187,7 +187,8 @@ async fn materialise_on_first_turn_populates_worker() {
.expect("system prompt materialised")
.to_string();
assert!(rendered.contains("date="));
assert!(rendered.contains(&pwd.display().to_string()));
assert!(rendered.contains("## Workdir execution environment"));
assert!(!rendered.contains(&pwd.display().to_string()));
assert!(rendered.starts_with("date="));
}
@@ -206,7 +207,8 @@ async fn session_start_state_captures_rendered_prompt() {
LogEntry::AnnotatedSegmentStart { system_prompt, .. } => {
let sp = system_prompt.as_deref().expect("system prompt set");
assert!(sp.starts_with("hello"));
assert!(sp.contains(&pwd.display().to_string()));
assert!(sp.contains("## Workdir execution environment"));
assert!(!sp.contains(&pwd.display().to_string()));
}
other => panic!("expected AnnotatedSegmentStart as first entry, got {other:?}"),
}