Compare commits

..

No commits in common. "0b64eab148edbafb43187173455cda33ca4c570a" and "acc7281414f4ba5a383fbf3f4b92ea2630cf6342" have entirely different histories.

32 changed files with 751 additions and 1360 deletions

View File

@ -21,7 +21,7 @@ pub use backend_auth::{
poll_device_login, start_device_login, wait_for_device_login,
};
pub use backend_runtime::{
BackendDiagnostic, BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeListResponse,
BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeListResponse,
BackendRuntimeListTarget, BackendRuntimeSummary, BackendRuntimeTarget,
BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary,
BackendWorkerRestoreResponse, BackendWorkerRestoreResult, BackendWorkerSummary,

View File

@ -54,22 +54,11 @@ impl BackendTarget {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerListRequest {
pub runtime_id: Option<String>,
pub include_stopped: bool,
}
impl WorkerListRequest {
pub fn new(runtime_id: Option<String>) -> Self {
Self {
runtime_id,
include_stopped: false,
}
}
pub fn with_stopped(runtime_id: Option<String>) -> Self {
Self {
runtime_id,
include_stopped: true,
}
Self { runtime_id }
}
}
@ -110,9 +99,7 @@ pub struct Dashboard {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerList {
pub local_runtime_command: Option<WorkerRuntimeCommand>,
pub backend_target: Option<BackendRuntimeListTarget>,
pub include_stopped: bool,
pub target: BackendRuntimeListTarget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -195,18 +182,11 @@ impl Target for LocalTarget {
})
}
fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError> {
if request.runtime_id.is_some() {
return Err(TargetError::unsupported(
"Explicit runtime id for local worker listing",
fn list_workers(&self, _request: WorkerListRequest) -> Result<WorkerList, TargetError> {
Err(TargetError::unsupported(
"Backend runtime worker listing",
self.kind(),
));
}
Ok(WorkerList {
local_runtime_command: Some(self.runtime_command()?),
backend_target: None,
include_stopped: request.include_stopped,
})
))
}
fn connect_worker(
@ -246,13 +226,11 @@ impl Target for BackendTarget {
fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError> {
Ok(WorkerList {
local_runtime_command: None,
backend_target: Some(BackendRuntimeListTarget::new(
target: BackendRuntimeListTarget::new(
self.base_url.clone(),
self.workspace_id.clone(),
request.runtime_id,
)),
include_stopped: request.include_stopped,
),
})
}
@ -281,28 +259,9 @@ mod tests {
.list_workers(WorkerListRequest::new(Some("runtime-a".to_string())))
.unwrap();
assert_eq!(
workers.backend_target.as_ref().unwrap().base_url,
"http://127.0.0.1:8787"
);
assert_eq!(
workers
.backend_target
.as_ref()
.unwrap()
.workspace_id
.as_deref(),
Some("workspace-a")
);
assert_eq!(
workers
.backend_target
.as_ref()
.unwrap()
.runtime_id
.as_deref(),
Some("runtime-a")
);
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
assert_eq!(workers.target.workspace_id.as_deref(), Some("workspace-a"));
assert_eq!(workers.target.runtime_id.as_deref(), Some("runtime-a"));
}
#[test]
@ -329,14 +288,15 @@ mod tests {
}
#[test]
fn local_target_builds_local_worker_list() {
fn local_target_rejects_backend_worker_operations() {
let target = LocalTarget::new();
let workers = target
.list_workers(WorkerListRequest::with_stopped(None))
.unwrap();
let err = target
.list_workers(WorkerListRequest::new(None))
.unwrap_err();
assert!(workers.local_runtime_command.is_some());
assert!(workers.backend_target.is_none());
assert!(workers.include_stopped);
assert_eq!(
err.to_string(),
"Backend runtime worker listing is not supported by local target"
);
}
}

View File

@ -3539,7 +3539,10 @@ mod completion_flow_tests {
arguments: args.into(),
});
}
assert!(app.task_store.tasks().is_empty());
assert_eq!(
app.task_store.tasks()[0].status,
crate::task::TaskStatus::Completed
);
}
#[test]

View File

@ -3,8 +3,7 @@ use std::io;
use std::time::Duration;
use client::{
BackendRuntimeListTarget, BackendRuntimeTarget, BackendWorkerSummary,
list_backend_stopped_workers, list_backend_workers, restore_backend_worker,
BackendRuntimeListTarget, BackendRuntimeTarget, BackendWorkerSummary, list_backend_workers,
};
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
use ratatui::backend::CrosstermBackend;
@ -19,30 +18,13 @@ use crate::console;
const MAX_ROWS: usize = 10;
const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
pub(crate) async fn run(
target: BackendRuntimeListTarget,
include_stopped: bool,
) -> Result<(), Box<dyn Error>> {
let mut response = list_backend_workers(&target).await.map_err(|error| {
pub(crate) async fn run(target: BackendRuntimeListTarget) -> Result<(), Box<dyn Error>> {
let response = list_backend_workers(&target).await.map_err(|error| {
io::Error::other(format!(
"failed to list Backend runtime workers from {}: {error}",
target.base_url
))
})?;
if include_stopped {
match list_backend_stopped_workers(&target).await {
Ok(stopped) => {
response.items.extend(stopped.items);
response.diagnostics.extend(stopped.diagnostics);
}
Err(error) => response.diagnostics.push(client::BackendDiagnostic {
code: "backend_stopped_workers_list_failed".to_string(),
severity: Some("error".to_string()),
message: error.to_string(),
}),
}
}
dedup_workers(&mut response.items);
if response.items.is_empty() {
let diagnostics = response
.diagnostics
@ -62,36 +44,11 @@ pub(crate) async fn run(
}
let selected = pick_worker(target.clone(), response.items)?;
let worker = if selected.state == "stopped" {
let restore_target = BackendRuntimeTarget::new(
target.base_url.clone(),
selected.runtime_id.clone(),
selected.worker_id.clone(),
);
restore_backend_worker(&restore_target)
.await
.map_err(|error| {
io::Error::other(format!(
"failed to restore Backend worker {}/{}: {error}",
selected.runtime_id, selected.worker_id
))
})?
.result
.worker
.unwrap_or(selected)
} else {
selected
};
let attach_target =
BackendRuntimeTarget::new(target.base_url, worker.runtime_id, worker.worker_id);
BackendRuntimeTarget::new(target.base_url, selected.runtime_id, selected.worker_id);
console::run_backend_runtime(attach_target).await
}
fn dedup_workers(workers: &mut Vec<BackendWorkerSummary>) {
let mut seen = std::collections::HashSet::new();
workers.retain(|worker| seen.insert((worker.runtime_id.clone(), worker.worker_id.clone())));
}
fn pick_worker(
target: BackendRuntimeListTarget,
mut workers: Vec<BackendWorkerSummary>,

View File

@ -331,15 +331,6 @@ pub(crate) async fn run_resume(
runtime_command: WorkerRuntimeCommand,
workspace_root: PathBuf,
all: bool,
) -> Result<(), Box<dyn std::error::Error>> {
run_worker_picker(runtime_command, workspace_root, all, true).await
}
pub(crate) async fn run_worker_picker(
runtime_command: WorkerRuntimeCommand,
workspace_root: PathBuf,
all: bool,
include_stopped: bool,
) -> Result<(), Box<dyn std::error::Error>> {
// Pick a Worker in its own inline viewport, dropping the viewport before
// attaching/restoring so each phase gets fresh vertical room.
@ -347,8 +338,7 @@ pub(crate) async fn run_worker_picker(
picker::PickerOptions::all()
} else {
picker::PickerOptions::workspace(workspace_root)
}
.with_stopped(include_stopped);
};
let (worker_name, socket_override) = match picker::run(picker_options).await? {
PickerOutcome::Picked {
worker_name,

View File

@ -118,11 +118,8 @@ pub(crate) enum DashboardOutcome {
Open(OpenWorkerRequest),
}
pub(crate) async fn launch(
runtime_command: WorkerRuntimeCommand,
include_stopped: bool,
) -> Result<(), Box<dyn Error>> {
let mut app = load_app(runtime_command.clone(), include_stopped).await?;
pub(crate) async fn launch(runtime_command: WorkerRuntimeCommand) -> Result<(), Box<dyn Error>> {
let mut app = load_app(runtime_command.clone()).await?;
let mut terminal = crate::console::enter_dashboard_fullscreen()?;
loop {
match run_loop(&mut terminal, &mut app).await? {
@ -177,9 +174,8 @@ pub(crate) struct OpenWorkerRequest {
pub(crate) async fn load_app(
runtime_command: WorkerRuntimeCommand,
include_stopped: bool,
) -> Result<DashboardApp, DashboardError> {
Ok(DashboardApp::loading(runtime_command, include_stopped))
Ok(DashboardApp::loading(runtime_command))
}
async fn run_loop(
@ -226,14 +222,14 @@ async fn run_loop(
}
if let Some(mode) = deferred_enter_reload.take() {
if pending_reload.start(mode, app.include_stopped) {
if pending_reload.start(mode) {
app.refreshing = true;
}
}
let now = Instant::now();
if now >= next_poll {
pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped);
pending_reload.start(OrchestratorLifecycleMode::Observe);
next_poll = now + DASHBOARD_POLL_INTERVAL;
continue;
}
@ -279,8 +275,7 @@ async fn run_loop(
terminal.draw(|f| render::draw(f, app))?;
let result = dispatch_ticket_action(request).await;
app.finish_ticket_action_dispatch(result);
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
{
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
app.refreshing = true;
}
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
@ -316,8 +311,7 @@ async fn run_loop(
}
Err(error) => app.finish_ready_ticket_planning_return_error(error),
}
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
{
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
app.refreshing = true;
}
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
@ -334,8 +328,7 @@ async fn run_loop(
terminal.draw(|f| render::draw(f, app))?;
let result = launch_intake_with_handoff(request).await;
app.finish_intake_launch(result);
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
{
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
app.refreshing = true;
}
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
@ -352,8 +345,7 @@ async fn run_loop(
terminal.draw(|f| render::draw(f, app))?;
let result = dispatch_companion_message(request).await;
app.finish_companion_send(result);
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
{
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
app.refreshing = true;
}
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
@ -374,7 +366,7 @@ struct PendingReload {
}
impl PendingReload {
fn start(&mut self, lifecycle_mode: OrchestratorLifecycleMode, include_stopped: bool) -> bool {
fn start(&mut self, lifecycle_mode: OrchestratorLifecycleMode) -> bool {
if self.handle.is_some() {
return false;
}
@ -390,7 +382,7 @@ impl PendingReload {
self.handle = Some(tokio::spawn(async move {
#[cfg(feature = "e2e-test")]
crate::e2e_observer::hold_background_task_if_requested("reload").await;
load_dashboard_snapshot(None, lifecycle_mode, include_stopped).await
load_dashboard_snapshot(None, lifecycle_mode).await
}));
true
}
@ -1227,7 +1219,6 @@ pub(crate) struct DashboardApp {
refreshing: bool,
enter_reload: Option<OrchestratorLifecycleMode>,
runtime_command: WorkerRuntimeCommand,
include_stopped: bool,
last_companion_lifecycle_failure: Option<CompanionPanelState>,
last_orchestrator_lifecycle_failure: Option<OrchestratorPanelState>,
orchestrator_work_set: OrchestratorWorkSet,
@ -1237,7 +1228,7 @@ pub(crate) struct DashboardApp {
}
impl DashboardApp {
fn loading(runtime_command: WorkerRuntimeCommand, include_stopped: bool) -> Self {
fn loading(runtime_command: WorkerRuntimeCommand) -> Self {
let workspace_root = current_workspace_root();
let mut panel = WorkspacePanelViewModel::empty(&workspace_root);
panel
@ -1266,7 +1257,6 @@ impl DashboardApp {
runtime_command: runtime_command.clone(),
}),
runtime_command,
include_stopped,
last_companion_lifecycle_failure: None,
last_orchestrator_lifecycle_failure: None,
orchestrator_work_set: OrchestratorWorkSet::default(),
@ -2521,7 +2511,6 @@ enum OrchestratorLifecycleMode {
async fn load_dashboard_snapshot(
selected_name: Option<String>,
lifecycle_mode: OrchestratorLifecycleMode,
include_stopped: bool,
) -> Result<DashboardSnapshot, DashboardError> {
let workspace_root = current_workspace_root();
#[cfg(feature = "e2e-test")]
@ -2535,8 +2524,7 @@ async fn load_dashboard_snapshot(
#[cfg(feature = "e2e-test")]
let source_started = Instant::now();
let mut list =
load_worker_list(list_selected_name.clone(), MAX_ENTRIES, include_stopped).await?;
let mut list = load_worker_list(list_selected_name.clone(), MAX_ENTRIES).await?;
#[cfg(feature = "e2e-test")]
source_timings.push(PanelE2eSourceTiming {
source: "pod_metadata_status_probe.initial",
@ -2576,7 +2564,7 @@ async fn load_dashboard_snapshot(
if companion.reload_workers {
#[cfg(feature = "e2e-test")]
let source_started = Instant::now();
list = load_worker_list(list_selected_name.clone(), MAX_ENTRIES, include_stopped).await?;
list = load_worker_list(list_selected_name.clone(), MAX_ENTRIES).await?;
#[cfg(feature = "e2e-test")]
source_timings.push(PanelE2eSourceTiming {
source: "pod_metadata_status_probe.after_companion_reload",
@ -2634,7 +2622,7 @@ async fn load_dashboard_snapshot(
if orchestrator.reload_workers {
#[cfg(feature = "e2e-test")]
let source_started = Instant::now();
list = load_worker_list(list_selected_name, MAX_ENTRIES, include_stopped).await?;
list = load_worker_list(list_selected_name, MAX_ENTRIES).await?;
#[cfg(feature = "e2e-test")]
source_timings.push(PanelE2eSourceTiming {
source: "pod_metadata_status_probe.after_orchestrator_reload",
@ -3432,7 +3420,6 @@ fn existing_ticket_claim_notice(
async fn load_worker_list(
selected_name: Option<String>,
max_entries: usize,
include_stopped: bool,
) -> Result<WorkerList, DashboardError> {
let store_dir = default_store_dir()?;
let store = FsStore::new(&store_dir)?;
@ -3442,18 +3429,14 @@ async fn load_worker_list(
let live = read_reachable_live_worker_infos(&store)
.await
.unwrap_or_default();
let mut list = WorkerList::from_workspace_sources(
Ok(WorkerList::from_workspace_sources(
WorkerVisibilitySource::ResumePicker,
stored,
live,
selected_name,
max_entries,
&current_workspace_root(),
);
if !include_stopped {
list.retain_live_entries();
}
Ok(list)
))
}
#[derive(Debug, Clone)]

View File

@ -2329,7 +2329,7 @@ fn dashboard_open_failure_keeps_composer_and_sets_notice() {
#[test]
fn dashboard_loading_app_defers_initial_snapshot_to_enter_reload() {
let app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"), false);
let app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"));
assert!(app.panel.rows.is_empty());
assert!(
@ -3241,7 +3241,6 @@ fn app_with_panel(list: WorkerList, panel: WorkspacePanelViewModel) -> Dashboard
refreshing: false,
enter_reload: None,
runtime_command: WorkerRuntimeCommand::for_executable("/tmp/yoi"),
include_stopped: false,
last_companion_lifecycle_failure,
last_orchestrator_lifecycle_failure,
orchestrator_work_set: OrchestratorWorkSet::default(),
@ -3393,7 +3392,7 @@ fn section_names<'a>(list: &'a WorkerList, section: &DashboardSection) -> Vec<&'
#[test]
fn ticket_action_error_records_f2_diagnostic_details() {
let mut app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"), false);
let mut app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"));
let long_error = "root-clean failed for Ticket 00001KTWPE3KQ at /home/hare/Projects/yoi: dirty file crates/tui/src/dashboard.rs";
app.finish_ticket_action_dispatch(Err(TicketActionError::Stale(long_error.to_string())));

View File

@ -58,11 +58,7 @@ pub enum LaunchMode {
},
/// `yoi workers` / `yoi --backend <url>`: list workers through the selected
/// connection target, then attach to the selected Worker.
Workers {
runtime_id: Option<String>,
include_stopped: bool,
all: bool,
},
Workers { runtime_id: Option<String> },
/// `yoi --backend <url> --runtime-id <id> --worker-id <id>`: open one Worker
/// through the selected connection target.
OpenWorker {
@ -80,7 +76,7 @@ pub enum LaunchMode {
worker_name: Option<String>,
},
/// `yoi panel`: open the workspace Dashboard from the current workspace.
Panel { include_stopped: bool },
Panel,
}
pub async fn launch(options: LaunchOptions) -> ExitCode {
@ -132,34 +128,12 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
}
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
LaunchMode::Workers {
runtime_id,
include_stopped,
all,
} => match target.list_workers(if include_stopped {
WorkerListRequest::with_stopped(runtime_id)
} else {
WorkerListRequest::new(runtime_id)
}) {
Ok(worker_list) => {
if let Some(target) = worker_list.backend_target {
backend_worker_picker::run(target, worker_list.include_stopped).await
} else if let Some(runtime_command) = worker_list.local_runtime_command {
console::run_worker_picker(
runtime_command,
workspace_root.clone(),
all,
worker_list.include_stopped,
)
.await
} else {
Err(Box::new(io::Error::other(
"worker list target did not include a local or backend source",
)) as Box<dyn std::error::Error>)
}
}
LaunchMode::Workers { runtime_id } => {
match target.list_workers(WorkerListRequest::new(runtime_id)) {
Ok(worker_list) => backend_worker_picker::run(worker_list.target).await,
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
}
}
LaunchMode::OpenWorker {
runtime_id,
worker_id,
@ -179,8 +153,8 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
}
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
LaunchMode::Panel { include_stopped } => match target.dashboard() {
Ok(dashboard) => dashboard::launch(dashboard.runtime_command, include_stopped).await,
LaunchMode::Panel => match target.dashboard() {
Ok(dashboard) => dashboard::launch(dashboard.runtime_command).await,
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
};

View File

@ -80,28 +80,20 @@ pub enum PickerOutcome {
#[derive(Debug, Clone)]
pub(crate) struct PickerOptions {
scope: PickerScope,
include_stopped: bool,
}
impl PickerOptions {
pub(crate) fn workspace(workspace_root: PathBuf) -> Self {
Self {
scope: PickerScope::Workspace(workspace_root),
include_stopped: true,
}
}
pub(crate) fn all() -> Self {
Self {
scope: PickerScope::All,
include_stopped: true,
}
}
pub(crate) fn with_stopped(mut self, include_stopped: bool) -> Self {
self.include_stopped = include_stopped;
self
}
}
#[derive(Debug, Clone)]
@ -142,11 +134,6 @@ fn list_for_options(
stored_workers: Vec<StoredWorkerInfo>,
live_workers: Vec<LiveWorkerInfo>,
) -> WorkerList {
let stored_workers = if options.include_stopped {
stored_workers
} else {
Vec::new()
};
match &options.scope {
PickerScope::Workspace(workspace_root) => WorkerList::from_workspace_sources(
WorkerVisibilitySource::ResumePicker,

View File

@ -27,12 +27,6 @@ pub enum TaskStatus {
Deleted,
}
impl TaskStatus {
fn is_active(self) -> bool {
matches!(self, Self::Pending | Self::Inprogress)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct TaskEntry {
pub taskid: u64,
@ -112,10 +106,8 @@ impl TaskStore {
}
"TaskUpdate" => {
if let Ok(p) = serde_json::from_str::<TaskUpdateParams>(arguments)
&& let Some(task_position) =
self.tasks.iter().position(|t| t.taskid == p.taskid)
&& let Some(t) = self.tasks.iter_mut().find(|t| t.taskid == p.taskid)
{
let t = &mut self.tasks[task_position];
if let Some(s) = p.status {
t.status = s;
}
@ -125,9 +117,6 @@ impl TaskStore {
if let Some(d) = p.description {
t.description = d;
}
if !t.status.is_active() {
self.tasks.remove(task_position);
}
}
}
_ => {}
@ -143,10 +132,6 @@ impl TaskStore {
}
fn replace_with(&mut self, tasks: Vec<TaskEntry>) {
let tasks: Vec<_> = tasks
.into_iter()
.filter(|task| task.status.is_active())
.collect();
self.next_taskid = tasks
.iter()
.map(|t| t.taskid)
@ -190,13 +175,7 @@ fn parse_snapshot_text(text: &str) -> Option<Vec<TaskEntry>> {
let rest = &text[start..];
let end = rest.find(end_marker)?;
let snapshot: TaskSnapshot = serde_json::from_str(&rest[..end]).ok()?;
Some(
snapshot
.tasks
.into_iter()
.filter(|task| task.status.is_active())
.collect(),
)
Some(snapshot.tasks)
}
#[cfg(test)]
@ -241,7 +220,7 @@ mod tests {
}
#[test]
fn counts_tracks_only_active_tasks_after_completion() {
fn counts_classifies_each_status() {
let mut s = TaskStore::new();
s.apply_tool_call("TaskCreate", r#"{"subject":"a","description":""}"#);
s.apply_tool_call("TaskCreate", r#"{"subject":"b","description":""}"#);
@ -251,9 +230,9 @@ mod tests {
let c = s.counts();
assert_eq!(c.pending, 1);
assert_eq!(c.inprogress, 1);
assert_eq!(c.completed, 0);
assert_eq!(c.completed, 1);
assert_eq!(c.deleted, 0);
assert_eq!(c.total(), 2);
assert_eq!(c.total(), 3);
assert_eq!(c.active(), 2);
}
@ -263,7 +242,7 @@ mod tests {
fn wrap_snapshot(json_body: &str, overview: &str) -> String {
format!(
"[Session TaskStore snapshot]\n\n{overview}\n\n```json\n{json_body}\n```\n\n\
This is the active session task list preserved across compaction. \
This is the complete session task list preserved across compaction. \
The following TaskList tool result presents the same state through the tool lane."
)
}
@ -288,19 +267,20 @@ mod tests {
}"#;
let text = wrap_snapshot(
body,
"TaskStore: 1 active task(s) (pending: 1, inprogress: 0)",
"TaskStore: 2 task(s) (pending: 1, inprogress: 0, completed: 1, deleted: 0)",
);
let mut s = TaskStore::new();
s.apply_tool_call("TaskCreate", r#"{"subject":"stale","description":""}"#);
s.apply_system_message_text(&text);
let tasks = s.tasks();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].taskid, 7);
assert_eq!(tasks[0].status, TaskStatus::Pending);
// Subsequent TaskCreate must continue beyond the highest active taskid
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0].taskid, 5);
assert_eq!(tasks[0].status, TaskStatus::Completed);
assert_eq!(tasks[1].taskid, 7);
// Subsequent TaskCreate must continue beyond the highest taskid
// observed in the snapshot.
s.apply_tool_call("TaskCreate", r#"{"subject":"new","description":""}"#);
assert_eq!(s.tasks()[1].taskid, 8);
assert_eq!(s.tasks()[2].taskid, 8);
}
#[test]
@ -324,7 +304,7 @@ mod tests {
}
]
}"#;
let text = wrap_snapshot(body, "TaskStore: 1 active task(s)");
let text = wrap_snapshot(body, "TaskStore: 1 task(s)");
let mut s = TaskStore::new();
s.apply_system_message_text(&text);
let t = &s.tasks()[0];
@ -349,13 +329,13 @@ mod snapshot_format_contract {
fn wrap_pod_style(snapshot_text: &str) -> String {
format!(
"[Session TaskStore snapshot]\n\n{snapshot_text}\n\n\
This is the active session task list preserved across compaction. \
This is the complete session task list preserved across compaction. \
The following TaskList tool result presents the same state through the tool lane."
)
}
fn snapshot_fixture() -> &'static str {
r#"TaskStore: 1 active task(s) (pending: 0, inprogress: 1)
r#"TaskStore: 2 task(s) (pending: 0, inprogress: 1, completed: 1, deleted: 0)
```json
{
@ -365,6 +345,12 @@ mod snapshot_format_contract {
"status": "inprogress",
"subject": "first",
"description": "first desc"
},
{
"taskid": 2,
"status": "completed",
"subject": "second",
"description": "second desc with\nnewline"
}
]
}
@ -372,7 +358,7 @@ mod snapshot_format_contract {
}
fn empty_snapshot_fixture() -> &'static str {
r#"TaskStore: 0 active task(s) (pending: 0, inprogress: 0)
r#"TaskStore: 0 task(s) (pending: 0, inprogress: 0, completed: 0, deleted: 0)
```json
{
@ -398,11 +384,15 @@ mod snapshot_format_contract {
downstream.apply_system_message_text(&envelope);
let tasks = downstream.tasks();
assert_eq!(tasks.len(), 1, "TUI parsed wrong number of tasks");
assert_eq!(tasks.len(), 2, "TUI parsed wrong number of tasks");
assert_eq!(tasks[0].taskid, 1);
assert_eq!(tasks[0].subject, "first");
assert_eq!(tasks[0].description, "first desc");
assert_eq!(status_label(tasks[0].status), "inprogress");
assert_eq!(tasks[1].taskid, 2);
assert_eq!(tasks[1].subject, "second");
assert_eq!(tasks[1].description, "second desc with\nnewline");
assert_eq!(status_label(tasks[1].status), "completed");
}
#[test]

View File

@ -126,17 +126,6 @@ impl WorkerList {
self.selected_name = self.entries.get(index).map(|entry| entry.name.clone());
}
pub(crate) fn retain_live_entries(&mut self) {
self.entries.retain(|entry| entry.live.is_some());
if !self
.selected_name
.as_ref()
.is_some_and(|selected| self.entries.iter().any(|entry| entry.name == *selected))
{
self.selected_name = self.entries.first().map(|entry| entry.name.clone());
}
}
pub(crate) fn selected_entry(&self) -> Option<&WorkerListEntry> {
let index = self.selected_index();
self.entries.get(index)
@ -747,28 +736,6 @@ mod tests {
);
}
#[test]
fn retain_live_entries_removes_stored_only_rows_and_reselects() {
let dir = tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap();
let session_id = new_session_id();
let segment_id = new_segment_id();
append_start(&store, session_id, segment_id, 10);
let mut list = WorkerList::from_sources(
SOURCE,
vec![metadata_info(&store, "stored", session_id, segment_id)],
vec![live_info("live", WorkerStatus::Idle)],
Some("stored".to_string()),
10,
);
list.retain_live_entries();
assert_eq!(list.entries.len(), 1);
assert_eq!(list.entries[0].name, "live");
assert_eq!(list.selected_entry().unwrap().name, "live");
}
#[test]
fn stored_only_row_can_restore_and_open_but_not_direct_send() {
let dir = tempdir().unwrap();

View File

@ -7,7 +7,7 @@ license.workspace = true
autobins = false
[[bin]]
name = "yoi-runtime"
name = "worker-runtime-rest-server"
path = "src/main.rs"
required-features = ["ws-server", "fs-store"]
@ -17,7 +17,7 @@ path = "src/worker_runtime_bin.rs"
required-features = ["ws-server", "fs-store"]
[features]
default = ["ws-server", "fs-store"]
default = []
fs-store = []
http-server = ["dep:axum", "dep:tower", "dep:reqwest"]
ws-server = ["http-server", "axum/ws", "dep:futures", "tokio/sync"]

View File

@ -8,7 +8,8 @@ From the repository root:
```bash
cargo run -p worker-runtime \
--bin yoi-runtime \
--features ws-server,fs-store \
--bin worker-runtime-rest-server \
-- --bind 127.0.0.1:38800
```
@ -22,7 +23,8 @@ To bind another address explicitly:
```bash
cargo run -p worker-runtime \
--bin yoi-runtime \
--features ws-server,fs-store \
--bin worker-runtime-rest-server \
-- --bind 0.0.0.0:38800
```

View File

@ -30,7 +30,7 @@ fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("yoi-runtime: {error}");
eprintln!("worker-runtime-rest-server: {error}");
if let ProcessError::Usage(_) = error {
eprintln!();
eprintln!("{}", usage());
@ -64,7 +64,7 @@ fn run() -> Result<(), ProcessError> {
let local_addr = listener.local_addr()?;
let worker_runtime = build_runtime(&config)?;
eprintln!(
"yoi-runtime listening on {local_addr}; intended client is a trusted backend/proxy, not a browser"
"worker-runtime REST server listening on {local_addr}; intended client is a trusted backend/proxy, not a browser"
);
worker_runtime::http_server::serve_runtime_http_with_auth(
worker_runtime,
@ -796,7 +796,7 @@ fn run_trust_server_command(mut args: VecDeque<String>) -> Result<(), ProcessErr
}
fn usage() -> &'static str {
r#"Usage: yoi-runtime [OPTIONS]
r#"Usage: worker-runtime-rest-server [OPTIONS]
Starts a worker-backed Runtime REST command API for a trusted backend/proxy.
Browsers must not connect to this Runtime process directly.

View File

@ -76,7 +76,7 @@ impl TaskFeature {
/// pointing at the same feature-owned store after rewind.
pub fn restore_from_history(&self, history: &[Item]) {
let restored = TaskStore::from_history(history);
self.state.task_store.replace_with(restored.list_active());
self.state.task_store.replace_with(restored.list());
}
/// Feature-owned snapshot text used by compaction to preserve Task state.
@ -86,7 +86,7 @@ impl TaskFeature {
/// Feature-owned compact summary used for the synthetic TaskList result.
pub fn snapshot_overview(&self) -> String {
snapshot_overview(&self.state.task_store.list_active())
snapshot_overview(&self.state.task_store.list())
}
#[cfg(test)]

View File

@ -18,12 +18,6 @@ pub enum TaskStatus {
Deleted,
}
impl TaskStatus {
pub fn is_active(self) -> bool {
matches!(self, Self::Pending | Self::Inprogress)
}
}
impl std::fmt::Display for TaskStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
@ -60,8 +54,6 @@ pub struct TaskSnapshot {
pub tasks: Vec<TaskEntry>,
}
pub const DEFAULT_TASK_LIST_LIMIT: usize = 20;
impl TaskStore {
pub fn new() -> Self {
Self {
@ -93,13 +85,6 @@ impl TaskStore {
.clone()
}
pub fn list_active(&self) -> Vec<TaskEntry> {
self.list()
.into_iter()
.filter(|task| task.status.is_active())
.collect()
}
pub fn get(&self, taskid: u64) -> Option<TaskEntry> {
self.inner
.lock()
@ -121,12 +106,11 @@ impl TaskStore {
return Err(TaskStoreError::NoFields);
}
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
let task_position = inner
let task = inner
.tasks
.iter()
.position(|t| t.taskid == taskid)
.iter_mut()
.find(|t| t.taskid == taskid)
.ok_or(TaskStoreError::Missing(taskid))?;
let task = &mut inner.tasks[task_position];
if let Some(status) = status {
task.status = status;
}
@ -136,21 +120,11 @@ impl TaskStore {
if let Some(description) = description {
task.description = description;
}
let updated = task.clone();
if !updated.status.is_active() {
inner.tasks.remove(task_position);
}
Ok(updated)
Ok(task.clone())
}
pub fn snapshot(&self) -> TaskSnapshot {
self.snapshot_limited(DEFAULT_TASK_LIST_LIMIT)
}
pub fn snapshot_limited(&self, limit: usize) -> TaskSnapshot {
TaskSnapshot {
tasks: self.list_active().into_iter().take(limit).collect(),
}
TaskSnapshot { tasks: self.list() }
}
pub fn replay_history(&self, history: &[Item]) {
@ -194,10 +168,6 @@ impl TaskStore {
}
pub fn replace_with(&self, tasks: Vec<TaskEntry>) {
let tasks: Vec<_> = tasks
.into_iter()
.filter(|task| task.status.is_active())
.collect();
let next_taskid = tasks
.iter()
.map(|t| t.taskid)
@ -267,26 +237,27 @@ pub fn snapshot_overview(tasks: &[TaskEntry]) -> String {
.iter()
.filter(|t| t.status == TaskStatus::Inprogress)
.count();
let active = pending + inprogress;
format!("TaskStore: {active} active task(s) (pending: {pending}, inprogress: {inprogress})")
let completed = tasks
.iter()
.filter(|t| t.status == TaskStatus::Completed)
.count();
let deleted = tasks
.iter()
.filter(|t| t.status == TaskStatus::Deleted)
.count();
format!(
"TaskStore: {} task(s) (pending: {pending}, inprogress: {inprogress}, completed: {completed}, deleted: {deleted})",
tasks.len()
)
}
pub fn render_snapshot(tasks: &[TaskEntry]) -> String {
let active_tasks: Vec<_> = tasks
.iter()
.filter(|task| task.status.is_active())
.cloned()
.collect();
let snapshot = TaskSnapshot {
tasks: active_tasks.clone(),
tasks: tasks.to_vec(),
};
let json =
serde_json::to_string_pretty(&snapshot).unwrap_or_else(|_| String::from("{\"tasks\":[]}"));
format!(
"{}\n\n```json\n{}\n```\n",
snapshot_overview(&active_tasks),
json
)
format!("{}\n\n```json\n{}\n```\n", snapshot_overview(tasks), json)
}
pub(super) fn parse_compact_snapshot_text(text: &str) -> Option<Vec<TaskEntry>> {
@ -299,13 +270,7 @@ pub(super) fn parse_compact_snapshot_text(text: &str) -> Option<Vec<TaskEntry>>
let rest = &text[start..];
let end = rest.find(end_marker)?;
let snapshot: TaskSnapshot = serde_json::from_str(&rest[..end]).ok()?;
Some(
snapshot
.tasks
.into_iter()
.filter(|task| task.status.is_active())
.collect(),
)
Some(snapshot.tasks)
}
#[cfg(test)]
@ -323,9 +288,11 @@ mod tests {
];
let store = TaskStore::from_history(&history);
let tasks = store.list();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0].taskid, 1);
assert_eq!(tasks[0].status, TaskStatus::Pending);
assert_eq!(tasks[1].taskid, 2);
assert_eq!(tasks[1].status, TaskStatus::Completed);
}
/// Wrap snapshot text the way `Worker::try_pre_run_compact` does, so tests
@ -333,7 +300,7 @@ mod tests {
fn wrap_snapshot_system_message(snapshot: &str) -> String {
format!(
"[Session TaskStore snapshot]\n\n{snapshot}\n\n\
This is the active session task list preserved across compaction. \
This is the complete session task list preserved across compaction. \
The following TaskList tool result presents the same state through the tool lane."
)
}
@ -355,9 +322,11 @@ mod tests {
];
let store = TaskStore::from_history(&history);
let tasks = store.list();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].taskid, 2);
assert_eq!(tasks[0].subject, "new");
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0].taskid, 1);
assert_eq!(tasks[0].status, TaskStatus::Completed);
assert_eq!(tasks[1].taskid, 2);
assert_eq!(tasks[1].subject, "new");
}
#[test]
@ -396,12 +365,15 @@ mod tests {
];
let store = TaskStore::from_history(&history);
let tasks = store.list();
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0].taskid, 2);
assert_eq!(tasks[0].subject, "B");
assert_eq!(tasks[0].status, TaskStatus::Inprogress);
assert_eq!(tasks[1].taskid, 3);
assert_eq!(tasks[1].subject, "C");
assert_eq!(tasks.len(), 3);
assert_eq!(tasks[0].taskid, 1);
assert_eq!(tasks[0].subject, "A");
assert_eq!(tasks[0].status, TaskStatus::Completed);
assert_eq!(tasks[1].taskid, 2);
assert_eq!(tasks[1].subject, "B");
assert_eq!(tasks[1].status, TaskStatus::Inprogress);
assert_eq!(tasks[2].taskid, 3);
assert_eq!(tasks[2].subject, "C");
}
#[test]
@ -443,10 +415,9 @@ mod tests {
let snapshot_text = pre.snapshot_text();
let system = Item::system_message(wrap_snapshot_system_message(&snapshot_text));
let call = Item::tool_call("compact-tasklist", "TaskList", "{}");
let active_tasks = pre.list_active();
let result = Item::tool_result_with_content(
"compact-tasklist",
snapshot_overview(&active_tasks),
snapshot_overview(&pre.list()),
snapshot_text.clone(),
);
@ -455,7 +426,7 @@ mod tests {
.as_text()
.and_then(parse_compact_snapshot_text)
.expect("system message should parse as snapshot");
assert_eq!(extracted, active_tasks);
assert_eq!(extracted, pre.list());
// The synthetic call/result pair shares one call_id and carries the
// expected tool name + detailed content.
@ -482,6 +453,6 @@ mod tests {
// Replaying the full triple reconstructs the same TaskStore.
let store = TaskStore::from_history(&[system, call, result]);
assert_eq!(store.list(), active_tasks);
assert_eq!(store.list(), pre.list());
}
}

View File

@ -6,7 +6,7 @@ use async_trait::async_trait;
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use serde::Deserialize;
use super::store::{DEFAULT_TASK_LIST_LIMIT, TaskEntry, TaskStatus, TaskStore, snapshot_overview};
use super::store::{TaskEntry, TaskStatus, TaskStore, render_snapshot, snapshot_overview};
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct TaskCreateParams {
@ -17,11 +17,7 @@ struct TaskCreateParams {
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct TaskListParams {
/// Maximum number of active tasks to return. Defaults to 20.
#[serde(default)]
limit: Option<usize>,
}
struct TaskListParams {}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct TaskGetParams {
@ -62,7 +58,9 @@ coordination. Do not create a task just because a request has several steps, and
one for short questions, quick checks, single reviews, or one-off commands. Prefer updating an \
existing active task over creating a duplicate. Input only `subject` and `description`; `taskid` \
is assigned automatically and initial `status` is `pending`.";
const LIST_DESCRIPTION: &str = "List active session-lifetime tasks. Completed and deleted tasks are forgotten and omitted. Defaults to 20 tasks unless `limit` is provided.";
const LIST_DESCRIPTION: &str = "List every session-lifetime task, including completed and \
deleted entries. Tasks are user-visible real-time status for short-term current-work tracking. \
Takes an empty object as input.";
const GET_DESCRIPTION: &str = "Get one session-lifetime task by `taskid`. Tasks are \
user-visible real-time status for short-term current-work tracking. Returns an error if the task \
does not exist.";
@ -83,7 +81,7 @@ impl Tool for TaskCreateTool {
let params: TaskCreateParams = serde_json::from_str(input_json)
.map_err(|e| ToolError::InvalidArgument(format!("invalid TaskCreate input: {e}")))?;
let created = self.store.create(params.subject, params.description);
let tasks = self.store.list_active();
let tasks = self.store.list();
Ok(task_output(
format!(
"Created task {} ({})\n{}",
@ -92,6 +90,7 @@ impl Tool for TaskCreateTool {
snapshot_overview(&tasks)
),
&created,
&tasks,
))
}
}
@ -103,14 +102,12 @@ impl Tool for TaskListTool {
input_json: &str,
_ctx: llm_engine::tool::ToolExecutionContext,
) -> Result<ToolOutput, ToolError> {
let params: TaskListParams = serde_json::from_str(input_json)
let _: TaskListParams = serde_json::from_str(input_json)
.map_err(|e| ToolError::InvalidArgument(format!("invalid TaskList input: {e}")))?;
let limit = params.limit.unwrap_or(DEFAULT_TASK_LIST_LIMIT);
let active_tasks = self.store.list_active();
let tasks: Vec<_> = active_tasks.iter().take(limit).cloned().collect();
let tasks = self.store.list();
Ok(ToolOutput {
summary: list_overview(active_tasks.len(), tasks.len()),
content: Some(render_task_list(&tasks)),
summary: snapshot_overview(&tasks),
content: Some(render_snapshot(&tasks)),
})
}
}
@ -153,7 +150,7 @@ impl Tool for TaskUpdateTool {
params.description,
)
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
let tasks = self.store.list_active();
let tasks = self.store.list();
Ok(task_output(
format!(
"Updated task {} ({})\n{}",
@ -162,32 +159,21 @@ impl Tool for TaskUpdateTool {
snapshot_overview(&tasks)
),
&updated,
&tasks,
))
}
}
fn task_output(summary: String, task: &TaskEntry) -> ToolOutput {
fn task_output(summary: String, task: &TaskEntry, tasks: &[TaskEntry]) -> ToolOutput {
let content = serde_json::json!({
"task": task,
"snapshot": { "tasks": tasks },
});
ToolOutput {
summary,
content: Some(serde_json::to_string_pretty(task).unwrap_or_default()),
content: Some(serde_json::to_string_pretty(&content).unwrap_or_default()),
}
}
fn list_overview(total_active: usize, returned: usize) -> String {
if returned < total_active {
format!(
"TaskStore: {returned} active task(s) shown; {} omitted.",
total_active - returned
)
} else {
format!("TaskStore: {returned} active task(s)")
}
}
fn render_task_list(tasks: &[TaskEntry]) -> String {
serde_json::to_string_pretty(tasks).unwrap_or_default()
}
fn task_create_tool(store: TaskStore) -> ToolDefinition {
Arc::new(move || {
let schema = schemars::schema_for!(TaskCreateParams);
@ -278,10 +264,6 @@ mod tests {
.await
.unwrap();
assert!(out.summary.contains("Created task 1"));
let created_content = out.content.unwrap();
let created_json: serde_json::Value = serde_json::from_str(&created_content).unwrap();
assert_eq!(created_json["taskid"], 1);
assert!(created_json.get("task").is_none());
assert_eq!(store.get(1).unwrap().status, TaskStatus::Pending);
let out = update
@ -304,106 +286,10 @@ mod tests {
assert!(out.content.unwrap().contains("implement tasks"));
let out = list.execute("{}", Default::default()).await.unwrap();
assert!(out.summary.contains("1 active task(s)"));
assert!(out.summary.contains("1 task(s)"));
let content = out.content.unwrap();
assert!(content.contains("\"taskid\": 1"));
assert!(!content.contains("\"limit\""));
assert!(!content.contains("```json"));
}
#[tokio::test]
async fn task_list_omits_completed_deleted_and_defaults_to_twenty() {
let store = TaskStore::new();
let create = tool(task_create_tool(store.clone()));
let update = tool(task_update_tool(store.clone()));
let list = tool(task_list_tool(store.clone()));
for i in 0..25 {
create
.execute(
&format!(r#"{{"subject":"task {i}","description":"desc {i}"}}"#),
Default::default(),
)
.await
.unwrap();
}
update
.execute(r#"{"taskid":1,"status":"completed"}"#, Default::default())
.await
.unwrap();
update
.execute(r#"{"taskid":2,"status":"deleted"}"#, Default::default())
.await
.unwrap();
let out = list.execute("{}", Default::default()).await.unwrap();
assert_eq!(
out.summary,
"TaskStore: 20 active task(s) shown; 3 omitted."
);
let content = out.content.unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
let tasks = json.as_array().unwrap();
assert_eq!(tasks.len(), 20);
let ids: Vec<u64> = tasks
.iter()
.map(|task| task["taskid"].as_u64().unwrap())
.collect();
assert!(!ids.contains(&1));
assert!(!ids.contains(&2));
assert!(!content.contains("\"limit\""));
assert!(!content.contains("\"total_active\""));
assert!(!content.contains("\"truncated\""));
let out = list
.execute(r#"{"limit":3}"#, Default::default())
.await
.unwrap();
assert_eq!(
out.summary,
"TaskStore: 3 active task(s) shown; 20 omitted."
);
let content = out.content.unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(json.as_array().unwrap().len(), 3);
}
#[tokio::test]
async fn task_create_and_update_results_do_not_include_full_snapshot() {
let store = TaskStore::new();
let create = tool(task_create_tool(store.clone()));
let update = tool(task_update_tool(store.clone()));
create
.execute(
r#"{"subject":"done","description":"completed task"}"#,
Default::default(),
)
.await
.unwrap();
update
.execute(r#"{"taskid":1,"status":"completed"}"#, Default::default())
.await
.unwrap();
create
.execute(
r#"{"subject":"active","description":"active task"}"#,
Default::default(),
)
.await
.unwrap();
let out = update
.execute(r#"{"taskid":2,"status":"inprogress"}"#, Default::default())
.await
.unwrap();
let content = out.content.unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(json["taskid"], 2);
assert!(json.get("task").is_none());
assert!(json.get("snapshot").is_none());
assert!(!content.contains("\"taskid\": 1"));
assert!(!content.contains("completed task"));
assert!(content.contains("```json"));
}
#[tokio::test]

View File

@ -2733,7 +2733,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
}
let task_snapshot_message = Item::system_message(format!(
"[Session TaskStore snapshot]\n\n{task_snapshot_text}\n\n\
This is the active session task list preserved across compaction. \
This is the complete session task list preserved across compaction. \
The following TaskList tool result presents the same state through the tool lane."
));
compact_introduced_system_messages.push(task_snapshot_message.clone());

View File

@ -4,11 +4,6 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false
autobins = false
[[bin]]
name = "yoi-server"
path = "src/main.rs"
[dependencies]
async-trait.workspace = true
@ -30,7 +25,7 @@ memory.workspace = true
tokio = { workspace = true, features = ["fs", "macros", "net", "rt-multi-thread", "sync"] }
tokio-tungstenite.workspace = true
worker.workspace = true
worker-runtime.workspace = true
worker-runtime = { workspace = true, features = ["ws-server", "fs-store"] }
toml.workspace = true
tracing.workspace = true
url.workspace = true

View File

@ -306,7 +306,7 @@ impl WorkspaceBackendConfigFile {
match fs::read_to_string(&path) {
Ok(local) => Ok(ConfigDiff::new(WORKSPACE_BACKEND_CONFIG_TEMPLATE, &local)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Err(Error::Config(format!(
"workspace backend local config `{}` does not exist; run `yoi-server init --workspace {}` first",
"workspace backend local config `{}` does not exist; run `yoi workspace init --workspace {}` first",
path.display(),
workspace_root.display()
))),

View File

@ -46,7 +46,7 @@ impl WorkspaceIdentity {
Ok(raw) => Self::parse_str(&raw, &path),
Err(error) if error.kind() == ErrorKind::NotFound => {
Err(Error::WorkspaceIdentity(format!(
"workspace is not initialized at {}; run `yoi-server init --workspace {}` first",
"workspace is not initialized at {}; run `yoi workspace init --workspace {}` first",
workspace_root.as_ref().display(),
workspace_root.as_ref().display()
)))

View File

@ -65,7 +65,7 @@ async fn main() -> ExitCode {
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("yoi-server: {error}");
eprintln!("yoi-workspace-server: {error}");
ExitCode::FAILURE
}
}
@ -160,7 +160,7 @@ async fn run_init_with_database_path(
})?;
eprintln!(
"yoi-server: initialized workspace `{}` ({}) in server DB `{}`",
"yoi-workspace-server: initialized workspace `{}` ({}) in server DB `{}`",
options.workspace.display(),
identity.workspace_id,
database_path.display()
@ -572,7 +572,7 @@ async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Erro
let listener = TcpListener::bind(resolved.listen).await?;
eprintln!(
"yoi-server: serving workspace `{}` from server DB `{}` on http://{}",
"yoi-workspace-server: serving workspace `{}` from server DB `{}` on http://{}",
workspace.workspace_id,
database_path.display(),
listener.local_addr()?
@ -588,7 +588,7 @@ fn append_trusted_runtime_sources(
let Some(server_identity) = read_server_identity_file(&server_identity_path())? else {
if !store.list_trusted_runtimes(false)?.is_empty() {
return Err(Box::new(CliError(
"trusted runtimes are registered but server identity is not initialized; run `yoi-server identity init`".to_string(),
"trusted runtimes are registered but server identity is not initialized; run `yoi-workspace-server identity init`".to_string(),
)));
}
return Ok(());
@ -617,8 +617,7 @@ fn select_serve_workspace(store: &SqliteWorkspaceStore) -> Result<WorkspaceRecor
.map_err(|error| CliError(format!("failed to list workspaces from server DB: {error}")))?;
match workspaces.as_slice() {
[] => Err(CliError(
"server DB has no workspace records; run `yoi-server init --workspace <PATH>`"
.to_string(),
"server DB has no workspace records; run `yoi-workspace-server init --workspace <PATH>`".to_string(),
)),
[workspace] => Ok(workspace.clone()),
_ => Err(CliError(format!(
@ -826,31 +825,31 @@ fn parse_listen(value: &str) -> Result<SocketAddr, CliError> {
fn print_help() {
println!(
"yoi-server\n\nUsage:\n yoi-server init [OPTIONS]\n yoi-server config <COMMAND> [OPTIONS]\n yoi-server identity init --server-id <SERVER_ID> [--replace]\n yoi-server identity show [--json]\n yoi-server trust-runtime add --runtime-id <RUNTIME_ID> --base-url <URL> --public-key <KEY> [--display-name <NAME>] [--replace]\n yoi-server trust-runtime list [--json] [--include-revoked]\n yoi-server trust-runtime revoke --runtime-id <RUNTIME_ID>\n yoi-server skills <COMMAND> [OPTIONS]\n yoi-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help"
"yoi-workspace-server\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n yoi-workspace-server config <COMMAND> [OPTIONS]\n yoi-workspace-server identity init --server-id <SERVER_ID> [--replace]\n yoi-workspace-server identity show [--json]\n yoi-workspace-server trust-runtime add --runtime-id <RUNTIME_ID> --base-url <URL> --public-key <KEY> [--display-name <NAME>] [--replace]\n yoi-workspace-server trust-runtime list [--json] [--include-revoked]\n yoi-workspace-server trust-runtime revoke --runtime-id <RUNTIME_ID>\n yoi-workspace-server skills <COMMAND> [OPTIONS]\n yoi-workspace-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help"
);
}
fn print_init_help() {
println!(
"yoi-server init\n\nUsage:\n yoi-server init [OPTIONS]\n\nDescription:\n Initializes a Workspace identity, copies the packaged Backend config template to .yoi/workspace-backend.local.toml, and registers the Workspace in the Yoi server DB.\n\nOptions:\n --workspace <PATH> Workspace root to initialize (defaults to cwd)\n -h, --help Print help"
"yoi-workspace-server init\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n\nDescription:\n Initializes a Workspace identity, copies the packaged Backend config template to .yoi/workspace-backend.local.toml, and registers the Workspace in the Yoi server DB.\n\nOptions:\n --workspace <PATH> Workspace root to initialize (defaults to cwd)\n -h, --help Print help"
);
}
fn print_config_help() {
println!(
"yoi-server config\n\nUsage:\n yoi-server config default\n yoi-server config diff [OPTIONS]\n\nDescription:\n Prints the packaged Workspace Backend config template or compares it with the workspace-local config.\n\nOptions for diff:\n --workspace <PATH> Workspace root (defaults to cwd)\n -h, --help Print help"
"yoi-workspace-server config\n\nUsage:\n yoi-workspace-server config default\n yoi-workspace-server config diff [OPTIONS]\n\nDescription:\n Prints the packaged Workspace Backend config template or compares it with the workspace-local config.\n\nOptions for diff:\n --workspace <PATH> Workspace root (defaults to cwd)\n -h, --help Print help"
);
}
fn print_skills_help() {
println!(
"yoi-server skills\n\nUsage:\n yoi-server skills list [OPTIONS]\n yoi-server skills lint [OPTIONS]\n yoi-server skills show <NAME> [OPTIONS]\n\nDescription:\n Uses the Workspace backend Skill catalog/lint/detail authority. Catalog output is lightweight and omits full SKILL.md bodies; detail output includes the body. allowed-tools and scripts are diagnostics only.\n\nOptions:\n --workspace <PATH> Workspace root (defaults to cwd)\n -h, --help Print help"
"yoi-workspace-server skills\n\nUsage:\n yoi-workspace-server skills list [OPTIONS]\n yoi-workspace-server skills lint [OPTIONS]\n yoi-workspace-server skills show <NAME> [OPTIONS]\n\nDescription:\n Uses the Workspace backend Skill catalog/lint/detail authority. Catalog output is lightweight and omits full SKILL.md bodies; detail output includes the body. allowed-tools and scripts are diagnostics only.\n\nOptions:\n --workspace <PATH> Workspace root (defaults to cwd)\n -h, --help Print help"
);
}
fn print_serve_help() {
println!(
"yoi-server serve\n\nUsage:\n yoi-server serve [OPTIONS]\n\nDescription:\n Serves the Workspace recorded in the Yoi server DB. Workspace records are stored in the XDG/Yoi data directory, and runtime sources are loaded from XDG runtimes.toml.\n\nOptions:\n --listen <ADDR> Listen address (default 127.0.0.1:8787)\n -h, --help Print help"
"yoi-workspace-server serve\n\nUsage:\n yoi-workspace-server serve [OPTIONS]\n\nDescription:\n Serves the Workspace recorded in the Yoi server DB. Workspace records are stored in the XDG/Yoi data directory, and runtime sources are loaded from XDG runtimes.toml.\n\nOptions:\n --listen <ADDR> Listen address (default 127.0.0.1:8787)\n -h, --help Print help"
);
}

View File

@ -1,7 +1,7 @@
use client::{BackendTarget, LocalTarget, Target, TargetKind};
use serde::Deserialize;
use super::{ParseError, read_client_default_connection, resolve_backend_url};
use super::{ParseError, resolve_backend_url};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CliConnectionRequirement {
@ -26,6 +26,7 @@ pub(crate) enum CliCommand {
Mcp,
MemoryLint,
Session,
WorkspaceServer,
Login,
}
@ -46,18 +47,18 @@ impl CliCommand {
CliCommand::Mcp => "yoi mcp",
CliCommand::MemoryLint => "yoi memory lint",
CliCommand::Session => "yoi session",
CliCommand::WorkspaceServer => "yoi workspace/server",
CliCommand::Login => "yoi login",
}
}
pub(crate) fn connection_requirement(self) -> CliConnectionRequirement {
match self {
CliCommand::DefaultTui
| CliCommand::Workers
| CliCommand::Resume
| CliCommand::Panel => CliConnectionRequirement::ConnectionAware,
CliCommand::Login => CliConnectionRequirement::BackendOnly,
CliCommand::Keys
CliCommand::DefaultTui => CliConnectionRequirement::ConnectionAware,
CliCommand::Workers | CliCommand::Login => CliConnectionRequirement::BackendOnly,
CliCommand::Resume
| CliCommand::Panel
| CliCommand::Keys
| CliCommand::SetupModel
| CliCommand::WorkerRuntime
| CliCommand::WorkerCleanup
@ -66,13 +67,15 @@ impl CliCommand {
| CliCommand::Plugin
| CliCommand::Mcp
| CliCommand::MemoryLint
| CliCommand::Session => CliConnectionRequirement::LocalOnly,
| CliCommand::Session
| CliCommand::WorkspaceServer => CliConnectionRequirement::LocalOnly,
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub(crate) enum ClientDefaultConnection {
Local,
Backend,
@ -86,10 +89,7 @@ impl Default for ClientDefaultConnection {
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CliConnectionInput<'a> {
DefaultTarget {
workspace_id: Option<&'a str>,
},
LocalTarget,
LocalDefault,
BackendTarget {
explicit_backend_url: Option<String>,
workspace_id: Option<&'a str>,
@ -114,29 +114,21 @@ impl CliConnectionResolver for ClientConfigCliConnectionResolver {
input: CliConnectionInput<'_>,
) -> Result<Box<dyn Target>, ParseError> {
match (command.connection_requirement(), input) {
(
CliConnectionRequirement::LocalOnly,
CliConnectionInput::DefaultTarget { .. } | CliConnectionInput::LocalTarget,
) => Ok(Box::new(LocalTarget::new())),
(CliConnectionRequirement::LocalOnly, CliConnectionInput::LocalDefault) => {
Ok(Box::new(LocalTarget::new()))
}
(CliConnectionRequirement::LocalOnly, CliConnectionInput::BackendTarget { .. }) => {
Err(ParseError(format!(
"{} uses a local connection target and cannot accept Backend target options",
command.display_name()
)))
}
(CliConnectionRequirement::BackendOnly, CliConnectionInput::LocalTarget) => {
(CliConnectionRequirement::BackendOnly, CliConnectionInput::LocalDefault) => {
Err(ParseError(format!(
"{} requires a Backend connection target",
command.display_name()
)))
}
(
CliConnectionRequirement::BackendOnly,
CliConnectionInput::DefaultTarget { workspace_id },
) => Ok(Box::new(BackendTarget::new(
resolve_backend_url(None, workspace_id)?,
workspace_id.map(str::to_string),
))),
(
CliConnectionRequirement::BackendOnly | CliConnectionRequirement::ConnectionAware,
CliConnectionInput::BackendTarget {
@ -147,19 +139,9 @@ impl CliConnectionResolver for ClientConfigCliConnectionResolver {
resolve_backend_url(explicit_backend_url, workspace_id)?,
workspace_id.map(str::to_string),
))),
(CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalTarget) => {
(CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalDefault) => {
Ok(Box::new(LocalTarget::new()))
}
(
CliConnectionRequirement::ConnectionAware,
CliConnectionInput::DefaultTarget { workspace_id },
) => match read_client_default_connection()? {
ClientDefaultConnection::Local => Ok(Box::new(LocalTarget::new())),
ClientDefaultConnection::Backend => Ok(Box::new(BackendTarget::new(
resolve_backend_url(None, workspace_id)?,
workspace_id.map(str::to_string),
))),
},
}
}
}
@ -168,7 +150,7 @@ pub(crate) fn resolve_local_cli_connection<R: CliConnectionResolver + ?Sized>(
resolver: &R,
command: CliCommand,
) -> Result<Box<dyn Target>, ParseError> {
let target = resolver.resolve_connection(command, CliConnectionInput::LocalTarget)?;
let target = resolver.resolve_connection(command, CliConnectionInput::LocalDefault)?;
match target.kind() {
TargetKind::Local => Ok(target),
TargetKind::Backend => Err(ParseError(format!(
@ -200,33 +182,6 @@ pub(crate) fn resolve_backend_cli_connection<R: CliConnectionResolver + ?Sized>(
}
}
pub(crate) fn resolve_connection_aware_cli_connection<R: CliConnectionResolver + ?Sized>(
resolver: &R,
command: CliCommand,
explicit_local: bool,
explicit_backend_url: Option<String>,
workspace_id: Option<&str>,
) -> Result<Box<dyn Target>, ParseError> {
if explicit_local && explicit_backend_url.is_some() {
return Err(ParseError(
"--local and --backend are mutually exclusive".to_string(),
));
}
if explicit_local {
return resolver.resolve_connection(command, CliConnectionInput::LocalTarget);
}
if explicit_backend_url.is_some() {
return resolver.resolve_connection(
command,
CliConnectionInput::BackendTarget {
explicit_backend_url,
workspace_id,
},
);
}
resolver.resolve_connection(command, CliConnectionInput::DefaultTarget { workspace_id })
}
pub(crate) fn backend_target_option_error_for_local_command(
command: CliCommand,
option: &str,
@ -254,25 +209,22 @@ mod tests {
#[test]
fn cli_command_connection_requirements_are_explicit() {
for command in [
CliCommand::DefaultTui,
CliCommand::Workers,
CliCommand::Resume,
CliCommand::Panel,
] {
assert_eq!(
command.connection_requirement(),
CliConnectionRequirement::ConnectionAware,
"{} should be connection-aware",
command.display_name()
CliCommand::DefaultTui.connection_requirement(),
CliConnectionRequirement::ConnectionAware
);
assert_eq!(
CliCommand::Workers.connection_requirement(),
CliConnectionRequirement::BackendOnly
);
}
assert_eq!(
CliCommand::Login.connection_requirement(),
CliConnectionRequirement::BackendOnly
);
for command in [
CliCommand::Resume,
CliCommand::Panel,
CliCommand::Keys,
CliCommand::SetupModel,
CliCommand::WorkerRuntime,
@ -283,6 +235,7 @@ mod tests {
CliCommand::Mcp,
CliCommand::MemoryLint,
CliCommand::Session,
CliCommand::WorkspaceServer,
] {
assert_eq!(
command.connection_requirement(),
@ -298,7 +251,7 @@ mod tests {
let resolver = ClientConfigCliConnectionResolver;
let err = resolver
.resolve_connection(
CliCommand::Keys,
CliCommand::Resume,
CliConnectionInput::BackendTarget {
explicit_backend_url: Some("http://127.0.0.1:8787".to_string()),
workspace_id: None,
@ -308,20 +261,20 @@ mod tests {
assert_eq!(
err.to_string(),
"yoi keys uses a local connection target and cannot accept Backend target options"
"yoi resume uses a local connection target and cannot accept Backend target options"
);
}
#[test]
fn cli_connection_resolver_rejects_backend_only_local_target() {
fn cli_connection_resolver_rejects_backend_only_local_default() {
let resolver = ClientConfigCliConnectionResolver;
let err = resolver
.resolve_connection(CliCommand::Login, CliConnectionInput::LocalTarget)
.resolve_connection(CliCommand::Workers, CliConnectionInput::LocalDefault)
.unwrap_err();
assert_eq!(
err.to_string(),
"yoi login requires a Backend connection target"
"yoi workers requires a Backend connection target"
);
}
@ -340,8 +293,7 @@ mod tests {
let workers = target
.list_workers(client::WorkerListRequest::new(None))
.unwrap();
let backend_target = workers.backend_target.as_ref().unwrap();
assert_eq!(backend_target.base_url, "http://127.0.0.1:8787");
assert_eq!(backend_target.workspace_id, None);
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
assert_eq!(workers.target.workspace_id, None);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -231,7 +231,7 @@ in
tag = imageTag;
copyToRoot = runtimeRoot;
config = {
Entrypoint = [ "/bin/yoi-runtime" ];
Entrypoint = [ "/bin/worker-runtime-rest-server" ];
Cmd = [
"--bind"
"0.0.0.0:38800"
@ -259,7 +259,7 @@ in
tag = imageTag;
copyToRoot = serverRoot;
config = {
Entrypoint = [ "/bin/yoi-server" ];
Entrypoint = [ "/bin/yoi-workspace-server" ];
Cmd = [
"serve"
"--listen"

View File

@ -32,10 +32,10 @@ Current image roles:
```text
yoi-runtime:latest
yoi-runtime
worker-runtime-rest-server
yoi-server:latest
yoi-server
yoi-workspace-server
yoi-webui:latest
nginx serving built WebUI assets and proxying API requests
@ -62,7 +62,7 @@ docker/workspace/.yoi/workspace.toml
docker/workspace/.yoi/workspace-backend.local.toml
```
The WebUI container serves static assets and proxies `/api` to the Backend Server. The Backend Server registers the Runtime container as a remote Runtime such as `docker-runtime`. The Runtime container runs `yoi-runtime` and owns Worker spawning/materialization for that runtime.
The WebUI container serves static assets and proxies `/api` to the Backend Server. The Backend Server registers the Runtime container as a remote Runtime such as `docker-runtime`. The Runtime container runs `worker-runtime-rest-server` and owns Worker spawning/materialization for that runtime.
Container user and writable data directories matter: runtime/server images must be able to write their configured data directories and named volumes. The current local-image Compose setup avoids an image-level `User` override and sets data-directory permissions accordingly.

View File

@ -28,13 +28,13 @@ RUNTIME_BASE_URL=http://127.0.0.1:38800
From the Workspace Server host:
```bash
yoi-server identity init --server-id server-main
yoi-workspace-server identity init --server-id server-main
```
Show the public identity and copy the `public_key` value:
```bash
yoi-server identity show --json
yoi-workspace-server identity show --json
```
The Server private identity is stored in the Yoi data directory under the Server data root, currently:
@ -50,13 +50,13 @@ On Unix this file is written with `0600` permissions. Do not copy the private ke
From the Runtime host, using the same Runtime storage flags that the Runtime server process will use:
```bash
yoi-runtime identity init --runtime-id runtime-main
worker-runtime-rest-server identity init --runtime-id runtime-main
```
Show the public identity and copy the `public_key` value:
```bash
yoi-runtime identity show --json
worker-runtime-rest-server identity show --json
```
By default, Runtime auth state is stored at:
@ -70,21 +70,21 @@ If the Runtime process is launched with `--fs-root` or `--fs-runtime-dir`, pass
Example with explicit Runtime storage:
```bash
yoi-runtime identity init \
worker-runtime-rest-server identity init \
--runtime-id runtime-main \
--fs-root /var/lib/yoi-runtime
yoi-runtime identity show \
worker-runtime-rest-server identity show \
--json \
--fs-root /var/lib/yoi-runtime
```
## 3. Register the Server public key on Runtime
On the Runtime host, register the Server public key copied from `yoi-server identity show --json`:
On the Runtime host, register the Server public key copied from `yoi-workspace-server identity show --json`:
```bash
yoi-runtime trust-server add \
worker-runtime-rest-server trust-server add \
--server-id server-main \
--public-key '<SERVER_PUBLIC_KEY>'
```
@ -92,7 +92,7 @@ yoi-runtime trust-server add \
With explicit Runtime storage, keep using the same storage flags:
```bash
yoi-runtime trust-server add \
worker-runtime-rest-server trust-server add \
--server-id server-main \
--public-key '<SERVER_PUBLIC_KEY>' \
--fs-root /var/lib/yoi-runtime
@ -101,27 +101,27 @@ yoi-runtime trust-server add \
Verify:
```bash
yoi-runtime trust-server list --json
worker-runtime-rest-server trust-server list --json
```
## 4. Register the Runtime public key and endpoint on Server
On the Workspace Server host, register the Runtime public key copied from `yoi-runtime identity show --json`:
On the Workspace Server host, register the Runtime public key copied from `worker-runtime-rest-server identity show --json`:
```bash
yoi-server trust-runtime add \
yoi-workspace-server trust-runtime add \
--runtime-id runtime-main \
--base-url http://127.0.0.1:38800 \
--public-key '<RUNTIME_PUBLIC_KEY>' \
--display-name 'Runtime main'
```
This writes a trusted Runtime record to the Server DB. During `yoi-server serve`, active trusted Runtime records are loaded as remote Runtime sources and receive signed capability tokens. You do not need to duplicate the same Runtime in `runtimes.toml` for this trust-backed path.
This writes a trusted Runtime record to the Server DB. During `yoi-workspace-server serve`, active trusted Runtime records are loaded as remote Runtime sources and receive signed capability tokens. You do not need to duplicate the same Runtime in `runtimes.toml` for this trust-backed path.
Verify:
```bash
yoi-server trust-runtime list --json
yoi-workspace-server trust-runtime list --json
```
## 5. Start Runtime and Workspace Server
@ -129,7 +129,7 @@ yoi-server trust-runtime list --json
Start Runtime with the same storage flags used during Runtime identity/trust setup:
```bash
yoi-runtime \
worker-runtime-rest-server \
--bind 127.0.0.1:38800
```
@ -137,26 +137,27 @@ For repository builds, the equivalent cargo command is:
```bash
cargo run -p worker-runtime \
--bin yoi-runtime \
--features ws-server,fs-store \
--bin worker-runtime-rest-server \
-- --bind 127.0.0.1:38800
```
Start Workspace Server:
```bash
yoi-server serve --listen 127.0.0.1:8787
yoi-workspace-server serve --listen 127.0.0.1:8787
```
For repository builds:
```bash
cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787
cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787
```
If the Server DB has no workspace record yet, initialize it first:
```bash
yoi-server init --workspace <WORKSPACE_ROOT>
yoi-workspace-server init --workspace <WORKSPACE_ROOT>
```
## Smoke checks
@ -164,8 +165,8 @@ yoi-server init --workspace <WORKSPACE_ROOT>
Check both trust stores:
```bash
yoi-server trust-runtime list --json
yoi-runtime trust-server list --json
yoi-workspace-server trust-runtime list --json
worker-runtime-rest-server trust-server list --json
```
Check that Workspace Server can see Runtime workers through the authenticated path. From the CLI:
@ -185,13 +186,13 @@ Identity and trust records are intentionally not overwritten by default.
Rotate Server identity:
```bash
yoi-server identity init --server-id server-main --replace
yoi-workspace-server identity init --server-id server-main --replace
```
After Server identity rotation, every Runtime that trusts that Server must be updated with the new Server public key:
```bash
yoi-runtime trust-server add \
worker-runtime-rest-server trust-server add \
--server-id server-main \
--public-key '<NEW_SERVER_PUBLIC_KEY>' \
--replace
@ -200,13 +201,13 @@ yoi-runtime trust-server add \
Rotate Runtime identity:
```bash
yoi-runtime identity init --runtime-id runtime-main --replace
worker-runtime-rest-server identity init --runtime-id runtime-main --replace
```
After Runtime identity rotation, Server must be updated with the new Runtime public key:
```bash
yoi-server trust-runtime add \
yoi-workspace-server trust-runtime add \
--runtime-id runtime-main \
--base-url http://127.0.0.1:38800 \
--public-key '<NEW_RUNTIME_PUBLIC_KEY>' \
@ -218,13 +219,13 @@ yoi-server trust-runtime add \
Revoke a trusted Runtime on Server:
```bash
yoi-server trust-runtime revoke --runtime-id runtime-main
yoi-workspace-server trust-runtime revoke --runtime-id runtime-main
```
Remove a trusted Server from Runtime:
```bash
yoi-runtime trust-server revoke --server-id server-main
worker-runtime-rest-server trust-server revoke --server-id server-main
```
## Troubleshooting
@ -234,7 +235,7 @@ yoi-runtime trust-server revoke --server-id server-main
The Server DB contains trusted Runtime records, but the Server signing identity file does not exist. Run:
```bash
yoi-server identity init --server-id server-main
yoi-workspace-server identity init --server-id server-main
```
If the identity was created in another environment, ensure the Server process is using the same Yoi data directory.
@ -244,8 +245,8 @@ If the identity was created in another environment, ensure the Server process is
Runtime only enables signed capability-token auth when both a Runtime identity and at least one trusted Server are present in its auth file. Check:
```bash
yoi-runtime identity show --json
yoi-runtime trust-server list --json
worker-runtime-rest-server identity show --json
worker-runtime-rest-server trust-server list --json
```
Also confirm the Runtime process was started with the same `--fs-root` / `--fs-runtime-dir` used for setup.
@ -255,8 +256,8 @@ Also confirm the Runtime process was started with the same `--fs-root` / `--fs-r
Confirm the `--runtime-id` registered on Server exactly matches the Runtime identity id:
```bash
yoi-runtime identity show --json
yoi-server trust-runtime list --json
worker-runtime-rest-server identity show --json
yoi-workspace-server trust-runtime list --json
```
`RUNTIME_ID` is the token audience; mismatches are rejected by Runtime.

View File

@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter;
};
cargoHash = "sha256-R33Ty4414wGkqnwCt08zbDQbVu9ggMC5I3ZawgloNT0=";
cargoHash = "sha256-iZaTREhL/aLixn67A1+Gi9opqh2j/yyFuGMyBBvuATM=";
depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
@ -92,8 +92,8 @@ rustPlatform.buildRustPackage rec {
];
postBuild = ''
cargo build --offline --profile release -p yoi-workspace-server --bin yoi-server
cargo build --offline --profile release -p worker-runtime --bin yoi-runtime
cargo build --offline --profile release -p yoi-workspace-server --bin yoi-workspace-server
cargo build --offline --profile release -p worker-runtime --bin worker-runtime-rest-server --features ws-server,fs-store
'';
# The package check is a credential-free install smoke check below. Running the
@ -105,16 +105,16 @@ rustPlatform.buildRustPackage rec {
runHook preInstall
yoi_bin=$(find . -type f -name yoi | head -n 1)
workspace_server_bin=$(find . -type f -name yoi-server | head -n 1)
worker_runtime_bin=$(find . -type f -name yoi-runtime | head -n 1)
workspace_server_bin=$(find . -type f -name yoi-workspace-server | head -n 1)
worker_runtime_bin=$(find . -type f -name worker-runtime-rest-server | head -n 1)
if [ -z "$yoi_bin" ] || [ -z "$workspace_server_bin" ] || [ -z "$worker_runtime_bin" ]; then
echo "built binaries not found" >&2
find . -maxdepth 6 -type f \( -name yoi -o -name yoi-server -o -name yoi-runtime \) -print >&2
find . -maxdepth 6 -type f \( -name yoi -o -name yoi-workspace-server -o -name worker-runtime-rest-server \) -print >&2
exit 1
fi
install -Dm755 "$yoi_bin" "$out/bin/yoi"
install -Dm755 "$workspace_server_bin" "$out/bin/yoi-server"
install -Dm755 "$worker_runtime_bin" "$out/bin/yoi-runtime"
install -Dm755 "$workspace_server_bin" "$out/bin/yoi-workspace-server"
install -Dm755 "$worker_runtime_bin" "$out/bin/worker-runtime-rest-server"
runHook postInstall
'';
@ -125,10 +125,10 @@ rustPlatform.buildRustPackage rec {
"$out/bin/yoi" worker --help >/dev/null
test -x "$out/bin/yoi"
test -x "$out/bin/yoi-server"
test -x "$out/bin/yoi-runtime"
"$out/bin/yoi-server" --help >/dev/null
"$out/bin/yoi-runtime" --help >/dev/null
test -x "$out/bin/yoi-workspace-server"
test -x "$out/bin/worker-runtime-rest-server"
"$out/bin/yoi-workspace-server" --help >/dev/null
"$out/bin/worker-runtime-rest-server" --help >/dev/null
test ! -e "$out/bin/yoi-pod"
test ! -e "$out/share/yoi/resources"
if "$out/bin/yoi" --session not-a-uuid 2>yoi.err; then

View File

@ -1,14 +1,14 @@
# Workspace Backend local config template.
#
# `yoi-server init` copies this packaged template to
# `yoi workspace init` copies this packaged template to
# `.yoi/workspace-backend.local.toml` without overwriting an existing file.
# The `.local` file is intentionally git-ignored.
#
# Print the latest packaged template with:
# yoi-server config default
# yoi workspace config default
#
# Compare the local config with the latest packaged template with:
# yoi-server config diff
# yoi workspace config diff
#
# Omit a key to use the built-in fallback. TOML has no `null`, so optional
# settings are represented by leaving the key commented out.

View File

@ -57,8 +57,8 @@ usage() {
Usage: $(basename "$0") <start|stop|restart|status>
Manage the local Yoi development stack for this checkout:
runtime target/debug/yoi-runtime --bind $RUNTIME_BIND
backend target/debug/yoi-server serve --listen $BACKEND_LISTEN
runtime target/debug/worker-runtime-rest-server --bind $RUNTIME_BIND
backend target/debug/yoi-workspace-server serve --listen $BACKEND_LISTEN
frontend deno run -A npm:vite@7.2.7 dev --host $FRONTEND_HOST --port $FRONTEND_PORT (cwd: web/workspace)
Actions:
@ -307,7 +307,7 @@ build_runtime_backend() {
log "building runtime binary"
(
cd "$ROOT_DIR"
run_cargo build -p worker-runtime --bin yoi-runtime
run_cargo build -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server
)
else
log "runtime disabled by YOI_DEV_RUNTIME_ENABLED=0; skipping runtime build"
@ -316,7 +316,7 @@ build_runtime_backend() {
log "building backend binary"
(
cd "$ROOT_DIR"
run_cargo build -p yoi-workspace-server --bin yoi-server
run_cargo build -p yoi-workspace-server --bin yoi-workspace-server
)
}
@ -327,7 +327,7 @@ start_runtime() {
fi
local port runtime_bin
port="$(port_for_addr "$RUNTIME_BIND")"
runtime_bin="$ROOT_DIR/target/debug/yoi-runtime"
runtime_bin="$ROOT_DIR/target/debug/worker-runtime-rest-server"
if [[ ! -x "$runtime_bin" ]]; then
printf 'runtime binary not found or not executable: %s\n' "$runtime_bin" >&2
return 1
@ -339,7 +339,7 @@ start_runtime() {
start_backend() {
local port backend_bin
port="$(port_for_addr "$BACKEND_LISTEN")"
backend_bin="$ROOT_DIR/target/debug/yoi-server"
backend_bin="$ROOT_DIR/target/debug/yoi-workspace-server"
if [[ ! -x "$backend_bin" ]]; then
printf 'backend binary not found or not executable: %s\n' "$backend_bin" >&2
return 1

View File

@ -3,7 +3,7 @@
SvelteKit static SPA for the Yoi workspace control plane.
The frontend is intentionally static. Workspace authority, validation, and API
behavior live in the Rust `yoi-server` backend.
behavior live in the Rust `yoi-workspace-server` backend.
## Development
@ -30,12 +30,12 @@ printed by `deno task dev`.
If you want to run the backend from the repository root instead:
```bash
cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787
cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787
```
The backend reads Workspace records from the Yoi server DB at
`<data_dir>/server/server.db`. Run
`cargo run -p yoi-workspace-server --bin yoi-server -- init --workspace .` first when the server
`cargo run -p yoi-workspace-server -- init --workspace .` first when the server
DB has not been initialized.
## Static build

View File

@ -4,7 +4,7 @@
"tasks": {
"install": "deno install",
"dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src --allow-env=VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/sidebar/repository-nav.test.ts",
"build": "deno run -A npm:vite@7.2.7 build",