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
+1
View File
@@ -869,6 +869,7 @@ async fn open_workdir_session(
session_id: session_id.clone(),
workdir_id: session.workdir().id().clone(),
capabilities: session.capabilities(),
environment: session.environment(),
};
state
.workdir_sessions
@@ -111,6 +111,9 @@ impl Harness {
session_id: WorkdirSessionId::new("session-transport").unwrap(),
workdir_id: Workdir::new("wd-transport").id().clone(),
capabilities: WorkdirSessionCapabilities::ALL,
environment: workdir::WorkdirSessionEnvironment::root(
WorkdirSessionCapabilities::ALL,
),
})
}),
)
@@ -55,6 +55,10 @@ impl WorkdirSession for RetainedWorkdirSession {
self.inner.capabilities()
}
fn environment(&self) -> workdir::WorkdirSessionEnvironment {
self.inner.environment()
}
async fn authorize_scope_path(
&self,
request: WorkdirScopeAuthorizationRequest,
+64 -1
View File
@@ -1127,8 +1127,17 @@ fn runtime_local_workdir_router(
),
None => source,
};
let cwd = binding
.cwd()
.strip_prefix(binding.root())
.map_err(|_| format!("Workdir attachment `{alias}` cwd is outside its logical root"))?;
let cwd = workdir::WorkdirPath::new(
cwd.to_str()
.ok_or_else(|| format!("Workdir attachment `{alias}` cwd is not UTF-8"))?,
)
.map_err(|error| error.to_string())?;
router
.attach(alias.clone(), source)
.attach_at_cwd(alias.clone(), source, cwd)
.map_err(|error| format!("bind Workdir attachment `{alias}`: {error}"))?;
}
Ok(router)
@@ -10550,6 +10559,60 @@ mod tests {
assert!(!binding.working_directory_root().exists());
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn local_attachment_router_keeps_each_binding_cwd_and_capability_ceiling() {
let runtime_base = tempfile::tempdir().unwrap();
let repo = create_clean_repo();
let materializer = RuntimeGitMaterializer::new(runtime_base.path());
let root_binding = materializer
.create(&working_directory_request(repo.path()))
.unwrap();
std::fs::create_dir(root_binding.root().join("nested")).unwrap();
std::fs::write(root_binding.root().join("marker"), "root").unwrap();
std::fs::write(root_binding.root().join("nested/marker"), "nested").unwrap();
let mut nested_binding = root_binding.clone();
nested_binding.cwd = root_binding.root().join("nested");
let root_alias = WorkdirAttachmentAlias::new("root").unwrap();
let nested_alias = WorkdirAttachmentAlias::new("nested").unwrap();
let attachments = BTreeMap::from([
(root_alias.clone(), root_binding),
(nested_alias.clone(), nested_binding),
]);
let ceilings = BTreeMap::from([
(root_alias, WorkdirSessionCapabilities::ALL),
(nested_alias, WorkdirSessionCapabilities::READ_ONLY),
]);
let router = runtime_local_workdir_router(&attachments, &ceilings, None).unwrap();
assert!(router.resolve(None).is_err());
for (alias, cwd, content, command) in [
("root", "", "root", true),
("nested", "nested", "nested", false),
] {
let selected = router.resolve(Some(alias)).unwrap().session;
let environment = selected.environment();
assert_eq!(environment.cwd.as_str(), cwd);
assert_eq!(
environment
.capabilities
.supports(workdir::WorkdirSessionCapability::Command),
command
);
let read = selected
.read(workdir::ReadRequest {
path: workdir::WorkdirPath::new("marker").unwrap(),
offset: 0,
limit: 10,
max_bytes: 100,
})
.await
.unwrap();
assert_eq!(read.bytes, content.as_bytes());
assert_eq!(read.path.as_str(), "marker");
}
router.close_all().await.unwrap();
}
#[test]
fn spawn_failure_with_existing_working_directory_preserves_workdir() {
let runtime_base = tempfile::tempdir().unwrap();
@@ -124,6 +124,18 @@ macro_rules! impl_deferred_session {
self.capabilities
}
fn environment(&self) -> WorkdirSessionEnvironment {
let mut environment = self.state.try_lock().ok().and_then(|state| {
state.session.as_ref().map(|session| session.environment())
}).unwrap_or_else(|| {
let mut environment = WorkdirSessionEnvironment::root(self.capabilities);
environment.restrictions.push("Deferred provider: scope is checked on connection and every operation".into());
environment
});
environment.capabilities = self.capabilities;
environment
}
$(
async fn $method(&self, $request: $request_type) -> Result<$result, WorkdirError> {
self.session().await?.$method($request).await