feat: route worker workspace access through backend authority

This commit is contained in:
2026-07-21 11:21:13 +09:00
parent 2572dde691
commit 1251edae04
10 changed files with 459 additions and 1206 deletions
+18 -126
View File
@@ -6,10 +6,7 @@ use llm_engine::EngineError;
use llm_engine::llm_client::client::LlmClient;
use session_store::WorkerMetadataStore;
use session_store::{LogEntry, Store};
use ticket::LocalTicketBackend;
use ticket::config::TicketConfig;
use tokio::sync::{broadcast, mpsc, oneshot};
use tracing::{debug, warn};
use crate::discovery::{
WorkerDiscovery, list_workers_tool, restore_worker_tool, send_to_peer_worker_tool,
@@ -29,9 +26,6 @@ use crate::shutdown_after_idle::{
use crate::spawn::comm_tools::{read_worker_output_tool, send_to_worker_tool, stop_worker_tool};
use crate::spawn::registry::SpawnedWorkerRegistry;
use crate::spawn::tool::spawn_worker_tool;
use crate::ticket_event_notify::{
TicketEventCompanionNotifyHook, companion_worker_name_for_workspace,
};
use crate::worker::{SystemItemCommitter, Worker, WorkerError, WorkerRunResult, WorkspaceClient};
use protocol::{
AlertLevel, AlertSource, ErrorCode, Event, Method, RewindTargetId, RunResult, Segment,
@@ -288,12 +282,6 @@ impl WorkerController {
)
.await?;
install_ticket_event_companion_notify_hook(
&mut worker,
runtime_base.to_path_buf(),
spawned_registry.clone(),
);
// Intake role Workers self-terminate only after a successful
// TicketIntakeReady turn has fully settled back to Idle. The request
// is transient controller state, not model-visible context or ticket
@@ -533,84 +521,6 @@ fn wire_event_bridges_on_engine<C, St>(
// per-item commit channel is wired at the top of this function.
}
fn install_ticket_event_companion_notify_hook<C, St>(
worker: &mut Worker<C, St>,
runtime_base: PathBuf,
spawned_registry: Arc<SpawnedWorkerRegistry>,
) where
C: LlmClient + Clone + 'static,
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
{
if !is_ticket_orchestrator_role(worker.runtime_ticket_role()) {
return;
}
let ticket_feature = &worker.manifest().feature.ticket;
if !ticket_feature.enabled || !ticket_feature.orchestration_control {
return;
}
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 Ok(ticket_config) = TicketConfig::load_workspace(&local.cwd) else {
return;
};
let backend_root = ticket_config.backend_root().to_path_buf();
if !backend_root.is_dir() {
return;
}
let discovery = WorkerDiscovery::new(
worker.worker_metadata_store(),
worker.manifest().worker.name.clone(),
runtime_base,
Some(local.cwd.clone()),
spawned_registry,
);
match discovery.ensure_existing_peer(&companion_worker_name) {
Ok(Some(_)) => {
debug!(
companion = %companion_worker_name,
orchestrator = %worker.manifest().worker.name,
"ensured Companion peer relationship for Orchestrator Ticket event notifications"
);
}
Ok(None) => {
debug!(
companion = %companion_worker_name,
orchestrator = %worker.manifest().worker.name,
"Companion metadata is missing; Ticket event notifications will skip until Companion exists"
);
}
Err(error) => {
warn!(
companion = %companion_worker_name,
orchestrator = %worker.manifest().worker.name,
error = %error,
"failed to ensure Companion peer relationship for Orchestrator Ticket event notifications"
);
}
}
worker.add_post_tool_call_hook(TicketEventCompanionNotifyHook::new(
LocalTicketBackend::new(backend_root),
discovery,
companion_worker_name,
));
}
fn is_ticket_orchestrator_role(role: Option<&str>) -> bool {
role.map(|role| role.eq_ignore_ascii_case("orchestrator"))
.unwrap_or(false)
}
/// Register the builtin file-manipulation tools, optional memory tools,
/// and the Worker-orchestration tools (SpawnWorker + comm) on the Worker's
/// Engine. Returns the `ScopedFs` clone used to attach a `WorkerFsView` to
@@ -632,7 +542,6 @@ where
let local_filesystem = worker.local_working_directory().cloned();
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();
let web_config = worker.manifest().web.clone();
let mcp_config = worker.manifest().mcp.clone();
@@ -679,8 +588,8 @@ where
orchestration_control: feature_config.ticket.orchestration_control,
};
// Ticket tools are typed operations over the current workspace Ticket backend.
// Runtime-hosted Workers prefer the workspace API URI carried by the
// Worker context; legacy/local Workers fall back to the checked-out worktree.
// Workspace access must be authority-bound to the Backend Workspace API; the
// Worker must not fall back to a local `.yoi/tickets` store.
let ticket_backend = match worker.workspace_client() {
WorkspaceClient::Http {
workspace_id,
@@ -690,18 +599,10 @@ where
base_url: base_url.clone(),
},
_ => {
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",
)
})?;
crate::feature::builtin::ticket::TicketFeatureBackend::LocalWorkspace {
workspace_root: ticket_cwd.clone(),
}
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"ticket tools require Backend Workspace API authority",
));
}
};
feature_registry.add_module(
@@ -729,27 +630,18 @@ where
let workspace_client = worker.workspace_client().clone();
let worker = worker.engine_mut();
// Memory tools require explicit feature exposure. Storage access may be
// provided by local filesystem authority or the path-free Workspace HTTP API.
// Memory tools require explicit feature exposure. Workspace memory access
// is authority-bound to the Backend Workspace API; the Worker must not
// register local filesystem memory tools even when it has local cwd/root
// authority for shell/file tools.
if feature_config.memory.enabled {
if let Some(workspace_root) = local_workspace_root.as_ref() {
let mem = memory_config.as_ref().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"[feature.memory].enabled = true requires a [memory] configuration section",
)
})?;
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(),
session_id_for_usage,
));
worker.register_tool(memory::tool::write_tool(layout.clone()));
worker.register_tool(memory::tool::edit_tool(layout.clone()));
worker.register_tool(memory::tool::delete_tool(layout.clone()));
worker.register_tool(memory::tool::memory_query_tool(layout, query_cfg));
} else if let WorkspaceClient::Http {
let _mem = memory_config.as_ref().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"[feature.memory].enabled = true requires a [memory] configuration section",
)
})?;
if let WorkspaceClient::Http {
workspace_id,
base_url,
} = workspace_client
@@ -763,7 +655,7 @@ where
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"memory tools require Workspace HTTP API or local Worker filesystem authority",
"memory tools require Backend Workspace API authority",
));
}
}