refactor: separate worker workspace identity
This commit is contained in:
parent
74b07bd787
commit
142b60e1b3
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -5848,6 +5848,7 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.29.0",
|
||||
"toml",
|
||||
"tower",
|
||||
"worker",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -100,8 +100,13 @@ pub struct WorkerMetadata {
|
|||
pub worker_name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub active: Option<WorkerActiveSegmentRef>,
|
||||
/// Legacy local path hint retained for host/runtime compatibility. It is not
|
||||
/// Worker workspace identity or authority.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_root: Option<PathBuf>,
|
||||
/// Path-free workspace identity supplied by the host/runtime boundary.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub spawned_children: Vec<WorkerSpawnedChild>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
|
|
@ -119,6 +124,7 @@ impl WorkerMetadata {
|
|||
worker_name: worker_name.into(),
|
||||
active,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
|
|
@ -130,6 +136,11 @@ impl WorkerMetadata {
|
|||
self.workspace_root = Some(workspace_root);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
|
||||
self.workspace_id = Some(workspace_id.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync persistence backend for Worker metadata.
|
||||
|
|
@ -180,6 +191,44 @@ pub trait WorkerMetadataStore: Send + Sync {
|
|||
}
|
||||
|
||||
/// Set the active pointer and workspace ownership while preserving unrelated fields.
|
||||
fn set_active_with_workspace_context(
|
||||
&self,
|
||||
worker_name: &str,
|
||||
active: Option<WorkerActiveSegmentRef>,
|
||||
resolved_manifest_snapshot: Option<serde_json::Value>,
|
||||
workspace_id: Option<String>,
|
||||
workspace_root: Option<PathBuf>,
|
||||
) -> Result<WorkerMetadata, WorkerStoreError> {
|
||||
self.update_by_name(worker_name, |metadata| {
|
||||
metadata.active = active;
|
||||
metadata.resolved_manifest_snapshot = resolved_manifest_snapshot;
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
metadata.workspace_id = Some(workspace_id);
|
||||
}
|
||||
if let Some(workspace_root) = workspace_root {
|
||||
metadata.workspace_root = Some(workspace_root);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the active pointer and path-free workspace identity while preserving unrelated fields.
|
||||
fn set_active_with_workspace_id(
|
||||
&self,
|
||||
worker_name: &str,
|
||||
active: Option<WorkerActiveSegmentRef>,
|
||||
resolved_manifest_snapshot: Option<serde_json::Value>,
|
||||
workspace_id: Option<String>,
|
||||
) -> Result<WorkerMetadata, WorkerStoreError> {
|
||||
self.update_by_name(worker_name, |metadata| {
|
||||
metadata.active = active;
|
||||
metadata.resolved_manifest_snapshot = resolved_manifest_snapshot;
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
metadata.workspace_id = Some(workspace_id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the active pointer and legacy local workspace-root hint while preserving unrelated fields.
|
||||
fn set_active_with_workspace_root(
|
||||
&self,
|
||||
worker_name: &str,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ reqwest = { version = "0.13", optional = true, default-features = false, feature
|
|||
tar.workspace = true
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net", "rt", "sync", "time"] }
|
||||
toml.workspace = true
|
||||
tower = { workspace = true, features = ["util"], optional = true }
|
||||
worker.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
|
|
@ -35,7 +35,10 @@ use session_store::{CombinedStore, FsWorkerStore};
|
|||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use worker::{Worker, WorkerController, WorkerFilesystemAuthority, WorkerHandle};
|
||||
use worker::{
|
||||
Worker, WorkerController, WorkerFilesystemAuthority, WorkerHandle, WorkerWorkspaceContext,
|
||||
WorkspaceId,
|
||||
};
|
||||
|
||||
const DEFAULT_BACKEND_ID: &str = "worker-crate";
|
||||
const RUNTIME_TASK_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
|
@ -230,6 +233,41 @@ fn sanitize_worker_name_component(value: &str) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum RuntimeWorkspaceBackendRef {
|
||||
None,
|
||||
LocalFilesystem { root: PathBuf },
|
||||
}
|
||||
|
||||
impl RuntimeWorkspaceBackendRef {
|
||||
fn from_working_directory(binding: Option<&WorkingDirectoryBinding>) -> Self {
|
||||
match binding {
|
||||
Some(binding) => Self::LocalFilesystem {
|
||||
root: binding.root().to_path_buf(),
|
||||
},
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn worker_context(&self) -> WorkerWorkspaceContext {
|
||||
match self {
|
||||
Self::None => WorkerWorkspaceContext::no_workspace(),
|
||||
Self::LocalFilesystem { root } => local_workspace_context(root),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn local_workspace_context(root: &Path) -> WorkerWorkspaceContext {
|
||||
WorkerWorkspaceContext::local_filesystem(read_workspace_id_hint(root))
|
||||
}
|
||||
|
||||
fn read_workspace_id_hint(root: &Path) -> Option<WorkspaceId> {
|
||||
let contents = std::fs::read_to_string(root.join(".yoi/workspace.toml")).ok()?;
|
||||
let value = toml::from_str::<toml::Value>(&contents).ok()?;
|
||||
let id = value.get("id")?.as_str()?.to_string();
|
||||
WorkspaceId::new(id).ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "http-server")]
|
||||
async fn fetch_profile_source_archive_http(
|
||||
location: &ProfileSourceArchiveHttpRef,
|
||||
|
|
@ -308,6 +346,9 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
|||
)
|
||||
})
|
||||
.unwrap_or(WorkerFilesystemAuthority::None);
|
||||
let workspace_backend_ref =
|
||||
RuntimeWorkspaceBackendRef::from_working_directory(request.working_directory.as_ref());
|
||||
let workspace_context = workspace_backend_ref.worker_context();
|
||||
let selector = profile.as_deref().unwrap_or("builtin:default");
|
||||
let archive = self
|
||||
.resolve_profile_source_archive(&request.request.profile_source)
|
||||
|
|
@ -344,7 +385,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
|||
manifest,
|
||||
store,
|
||||
loader,
|
||||
worker_root,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
)
|
||||
.await
|
||||
|
|
@ -927,12 +968,16 @@ mod tests {
|
|||
.as_ref()
|
||||
.map(|binding| binding.root().to_path_buf())
|
||||
.unwrap_or_else(|| self.cwd.clone());
|
||||
let workspace_backend_ref = RuntimeWorkspaceBackendRef::from_working_directory(
|
||||
request.working_directory.as_ref(),
|
||||
);
|
||||
let workspace_context = workspace_backend_ref.worker_context();
|
||||
let scope = Scope::writable(&scope_root).map_err(|err| err.to_string())?;
|
||||
let worker = Worker::new(
|
||||
manifest,
|
||||
Engine::new(self.client.clone()),
|
||||
store,
|
||||
self.cwd.clone(),
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
scope,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -522,17 +522,16 @@ fn install_ticket_event_companion_notify_hook<C, St>(
|
|||
return;
|
||||
}
|
||||
|
||||
let Some(companion_worker_name) = companion_worker_name_for_workspace(worker.workspace_root())
|
||||
else {
|
||||
let Some(local) = worker.local_working_directory() else {
|
||||
return;
|
||||
};
|
||||
let Some(companion_worker_name) = companion_worker_name_for_workspace(&local.root) else {
|
||||
return;
|
||||
};
|
||||
if companion_worker_name == worker.manifest().worker.name {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(local) = worker.local_working_directory() else {
|
||||
return;
|
||||
};
|
||||
let Ok(ticket_config) = TicketConfig::load_workspace(&local.cwd) else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -603,7 +602,7 @@ where
|
|||
// below so the worker borrow doesn't conflict with reads on `worker`.
|
||||
let scope_handle = worker.scope().clone();
|
||||
let local_filesystem = worker.local_working_directory().cloned();
|
||||
let workspace_root = worker.workspace_root().to_path_buf();
|
||||
let local_workspace_root = local_filesystem.as_ref().map(|local| local.root.clone());
|
||||
let task_feature = worker.task_feature();
|
||||
let session_id_for_usage = worker.segment_id().to_string();
|
||||
let memory_config = worker.manifest().memory.clone();
|
||||
|
|
@ -654,8 +653,8 @@ where
|
|||
}
|
||||
};
|
||||
// Ticket tools are typed operations over the currently checked-out work
|
||||
// tree. They require explicit local filesystem authority; workspace_root
|
||||
// is context only and must not be used as a cwd fallback.
|
||||
// tree. They require explicit local filesystem authority and must not
|
||||
// use workspace identity as a cwd fallback.
|
||||
let ticket_cwd = local_filesystem
|
||||
.as_ref()
|
||||
.map(|local| &local.cwd)
|
||||
|
|
@ -679,10 +678,12 @@ where
|
|||
) {
|
||||
feature_registry = feature_registry.with_module(module);
|
||||
}
|
||||
if let Some(module) =
|
||||
crate::feature::mcp::discover_stdio_tool_feature(&mcp_config, &workspace_root).await
|
||||
{
|
||||
feature_registry = feature_registry.with_module(module);
|
||||
if let Some(workspace_root) = local_workspace_root.as_ref() {
|
||||
if let Some(module) =
|
||||
crate::feature::mcp::discover_stdio_tool_feature(&mcp_config, workspace_root).await
|
||||
{
|
||||
feature_registry = feature_registry.with_module(module);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -698,7 +699,13 @@ where
|
|||
"[feature.memory].enabled = true requires a [memory] configuration section",
|
||||
)
|
||||
})?;
|
||||
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
|
||||
let workspace_root = local_workspace_root.as_ref().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"memory tools require local Worker filesystem authority",
|
||||
)
|
||||
})?;
|
||||
let layout = memory::WorkspaceLayout::resolve(mem, workspace_root);
|
||||
let query_cfg = memory::tool::QueryConfig::from(mem);
|
||||
worker.register_tool(memory::tool::read_tool_with_usage(
|
||||
layout.clone(),
|
||||
|
|
@ -732,11 +739,17 @@ where
|
|||
"worker spawn tools require local Worker filesystem authority",
|
||||
)
|
||||
})?;
|
||||
let spawner_workspace_root = local_workspace_root.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(),
|
||||
spawner_workspace_root,
|
||||
spawner_cwd.clone(),
|
||||
spawned_registry.clone(),
|
||||
self_parent_socket,
|
||||
|
|
|
|||
|
|
@ -1145,6 +1145,7 @@ mod tests {
|
|||
worker_name: "parent".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: vec![
|
||||
child("child-live", &live_socket),
|
||||
child("child-stale", &stale_socket),
|
||||
|
|
@ -1165,6 +1166,7 @@ mod tests {
|
|||
active_child_segment,
|
||||
)),
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
|
|
@ -1179,6 +1181,7 @@ mod tests {
|
|||
active_child_segment,
|
||||
)),
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
|
|
@ -1190,6 +1193,7 @@ mod tests {
|
|||
worker_name: "child-pending".into(),
|
||||
active: Some(WorkerActiveSegmentRef::pending_segment(pending_session_id)),
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
|
|
@ -1204,6 +1208,7 @@ mod tests {
|
|||
new_segment_id(),
|
||||
)),
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
|
|
@ -1215,6 +1220,7 @@ mod tests {
|
|||
worker_name: "peer".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: vec![session_store::WorkerPeer {
|
||||
|
|
@ -1421,6 +1427,7 @@ mod tests {
|
|||
worker_name: "source".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: vec![session_store::WorkerPeer {
|
||||
|
|
@ -1461,6 +1468,7 @@ mod tests {
|
|||
worker_name: "source".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: vec![session_store::WorkerPeer {
|
||||
|
|
@ -1474,6 +1482,7 @@ mod tests {
|
|||
worker_name: "target".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: vec![session_store::WorkerPeer {
|
||||
|
|
@ -1579,6 +1588,7 @@ mod tests {
|
|||
worker_name: "source".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: vec![session_store::WorkerPeer {
|
||||
|
|
@ -1592,6 +1602,7 @@ mod tests {
|
|||
worker_name: "target".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: vec![session_store::WorkerPeer {
|
||||
|
|
@ -1695,6 +1706,7 @@ mod tests {
|
|||
worker_name: "source".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: vec![child("target", &socket)],
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ use std::ffi::OsString;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
|
||||
use crate::{PromptLoader, Worker, WorkerController, WorkerFilesystemAuthority};
|
||||
use crate::{
|
||||
PromptLoader, Worker, WorkerController, WorkerFilesystemAuthority, WorkerWorkspaceContext,
|
||||
WorkspaceId,
|
||||
};
|
||||
use clap::{CommandFactory, FromArgMatches, Parser};
|
||||
use manifest::{
|
||||
Permission, ProfileResolveOptions, ProfileResolver, ProfileSelector, ScopeConfig, ScopeRule,
|
||||
|
|
@ -103,6 +106,24 @@ fn runtime_workspace_root(cli: &Cli) -> Result<PathBuf, String> {
|
|||
}
|
||||
}
|
||||
|
||||
fn runtime_workspace_context(workspace_root: &Path) -> WorkerWorkspaceContext {
|
||||
WorkerWorkspaceContext::local_filesystem(read_workspace_id_hint(workspace_root))
|
||||
}
|
||||
|
||||
fn read_workspace_id_hint(workspace_root: &Path) -> Option<WorkspaceId> {
|
||||
let path = workspace_root.join(".yoi/workspace.toml");
|
||||
let contents = std::fs::read_to_string(path).ok()?;
|
||||
let value = toml::from_str::<toml::Value>(&contents).ok()?;
|
||||
let id = value.get("id")?.as_str()?.to_string();
|
||||
match WorkspaceId::new(id) {
|
||||
Ok(id) => Some(id),
|
||||
Err(err) => {
|
||||
tracing::warn!("ignoring invalid workspace id in .yoi/workspace.toml: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_worker_name(cli: &Cli, workspace_root: &Path) -> String {
|
||||
cli.worker
|
||||
.as_deref()
|
||||
|
|
@ -513,6 +534,7 @@ 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 workspace_context = runtime_workspace_context(&workspace_root);
|
||||
|
||||
let mut worker = if cli.adopt {
|
||||
let callback = match cli.callback.clone() {
|
||||
|
|
@ -527,7 +549,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
|||
store,
|
||||
loader,
|
||||
callback,
|
||||
workspace_root.clone(),
|
||||
workspace_context.clone(),
|
||||
filesystem_authority.clone(),
|
||||
)
|
||||
.await
|
||||
|
|
@ -558,7 +580,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
|||
manifest,
|
||||
store,
|
||||
loader,
|
||||
workspace_root.clone(),
|
||||
workspace_context.clone(),
|
||||
filesystem_authority.clone(),
|
||||
)
|
||||
.await
|
||||
|
|
@ -578,7 +600,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
|||
manifest,
|
||||
store,
|
||||
loader,
|
||||
workspace_root.clone(),
|
||||
workspace_context.clone(),
|
||||
filesystem_authority.clone(),
|
||||
)
|
||||
.await
|
||||
|
|
@ -599,7 +621,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
|||
manifest,
|
||||
store,
|
||||
loader,
|
||||
workspace_root.clone(),
|
||||
workspace_context.clone(),
|
||||
filesystem_authority.clone(),
|
||||
)
|
||||
.await
|
||||
|
|
@ -621,7 +643,7 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
|||
manifest,
|
||||
store,
|
||||
loader,
|
||||
workspace_root.clone(),
|
||||
workspace_context.clone(),
|
||||
filesystem_authority.clone(),
|
||||
)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -40,5 +40,5 @@ pub use segment_log_sink::SegmentLogSink;
|
|||
pub use shared_state::WorkerSharedState;
|
||||
pub use worker::{
|
||||
LocalWorkingDirectory, Worker, WorkerError, WorkerFilesystemAuthority, WorkerRunResult,
|
||||
apply_worker_manifest,
|
||||
WorkerWorkspaceContext, WorkspaceClient, WorkspaceId, WorkspaceIdError, apply_worker_manifest,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -384,6 +384,7 @@ mod tests {
|
|||
worker_name: "orchestrator".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
|
|
@ -395,6 +396,7 @@ mod tests {
|
|||
worker_name: "companion".into(),
|
||||
active: None,
|
||||
workspace_root: None,
|
||||
workspace_id: None,
|
||||
spawned_children: Vec::new(),
|
||||
reclaimed_children: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
#[cfg(test)]
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
|
@ -92,6 +94,113 @@ pub struct LocalWorkingDirectory {
|
|||
pub cwd: PathBuf,
|
||||
}
|
||||
|
||||
/// Path-free workspace identity carried by a Worker.
|
||||
///
|
||||
/// The value is intentionally opaque to Worker code: Runtime/host layers own
|
||||
/// backend lookup, endpoint/auth/secret materialisation, and any mapping from a
|
||||
/// local checkout path to an id. Worker code may only compare/log the id and pass
|
||||
/// it through to narrow workspace-aware handles.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct WorkspaceId(String);
|
||||
|
||||
impl WorkspaceId {
|
||||
pub fn new(id: impl Into<String>) -> Result<Self, WorkspaceIdError> {
|
||||
let id = id.into();
|
||||
if id.trim().is_empty() {
|
||||
return Err(WorkspaceIdError::Empty);
|
||||
}
|
||||
Ok(Self(id))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
|
||||
pub enum WorkspaceIdError {
|
||||
#[error("workspace id must not be empty")]
|
||||
Empty,
|
||||
}
|
||||
|
||||
/// Narrow path-free workspace API handle injected by Runtime/host code.
|
||||
///
|
||||
/// This is deliberately not a filesystem authority surface. A Worker may have a
|
||||
/// workspace client without local filesystem authority, or neither. Local
|
||||
/// path-backed implementations are represented only as a capability marker here;
|
||||
/// the actual paths remain under [`WorkerFilesystemAuthority::Local`] or in host
|
||||
/// adapter code.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WorkspaceClient {
|
||||
/// Runtime/host supplied a workspace API handle. The string is an opaque
|
||||
/// diagnostic/backend kind, not an endpoint, path, or secret-bearing value.
|
||||
Available { kind: String },
|
||||
/// Workspace-aware operations must fail closed or stay disabled.
|
||||
Unavailable { reason: String },
|
||||
}
|
||||
|
||||
impl WorkspaceClient {
|
||||
pub fn available(kind: impl Into<String>) -> Self {
|
||||
Self::Available { kind: kind.into() }
|
||||
}
|
||||
|
||||
pub fn unavailable(reason: impl Into<String>) -> Self {
|
||||
Self::Unavailable {
|
||||
reason: reason.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_filesystem() -> Self {
|
||||
Self::available("local-filesystem")
|
||||
}
|
||||
|
||||
pub fn is_available(&self) -> bool {
|
||||
matches!(self, Self::Available { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// Workspace context supplied to a Worker separately from filesystem authority.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerWorkspaceContext {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
client: WorkspaceClient,
|
||||
}
|
||||
|
||||
impl WorkerWorkspaceContext {
|
||||
pub fn no_workspace() -> Self {
|
||||
Self {
|
||||
workspace_id: None,
|
||||
client: WorkspaceClient::unavailable("no workspace configured"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unavailable(workspace_id: Option<WorkspaceId>, reason: impl Into<String>) -> Self {
|
||||
Self {
|
||||
workspace_id,
|
||||
client: WorkspaceClient::unavailable(reason),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_client(workspace_id: Option<WorkspaceId>, client: WorkspaceClient) -> Self {
|
||||
Self {
|
||||
workspace_id,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_filesystem(workspace_id: Option<WorkspaceId>) -> Self {
|
||||
Self::with_client(workspace_id, WorkspaceClient::local_filesystem())
|
||||
}
|
||||
|
||||
pub fn workspace_id(&self) -> Option<&WorkspaceId> {
|
||||
self.workspace_id.as_ref()
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &WorkspaceClient {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
/// `(SessionId, SegmentId)` pair the Worker is currently writing to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SegmentLocation {
|
||||
|
|
@ -109,10 +218,11 @@ where
|
|||
let store = store.clone();
|
||||
Arc::new(move |metadata| {
|
||||
store
|
||||
.set_active_with_workspace_root(
|
||||
.set_active_with_workspace_context(
|
||||
&metadata.worker_name,
|
||||
metadata.active,
|
||||
metadata.resolved_manifest_snapshot,
|
||||
metadata.workspace_id,
|
||||
metadata.workspace_root,
|
||||
)
|
||||
.map(|_| ())
|
||||
|
|
@ -287,9 +397,9 @@ pub struct Worker<C: LlmClient, St: Store> {
|
|||
/// 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,
|
||||
/// Path-free workspace identity/client context injected by Runtime/host.
|
||||
/// This never grants local filesystem authority.
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
/// Shared, atomically-swappable view of the Worker's resolved scope.
|
||||
/// Cloned out to `ScopedFs` instances (builtin tools, fs_view,
|
||||
/// compact worker) so scope updates propagate to every consumer
|
||||
|
|
@ -483,7 +593,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Worker<C, St>
|
|||
worker_metadata_writer: None,
|
||||
segment_state: self.segment_state.clone(),
|
||||
filesystem_authority: self.filesystem_authority.clone(),
|
||||
workspace_root: self.workspace_root.clone(),
|
||||
workspace_context: self.workspace_context.clone(),
|
||||
scope: self.scope.clone(),
|
||||
delegation_scope: self.delegation_scope.clone(),
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -643,10 +753,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 pass explicit filesystem authority and build a [`Scope`]
|
||||
/// — typically via [`Scope::from_config`] when coming from a
|
||||
/// manifest, or [`Scope::writable`] in tests. Use
|
||||
/// [`WorkerFilesystemAuthority::None`] for no-workdir Workers.
|
||||
/// Callers must pass path-free workspace context separately from explicit
|
||||
/// filesystem authority and build a [`Scope`] — typically via
|
||||
/// [`Scope::from_config`] when coming from a 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
|
||||
|
|
@ -656,7 +766,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
manifest: WorkerManifest,
|
||||
worker: Engine<C>,
|
||||
store: St,
|
||||
workspace_root: PathBuf,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
scope: Scope,
|
||||
) -> Result<Self, WorkerError> {
|
||||
|
|
@ -675,7 +785,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
worker_metadata_writer: None,
|
||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||
filesystem_authority,
|
||||
workspace_root,
|
||||
workspace_context,
|
||||
scope: SharedScope::new(scope),
|
||||
delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -797,10 +907,16 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
self.filesystem_authority.as_local()
|
||||
}
|
||||
|
||||
/// The Worker's runtime workspace root. This stays separate from `cwd` for
|
||||
/// spawned children whose SpawnWorker `cwd` only changes tool defaults.
|
||||
pub fn workspace_root(&self) -> &Path {
|
||||
&self.workspace_root
|
||||
/// Path-free workspace identity, if Runtime/host associated this Worker
|
||||
/// with a workspace.
|
||||
pub fn workspace_id(&self) -> Option<&WorkspaceId> {
|
||||
self.workspace_context.workspace_id()
|
||||
}
|
||||
|
||||
/// Narrow workspace client/availability handle injected by Runtime/host.
|
||||
/// This never grants local filesystem authority.
|
||||
pub fn workspace_client(&self) -> &WorkspaceClient {
|
||||
self.workspace_context.client()
|
||||
}
|
||||
|
||||
pub(crate) fn worker_metadata_store(&self) -> St
|
||||
|
|
@ -988,7 +1104,14 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
}
|
||||
|
||||
fn worker_metadata(&self, active: Option<WorkerActiveSegmentRef>) -> WorkerMetadata {
|
||||
worker_metadata_for_manifest(&self.manifest, &self.workspace_root, active)
|
||||
worker_metadata_for_manifest(
|
||||
&self.manifest,
|
||||
self.workspace_id(),
|
||||
self.filesystem_authority
|
||||
.as_local()
|
||||
.map(|local| local.root.as_path()),
|
||||
active,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_worker_metadata_pending(&self) -> Result<(), WorkerError> {
|
||||
|
|
@ -1363,10 +1486,15 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
.map(|d| d.name)
|
||||
.collect()
|
||||
};
|
||||
let agents_md_read = read_agents_md(&self.workspace_root);
|
||||
for warning in agents_md_read.warnings {
|
||||
if let Some(n) = alerter.as_ref() {
|
||||
n.alert(AlertLevel::Warn, AlertSource::AgentsMd, warning);
|
||||
let agents_md_read = self
|
||||
.filesystem_authority
|
||||
.as_local()
|
||||
.map(|local| read_agents_md(&local.root));
|
||||
if let Some(read) = agents_md_read.as_ref() {
|
||||
for warning in &read.warnings {
|
||||
if let Some(n) = alerter.as_ref() {
|
||||
n.alert(AlertLevel::Warn, AlertSource::AgentsMd, warning.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Resident-injection collection. Each resident section has its own
|
||||
|
|
@ -1429,7 +1557,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
language: worker_language,
|
||||
scope: &scope_snapshot,
|
||||
tool_names,
|
||||
agents_md: agents_md_read.body,
|
||||
agents_md: agents_md_read.and_then(|read| read.body),
|
||||
resident_summary: resident_summary.as_deref(),
|
||||
resident_knowledge: resident_slice,
|
||||
resident_workflows: resident_workflow_slice,
|
||||
|
|
@ -2963,6 +3091,15 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
total_now.saturating_sub(total_at_pointer)
|
||||
}
|
||||
|
||||
fn local_memory_layout(
|
||||
&self,
|
||||
memory_cfg: &manifest::MemoryConfig,
|
||||
) -> Option<memory::WorkspaceLayout> {
|
||||
self.filesystem_authority
|
||||
.as_local()
|
||||
.map(|local| memory::WorkspaceLayout::resolve(memory_cfg, &local.root))
|
||||
}
|
||||
|
||||
/// extract (memory.extract) post-run trigger.
|
||||
///
|
||||
/// Called by the Controller before spawning the background memory task so
|
||||
|
|
@ -2979,10 +3116,13 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
let Some(memory_cfg) = self.manifest.memory.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(layout) = self.local_memory_layout(&memory_cfg) else {
|
||||
tracing::debug!("workspace memory extract unavailable: no local filesystem authority");
|
||||
return Ok(());
|
||||
};
|
||||
// `Some(0)` means disabled, same as `None`. Otherwise the
|
||||
// `tokens_since >= 0` comparison would fire on every post-run.
|
||||
let Some(threshold) = memory_cfg.extract_threshold.filter(|n| *n > 0) else {
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.extract_model
|
||||
.as_ref()
|
||||
|
|
@ -3012,7 +3152,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_err()
|
||||
{
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.extract_model
|
||||
.as_ref()
|
||||
|
|
@ -3068,7 +3207,10 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
) -> Result<ExtractDecision, WorkerError> {
|
||||
use memory::extract;
|
||||
|
||||
let layout = memory::WorkspaceLayout::resolve(memory_cfg, &self.workspace_root);
|
||||
let Some(layout) = self.local_memory_layout(memory_cfg) else {
|
||||
tracing::debug!("workspace memory extract unavailable: no local filesystem authority");
|
||||
return Ok(ExtractDecision::Skipped);
|
||||
};
|
||||
let model = memory_cfg
|
||||
.extract_model
|
||||
.as_ref()
|
||||
|
|
@ -3383,6 +3525,12 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
let Some(memory_cfg) = self.manifest.memory.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(layout) = self.local_memory_layout(&memory_cfg) else {
|
||||
tracing::debug!(
|
||||
"workspace memory consolidation unavailable: no local filesystem authority"
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
// `Some(0)` collapses to `None` — staging count / bytes always
|
||||
// satisfies `>= 0`, which would fire consolidation on every post-run.
|
||||
// Treating zero as disabled lines up with `extract_threshold` and
|
||||
|
|
@ -3391,7 +3539,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
let files_threshold = memory_cfg.consolidation_threshold_files.filter(|n| *n > 0);
|
||||
let bytes_threshold = memory_cfg.consolidation_threshold_bytes.filter(|n| *n > 0);
|
||||
if files_threshold.is_none() && bytes_threshold.is_none() {
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.consolidation_model
|
||||
.as_ref()
|
||||
|
|
@ -3419,7 +3566,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_err()
|
||||
{
|
||||
let layout = memory::WorkspaceLayout::resolve(&memory_cfg, &self.workspace_root);
|
||||
let model = memory_cfg
|
||||
.consolidation_model
|
||||
.as_ref()
|
||||
|
|
@ -3472,7 +3618,12 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||
) -> Result<ConsolidateDecision, WorkerError> {
|
||||
use memory::consolidate;
|
||||
|
||||
let layout = memory::WorkspaceLayout::resolve(memory_cfg, &self.workspace_root);
|
||||
let Some(layout) = self.local_memory_layout(memory_cfg) else {
|
||||
tracing::debug!(
|
||||
"workspace memory consolidation unavailable: no local filesystem authority"
|
||||
);
|
||||
return Ok(ConsolidateDecision::Skipped);
|
||||
};
|
||||
let model = memory_cfg
|
||||
.consolidation_model
|
||||
.as_ref()
|
||||
|
|
@ -3892,21 +4043,23 @@ where
|
|||
) -> Result<Self, WorkerError> {
|
||||
let cwd = current_cwd()?;
|
||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||
Self::from_manifest_with_context(manifest, store, loader, cwd, authority).await
|
||||
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
||||
Self::from_manifest_with_context(manifest, store, loader, workspace_context, authority)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn from_manifest_with_context(
|
||||
manifest: WorkerManifest,
|
||||
store: St,
|
||||
loader: PromptLoader,
|
||||
workspace_root: PathBuf,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
) -> Result<Self, WorkerError> {
|
||||
let mut common = prepare_worker_common_with_context(
|
||||
&manifest,
|
||||
&loader,
|
||||
/* parse_template */ true,
|
||||
workspace_root,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
manifest.scope.clone(),
|
||||
)?;
|
||||
|
|
@ -3947,7 +4100,7 @@ where
|
|||
worker_metadata_writer,
|
||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||
filesystem_authority: common.filesystem_authority,
|
||||
workspace_root: common.workspace_root,
|
||||
workspace_context: common.workspace_context,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -4007,12 +4160,13 @@ where
|
|||
) -> Result<Self, WorkerError> {
|
||||
let cwd = current_cwd()?;
|
||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
||||
Self::from_manifest_spawned_with_context(
|
||||
manifest,
|
||||
store,
|
||||
loader,
|
||||
callback_socket,
|
||||
cwd,
|
||||
workspace_context,
|
||||
authority,
|
||||
)
|
||||
.await
|
||||
|
|
@ -4023,14 +4177,14 @@ where
|
|||
store: St,
|
||||
loader: PromptLoader,
|
||||
callback_socket: PathBuf,
|
||||
workspace_root: PathBuf,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
) -> Result<Self, WorkerError> {
|
||||
let mut common = prepare_worker_common_with_context(
|
||||
&manifest,
|
||||
&loader,
|
||||
/* parse_template */ true,
|
||||
workspace_root,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
manifest.scope.clone(),
|
||||
)?;
|
||||
|
|
@ -4058,7 +4212,7 @@ where
|
|||
worker_metadata_writer,
|
||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||
filesystem_authority: common.filesystem_authority,
|
||||
workspace_root: common.workspace_root,
|
||||
workspace_context: common.workspace_context,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -4114,12 +4268,13 @@ where
|
|||
) -> Result<Self, WorkerError> {
|
||||
let cwd = current_cwd()?;
|
||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
||||
Self::restore_from_worker_metadata_with_context(
|
||||
worker_name,
|
||||
manifest,
|
||||
store,
|
||||
loader,
|
||||
cwd,
|
||||
workspace_context,
|
||||
authority,
|
||||
)
|
||||
.await
|
||||
|
|
@ -4130,7 +4285,7 @@ where
|
|||
manifest: WorkerManifest,
|
||||
store: St,
|
||||
loader: PromptLoader,
|
||||
workspace_root: PathBuf,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
) -> Result<Self, WorkerError> {
|
||||
let metadata =
|
||||
|
|
@ -4161,7 +4316,7 @@ where
|
|||
manifest,
|
||||
store,
|
||||
loader,
|
||||
workspace_root,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
)
|
||||
.await
|
||||
|
|
@ -4193,8 +4348,15 @@ where
|
|||
) -> Result<Self, WorkerError> {
|
||||
let cwd = current_cwd()?;
|
||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
||||
Self::restore_from_manifest_with_context(
|
||||
session_id, segment_id, manifest, store, loader, cwd, authority,
|
||||
session_id,
|
||||
segment_id,
|
||||
manifest,
|
||||
store,
|
||||
loader,
|
||||
workspace_context,
|
||||
authority,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -4205,7 +4367,7 @@ where
|
|||
manifest: WorkerManifest,
|
||||
store: St,
|
||||
loader: PromptLoader,
|
||||
workspace_root: PathBuf,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
) -> Result<Self, WorkerError> {
|
||||
// Read raw entries once so we can both reconstruct state and
|
||||
|
|
@ -4223,7 +4385,7 @@ where
|
|||
&manifest,
|
||||
&loader,
|
||||
/* parse_template */ false,
|
||||
workspace_root,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
scope_config,
|
||||
)?;
|
||||
|
|
@ -4289,7 +4451,7 @@ where
|
|||
worker_metadata_writer,
|
||||
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
|
||||
filesystem_authority: common.filesystem_authority,
|
||||
workspace_root: common.workspace_root,
|
||||
workspace_context: common.workspace_context,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -4441,11 +4603,17 @@ fn request_config_from_engine_manifest(wm: &manifest::EngineManifest) -> Request
|
|||
|
||||
fn worker_metadata_for_manifest(
|
||||
manifest: &WorkerManifest,
|
||||
workspace_root: &Path,
|
||||
workspace_id: Option<&WorkspaceId>,
|
||||
local_workspace_root: Option<&std::path::Path>,
|
||||
active: Option<WorkerActiveSegmentRef>,
|
||||
) -> WorkerMetadata {
|
||||
let mut metadata = WorkerMetadata::new(manifest.worker.name.clone(), active)
|
||||
.with_workspace_root(workspace_root.to_path_buf());
|
||||
let mut metadata = WorkerMetadata::new(manifest.worker.name.clone(), active);
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
metadata = metadata.with_workspace_id(workspace_id.as_str().to_owned());
|
||||
}
|
||||
if let Some(local_workspace_root) = local_workspace_root {
|
||||
metadata = metadata.with_workspace_root(local_workspace_root.to_path_buf());
|
||||
}
|
||||
if should_persist_resolved_manifest_snapshot(manifest) {
|
||||
metadata.resolved_manifest_snapshot = serde_json::to_value(manifest).ok();
|
||||
}
|
||||
|
|
@ -4875,15 +5043,15 @@ pub enum WorkerError {
|
|||
#[error(transparent)]
|
||||
Scope(ScopeError),
|
||||
|
||||
#[error("workspace root is not readable under the configured scope: {}", .workspace_root.display())]
|
||||
WorkspaceRootOutsideScope { workspace_root: PathBuf },
|
||||
#[error("local filesystem authority root is not readable under the configured scope: {}", .root.display())]
|
||||
LocalFilesystemRootOutsideScope { root: PathBuf },
|
||||
|
||||
#[error("cwd is not readable under the configured scope: {}", .cwd.display())]
|
||||
CwdOutsideScope { cwd: PathBuf },
|
||||
|
||||
#[error("failed to resolve workspace root {}: {source}", .workspace_root.display())]
|
||||
InvalidWorkspaceRoot {
|
||||
workspace_root: PathBuf,
|
||||
#[error("failed to resolve local filesystem authority root {}: {source}", .root.display())]
|
||||
InvalidLocalFilesystemRoot {
|
||||
root: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
|
@ -4974,13 +5142,13 @@ pub enum WorkerError {
|
|||
}
|
||||
|
||||
/// Bundle of resources that every high-level Worker constructor needs:
|
||||
/// cwd, runtime workspace root, scope, an LLM client, the prompt catalog,
|
||||
/// filesystem authority, path-free workspace context, scope, an LLM client, the prompt catalog,
|
||||
/// and (optionally) a parsed system-prompt template. Built once by
|
||||
/// [`prepare_worker_common_with_context`] from the resolved manifest and then split into Worker
|
||||
/// fields.
|
||||
struct WorkerCommon {
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
workspace_root: PathBuf,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
scope: Scope,
|
||||
delegation_scope: DelegationScope,
|
||||
client: Box<dyn LlmClient>,
|
||||
|
|
@ -5067,22 +5235,16 @@ fn prepare_worker_common_with_context(
|
|||
manifest: &WorkerManifest,
|
||||
loader: &PromptLoader,
|
||||
parse_template: bool,
|
||||
workspace_root: PathBuf,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
scope_config: ScopeConfig,
|
||||
) -> Result<WorkerCommon, WorkerError> {
|
||||
let workspace_root = std::fs::canonicalize(&workspace_root).map_err(|source| {
|
||||
WorkerError::InvalidWorkspaceRoot {
|
||||
workspace_root: workspace_root.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(),
|
||||
WorkerError::InvalidLocalFilesystemRoot {
|
||||
root: local.root.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
|
@ -5095,8 +5257,8 @@ fn prepare_worker_common_with_context(
|
|||
}
|
||||
};
|
||||
let mut scope_config = scope_config;
|
||||
if let Some(mem) = manifest.memory.as_ref() {
|
||||
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
|
||||
if let (Some(mem), Some(local)) = (manifest.memory.as_ref(), filesystem_authority.as_local()) {
|
||||
let layout = memory::WorkspaceLayout::resolve(mem, &local.root);
|
||||
scope_config.deny.extend(memory::deny_write_rules(&layout));
|
||||
scope_config
|
||||
.deny
|
||||
|
|
@ -5108,7 +5270,7 @@ fn prepare_worker_common_with_context(
|
|||
manifest,
|
||||
loader,
|
||||
parse_template,
|
||||
workspace_root,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
scope,
|
||||
)
|
||||
|
|
@ -5118,14 +5280,16 @@ fn prepare_worker_common_from_scope(
|
|||
manifest: &WorkerManifest,
|
||||
loader: &PromptLoader,
|
||||
parse_template: bool,
|
||||
workspace_root: PathBuf,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
scope: Scope,
|
||||
) -> Result<WorkerCommon, WorkerError> {
|
||||
if !scope.is_readable(&workspace_root) {
|
||||
return Err(WorkerError::WorkspaceRootOutsideScope { workspace_root });
|
||||
}
|
||||
if let Some(local) = filesystem_authority.as_local() {
|
||||
if !scope.is_readable(&local.root) {
|
||||
return Err(WorkerError::LocalFilesystemRootOutsideScope {
|
||||
root: local.root.clone(),
|
||||
});
|
||||
}
|
||||
if !scope.is_readable(&local.cwd) {
|
||||
return Err(WorkerError::CwdOutsideScope {
|
||||
cwd: local.cwd.clone(),
|
||||
|
|
@ -5137,10 +5301,11 @@ fn prepare_worker_common_from_scope(
|
|||
|
||||
let client = provider::build_client(&manifest.model)?;
|
||||
let prompts = PromptCatalog::load(loader, manifest.worker.prompt_pack.as_deref())?;
|
||||
let memory_layout = manifest
|
||||
.memory
|
||||
.as_ref()
|
||||
.map(|mem| memory::WorkspaceLayout::resolve(mem, &workspace_root));
|
||||
let memory_layout = manifest.memory.as_ref().and_then(|mem| {
|
||||
filesystem_authority
|
||||
.as_local()
|
||||
.map(|local| memory::WorkspaceLayout::resolve(mem, &local.root))
|
||||
});
|
||||
let mut workflow_registry = match memory_layout.as_ref() {
|
||||
Some(layout) => {
|
||||
workflow_crate::load_workflows(layout).map_err(WorkerError::WorkflowLoad)?
|
||||
|
|
@ -5160,7 +5325,7 @@ fn prepare_worker_common_from_scope(
|
|||
|
||||
Ok(WorkerCommon {
|
||||
filesystem_authority,
|
||||
workspace_root,
|
||||
workspace_context,
|
||||
scope,
|
||||
delegation_scope,
|
||||
client,
|
||||
|
|
@ -5249,7 +5414,7 @@ mod spawned_context_tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn spawn_worker_context_keeps_workspace_root_separate_from_tool_pwd() {
|
||||
fn spawn_worker_context_separates_workspace_identity_from_tool_pwd() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let workspace_root = tmp.path().join("workspace-root");
|
||||
let cwd = tmp.path().join("child-worktree");
|
||||
|
|
@ -5262,14 +5427,21 @@ mod spawned_context_tests {
|
|||
&manifest,
|
||||
&PromptLoader::builtins_only(),
|
||||
false,
|
||||
workspace_root.clone(),
|
||||
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
||||
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
||||
manifest.scope.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
common.workspace_root,
|
||||
common
|
||||
.workspace_context
|
||||
.workspace_id()
|
||||
.map(WorkspaceId::as_str),
|
||||
Some("ws-test")
|
||||
);
|
||||
assert_eq!(
|
||||
common.filesystem_authority.as_local().unwrap().root,
|
||||
workspace_root.canonicalize().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
@ -5283,7 +5455,42 @@ mod spawned_context_tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_context_reports_workspace_root_when_workspace_root_is_unreadable() {
|
||||
fn workspace_identity_and_client_do_not_grant_filesystem_authority() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let workspace_root = tmp.path().join("workspace-root");
|
||||
let cwd = workspace_root.join("nested");
|
||||
std::fs::create_dir_all(&cwd).unwrap();
|
||||
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
||||
manifest.memory = Some(manifest::MemoryConfig::default());
|
||||
let loader = PromptLoader::new(None, Some(workspace_root.clone()));
|
||||
let workspace_id = WorkspaceId::new("ws-api-only").unwrap();
|
||||
let common = prepare_worker_common_with_context(
|
||||
&manifest,
|
||||
&loader,
|
||||
false,
|
||||
WorkerWorkspaceContext::with_client(
|
||||
Some(workspace_id.clone()),
|
||||
WorkspaceClient::available("test-api"),
|
||||
),
|
||||
WorkerFilesystemAuthority::None,
|
||||
manifest.scope.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(common.filesystem_authority, WorkerFilesystemAuthority::None);
|
||||
assert_eq!(
|
||||
common
|
||||
.workspace_context
|
||||
.workspace_id()
|
||||
.map(WorkspaceId::as_str),
|
||||
Some(workspace_id.as_str())
|
||||
);
|
||||
assert!(common.workspace_context.client().is_available());
|
||||
assert!(common.memory_layout.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_context_reports_local_filesystem_root_when_unreadable() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let workspace_root = tmp.path().join("workspace-root");
|
||||
let cwd = tmp.path().join("child-worktree");
|
||||
|
|
@ -5295,7 +5502,7 @@ mod spawned_context_tests {
|
|||
&manifest,
|
||||
&PromptLoader::builtins_only(),
|
||||
false,
|
||||
workspace_root.clone(),
|
||||
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
||||
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
||||
ScopeConfig {
|
||||
allow: vec![ScopeRule {
|
||||
|
|
@ -5306,17 +5513,15 @@ mod spawned_context_tests {
|
|||
deny: Vec::new(),
|
||||
},
|
||||
) {
|
||||
Ok(_) => panic!("expected workspace-root scope error"),
|
||||
Ok(_) => panic!("expected local filesystem root scope error"),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
match err {
|
||||
WorkerError::WorkspaceRootOutsideScope {
|
||||
workspace_root: got,
|
||||
} => {
|
||||
WorkerError::LocalFilesystemRootOutsideScope { root: got } => {
|
||||
assert_eq!(got, workspace_root.canonicalize().unwrap());
|
||||
}
|
||||
other => panic!("expected workspace-root scope error, got {other:?}"),
|
||||
other => panic!("expected local filesystem root scope error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5333,7 +5538,7 @@ mod spawned_context_tests {
|
|||
&manifest,
|
||||
&PromptLoader::builtins_only(),
|
||||
false,
|
||||
workspace_root.clone(),
|
||||
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
||||
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
||||
ScopeConfig {
|
||||
allow: vec![ScopeRule {
|
||||
|
|
@ -5390,23 +5595,15 @@ mod worker_metadata_restore_manifest_tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn metadata_writer_persists_workspace_root_through_store_update() {
|
||||
fn metadata_writer_persists_workspace_id_through_store_update() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let store = session_store::FsWorkerStore::new(temp.path().join("workers")).unwrap();
|
||||
let workspace_root = temp.path().join("workspace-root");
|
||||
std::fs::create_dir_all(&workspace_root).unwrap();
|
||||
let writer = worker_metadata_writer_for_store(&store);
|
||||
|
||||
writer(
|
||||
WorkerMetadata::new("runtime-worker", None).with_workspace_root(workspace_root.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
writer(WorkerMetadata::new("runtime-worker", None).with_workspace_id("ws-test")).unwrap();
|
||||
|
||||
let stored = store.read_by_name("runtime-worker").unwrap().unwrap();
|
||||
assert_eq!(
|
||||
stored.workspace_root.as_deref(),
|
||||
Some(workspace_root.as_path())
|
||||
);
|
||||
assert_eq!(stored.workspace_id.as_deref(), Some("ws-test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -5503,7 +5700,7 @@ permission = "read"
|
|||
.unwrap();
|
||||
assert!(manifest.profile.is_none());
|
||||
assert!(
|
||||
worker_metadata_for_manifest(&manifest, Path::new("/snapshot/workspace"), None)
|
||||
worker_metadata_for_manifest(&manifest, None, None, None)
|
||||
.resolved_manifest_snapshot
|
||||
.is_none()
|
||||
);
|
||||
|
|
@ -5540,8 +5737,7 @@ permission = "read"
|
|||
config: None,
|
||||
}];
|
||||
|
||||
let metadata =
|
||||
worker_metadata_for_manifest(&manifest, Path::new("/snapshot/workspace"), None);
|
||||
let metadata = worker_metadata_for_manifest(&manifest, None, None, None);
|
||||
let snapshot = metadata
|
||||
.resolved_manifest_snapshot
|
||||
.expect("plugin-resolved manifest should be snapshotted");
|
||||
|
|
@ -5800,7 +5996,7 @@ mod build_summary_prompt_tests {
|
|||
manifest,
|
||||
Engine::new(NoopClient),
|
||||
store,
|
||||
cwd.clone(),
|
||||
WorkerWorkspaceContext::local_filesystem(None),
|
||||
authority,
|
||||
scope,
|
||||
)
|
||||
|
|
@ -5956,7 +6152,7 @@ mod build_summary_prompt_tests {
|
|||
manifest,
|
||||
Engine::new(NoopClient),
|
||||
store,
|
||||
cwd.clone(),
|
||||
WorkerWorkspaceContext::local_filesystem(None),
|
||||
authority,
|
||||
scope,
|
||||
)
|
||||
|
|
@ -6091,7 +6287,7 @@ mod build_summary_prompt_tests {
|
|||
manifest,
|
||||
Engine::new(NoopClient),
|
||||
store,
|
||||
cwd.clone(),
|
||||
WorkerWorkspaceContext::local_filesystem(None),
|
||||
authority,
|
||||
scope,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ async fn make_worker_with_manifest(
|
|||
manifest,
|
||||
worker,
|
||||
store,
|
||||
pwd.clone(),
|
||||
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||
scope,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ async fn make_worker_with(
|
|||
manifest,
|
||||
worker,
|
||||
store,
|
||||
pwd.clone(),
|
||||
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||
scope,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use session_store::{FsStore, LogEntry};
|
|||
|
||||
use worker::{
|
||||
Event, Method, Worker, WorkerController, WorkerFilesystemAuthority, WorkerHandle,
|
||||
WorkerManifest, WorkerStatus,
|
||||
WorkerManifest, WorkerStatus, WorkerWorkspaceContext,
|
||||
};
|
||||
|
||||
type TestStore = CombinedStore<FsStore, FsWorkerStore>;
|
||||
|
|
@ -190,9 +190,16 @@ async fn make_worker_with_pwd_and_manifest(
|
|||
|
||||
let worker = Engine::new(client);
|
||||
let authority = WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone());
|
||||
let worker = Worker::new(manifest, worker, store, pwd.clone(), authority, scope)
|
||||
.await
|
||||
.unwrap();
|
||||
let worker = Worker::new(
|
||||
manifest,
|
||||
worker,
|
||||
store,
|
||||
WorkerWorkspaceContext::local_filesystem(None),
|
||||
authority,
|
||||
scope,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
(worker, pwd)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ async fn make_worker(
|
|||
manifest,
|
||||
worker,
|
||||
store,
|
||||
pwd.clone(),
|
||||
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||
scope,
|
||||
)
|
||||
|
|
@ -464,7 +464,7 @@ async fn metric_write_failure_emits_warn_alert_and_does_not_abort_run() {
|
|||
manifest,
|
||||
worker,
|
||||
store.clone(),
|
||||
pwd.clone(),
|
||||
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||
scope,
|
||||
)
|
||||
|
|
@ -540,7 +540,7 @@ permission = "write"
|
|||
manifest,
|
||||
worker,
|
||||
store.clone(),
|
||||
pwd.clone(),
|
||||
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||
scope,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ async fn make_worker_with_body(
|
|||
manifest,
|
||||
worker,
|
||||
store,
|
||||
pwd.clone(),
|
||||
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||
scope,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
|
|||
filter = sourceFilter;
|
||||
};
|
||||
|
||||
cargoHash = "sha256-vM2ea6W3hqnmzqOXfzQo+ZYrQBoy2Kh69vz1fkU+DIs=";
|
||||
cargoHash = "sha256-av+Iix50MMLcrzxw8UDTT0tUqBFIPkSN1i23WYegNcM=";
|
||||
|
||||
depsExtraArgs = {
|
||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user