refactor: broker SubWorker Workdir tools through parent
This commit is contained in:
@@ -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,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
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user