refactor: broker SubWorker Workdir tools through parent

This commit is contained in:
2026-09-06 02:17:44 +09:00
parent 1239c638a5
commit 68f00bc948
17 changed files with 715 additions and 1117 deletions
+3 -41
View File
@@ -68,12 +68,10 @@ pub enum WorkdirSessionOperation {
CommandCancel(CommandHandle),
}
/// Wire envelope for an operation and its optional provider-enforced child scope.
/// Wire envelope for one provider operation.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkdirSessionOperationRequest {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub delegations: Vec<crate::WorkdirDelegationRequest>,
pub operation: WorkdirSessionOperation,
}
@@ -289,7 +287,7 @@ mod client {
use reqwest::{Client, StatusCode, Url};
use super::*;
use crate::{Workdir, WorkdirSession, WorkdirSessionHandle};
use crate::{Workdir, WorkdirSession};
/// Provides a fresh bearer token for each Runtime request. Backend
/// implementations can mint short-lived capability tokens without making a
@@ -324,7 +322,6 @@ mod client {
workdir: Workdir,
session_id: WorkdirSessionId,
capabilities: WorkdirSessionCapabilities,
delegations: Vec<crate::WorkdirDelegationRequest>,
closed: AtomicBool,
}
@@ -377,7 +374,6 @@ mod client {
workdir: Workdir::new(opened.workdir_id.as_str()),
session_id: opened.session_id,
capabilities: opened.capabilities,
delegations: Vec::new(),
closed: AtomicBool::new(false),
})
}
@@ -404,10 +400,7 @@ mod client {
"operations",
],
)?;
let operation = WorkdirSessionOperationRequest {
delegations: self.delegations.clone(),
operation,
};
let operation = WorkdirSessionOperationRequest { operation };
let response = self
.client
.post(url)
@@ -436,37 +429,6 @@ mod client {
self.capabilities
}
fn transports_delegation_context(&self) -> bool {
true
}
async fn capture_delegation_source(
&self,
request: &crate::WorkdirDelegationRequest,
) -> Result<WorkdirSessionHandle, WorkdirError> {
if self.closed.load(Ordering::Acquire) {
return Err(WorkdirError::SessionClosed);
}
let mut delegations = self.delegations.clone();
delegations.push(request.clone());
let candidate = Arc::new(Self {
client: self.client.clone(),
base_url: self.base_url.clone(),
authorization: self.authorization.clone(),
workdir: self.workdir.clone(),
session_id: self.session_id.clone(),
capabilities: self.capabilities,
delegations,
closed: AtomicBool::new(false),
});
candidate
.stat(StatRequest {
path: fs_operation::FsPath::new("").expect("empty Workdir path is valid"),
})
.await?;
Ok(candidate)
}
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
match self.operate(WorkdirSessionOperation::Stat(request)).await? {
WorkdirSessionOperationResult::Stat(result) => Ok(result),
+5 -39
View File
@@ -5,10 +5,10 @@
//! bound to one Worker. Tools consume sessions; they do not own Workdir
//! materialization or cleanup.
mod delegation;
pub mod http;
mod local;
mod operation;
mod scope;
pub mod workspace;
use std::path::{Path, PathBuf};
@@ -18,11 +18,6 @@ use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
pub use delegation::{
AppliedWorkdirDelegation, ReadOnlyWorkdirSession, WorkdirDelegation,
WorkdirDelegationPermission, WorkdirDelegationRequest, WorkdirDelegationRule,
apply_delegation_chain, delegation_capable_session,
};
pub use fs_operation::{
ContentHash, EditRequest, EditResult, EntryKind, FsPath as WorkdirPath, GlobRequest,
GlobResult, GrepOutputMode, GrepRequest, GrepResult, ListEntry, ListRequest, ListResult,
@@ -32,6 +27,10 @@ pub use local::{
LocalWorkdirSession, SymlinkInfo, WorkdirSessionResource, direct_symlink, first_symlink,
};
pub use operation::*;
pub use scope::{
ReadOnlyWorkdirSession, WorkdirScopeLease, WorkdirToolBroker, WorkdirToolScope,
WorkdirToolScopePermission, WorkdirToolScopeRule,
};
/// Persistent, opaque identity of one materialized Workdir.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -148,39 +147,6 @@ pub trait WorkdirSession: std::fmt::Debug + Send + Sync {
fn workdir(&self) -> &Workdir;
fn capabilities(&self) -> WorkdirSessionCapabilities;
fn is_delegation_capable(&self) -> bool {
false
}
/// Whether this session transports the delegation chain to another
/// provider boundary that will apply logical cwd/path resolution there.
fn transports_delegation_context(&self) -> bool {
false
}
/// Capture a provider-specific source for a delegated child session.
/// Remote providers use this boundary to pin attachment identity without
/// exposing transport handles or host paths.
async fn capture_delegation_source(
&self,
_request: &WorkdirDelegationRequest,
) -> Result<WorkdirSessionHandle, WorkdirError> {
Err(WorkdirError::Denied(
"workdir provider does not support delegated sessions".into(),
))
}
/// Attenuate this session into a revocable child lease. Only sessions
/// created with [`delegation_capable_session`] implement this operation.
async fn delegate(
&self,
_request: WorkdirDelegationRequest,
) -> Result<WorkdirDelegation, WorkdirError> {
Err(WorkdirError::Denied(
"workdir session is not delegation-capable".into(),
))
}
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError>;
async fn read(&self, request: ReadRequest) -> Result<ReadResult, WorkdirError>;
async fn write(&self, request: WriteRequest) -> Result<WriteResult, WorkdirError>;
+23 -69
View File
@@ -18,7 +18,7 @@ use std::sync::{Arc, Mutex as StdMutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use manifest::{Permission, Scope, ScopeConfig, ScopeRule, SharedScope};
use manifest::{Scope, SharedScope};
use sha2::{Digest, Sha256};
use tokio::process::Command;
use tokio::sync::{Mutex, broadcast, watch};
@@ -28,10 +28,8 @@ use crate::{
CommandEvent, CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest,
CommandSnapshot, CommandStatus, CommandStream, CommandStreamSlice, EditRequest, EditResult,
GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult, ReadRequest,
ReadResult, StatRequest, StatResult, Workdir, WorkdirDelegationPermission,
WorkdirDelegationRequest, WorkdirError, WorkdirPath, WorkdirSession,
WorkdirSessionCapabilities, WorkdirSessionCapability, WorkdirSessionHandle, WriteRequest,
WriteResult,
ReadResult, StatRequest, StatResult, Workdir, WorkdirError, WorkdirPath, WorkdirSession,
WorkdirSessionCapabilities, WorkdirSessionCapability, WriteRequest, WriteResult,
};
#[cfg(test)]
use crate::{EntryKind, WriteOutcome};
@@ -558,69 +556,6 @@ impl WorkdirSession for LocalWorkdirSession {
self.inner.capabilities
}
async fn capture_delegation_source(
&self,
request: &WorkdirDelegationRequest,
) -> Result<WorkdirSessionHandle, WorkdirError> {
let host_rules = request
.rules
.iter()
.map(|rule| ScopeRule {
target: self.inner.root.join(rule.target.as_str()),
permission: match rule.permission {
WorkdirDelegationPermission::Read => Permission::Read,
WorkdirDelegationPermission::Write => Permission::Write,
},
recursive: rule.recursive,
})
.collect::<Vec<_>>();
for (logical, host) in request.rules.iter().zip(&host_rules) {
if logical.permission == WorkdirDelegationPermission::Write {
let resolved = Scope::resolved_target(host)
.map_err(|error| WorkdirError::Denied(error.to_string()))?;
if resolved != host.target {
return Err(WorkdirError::Denied(format!(
"write delegation target `{}` traverses a symlink",
logical.target
)));
}
}
}
let parent_scope = self.inner.scope.snapshot();
for rule in &host_rules {
if !parent_scope
.allows_rule(rule)
.map_err(|error| WorkdirError::Denied(error.to_string()))?
{
return Err(WorkdirError::Denied(format!(
"delegated provider scope `{}` exceeds the parent session",
rule.target.display()
)));
}
}
let child_scope = Scope::from_config(&ScopeConfig {
allow: host_rules,
deny: Vec::new(),
})
.map_err(|error| WorkdirError::Denied(error.to_string()))?;
let child_cwd = self.inner.root.join(request.cwd.as_str());
if !child_scope.is_readable(&child_cwd)
|| !std::fs::metadata(&child_cwd).is_ok_and(|metadata| metadata.is_dir())
{
return Err(WorkdirError::Denied(format!(
"delegated cwd `{}` is not a readable Workdir directory",
request.cwd
)));
}
Ok(Arc::new(LocalWorkdirSession::materialized_bound(
self.inner.workdir.clone(),
self.inner.root.clone(),
self.inner.root.clone(),
SharedScope::new(child_scope),
self.inner.capabilities,
)))
}
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
self.ensure_capability(WorkdirSessionCapability::Read)?;
let logical = request.path.clone();
@@ -694,9 +629,20 @@ impl WorkdirSession for LocalWorkdirSession {
{
return Err(WorkdirError::OutOfScope(spill_dir.to_path_buf()));
}
let cwd = if let Some(logical_cwd) = request.cwd.as_ref() {
let cwd = self.resolve(logical_cwd);
let scope = self.inner.scope.snapshot();
if !scope.is_readable(&cwd)
|| !std::fs::metadata(&cwd).is_ok_and(|metadata| metadata.is_dir())
{
return Err(WorkdirError::OutOfScope(cwd));
}
cwd
} else {
self.inner.cwd.clone()
};
let id = self.inner.next_command_id.fetch_add(1, Ordering::Relaxed);
let handle = CommandHandle(format!("command-{id}"));
let cwd = self.inner.cwd.clone();
let (completion_tx, completion) = watch::channel(false);
let command_id = handle.0.clone();
let telemetry = self.inner.command_telemetry.clone();
@@ -1516,6 +1462,7 @@ mod tests {
command: "sleep 30".to_owned(),
timeout_secs: 60,
output_limit: 1024,
cwd: None,
spill_dir: None,
tool_call_id: None,
},
@@ -2043,6 +1990,7 @@ mod tests {
command: "pwd && printf provider-command".into(),
timeout_secs: 5,
output_limit: 4096,
cwd: None,
spill_dir: None,
tool_call_id: None,
},
@@ -2141,6 +2089,7 @@ mod tests {
command: "printf hidden".into(),
timeout_secs: 5,
output_limit: 1,
cwd: None,
spill_dir: Some(spill.path().to_path_buf()),
tool_call_id: None,
},
@@ -2178,6 +2127,7 @@ mod tests {
command: "i=0; while [ $i -lt 200 ]; do printf 'line-%03d\\n' \"$i\"; i=$((i+1)); done; printf 'FINAL-NEEDLE\\n'".into(),
timeout_secs: 5,
output_limit: 64,
cwd: None,
spill_dir: Some(spill.path().to_path_buf()),
tool_call_id: None,
},
@@ -2224,6 +2174,7 @@ mod tests {
command: "printf 'aéz'".into(),
timeout_secs: 5,
output_limit: 1024,
cwd: None,
spill_dir: None,
tool_call_id: None,
},
@@ -2449,6 +2400,7 @@ mod tests {
command: "printf ready; printf warning >&2; sleep 0.2; printf done".into(),
timeout_secs: 5,
output_limit: 1024,
cwd: None,
spill_dir: None,
tool_call_id: Some("tool-7".into()),
},
@@ -2553,6 +2505,7 @@ mod tests {
command: "sleep 30".into(),
timeout_secs: 1,
output_limit: 1024,
cwd: None,
spill_dir: None,
tool_call_id: None,
},
@@ -2623,6 +2576,7 @@ mod tests {
command: "sleep 30".into(),
timeout_secs: 60,
output_limit: 1024,
cwd: None,
spill_dir: None,
tool_call_id: None,
},
+4
View File
@@ -11,6 +11,10 @@ pub struct CommandRequest {
pub command: String,
pub timeout_secs: u64,
pub output_limit: usize,
/// Workdir-relative command directory. Providers validate it against the
/// active session before process start.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<fs_operation::FsPath>,
/// Provider-local directory where complete output is retained when the
/// inline result exceeds `output_limit`.
pub spill_dir: Option<PathBuf>,
File diff suppressed because it is too large Load Diff
-10
View File
@@ -104,15 +104,5 @@ mod tests {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceWorkdirSessionOperationRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_session_fence: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub delegations: Vec<crate::WorkdirDelegationRequest>,
pub operation: crate::http::WorkdirSessionOperation,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceWorkdirSessionFence {
pub value: String,
}