feat: capture bounded Bash output over replayable byte transport

This commit is contained in:
2026-10-20 03:01:22 +09:00
parent e5749b9be0
commit 4555aace28
46 changed files with 4183 additions and 1914 deletions
+1 -3
View File
@@ -47,9 +47,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let worker = worker::Worker::from_manifest_toml(&toml, store).await?;
let runtime_tmp = tempfile::tempdir()?;
let bash_output_dir = runtime_tmp.path().join("bash-output");
let (handle, _shutdown_rx) =
WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir).await?;
let (handle, _shutdown_rx) = WorkerController::spawn(worker, runtime_tmp.path()).await?;
// Check initial status via shared state
println!("[shared_state] {}", handle.shared_state.status_json());
+6 -59
View File
@@ -17,28 +17,9 @@ use manifest::WorkerManifest;
#[derive(Debug, Clone)]
pub enum WorkerBootstrapLayout {
/// A direct Worker rooted below the supplied runtime base directory.
Direct {
runtime_base: PathBuf,
bash_output_dir: PathBuf,
},
Direct { runtime_base: PathBuf },
/// A runtime-managed Worker with an exact persisted run directory.
RuntimeManagedRun {
run_dir: PathBuf,
bash_output_dir: PathBuf,
},
}
/// Return the temporary Bash spill directory owned by a stable Worker identity.
///
/// The directory deliberately lives outside session/run-generation storage so a
/// restarted controller for the same Worker keeps the same readable artifact
/// boundary.
pub fn bash_output_dir_for_worker_id(worker_id: impl std::fmt::Display) -> PathBuf {
std::env::temp_dir()
.join("yoi")
.join("workers")
.join(worker_id.to_string())
.join("bash-output")
RuntimeManagedRun { run_dir: PathBuf },
}
/// Construction and controller inputs that are stable for one Worker launch.
@@ -272,27 +253,12 @@ where
{
let cleanup_sessions = worker.workdir_sessions();
let controller = match layout {
WorkerBootstrapLayout::Direct {
runtime_base,
bash_output_dir,
} => {
WorkerController::spawn_with_transport_owned(
worker,
&runtime_base,
&bash_output_dir,
transport,
)
.await
WorkerBootstrapLayout::Direct { runtime_base } => {
WorkerController::spawn_with_transport_owned(worker, &runtime_base, transport).await
}
WorkerBootstrapLayout::RuntimeManagedRun {
run_dir,
bash_output_dir,
} => {
WorkerBootstrapLayout::RuntimeManagedRun { run_dir } => {
WorkerController::spawn_runtime_managed_run_with_transport_owned(
worker,
&run_dir,
&bash_output_dir,
transport,
worker, &run_dir, transport,
)
.await
}
@@ -313,22 +279,3 @@ where
}
}
}
#[cfg(test)]
mod tests {
use super::bash_output_dir_for_worker_id;
#[test]
fn bash_output_directory_is_stable_per_worker_below_system_temp() {
let path = bash_output_dir_for_worker_id("019c1234-worker");
assert_eq!(
path,
std::env::temp_dir()
.join("yoi")
.join("workers")
.join("019c1234-worker")
.join("bash-output")
);
}
}
+5 -1
View File
@@ -446,7 +446,11 @@ async fn checkout_wip_policy_is_not_feature_authority_and_compatibility_is_repla
let mut registry = WipMountRegistry::new();
crate::checkout::mount_checkouts(&mut registry, router.clone(), tools::Tracker::new(), None)
.unwrap();
for def in tools::routed_builtin_tools(router, tools::Tracker::new(), dir.path().join("bash")) {
for def in tools::routed_builtin_tools(
router,
tools::Tracker::new(),
tools::bash_output::BashOutputView::new(),
) {
let (meta, tool) = def();
let p = compatibility_projection(meta, tool, None).unwrap();
registry.mount(p).unwrap();
+23 -72
View File
@@ -692,7 +692,6 @@ impl WorkerController {
pub async fn spawn<C, St>(
worker: Worker<C, St>,
runtime_base: &Path,
bash_output_dir: &Path,
) -> Result<(WorkerHandle, ShutdownReceiver), std::io::Error>
where
C: LlmClient + Clone + 'static,
@@ -701,7 +700,6 @@ impl WorkerController {
let (handle, shutdown, _task) = Self::spawn_inner(
worker,
runtime_base,
bash_output_dir,
false,
None,
WorkerControllerTransport::UnixSocket,
@@ -715,7 +713,7 @@ impl WorkerController {
pub async fn spawn_with_transport<C, St>(
worker: Worker<C, St>,
runtime_base: &Path,
bash_output_dir: &Path,
transport: WorkerControllerTransport,
) -> Result<(WorkerHandle, ShutdownReceiver), std::io::Error>
where
@@ -723,42 +721,32 @@ impl WorkerController {
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
{
let (handle, shutdown, _task) =
Self::spawn_with_transport_owned(worker, runtime_base, bash_output_dir, transport)
.await?;
Self::spawn_with_transport_owned(worker, runtime_base, transport).await?;
Ok((handle, shutdown))
}
pub(crate) async fn spawn_with_transport_owned<C, St>(
worker: Worker<C, St>,
runtime_base: &Path,
bash_output_dir: &Path,
transport: WorkerControllerTransport,
) -> Result<(WorkerHandle, ShutdownReceiver, tokio::task::JoinHandle<()>), std::io::Error>
where
C: LlmClient + Clone + 'static,
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
{
Self::spawn_inner(
worker,
runtime_base,
bash_output_dir,
false,
None,
transport,
)
.await
Self::spawn_inner(worker, runtime_base, false, None, transport).await
}
/// Spawn a Worker owned by `worker-runtime`.
///
/// The controller uses an ephemeral directory for Unix sockets while tool
/// spill artifacts use the separately supplied Worker-owned temporary path.
/// The controller uses an ephemeral directory for Unix sockets. Bash output is
/// owned by its live Feature instance, independently of filesystem layout.
/// Runtime-managed Workers do not write legacy pid/status/manifest liveness
/// projections.
pub async fn spawn_runtime_managed<C, St>(
worker: Worker<C, St>,
runtime_base: &Path,
bash_output_dir: &Path,
) -> Result<(WorkerHandle, ShutdownReceiver), std::io::Error>
where
C: LlmClient + Clone + 'static,
@@ -767,7 +755,6 @@ impl WorkerController {
let (handle, shutdown, _task) = Self::spawn_inner(
worker,
runtime_base,
bash_output_dir,
true,
None,
WorkerControllerTransport::UnixSocket,
@@ -780,7 +767,6 @@ impl WorkerController {
pub async fn spawn_runtime_managed_run<C, St>(
worker: Worker<C, St>,
run_dir: &Path,
bash_output_dir: &Path,
) -> Result<(WorkerHandle, ShutdownReceiver), std::io::Error>
where
C: LlmClient + Clone + 'static,
@@ -789,7 +775,6 @@ impl WorkerController {
Self::spawn_runtime_managed_run_with_transport(
worker,
run_dir,
bash_output_dir,
WorkerControllerTransport::UnixSocket,
)
.await
@@ -800,27 +785,23 @@ impl WorkerController {
pub async fn spawn_runtime_managed_run_with_transport<C, St>(
worker: Worker<C, St>,
run_dir: &Path,
bash_output_dir: &Path,
transport: WorkerControllerTransport,
) -> Result<(WorkerHandle, ShutdownReceiver), std::io::Error>
where
C: LlmClient + Clone + 'static,
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
{
let (handle, shutdown, _task) = Self::spawn_runtime_managed_run_with_transport_owned(
worker,
run_dir,
bash_output_dir,
transport,
)
.await?;
let (handle, shutdown, _task) =
Self::spawn_runtime_managed_run_with_transport_owned(worker, run_dir, transport)
.await?;
Ok((handle, shutdown))
}
pub(crate) async fn spawn_runtime_managed_run_with_transport_owned<C, St>(
worker: Worker<C, St>,
run_dir: &Path,
bash_output_dir: &Path,
transport: WorkerControllerTransport,
) -> Result<(WorkerHandle, ShutdownReceiver, tokio::task::JoinHandle<()>), std::io::Error>
where
@@ -830,21 +811,13 @@ impl WorkerController {
let parent = run_dir
.parent()
.ok_or_else(|| std::io::Error::other("run path has no parent"))?;
Self::spawn_inner(
worker,
parent,
bash_output_dir,
true,
Some(run_dir),
transport,
)
.await
Self::spawn_inner(worker, parent, true, Some(run_dir), transport).await
}
async fn spawn_inner<C, St>(
worker: Worker<C, St>,
runtime_base: &Path,
bash_output_dir: &Path,
runtime_managed: bool,
runtime_run: Option<&Path>,
transport: WorkerControllerTransport,
@@ -857,7 +830,6 @@ impl WorkerController {
let result = Self::spawn_initialized(
worker,
runtime_base,
bash_output_dir,
runtime_managed,
runtime_run,
transport,
@@ -874,7 +846,7 @@ impl WorkerController {
async fn spawn_initialized<C, St>(
mut worker: Worker<C, St>,
runtime_base: &Path,
bash_output_dir: &Path,
runtime_managed: bool,
runtime_run: Option<&Path>,
transport: WorkerControllerTransport,
@@ -888,8 +860,7 @@ impl WorkerController {
.map_err(std::io::Error::other)?;
// === 1. Initialization (channels / RuntimeDir / worker-immutable
// snapshots / SpawnedWorkerRegistry / alerter attach /
// bash-output scope) ===
// snapshots / SpawnedWorkerRegistry / alerter attach) ===
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let (method_tx, method_rx) = mpsc::channel::<Method>(32);
let (working_event_tx, _) = broadcast::channel::<Event>(256);
@@ -897,8 +868,8 @@ impl WorkerController {
let in_flight = InFlightEvents::new(working_event_tx.clone());
worker.attach_in_flight_events(in_flight.clone());
// Runtime directory is created before tool registration because it owns
// bounded tool artifacts, and before initial status/history writes consume
// Runtime directory is created before tool registration and before
// initial status/history writes consume
// the greeting we build after registration is complete.
let runtime_dir = Arc::new(if let Some(run_dir) = runtime_run {
RuntimeDir::create_worker_run(run_dir).await?
@@ -935,26 +906,6 @@ impl WorkerController {
worker.attach_internal_worker_registry(spawned_registry.clone());
worker.attach_working_event_tx(working_event_tx.clone());
// Bash spill artifacts are owned by the stable Worker identity rather
// than a controller session/run generation. Push a recursive
// `allow(Read)` for the exact tool output path into the Worker's shared
// runtime scope so the Workdir session and system prompt stay aligned.
let bash_output_dir = bash_output_dir.to_path_buf();
std::fs::create_dir_all(&bash_output_dir).map_err(|e| {
std::io::Error::other(format!(
"create bash output dir {}: {e}",
bash_output_dir.display()
))
})?;
worker
.add_scope_rules([manifest::ScopeRule {
target: bash_output_dir.clone(),
permission: manifest::Permission::Read,
recursive: true,
symlink_policy: Default::default(),
}])
.map_err(std::io::Error::other)?;
// === 1.5. Direct writer wiring ===
//
// Engine callbacks fire `on_history_append` for each assistant
@@ -974,7 +925,6 @@ impl WorkerController {
// === 3. Tool registration (builtin / memory / spawn-orchestration) ===
let fs_for_view = register_worker_tools(
&mut worker,
bash_output_dir,
runtime_base.to_path_buf(),
spawned_registry.clone(),
Some(method_tx.downgrade()),
@@ -1440,7 +1390,7 @@ fn validate_memory_lifecycle_targets(
/// the shared state.
pub(crate) async fn register_worker_tools<C, St>(
worker: &mut Worker<C, St>,
bash_output_dir: PathBuf,
runtime_base: PathBuf,
spawned_registry: Arc<SpawnedWorkerRegistry>,
parent_method_tx: Option<mpsc::WeakSender<Method>>,
@@ -1485,6 +1435,7 @@ where
let lifecycle_enabled = worker.manifest_lifecycle_features_enabled() && backend_job.is_none();
let prompts = worker.prompts().clone();
let tracker = tools::Tracker::new();
let bash_output = spawned_registry.bash_output();
{
let paste_store = worker.store().clone();
let paste_session_id = worker.session_id();
@@ -1507,13 +1458,13 @@ where
tools::routed_builtin_tools_for_current_capabilities(
workdir_sessions.clone(),
tracker.clone(),
bash_output_dir.clone(),
bash_output.clone(),
)
} else {
tools::routed_builtin_tools(
workdir_sessions.clone(),
tracker.clone(),
bash_output_dir.clone(),
bash_output.clone(),
)
};
worker.engine_mut().register_tools(workdir_tools);
@@ -2040,7 +1991,6 @@ where
spawner_workspace_context,
parent_notifications,
runtime_base.clone(),
bash_output_dir.clone(),
spawner_workspace_root,
workdir_tool_broker,
spawned_registry.clone(),
@@ -3326,6 +3276,8 @@ async fn controller_loop<C, St>(
}
}
spawned_registry.bash_output().revoke();
if let Some(observer) = command_observer.take() {
observer.abort();
let _ = observer.await;
@@ -4212,7 +4164,6 @@ mod tests {
.descriptor();
register_worker_tools(
&mut worker,
root.path().join("bash-output"),
root.path().join("runtime"),
SpawnedWorkerRegistry::new_for_internal_services(),
None,
@@ -64,7 +64,6 @@ async fn prepared(
WorkerFilesystemAuthority::None,
WorkerBootstrapLayout::Direct {
runtime_base: temp.path().join("runtime"),
bash_output_dir: temp.path().join("bash"),
},
WorkerControllerTransport::InProcess,
)
+1 -2
View File
@@ -635,12 +635,11 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
return ExitCode::FAILURE;
}
};
let bash_output_dir = crate::bash_output_dir_for_worker_id(&worker_name);
let started = match start_worker_controller(
worker,
WorkerBootstrapLayout::Direct {
runtime_base: runtime_base.clone(),
bash_output_dir,
},
WorkerControllerTransport::UnixSocket,
)
@@ -505,13 +505,7 @@ impl WorkdirSession for WorkspaceAttachedWorkdirSession {
}
}
async fn start_command(
&self,
mut request: CommandRequest,
) -> Result<CommandHandle, WorkdirError> {
// `spill_dir` belongs to the Worker host running BashTool. This remote
// WorkdirSession provider cannot safely resolve or write that host path.
request.spill_dir = None;
async fn start_command(&self, request: CommandRequest) -> Result<CommandHandle, WorkdirError> {
match self.operate(WorkdirSessionOperation::CommandStart(request))? {
WorkdirSessionOperationResult::CommandStart(result) => Ok(result),
_ => Err(Self::mismatch("command_start")),
@@ -1660,9 +1654,9 @@ mod tests {
.start_command(CommandRequest {
command: "true".to_string(),
timeout_secs: 120,
output_limit: 1024,
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
cwd: workdir::WorkdirPath::root(),
spill_dir: Some("/worker-local/bash-output".into()),
tool_call_id: Some("call-1".to_string()),
})
.await
@@ -1675,7 +1669,7 @@ mod tests {
assert_eq!(body["operation"]["operation"], "command_start");
assert_eq!(body["operation"]["request"]["command"], "true");
assert_eq!(body["operation"]["request"]["cwd"], "");
assert!(body["operation"]["request"]["spill_dir"].is_null());
assert!(body["operation"]["request"].get("spill_dir").is_none());
assert_eq!(body["operation"]["request"]["tool_call_id"], "call-1");
assert!(
!requests[0]
@@ -1942,9 +1936,9 @@ mod tests {
.start_command(CommandRequest {
command: "printf done".to_string(),
timeout_secs: 5,
output_limit: 1024,
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
cwd: workdir::WorkdirPath::root(),
spill_dir: None,
tool_call_id: None,
})
.await
+1 -1
View File
@@ -39,7 +39,7 @@ mod worker;
pub use bootstrap::{
BootstrappedWorker, PreparedWorker, WorkerBootstrap, WorkerBootstrapError,
WorkerBootstrapLayout, bash_output_dir_for_worker_id, start_worker_controller,
WorkerBootstrapLayout, start_worker_controller,
};
pub use compact::token_counter::{EstimateSource, SplitPoint, TokenEstimate};
pub use controller::{ShutdownReceiver, WorkerController, WorkerControllerTransport, WorkerHandle};
+27 -7
View File
@@ -88,7 +88,7 @@ impl InternalSpawnedWorkerRecord {
pub(crate) fn new(
worker_name: String,
scope_delegated: Vec<ScopeRule>,
workdir_tool_scope: WorkdirScopeLeaseSet,
workdir_tool_scope: Arc<WorkdirScopeLeaseSet>,
#[cfg(test)] installed_tools: Vec<String>,
session: InternalWorkerSessionHandle,
child_registry: Arc<SpawnedWorkerRegistry>,
@@ -97,7 +97,7 @@ impl InternalSpawnedWorkerRecord {
Self {
worker_name: worker_name.clone(),
scope_delegated,
workdir_tool_scope: Arc::new(workdir_tool_scope),
workdir_tool_scope,
#[cfg(test)]
installed_tools: installed_tools.into(),
published: Arc::new(super::published::PublishedProtocol::new(
@@ -329,6 +329,7 @@ pub struct SpawnedWorkerRegistry {
pending_internal_notify: Notify,
internal_spawn_cleanup_failed: AtomicBool,
parent_scope: Option<SharedScope>,
bash_output: Mutex<tools::bash_output::BashOutputView>,
parent_protocol: Mutex<Option<(broadcast::Sender<Event>, String)>>,
protocol_publish_lock: Arc<Mutex<()>>,
}
@@ -340,6 +341,14 @@ pub struct SpawnedWorkerRegistryLoad {
}
impl SpawnedWorkerRegistry {
pub(crate) fn bash_output(&self) -> tools::bash_output::BashOutputView {
self.bash_output.lock().unwrap().clone()
}
pub(crate) fn inherit_bash_output(&self, view: tools::bash_output::BashOutputView) {
*self.bash_output.lock().unwrap() = view;
}
pub(crate) fn new_for_internal_services() -> Arc<Self> {
Arc::new(Self {
internal_records: std::sync::Mutex::new(Vec::new()),
@@ -352,6 +361,7 @@ impl SpawnedWorkerRegistry {
pending_internal_notify: Notify::new(),
internal_spawn_cleanup_failed: AtomicBool::new(false),
parent_scope: None,
bash_output: Mutex::new(tools::bash_output::BashOutputView::new()),
parent_protocol: Mutex::new(None),
protocol_publish_lock: Arc::new(Mutex::new(())),
})
@@ -370,6 +380,7 @@ impl SpawnedWorkerRegistry {
pending_internal_notify: Notify::new(),
internal_spawn_cleanup_failed: AtomicBool::new(false),
parent_scope: None,
bash_output: Mutex::new(tools::bash_output::BashOutputView::new()),
parent_protocol: Mutex::new(None),
protocol_publish_lock: Arc::new(Mutex::new(())),
})
@@ -387,6 +398,7 @@ impl SpawnedWorkerRegistry {
pending_internal_notify: Notify::new(),
internal_spawn_cleanup_failed: AtomicBool::new(false),
parent_scope: Some(parent_scope),
bash_output: Mutex::new(tools::bash_output::BashOutputView::new()),
parent_protocol: Mutex::new(None),
protocol_publish_lock: Arc::new(Mutex::new(())),
})
@@ -473,6 +485,7 @@ impl SpawnedWorkerRegistry {
pending_internal_notify: Notify::new(),
internal_spawn_cleanup_failed: AtomicBool::new(false),
parent_scope,
bash_output: Mutex::new(tools::bash_output::BashOutputView::new()),
parent_protocol: Mutex::new(None),
protocol_publish_lock: Arc::new(Mutex::new(())),
}),
@@ -966,6 +979,10 @@ impl SpawnedWorkerRegistry {
"an in-flight Internal SubWorker failed cleanup during shutdown",
));
}
let output = self.bash_output();
if !output.is_root() {
output.revoke();
}
first_error.map_or(Ok(()), Err)
}
@@ -1111,6 +1128,7 @@ fn internal_worker_live_snapshot(snapshot: InternalWorkerSnapshot) -> Option<Eve
impl Drop for SpawnedWorkerRegistry {
fn drop(&mut self) {
self.bash_output.get_mut().unwrap().revoke();
let Some(parent_scope) = &self.parent_scope else {
return;
};
@@ -1297,11 +1315,13 @@ mod tests {
InternalSpawnedWorkerRecord::new(
name.into(),
Vec::new(),
workdir::WorkdirScopeLeaseSet::from_single(
workdir::WorkdirAttachmentAlias::new("workdir").unwrap(),
delegation,
)
.unwrap(),
Arc::new(
workdir::WorkdirScopeLeaseSet::from_single(
workdir::WorkdirAttachmentAlias::new("workdir").unwrap(),
delegation,
)
.unwrap(),
),
Vec::new(),
session,
registry(),
+37 -38
View File
@@ -310,7 +310,7 @@ pub struct SubWorkerSpawnTool {
/// Runtime-owned root used for Internal Worker controller state.
runtime_base: PathBuf,
/// Parent Worker-owned temporary root used for bounded Bash spill output.
bash_output_dir: PathBuf,
/// Inherited runtime workspace root for Profile/project/Ticket/workflow/
/// memory context. SubWorkerSpawn `cwd` must not affect this value.
workspace_root: PathBuf,
@@ -345,7 +345,7 @@ impl SubWorkerSpawnTool {
workspace_context: crate::worker::WorkerWorkspaceContext,
parent_notifications: ParentNotificationTarget,
runtime_base: PathBuf,
bash_output_dir: PathBuf,
workspace_root: PathBuf,
workdir_tool_broker: Option<WorkdirToolBrokerRouter>,
registry: Arc<SpawnedWorkerRegistry>,
@@ -358,7 +358,7 @@ impl SubWorkerSpawnTool {
workspace_context,
parent_notifications,
runtime_base,
bash_output_dir,
workspace_root,
workdir_tool_broker,
registry,
@@ -430,15 +430,6 @@ impl Tool for SubWorkerSpawnTool {
.map_err(|error| ToolError::InvalidArgument(error.to_string()))?;
let workdir_rules = parse_workdir_scope(&input.scope)?;
let child_bash_output_dir = self.bash_output_dir.join("sub-workers").join(&input.name);
tokio::fs::create_dir_all(&child_bash_output_dir)
.await
.map_err(|error| {
ToolError::ExecutionFailed(format!(
"create Internal Worker Bash output directory {}: {error}",
child_bash_output_dir.display()
))
})?;
let workdir_tool_broker = require_workdir_tool_broker(self.workdir_tool_broker.as_ref())?;
let tool_scopes = routed_workdir_tool_scopes(
workdir_tool_broker,
@@ -452,6 +443,7 @@ impl Tool for SubWorkerSpawnTool {
.map_err(|error| {
ToolError::InvalidArgument(format!("scope parent-owned Workdir tools: {error}"))
})?;
let workdir_scope = Arc::new(workdir_scope);
let child_workdir_tool_broker = workdir_scope.broker_router();
let spawn_selector =
@@ -552,24 +544,17 @@ impl Tool for SubWorkerSpawnTool {
)
.await
.map_err(|error| ToolError::ExecutionFailed(format!("build Internal Worker: {error}")))?;
child
.add_scope_rules([ScopeRule {
target: child_bash_output_dir.clone(),
permission: manifest::Permission::Read,
recursive: true,
symlink_policy: Default::default(),
}])
.map_err(|error| {
ToolError::ExecutionFailed(format!(
"grant Internal Worker Bash output scope: {error}"
))
})?;
let child_scope = child.scope().clone();
child.bind_workdir_sessions(workdir_scope.session_router());
let child_registry = SpawnedWorkerRegistry::new_internal(input.name.clone(), child_scope);
let output_lease = workdir_scope.clone();
child_registry.inherit_bash_output(
self.registry
.bash_output()
.child(Arc::new(move || output_lease.is_active())),
);
register_worker_tools(
&mut child,
child_bash_output_dir,
self.runtime_base.clone(),
child_registry.clone(),
None,
@@ -1052,7 +1037,7 @@ pub(crate) fn sub_worker_spawn_tool(
workspace_context: crate::worker::WorkerWorkspaceContext,
parent_notifications: ParentNotificationTarget,
runtime_base: PathBuf,
bash_output_dir: PathBuf,
workspace_root: PathBuf,
workdir_tool_broker: Option<WorkdirToolBrokerRouter>,
registry: Arc<SpawnedWorkerRegistry>,
@@ -1064,7 +1049,6 @@ pub(crate) fn sub_worker_spawn_tool(
workspace_context,
parent_notifications,
runtime_base,
bash_output_dir,
workspace_root,
workdir_tool_broker,
registry,
@@ -1078,7 +1062,7 @@ fn sub_worker_spawn_tool_impl(
workspace_context: crate::worker::WorkerWorkspaceContext,
parent_notifications: ParentNotificationTarget,
runtime_base: PathBuf,
bash_output_dir: PathBuf,
workspace_root: PathBuf,
workdir_tool_broker: Option<WorkdirToolBrokerRouter>,
registry: Arc<SpawnedWorkerRegistry>,
@@ -1110,7 +1094,6 @@ fn sub_worker_spawn_tool_impl(
workspace_context.clone(),
parent_notifications.clone(),
runtime_base.clone(),
bash_output_dir.clone(),
workspace_root.clone(),
workdir_tool_broker.clone(),
registry.clone(),
@@ -1374,7 +1357,6 @@ enabled = false
ParentNotificationTarget::Durable(Arc::new(|_| {})),
),
runtime.path().to_path_buf(),
bash_output_dir.clone(),
workspace_root.clone(),
Some(workdir_tool_broker),
registry.clone(),
@@ -1425,12 +1407,7 @@ enabled = false
.await
.expect("spawn project reviewer as Internal Worker");
assert!(output.summary.contains("internal worker `reviewer-child`"));
assert!(
bash_output_dir
.join("sub-workers")
.join("reviewer-child")
.is_dir()
);
assert!(!bash_output_dir.join("sub-workers").exists());
assert!(spawner_scope.snapshot().is_writable(&workspace_root));
let record = registry
.get_internal("reviewer-child")
@@ -1460,6 +1437,27 @@ enabled = false
record.session.wait_until_idle().await,
crate::internal_worker::InternalWorkerSessionStatus::Idle
);
let child_output_view = record.child_registry.bash_output();
let child_bash = tools::BashTool::new(
record.workdir_tool_scope.session_router(),
child_output_view.clone(),
);
let child_output = child_bash
.execute_result(
r#"{"command":"printf retained-child-evidence","cwd":"."}"#,
Default::default(),
)
.await
.unwrap();
let output_id = child_output.metadata.id.clone();
assert!(child_output_view.metadata(&output_id).is_some());
assert_eq!(
registry
.bash_output()
.read(&output_id, workdir::CommandStream::Stdout, 0, 65536)
.unwrap(),
b"retained-child-evidence"
);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert!(!observed_parent_write_revoked.load(Ordering::SeqCst));
assert!(observed_instruction_override.load(Ordering::SeqCst));
@@ -1563,6 +1561,8 @@ enabled = false
!record.workdir_tool_scope.is_active(),
"stopped child must revoke cloned scoped sessions"
);
assert!(child_output_view.metadata(&output_id).is_none());
assert!(registry.bash_output().metadata(&output_id).is_some());
assert!(registry.get_internal("reviewer-child").is_some());
assert!(
@@ -1573,6 +1573,7 @@ enabled = false
.is_some()
);
assert!(registry.get_internal("reviewer-child").is_none());
assert!(registry.bash_output().metadata(&output_id).is_some());
assert!(spawner_scope.snapshot().is_writable(&workspace_root));
fail_requests.store(false, Ordering::SeqCst);
@@ -1672,7 +1673,6 @@ enabled = false
),
ParentNotificationTarget::Durable(Arc::new(|_| {})),
runtime.path().to_path_buf(),
bash_output_dir,
workspace_root.clone(),
Some(workdir_tool_broker),
registry.clone(),
@@ -1805,7 +1805,6 @@ enabled = false
ParentNotificationTarget::Durable(Arc::new(|_| {})),
),
runtime.path().to_path_buf(),
bash_output_dir.clone(),
workspace_root.clone(),
Some(workdir_tool_broker),
registry.clone(),
-4
View File
@@ -919,7 +919,6 @@ enabled = true
WorkerFilesystemAuthority::None,
WorkerBootstrapLayout::Direct {
runtime_base: temp.path().join("runtime"),
bash_output_dir: temp.path().join("bash-output"),
},
WorkerControllerTransport::InProcess,
)
@@ -1113,7 +1112,6 @@ enabled = true
WorkerFilesystemAuthority::None,
WorkerBootstrapLayout::Direct {
runtime_base: temp.path().join("runtime"),
bash_output_dir: temp.path().join("bash"),
},
WorkerControllerTransport::InProcess,
)
@@ -1279,7 +1277,6 @@ enabled = true
WorkerFilesystemAuthority::None,
WorkerBootstrapLayout::Direct {
runtime_base: temp.path().join("runtime"),
bash_output_dir: temp.path().join("bash"),
},
WorkerControllerTransport::InProcess,
)
@@ -1339,7 +1336,6 @@ enabled = true
WorkerFilesystemAuthority::None,
WorkerBootstrapLayout::Direct {
runtime_base: temp.path().join("runtime"),
bash_output_dir: temp.path().join("bash-output"),
},
WorkerControllerTransport::InProcess,
)
-2
View File
@@ -17214,12 +17214,10 @@ permission = "write"
let layout = if runtime_managed {
crate::WorkerBootstrapLayout::RuntimeManagedRun {
run_dir: temp.path().join("runtime/runs/1"),
bash_output_dir: temp.path().join("bash"),
}
} else {
crate::WorkerBootstrapLayout::Direct {
runtime_base: temp.path().join("runtime"),
bash_output_dir: temp.path().join("bash"),
}
};
crate::WorkerBootstrap::new(
+10 -17
View File
@@ -1495,11 +1495,9 @@ async fn post_compact_pause_bills_partial_without_rearming_or_consuming_correlat
let session_id = worker.session_id();
let runtime_tmp = tempfile::tempdir().unwrap();
let bash_output_dir = runtime_tmp.path().join("bash-output");
let (handle, shutdown_receiver) =
WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir)
.await
.unwrap();
let (handle, shutdown_receiver) = WorkerController::spawn(worker, runtime_tmp.path())
.await
.unwrap();
let mut rx = handle.subscribe();
handle
.send(Method::submit_text(
@@ -1705,11 +1703,9 @@ async fn controller_does_not_repeat_a_persisted_mid_run_compaction_failure() {
let (worker, store) = make_worker_with_manifest_and_store(MID_TURN_MANIFEST_TOML, client).await;
let session_id = worker.session_id();
let runtime_tmp = tempfile::tempdir().unwrap();
let bash_output_dir = runtime_tmp.path().join("bash-output");
let (handle, shutdown_receiver) =
WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir)
.await
.unwrap();
let (handle, shutdown_receiver) = WorkerController::spawn(worker, runtime_tmp.path())
.await
.unwrap();
let mut rx = handle.subscribe();
handle
@@ -1830,11 +1826,9 @@ async fn manual_compact_cancel_clears_progress_before_returning_idle() {
let source_session = worker.session_id();
let source_segment = worker.segment_id();
let runtime_tmp = tempfile::tempdir().unwrap();
let bash_output_dir = runtime_tmp.path().join("bash-output");
let (handle, shutdown_receiver) =
WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir)
.await
.unwrap();
let (handle, shutdown_receiver) = WorkerController::spawn(worker, runtime_tmp.path())
.await
.unwrap();
let mut rx = handle.subscribe();
handle
@@ -1996,8 +1990,7 @@ async fn controller_compact_method_publishes_progress_and_clear() {
]);
let worker = make_worker_with_manifest(MANUAL_ONLY_MANIFEST_TOML, client).await;
let runtime_tmp = tempfile::tempdir().unwrap();
let bash_output_dir = runtime_tmp.path().join("bash-output");
let (handle, _shutdown) = WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir)
let (handle, _shutdown) = WorkerController::spawn(worker, runtime_tmp.path())
.await
.unwrap();
let mut rx = handle.subscribe();
+34 -48
View File
@@ -311,8 +311,7 @@ async fn spawn_controller(worker: Worker<MockClient, TestStore>) -> WorkerHandle
let tmp = tempfile::tempdir().unwrap();
let runtime_base = tmp.path().to_owned();
std::mem::forget(tmp);
let bash_output_dir = runtime_base.join("bash-output");
let (handle, _shutdown_rx) = WorkerController::spawn(worker, &runtime_base, &bash_output_dir)
let (handle, _shutdown_rx) = WorkerController::spawn(worker, &runtime_base)
.await
.unwrap();
handle
@@ -342,13 +341,9 @@ async fn canonical_worker_handle_uploads_and_submits_attachment() {
)
.await
.unwrap();
let (handle, shutdown_rx) = WorkerController::spawn(
worker,
&root.path().join("runtime"),
&root.path().join("bash-output"),
)
.await
.unwrap();
let (handle, shutdown_rx) = WorkerController::spawn(worker, &root.path().join("runtime"))
.await
.unwrap();
let mut rx = handle.subscribe();
let file = handle
.upload_file_with_context(
@@ -408,24 +403,20 @@ async fn canonical_worker_handle_uploads_and_submits_attachment() {
}
#[tokio::test]
async fn controller_grants_read_scope_for_exact_bash_output_directory() {
async fn controller_does_not_grant_or_create_a_model_visible_bash_output_directory() {
let worker = make_worker(MockClient::new(simple_text_events())).await;
let shared_scope = worker.scope().clone();
let runtime_base = tempfile::tempdir().unwrap();
let worker_tmp = tempfile::tempdir().unwrap();
let bash_output_dir = worker_tmp.path().join("worker-1").join("bash-output");
let original_rules = shared_scope.snapshot().allow_rules().to_vec();
let (handle, shutdown_rx) =
WorkerController::spawn(worker, runtime_base.path(), &bash_output_dir)
.await
.unwrap();
let (handle, shutdown_rx) = WorkerController::spawn(worker, runtime_base.path())
.await
.unwrap();
assert!(bash_output_dir.is_dir());
assert!(shared_scope.snapshot().allow_rules().iter().any(|rule| {
rule.target == bash_output_dir
&& rule.permission == manifest::Permission::Read
&& rule.recursive
}));
assert_eq!(
shared_scope.snapshot().allow_rules(),
original_rules.as_slice()
);
assert!(!handle.runtime_dir.path().join("bash-output").exists());
handle
@@ -451,9 +442,9 @@ async fn shutdown_closes_bound_workdir_session() {
.start_command(CommandRequest {
command: "sleep 30".to_owned(),
timeout_secs: 60,
output_limit: 1024,
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
cwd: workdir::WorkdirPath::root(),
spill_dir: None,
tool_call_id: None,
})
.await
@@ -461,11 +452,9 @@ async fn shutdown_closes_bound_workdir_session() {
worker.bind_single_workdir_session(Some(Arc::clone(&session)));
let runtime_base = tempfile::tempdir().unwrap();
let bash_output_dir = runtime_base.path().join("bash-output");
let (handle, shutdown_rx) =
WorkerController::spawn(worker, runtime_base.path(), &bash_output_dir)
.await
.unwrap();
let (handle, shutdown_rx) = WorkerController::spawn(worker, runtime_base.path())
.await
.unwrap();
handle
.send(Method::Shutdown {
command: worker_command(&handle),
@@ -501,9 +490,9 @@ async fn controller_filters_workdir_command_not_owned_by_worker() {
.start_command(CommandRequest {
command: "sleep 0.2".to_owned(),
timeout_secs: 5,
output_limit: 1024,
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
cwd: workdir::WorkdirPath::root(),
spill_dir: None,
tool_call_id: Some("foreign-tool-call".into()),
})
.await
@@ -529,7 +518,10 @@ async fn controller_filters_workdir_command_not_owned_by_worker() {
})
.await
.unwrap();
assert_eq!(output.status, workdir::CommandStatus::Completed);
assert_eq!(
output.terminal.unwrap().status,
workdir::CommandStatus::Completed
);
handle
.send(Method::Shutdown {
command: worker_command(&handle),
@@ -738,9 +730,8 @@ async fn controller_startup_failure_closes_bound_workdir_session() {
let invalid_runtime_base = runtime_base.path().join("not-a-directory");
std::fs::write(&invalid_runtime_base, "file").unwrap();
let bash_output_dir = runtime_base.path().join("bash-output");
assert!(
WorkerController::spawn(worker, &invalid_runtime_base, &bash_output_dir)
WorkerController::spawn(worker, &invalid_runtime_base)
.await
.is_err()
);
@@ -749,9 +740,9 @@ async fn controller_startup_failure_closes_bound_workdir_session() {
.start_command(CommandRequest {
command: "printf unreachable".to_owned(),
timeout_secs: 5,
output_limit: 1024,
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
cwd: workdir::WorkdirPath::root(),
spill_dir: None,
tool_call_id: None,
})
.await,
@@ -1553,8 +1544,7 @@ permission = "write"
let client = MockClient::new(simple_text_events());
let worker = make_worker_with_pwd_and_manifest(client, manifest).await.0;
let tmp = tempfile::tempdir().unwrap();
let bash_output_dir = tmp.path().join("bash-output");
let result = WorkerController::spawn(worker, tmp.path(), &bash_output_dir).await;
let result = WorkerController::spawn(worker, tmp.path()).await;
assert!(
result.is_ok(),
"feature exposure must not imply delegation authority"
@@ -2927,11 +2917,9 @@ async fn status_json_reflects_worker_name() {
async fn shutdown_closes_method_admission_before_terminal_confirmation() {
let worker = make_worker(MockClient::new(simple_text_events())).await;
let runtime_base = tempfile::tempdir().unwrap();
let bash_output_dir = runtime_base.path().join("bash-output");
let (handle, mut shutdown_rx) =
WorkerController::spawn(worker, runtime_base.path(), &bash_output_dir)
.await
.unwrap();
let (handle, mut shutdown_rx) = WorkerController::spawn(worker, runtime_base.path())
.await
.unwrap();
handle
.send(Method::Shutdown {
command: worker_command(&handle),
@@ -2966,11 +2954,9 @@ async fn shutdown_joins_socket_server_with_active_connection() {
let worker = make_worker(MockClient::new(simple_text_events())).await;
let runtime_base = tempfile::tempdir().unwrap();
let bash_output_dir = runtime_base.path().join("bash-output");
let (handle, shutdown_rx) =
WorkerController::spawn(worker, runtime_base.path(), &bash_output_dir)
.await
.unwrap();
let (handle, shutdown_rx) = WorkerController::spawn(worker, runtime_base.path())
.await
.unwrap();
let socket_path = handle.runtime_dir.socket_path();
let _connection = UnixStream::connect(&socket_path).await.unwrap();