feat: add explicit worker filesystem authority

This commit is contained in:
2026-07-11 07:30:17 +09:00
parent db3a7165f7
commit b50b94612a
13 changed files with 422 additions and 155 deletions
+62 -31
View File
@@ -274,7 +274,9 @@ impl WorkerController {
manifest_toml.clone(),
greeting,
));
shared_state.set_fs_view(crate::fs_view::WorkerFsView::new(fs_for_view));
if let Some(fs_for_view) = fs_for_view {
shared_state.set_fs_view(crate::fs_view::WorkerFsView::new(fs_for_view));
}
shared_state.set_workflows(
worker
.workflow_completions()
@@ -528,7 +530,10 @@ fn install_ticket_event_companion_notify_hook<C, St>(
return;
}
let Ok(ticket_config) = TicketConfig::load_workspace(worker.cwd()) else {
let Some(local) = worker.local_working_directory() else {
return;
};
let Ok(ticket_config) = TicketConfig::load_workspace(&local.cwd) else {
return;
};
let backend_root = ticket_config.backend_root().to_path_buf();
@@ -540,7 +545,7 @@ fn install_ticket_event_companion_notify_hook<C, St>(
worker.worker_metadata_store(),
worker.manifest().worker.name.clone(),
runtime_base,
worker.cwd().to_path_buf(),
Some(local.cwd.clone()),
spawned_registry,
);
match discovery.ensure_existing_peer(&companion_worker_name) {
@@ -589,7 +594,7 @@ async fn register_worker_tools<C, St>(
spawner_socket: PathBuf,
runtime_base: PathBuf,
spawned_registry: Arc<SpawnedWorkerRegistry>,
) -> std::io::Result<tools::ScopedFs>
) -> std::io::Result<Option<tools::ScopedFs>>
where
C: LlmClient + Clone + 'static,
St: Store + WorkerMetadataStore + Clone + 'static,
@@ -597,7 +602,7 @@ where
// Worker-immutable snapshots taken before the mutable worker borrow
// below so the worker borrow doesn't conflict with reads on `worker`.
let scope_handle = worker.scope().clone();
let cwd = worker.cwd().to_path_buf();
let local_filesystem = worker.local_working_directory().cloned();
let workspace_root = worker.workspace_root().to_path_buf();
let task_feature = worker.task_feature();
let session_id_for_usage = worker.segment_id().to_string();
@@ -611,24 +616,24 @@ where
let worker_metadata_store = worker.store().clone();
let self_parent_socket = worker.callback_socket().cloned();
// The Worker's SharedScope (already augmented with the bash-output
// Read rule by the caller) is the single source of truth — every
// ScopedFs (builtin tools, fs_view, compact worker) reads from it,
// and any future scope mutation (SpawnWorker-style revoke, future
// GrantScope) propagates through it.
let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), cwd.clone());
let tracker = tools::Tracker::new();
// Same ScopedFs also powers the IPC `ListCompletions` query — keep
// a clone for the FS view we attach below, since the tools consume
// `fs` itself.
let fs_for_view = fs.clone();
worker
.engine_mut()
.register_tools(tools::core_builtin_tools(
fs,
tracker.clone(),
bash_output_dir,
));
// The Worker's SharedScope is the single source of truth for every
// ScopedFs when local filesystem authority exists. No-workdir Workers
// deliberately skip constructing/registering filesystem and Bash tools.
let (fs_for_view, tracker) = if let Some(local) = local_filesystem.as_ref() {
let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), local.cwd.clone());
let tracker = tools::Tracker::new();
let fs_for_view = fs.clone();
worker
.engine_mut()
.register_tools(tools::core_builtin_tools(
fs,
tracker.clone(),
bash_output_dir,
));
(Some(fs_for_view), Some(tracker))
} else {
(None, None)
};
if feature_config.web.enabled {
worker
.engine_mut()
@@ -649,11 +654,20 @@ where
}
};
// Ticket tools are typed operations over the currently checked-out work
// tree. Use the Worker cwd rather than the runtime workspace root so a
// dedicated Orchestrator worktree gets its own `.yoi/tickets` backend.
// tree. They require explicit local filesystem authority; workspace_root
// is context only and must not be used as a cwd fallback.
let ticket_cwd = local_filesystem
.as_ref()
.map(|local| &local.cwd)
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"ticket tools require local Worker filesystem authority",
)
})?;
feature_registry.add_module(
crate::feature::builtin::ticket::ticket_tools_feature_with_options(
&cwd,
ticket_cwd,
feature_config.ticket.enabled.then_some(ticket_access),
feature_config.ticket_orchestration.enabled,
),
@@ -709,12 +723,21 @@ where
"[feature.workers].enabled = true requires non-empty [[delegation_scope.allow]]",
));
}
let spawner_cwd = local_filesystem
.as_ref()
.map(|local| local.cwd.clone())
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"worker spawn tools require local Worker filesystem authority",
)
})?;
worker.register_tool(spawn_worker_tool(
spawner_name.clone(),
spawner_socket,
runtime_base.clone(),
workspace_root.clone(),
cwd.clone(),
spawner_cwd.clone(),
spawned_registry.clone(),
self_parent_socket,
spawner_manifest,
@@ -728,7 +751,7 @@ where
worker_metadata_store,
spawner_name,
runtime_base,
cwd,
Some(spawner_cwd),
spawned_registry,
);
worker.register_tool(list_workers_tool(discovery.clone()));
@@ -737,7 +760,9 @@ where
}
}
let _feature_install_report = worker.install_features(feature_registry);
worker.attach_tracker(tracker);
if let Some(tracker) = tracker {
worker.attach_tracker(tracker);
}
Ok(fs_for_view)
}
@@ -774,11 +799,14 @@ async fn controller_loop<C, St>(
.parent()
.map(PathBuf::from)
.unwrap_or_else(|| runtime_dir.path().to_path_buf());
let discovery_cwd = worker
.local_working_directory()
.map(|local| local.cwd.clone());
let discovery = WorkerDiscovery::new(
worker.store().clone(),
spawner_name.clone(),
discovery_runtime_base,
worker.cwd().to_path_buf(),
discovery_cwd,
spawned_registry.clone(),
);
let mut pending: Option<PendingRun> = None;
@@ -1445,7 +1473,10 @@ where
.collect();
protocol::Greeting {
worker_name: manifest.worker.name.clone(),
cwd: worker.cwd().display().to_string(),
cwd: worker
.local_working_directory()
.map(|local| local.cwd.display().to_string())
.unwrap_or_default(),
provider: provider_name,
model: model_id,
scope_summary: worker.scope_snapshot().summary(),
+16 -10
View File
@@ -42,7 +42,7 @@ pub struct WorkerDiscovery<St> {
store: St,
self_worker_name: String,
runtime_base: PathBuf,
cwd: PathBuf,
cwd: Option<PathBuf>,
store_dir: Option<PathBuf>,
spawned_registry: Arc<SpawnedWorkerRegistry>,
}
@@ -55,7 +55,7 @@ where
store: St,
self_worker_name: String,
runtime_base: PathBuf,
cwd: PathBuf,
cwd: Option<PathBuf>,
spawned_registry: Arc<SpawnedWorkerRegistry>,
) -> Self {
let store_dir = store.root_dir();
@@ -432,13 +432,19 @@ where
) -> Result<(), WorkerDiscoveryError> {
let runtime_command =
WorkerRuntimeCommand::resolve().map_err(WorkerDiscoveryError::RestoreSpawn)?;
let Some(cwd) = &self.cwd else {
return Err(WorkerDiscoveryError::NotRestorable {
worker_name: worker_name.to_string(),
reason: "restore requires local Worker filesystem authority".into(),
});
};
let mut command = Command::new(runtime_command.program());
command
.args(runtime_command.prefix_args())
.arg("--worker")
.arg(worker_name)
.arg("--require-worker-state")
.current_dir(&self.cwd)
.current_dir(cwd)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
@@ -1228,7 +1234,7 @@ mod tests {
store.clone(),
"parent".into(),
runtime_base.clone(),
root.path().to_path_buf(),
Some(root.path().to_path_buf()),
registry,
);
@@ -1355,7 +1361,7 @@ mod tests {
store.clone(),
"source".into(),
runtime_base.clone(),
root.path().to_path_buf(),
Some(root.path().to_path_buf()),
SpawnedWorkerRegistry::new(runtime_dir),
);
let result = discovery.register_peer("target").unwrap();
@@ -1390,7 +1396,7 @@ mod tests {
store,
"source".into(),
runtime_base,
root.path().to_path_buf(),
Some(root.path().to_path_buf()),
SpawnedWorkerRegistry::new(runtime_dir),
);
@@ -1430,7 +1436,7 @@ mod tests {
store.clone(),
"source".into(),
runtime_base,
root.path().to_path_buf(),
Some(root.path().to_path_buf()),
SpawnedWorkerRegistry::new(runtime_dir),
);
@@ -1481,7 +1487,7 @@ mod tests {
store,
"source".into(),
runtime_base.clone(),
root.path().to_path_buf(),
Some(root.path().to_path_buf()),
SpawnedWorkerRegistry::new(runtime_dir),
);
@@ -1599,7 +1605,7 @@ mod tests {
store,
"source".into(),
runtime_base.clone(),
root.path().to_path_buf(),
Some(root.path().to_path_buf()),
SpawnedWorkerRegistry::new(runtime_dir),
);
@@ -1701,7 +1707,7 @@ mod tests {
store,
"source".into(),
runtime_base,
root.path().to_path_buf(),
Some(root.path().to_path_buf()),
SpawnedWorkerRegistry::new(runtime_dir),
);
+8 -6
View File
@@ -2,7 +2,7 @@ use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use crate::{PromptLoader, Worker, WorkerController};
use crate::{PromptLoader, Worker, WorkerController, WorkerFilesystemAuthority};
use clap::{CommandFactory, FromArgMatches, Parser};
use manifest::{
Permission, ProfileResolveOptions, ProfileResolver, ProfileSelector, ScopeConfig, ScopeRule,
@@ -511,6 +511,8 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
}
};
let store = CombinedStore::new(session_store, worker_metadata_store);
let filesystem_authority =
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone());
let mut worker = if cli.adopt {
let callback = match cli.callback.clone() {
@@ -526,7 +528,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
loader,
callback,
workspace_root.clone(),
cwd.clone(),
filesystem_authority.clone(),
)
.await
{
@@ -557,7 +559,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
store,
loader,
workspace_root.clone(),
cwd.clone(),
filesystem_authority.clone(),
)
.await
{
@@ -577,7 +579,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
store,
loader,
workspace_root.clone(),
cwd.clone(),
filesystem_authority.clone(),
)
.await
{
@@ -598,7 +600,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
store,
loader,
workspace_root.clone(),
cwd.clone(),
filesystem_authority.clone(),
)
.await
{
@@ -620,7 +622,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
store,
loader,
workspace_root.clone(),
cwd.clone(),
filesystem_authority.clone(),
)
.await
{
+4 -1
View File
@@ -38,4 +38,7 @@ pub use provider::{ProviderError, build_client};
pub use runtime::dir::RuntimeDir;
pub use segment_log_sink::SegmentLogSink;
pub use shared_state::WorkerSharedState;
pub use worker::{Worker, WorkerError, WorkerRunResult, apply_worker_manifest};
pub use worker::{
LocalWorkingDirectory, Worker, WorkerError, WorkerFilesystemAuthority, WorkerRunResult,
apply_worker_manifest,
};
+8 -6
View File
@@ -13,7 +13,9 @@
//! `set_system_prompt`. Subsequent turns and compactions reuse that
//! materialised string verbatim.
use std::borrow::Cow;
use std::collections::BTreeMap;
#[cfg(test)]
use std::path::Path;
use std::sync::Arc;
@@ -146,7 +148,7 @@ impl std::fmt::Debug for SystemPromptTemplate {
/// templates cannot drop them on the floor.
pub struct SystemPromptContext<'a> {
pub now: DateTime<Utc>,
pub cwd: &'a Path,
pub cwd: Cow<'a, str>,
/// Language policy exposed to instruction templates as `{{ language }}`.
pub language: &'a str,
pub scope: &'a Scope,
@@ -189,7 +191,7 @@ impl<'a> SystemPromptContext<'a> {
"datetime".into(),
Value::from(self.now.to_rfc3339_opts(SecondsFormat::Secs, true)),
);
root.insert("cwd".into(), Value::from(self.cwd.display().to_string()));
root.insert("cwd".into(), Value::from(self.cwd.as_ref()));
root.insert("language".into(), Value::from(self.language));
root.insert(
"tools".into(),
@@ -442,7 +444,7 @@ mod tests {
) -> SystemPromptContext<'a> {
SystemPromptContext {
now: fixed_now(),
cwd,
cwd: cwd.display().to_string().into(),
language: manifest::defaults::WORKER_LANGUAGE,
scope,
tool_names: tools,
@@ -461,7 +463,7 @@ mod tests {
) -> SystemPromptContext<'a> {
SystemPromptContext {
now: fixed_now(),
cwd,
cwd: cwd.display().to_string().into(),
language: manifest::defaults::WORKER_LANGUAGE,
scope,
tool_names: Vec::new(),
@@ -480,7 +482,7 @@ mod tests {
) -> SystemPromptContext<'a> {
SystemPromptContext {
now: fixed_now(),
cwd,
cwd: cwd.display().to_string().into(),
language: manifest::defaults::WORKER_LANGUAGE,
scope,
tool_names: Vec::new(),
@@ -499,7 +501,7 @@ mod tests {
) -> SystemPromptContext<'a> {
SystemPromptContext {
now: fixed_now(),
cwd,
cwd: cwd.display().to_string().into(),
language: manifest::defaults::WORKER_LANGUAGE,
scope,
tool_names: Vec::new(),
+1 -1
View File
@@ -414,7 +414,7 @@ mod tests {
store,
"orchestrator".into(),
runtime_base.clone(),
root.path().to_path_buf(),
Some(root.path().to_path_buf()),
SpawnedWorkerRegistry::new(runtime_dir),
),
"companion",
+183 -67
View File
@@ -57,6 +57,41 @@ use tokio::task::JoinHandle;
const RESTORE_RECONCILIATION_REACHABILITY_TIMEOUT: Duration = Duration::from_millis(500);
/// Explicit filesystem authority held by a Worker.
///
/// `None` means the Worker has no local filesystem authority: no cwd, no
/// filesystem view, and no filesystem/Bash tool surface. Workspace context may
/// still exist separately for memory, workflows, and project records.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerFilesystemAuthority {
None,
Local(LocalWorkingDirectory),
}
impl WorkerFilesystemAuthority {
pub fn local(root: PathBuf, cwd: PathBuf) -> Self {
Self::Local(LocalWorkingDirectory { root, cwd })
}
pub fn as_local(&self) -> Option<&LocalWorkingDirectory> {
match self {
Self::None => None,
Self::Local(local) => Some(local),
}
}
}
/// Local filesystem authority for a Worker.
///
/// `root` is the authority root retained for control-plane semantics;
/// `cwd` is the default working directory used by filesystem tools, Bash,
/// file references, and local worktree-scoped features.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalWorkingDirectory {
pub root: PathBuf,
pub cwd: PathBuf,
}
/// `(SessionId, SegmentId)` pair the Worker is currently writing to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SegmentLocation {
@@ -249,8 +284,9 @@ pub struct Worker<C: LlmClient, St: Store> {
/// `segment_id` and append tally. `self.segment_id()` is a thin
/// wrapper over `segment_state.segment_id()`.
segment_state: Arc<SegmentState>,
/// Absolute tool/process working directory of the Worker.
cwd: PathBuf,
/// Explicit local filesystem authority, or `None` for Workers with no
/// local cwd and no filesystem/Bash tool surface.
filesystem_authority: WorkerFilesystemAuthority,
/// Absolute runtime workspace root used for project records, workflow,
/// memory, Ticket config, Profile context, and spawned-child inheritance.
workspace_root: PathBuf,
@@ -446,7 +482,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Worker<C, St>
store: self.store.clone(),
worker_metadata_writer: None,
segment_state: self.segment_state.clone(),
cwd: self.cwd.clone(),
filesystem_authority: self.filesystem_authority.clone(),
workspace_root: self.workspace_root.clone(),
scope: self.scope.clone(),
delegation_scope: self.delegation_scope.clone(),
@@ -607,9 +643,10 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Worker<C, St>
impl<C: LlmClient, St: Store> Worker<C, St> {
/// Create a new Worker from a pre-built Engine and store.
///
/// Callers must pre-resolve `cwd` (absolute) and build a [`Scope`]
/// Callers must pass explicit filesystem authority and build a [`Scope`]
/// — typically via [`Scope::from_config`] when coming from a
/// manifest, or [`Scope::writable`] in tests.
/// manifest, or [`Scope::writable`] in tests. Use
/// [`WorkerFilesystemAuthority::None`] for no-workdir Workers.
///
/// Note: this constructor does **not** parse `manifest.worker.system_prompt`
/// as a template. `Worker::from_manifest` is the production path for
@@ -619,7 +656,8 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
manifest: WorkerManifest,
worker: Engine<C>,
store: St,
cwd: PathBuf,
workspace_root: PathBuf,
filesystem_authority: WorkerFilesystemAuthority,
scope: Scope,
) -> Result<Self, WorkerError> {
// Segment creation is deferred to `ensure_segment_head` at first
@@ -636,8 +674,8 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
store,
worker_metadata_writer: None,
segment_state: SegmentState::new(session_id, segment_id, 0),
workspace_root: cwd.clone(),
cwd,
filesystem_authority,
workspace_root,
scope: SharedScope::new(scope),
delegation_scope,
hook_builder: HookRegistryBuilder::new(),
@@ -749,9 +787,14 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
self.runtime_ticket_role = role;
}
/// The Worker's tool/process working directory.
pub fn cwd(&self) -> &Path {
&self.cwd
/// Explicit filesystem authority held by this Worker.
pub fn filesystem_authority(&self) -> &WorkerFilesystemAuthority {
&self.filesystem_authority
}
/// Local working directory when this Worker has local filesystem authority.
pub fn local_working_directory(&self) -> Option<&LocalWorkingDirectory> {
self.filesystem_authority.as_local()
}
/// The Worker's runtime workspace root. This stays separate from `cwd` for
@@ -1376,9 +1419,13 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
self.resident_exposure_snapshots(&resident, &resident_workflows);
let worker_language = worker_language(&self.manifest.engine);
let scope_snapshot = self.scope.snapshot();
let cwd_for_prompt = self
.local_working_directory()
.map(|local| local.cwd.display().to_string())
.unwrap_or_else(|| "no local working directory".to_string());
let ctx = SystemPromptContext {
now: chrono::Utc::now(),
cwd: &self.cwd,
cwd: cwd_for_prompt.into(),
language: worker_language,
scope: &scope_snapshot,
tool_names,
@@ -1622,9 +1669,21 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
/// unresolved placeholder stays in the flattened user message so the LLM
/// still sees the intent.
fn resolve_file_refs(&self, segments: &[Segment]) -> Vec<SystemItem> {
let Some(local) = self.local_working_directory() else {
for seg in segments {
if let Segment::FileRef { path } = seg {
self.alert(
AlertLevel::Warn,
AlertSource::Worker,
format!("file ref @{path} could not be resolved: Worker has no local filesystem authority"),
);
}
}
return Vec::new();
};
let view = crate::fs_view::WorkerFsView::new(tools::ScopedFs::with_shared_scope(
self.scope.clone(),
self.cwd.clone(),
local.cwd.clone(),
));
let mut out = Vec::new();
for seg in segments {
@@ -2549,11 +2608,13 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
auto_read_budget,
)));
// Build an independent compact worker. Scope and cwd are shared
// with the main Worker (reads go through the same policy) but the
// Tracker is fresh — compact-time reads must not pollute the
// main session's recency list, which feeds `default_refs` above.
let scoped_fs = tools::ScopedFs::with_shared_scope(self.scope.clone(), self.cwd.clone());
// Build an independent compact worker. When the main Worker has local
// filesystem authority, compact-time reads go through the same scope
// and cwd policy. No-workdir Workers deliberately omit compact-time
// filesystem tools as well.
let scoped_fs = self
.local_working_directory()
.map(|local| tools::ScopedFs::with_shared_scope(self.scope.clone(), local.cwd.clone()));
let summary_tracker = tools::Tracker::new();
let summary_client: Box<dyn LlmClient> = self.build_compactor_client()?;
let summary_system_prompt = self
@@ -2591,10 +2652,12 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
// Tools: read_file (shared scope, fresh tracker), bounded session
// history exploration, and compact-specific tools that populate `ctx`.
let compact_target_items = Arc::new(items_to_summarise.clone());
summary_worker.register_tool(tools::read_tool(scoped_fs.clone(), summary_tracker));
if let Some(scoped_fs) = scoped_fs.clone() {
summary_worker.register_tool(tools::read_tool(scoped_fs.clone(), summary_tracker));
summary_worker.register_tool(mark_read_required_tool(scoped_fs, ctx.clone()));
}
summary_worker.register_tool(search_session_log_tool(compact_target_items.clone()));
summary_worker.register_tool(read_session_items_tool(compact_target_items));
summary_worker.register_tool(mark_read_required_tool(scoped_fs.clone(), ctx.clone()));
summary_worker.register_tool(add_reference_tool(ctx.clone()));
summary_worker.register_tool(write_summary_tool(ctx.clone()));
@@ -2671,8 +2734,12 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
// logged and skipped inside `render_auto_read` rather than
// aborting compaction — a missing / moved file should not fail
// the whole compact.
let auto_read_messages =
WorkerFsView::new(scoped_fs.clone()).render_auto_read(&final_ctx.read_required);
let auto_read_messages = scoped_fs
.clone()
.map(|scoped_fs| {
WorkerFsView::new(scoped_fs).render_auto_read(&final_ctx.read_required)
})
.unwrap_or_default();
// Reference list as a single system message; omitted when empty.
let reference_message = (!final_ctx.references.is_empty()).then(|| {
@@ -3824,7 +3891,8 @@ where
loader: PromptLoader,
) -> Result<Self, WorkerError> {
let cwd = current_cwd()?;
Self::from_manifest_with_context(manifest, store, loader, cwd.clone(), cwd).await
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
Self::from_manifest_with_context(manifest, store, loader, cwd, authority).await
}
pub async fn from_manifest_with_context(
@@ -3832,14 +3900,14 @@ where
store: St,
loader: PromptLoader,
workspace_root: PathBuf,
cwd: PathBuf,
filesystem_authority: WorkerFilesystemAuthority,
) -> Result<Self, WorkerError> {
let mut common = prepare_worker_common_with_context(
&manifest,
&loader,
/* parse_template */ true,
workspace_root,
cwd,
filesystem_authority,
manifest.scope.clone(),
)?;
let skill_shadows = std::mem::take(&mut common.skill_shadows);
@@ -3878,7 +3946,7 @@ where
store,
worker_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, 0),
cwd: common.cwd,
filesystem_authority: common.filesystem_authority,
workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope,
@@ -3938,13 +4006,14 @@ where
callback_socket: PathBuf,
) -> Result<Self, WorkerError> {
let cwd = current_cwd()?;
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
Self::from_manifest_spawned_with_context(
manifest,
store,
loader,
callback_socket,
cwd.clone(),
cwd,
authority,
)
.await
}
@@ -3955,14 +4024,14 @@ where
loader: PromptLoader,
callback_socket: PathBuf,
workspace_root: PathBuf,
cwd: PathBuf,
filesystem_authority: WorkerFilesystemAuthority,
) -> Result<Self, WorkerError> {
let mut common = prepare_worker_common_with_context(
&manifest,
&loader,
/* parse_template */ true,
workspace_root,
cwd,
filesystem_authority,
manifest.scope.clone(),
)?;
let skill_shadows = std::mem::take(&mut common.skill_shadows);
@@ -3988,7 +4057,7 @@ where
store,
worker_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, 0),
cwd: common.cwd,
filesystem_authority: common.filesystem_authority,
workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope,
@@ -4044,13 +4113,14 @@ where
loader: PromptLoader,
) -> Result<Self, WorkerError> {
let cwd = current_cwd()?;
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
Self::restore_from_worker_metadata_with_context(
worker_name,
manifest,
store,
loader,
cwd.clone(),
cwd,
authority,
)
.await
}
@@ -4061,7 +4131,7 @@ where
store: St,
loader: PromptLoader,
workspace_root: PathBuf,
cwd: PathBuf,
filesystem_authority: WorkerFilesystemAuthority,
) -> Result<Self, WorkerError> {
let metadata =
store
@@ -4092,7 +4162,7 @@ where
store,
loader,
workspace_root,
cwd,
filesystem_authority,
)
.await
}
@@ -4122,14 +4192,9 @@ where
loader: PromptLoader,
) -> Result<Self, WorkerError> {
let cwd = current_cwd()?;
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
Self::restore_from_manifest_with_context(
session_id,
segment_id,
manifest,
store,
loader,
cwd.clone(),
cwd,
session_id, segment_id, manifest, store, loader, cwd, authority,
)
.await
}
@@ -4141,7 +4206,7 @@ where
store: St,
loader: PromptLoader,
workspace_root: PathBuf,
cwd: PathBuf,
filesystem_authority: WorkerFilesystemAuthority,
) -> Result<Self, WorkerError> {
// Read raw entries once so we can both reconstruct state and
// seed the broadcast sink's mirror with the same prefix that
@@ -4159,7 +4224,7 @@ where
&loader,
/* parse_template */ false,
workspace_root,
cwd,
filesystem_authority,
scope_config,
)?;
let skill_shadows = std::mem::take(&mut common.skill_shadows);
@@ -4223,7 +4288,7 @@ where
store,
worker_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
cwd: common.cwd,
filesystem_authority: common.filesystem_authority,
workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope,
@@ -4914,7 +4979,7 @@ pub enum WorkerError {
/// [`prepare_worker_common_with_context`] from the resolved manifest and then split into Worker
/// fields.
struct WorkerCommon {
cwd: PathBuf,
filesystem_authority: WorkerFilesystemAuthority,
workspace_root: PathBuf,
scope: Scope,
delegation_scope: DelegationScope,
@@ -5003,7 +5068,7 @@ fn prepare_worker_common_with_context(
loader: &PromptLoader,
parse_template: bool,
workspace_root: PathBuf,
cwd: PathBuf,
filesystem_authority: WorkerFilesystemAuthority,
scope_config: ScopeConfig,
) -> Result<WorkerCommon, WorkerError> {
let workspace_root = std::fs::canonicalize(&workspace_root).map_err(|source| {
@@ -5012,10 +5077,23 @@ fn prepare_worker_common_with_context(
source,
}
})?;
let cwd = std::fs::canonicalize(&cwd).map_err(|source| WorkerError::InvalidCwd {
cwd: cwd.clone(),
source,
})?;
let filesystem_authority = match filesystem_authority {
WorkerFilesystemAuthority::None => WorkerFilesystemAuthority::None,
WorkerFilesystemAuthority::Local(local) => {
let root = std::fs::canonicalize(&local.root).map_err(|source| {
WorkerError::InvalidWorkspaceRoot {
workspace_root: local.root.clone(),
source,
}
})?;
let cwd =
std::fs::canonicalize(&local.cwd).map_err(|source| WorkerError::InvalidCwd {
cwd: local.cwd.clone(),
source,
})?;
WorkerFilesystemAuthority::Local(LocalWorkingDirectory { root, cwd })
}
};
let mut scope_config = scope_config;
if let Some(mem) = manifest.memory.as_ref() {
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
@@ -5026,7 +5104,14 @@ fn prepare_worker_common_with_context(
}
scope_config.allow.extend(skill_dir_read_rules(manifest));
let scope = Scope::from_config(&scope_config).map_err(WorkerError::Scope)?;
prepare_worker_common_from_scope(manifest, loader, parse_template, workspace_root, cwd, scope)
prepare_worker_common_from_scope(
manifest,
loader,
parse_template,
workspace_root,
filesystem_authority,
scope,
)
}
fn prepare_worker_common_from_scope(
@@ -5034,14 +5119,18 @@ fn prepare_worker_common_from_scope(
loader: &PromptLoader,
parse_template: bool,
workspace_root: PathBuf,
cwd: PathBuf,
filesystem_authority: WorkerFilesystemAuthority,
scope: Scope,
) -> Result<WorkerCommon, WorkerError> {
if !scope.is_readable(&workspace_root) {
return Err(WorkerError::WorkspaceRootOutsideScope { workspace_root });
}
if !scope.is_readable(&cwd) {
return Err(WorkerError::CwdOutsideScope { cwd });
if let Some(local) = filesystem_authority.as_local() {
if !scope.is_readable(&local.cwd) {
return Err(WorkerError::CwdOutsideScope {
cwd: local.cwd.clone(),
});
}
}
let delegation_scope =
DelegationScope::from_config(&manifest.delegation_scope).map_err(WorkerError::Scope)?;
@@ -5070,7 +5159,7 @@ fn prepare_worker_common_from_scope(
};
Ok(WorkerCommon {
cwd,
filesystem_authority,
workspace_root,
scope,
delegation_scope,
@@ -5174,7 +5263,7 @@ mod spawned_context_tests {
&PromptLoader::builtins_only(),
false,
workspace_root.clone(),
cwd.clone(),
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
manifest.scope.clone(),
)
.unwrap();
@@ -5183,7 +5272,10 @@ mod spawned_context_tests {
common.workspace_root,
workspace_root.canonicalize().unwrap()
);
assert_eq!(common.cwd, cwd.canonicalize().unwrap());
assert_eq!(
common.filesystem_authority.as_local().unwrap().cwd,
cwd.canonicalize().unwrap()
);
assert_eq!(
common.memory_layout.as_ref().unwrap().root(),
workspace_root.canonicalize().unwrap()
@@ -5204,7 +5296,7 @@ mod spawned_context_tests {
&PromptLoader::builtins_only(),
false,
workspace_root.clone(),
cwd.clone(),
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
ScopeConfig {
allow: vec![ScopeRule {
target: cwd.clone(),
@@ -5242,7 +5334,7 @@ mod spawned_context_tests {
&PromptLoader::builtins_only(),
false,
workspace_root.clone(),
cwd.clone(),
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
ScopeConfig {
allow: vec![ScopeRule {
target: workspace_root.clone(),
@@ -5703,9 +5795,17 @@ mod build_summary_prompt_tests {
let cwd = dir.path().join("workspace");
std::fs::create_dir_all(&cwd).unwrap();
let scope = Scope::writable(&cwd).unwrap();
let mut worker = Worker::new(manifest, Engine::new(NoopClient), store, cwd, scope)
.await
.unwrap();
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
let mut worker = Worker::new(
manifest,
Engine::new(NoopClient),
store,
cwd.clone(),
authority,
scope,
)
.await
.unwrap();
worker.ensure_segment_head().unwrap();
(dir, worker)
}
@@ -5851,9 +5951,17 @@ mod build_summary_prompt_tests {
let cwd = dir.path().join("workspace");
std::fs::create_dir_all(&cwd).unwrap();
let scope = Scope::writable(&cwd).unwrap();
let mut worker = Worker::new(manifest, Engine::new(NoopClient), store, cwd, scope)
.await
.unwrap();
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
let mut worker = Worker::new(
manifest,
Engine::new(NoopClient),
store,
cwd.clone(),
authority,
scope,
)
.await
.unwrap();
worker.ensure_segment_head().unwrap();
worker.wire_history_persistence();
@@ -5978,9 +6086,17 @@ mod build_summary_prompt_tests {
let mut manifest = minimal_manifest_with_skills(vec![]);
manifest.memory = memory_config;
let scope = Scope::writable(&cwd).unwrap();
let mut worker = Worker::new(manifest, Engine::new(NoopClient), store, cwd.clone(), scope)
.await
.unwrap();
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
let mut worker = Worker::new(
manifest,
Engine::new(NoopClient),
store,
cwd.clone(),
authority,
scope,
)
.await
.unwrap();
worker.memory_layout = worker
.manifest
.memory