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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user