Compare commits
9 Commits
acc7281414
...
0b64eab148
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b64eab148 | |||
| fb285b17e4 | |||
| 699290ccb1 | |||
| 406104babd | |||
| 297c56c72d | |||
| 8ec940f624 | |||
| cbbc860366 | |||
| bffbe6b551 | |||
| b6f83d81bb |
|
|
@ -21,7 +21,7 @@ pub use backend_auth::{
|
|||
poll_device_login, start_device_login, wait_for_device_login,
|
||||
};
|
||||
pub use backend_runtime::{
|
||||
BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeListResponse,
|
||||
BackendDiagnostic, BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeListResponse,
|
||||
BackendRuntimeListTarget, BackendRuntimeSummary, BackendRuntimeTarget,
|
||||
BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary,
|
||||
BackendWorkerRestoreResponse, BackendWorkerRestoreResult, BackendWorkerSummary,
|
||||
|
|
|
|||
|
|
@ -54,11 +54,22 @@ 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 }
|
||||
Self {
|
||||
runtime_id,
|
||||
include_stopped: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_stopped(runtime_id: Option<String>) -> Self {
|
||||
Self {
|
||||
runtime_id,
|
||||
include_stopped: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +110,9 @@ pub struct Dashboard {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerList {
|
||||
pub target: BackendRuntimeListTarget,
|
||||
pub local_runtime_command: Option<WorkerRuntimeCommand>,
|
||||
pub backend_target: Option<BackendRuntimeListTarget>,
|
||||
pub include_stopped: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -182,11 +195,18 @@ impl Target for LocalTarget {
|
|||
})
|
||||
}
|
||||
|
||||
fn list_workers(&self, _request: WorkerListRequest) -> Result<WorkerList, TargetError> {
|
||||
Err(TargetError::unsupported(
|
||||
"Backend runtime worker listing",
|
||||
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",
|
||||
self.kind(),
|
||||
))
|
||||
));
|
||||
}
|
||||
Ok(WorkerList {
|
||||
local_runtime_command: Some(self.runtime_command()?),
|
||||
backend_target: None,
|
||||
include_stopped: request.include_stopped,
|
||||
})
|
||||
}
|
||||
|
||||
fn connect_worker(
|
||||
|
|
@ -226,11 +246,13 @@ impl Target for BackendTarget {
|
|||
|
||||
fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError> {
|
||||
Ok(WorkerList {
|
||||
target: BackendRuntimeListTarget::new(
|
||||
local_runtime_command: None,
|
||||
backend_target: Some(BackendRuntimeListTarget::new(
|
||||
self.base_url.clone(),
|
||||
self.workspace_id.clone(),
|
||||
request.runtime_id,
|
||||
),
|
||||
)),
|
||||
include_stopped: request.include_stopped,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -259,9 +281,28 @@ mod tests {
|
|||
.list_workers(WorkerListRequest::new(Some("runtime-a".to_string())))
|
||||
.unwrap();
|
||||
|
||||
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"));
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -288,15 +329,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn local_target_rejects_backend_worker_operations() {
|
||||
fn local_target_builds_local_worker_list() {
|
||||
let target = LocalTarget::new();
|
||||
let err = target
|
||||
.list_workers(WorkerListRequest::new(None))
|
||||
.unwrap_err();
|
||||
let workers = target
|
||||
.list_workers(WorkerListRequest::with_stopped(None))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Backend runtime worker listing is not supported by local target"
|
||||
);
|
||||
assert!(workers.local_runtime_command.is_some());
|
||||
assert!(workers.backend_target.is_none());
|
||||
assert!(workers.include_stopped);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3539,10 +3539,7 @@ mod completion_flow_tests {
|
|||
arguments: args.into(),
|
||||
});
|
||||
}
|
||||
assert_eq!(
|
||||
app.task_store.tasks()[0].status,
|
||||
crate::task::TaskStatus::Completed
|
||||
);
|
||||
assert!(app.task_store.tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use std::io;
|
|||
use std::time::Duration;
|
||||
|
||||
use client::{
|
||||
BackendRuntimeListTarget, BackendRuntimeTarget, BackendWorkerSummary, list_backend_workers,
|
||||
BackendRuntimeListTarget, BackendRuntimeTarget, BackendWorkerSummary,
|
||||
list_backend_stopped_workers, list_backend_workers, restore_backend_worker,
|
||||
};
|
||||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
|
|
@ -18,13 +19,30 @@ use crate::console;
|
|||
const MAX_ROWS: usize = 10;
|
||||
const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
|
||||
|
||||
pub(crate) async fn run(target: BackendRuntimeListTarget) -> Result<(), Box<dyn Error>> {
|
||||
let response = list_backend_workers(&target).await.map_err(|error| {
|
||||
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| {
|
||||
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
|
||||
|
|
@ -44,11 +62,36 @@ pub(crate) async fn run(target: BackendRuntimeListTarget) -> Result<(), Box<dyn
|
|||
}
|
||||
|
||||
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, selected.runtime_id, selected.worker_id);
|
||||
BackendRuntimeTarget::new(target.base_url, worker.runtime_id, worker.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>,
|
||||
|
|
|
|||
|
|
@ -331,6 +331,15 @@ 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.
|
||||
|
|
@ -338,7 +347,8 @@ pub(crate) async fn run_resume(
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -118,8 +118,11 @@ pub(crate) enum DashboardOutcome {
|
|||
Open(OpenWorkerRequest),
|
||||
}
|
||||
|
||||
pub(crate) async fn launch(runtime_command: WorkerRuntimeCommand) -> Result<(), Box<dyn Error>> {
|
||||
let mut app = load_app(runtime_command.clone()).await?;
|
||||
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?;
|
||||
let mut terminal = crate::console::enter_dashboard_fullscreen()?;
|
||||
loop {
|
||||
match run_loop(&mut terminal, &mut app).await? {
|
||||
|
|
@ -174,8 +177,9 @@ pub(crate) struct OpenWorkerRequest {
|
|||
|
||||
pub(crate) async fn load_app(
|
||||
runtime_command: WorkerRuntimeCommand,
|
||||
include_stopped: bool,
|
||||
) -> Result<DashboardApp, DashboardError> {
|
||||
Ok(DashboardApp::loading(runtime_command))
|
||||
Ok(DashboardApp::loading(runtime_command, include_stopped))
|
||||
}
|
||||
|
||||
async fn run_loop(
|
||||
|
|
@ -222,14 +226,14 @@ async fn run_loop(
|
|||
}
|
||||
|
||||
if let Some(mode) = deferred_enter_reload.take() {
|
||||
if pending_reload.start(mode) {
|
||||
if pending_reload.start(mode, app.include_stopped) {
|
||||
app.refreshing = true;
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
if now >= next_poll {
|
||||
pending_reload.start(OrchestratorLifecycleMode::Observe);
|
||||
pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped);
|
||||
next_poll = now + DASHBOARD_POLL_INTERVAL;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -275,7 +279,8 @@ 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) {
|
||||
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
|
||||
{
|
||||
app.refreshing = true;
|
||||
}
|
||||
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
||||
|
|
@ -311,7 +316,8 @@ async fn run_loop(
|
|||
}
|
||||
Err(error) => app.finish_ready_ticket_planning_return_error(error),
|
||||
}
|
||||
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
|
||||
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
|
||||
{
|
||||
app.refreshing = true;
|
||||
}
|
||||
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
||||
|
|
@ -328,7 +334,8 @@ 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) {
|
||||
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
|
||||
{
|
||||
app.refreshing = true;
|
||||
}
|
||||
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
||||
|
|
@ -345,7 +352,8 @@ 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) {
|
||||
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
|
||||
{
|
||||
app.refreshing = true;
|
||||
}
|
||||
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
||||
|
|
@ -366,7 +374,7 @@ struct PendingReload {
|
|||
}
|
||||
|
||||
impl PendingReload {
|
||||
fn start(&mut self, lifecycle_mode: OrchestratorLifecycleMode) -> bool {
|
||||
fn start(&mut self, lifecycle_mode: OrchestratorLifecycleMode, include_stopped: bool) -> bool {
|
||||
if self.handle.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -382,7 +390,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).await
|
||||
load_dashboard_snapshot(None, lifecycle_mode, include_stopped).await
|
||||
}));
|
||||
true
|
||||
}
|
||||
|
|
@ -1219,6 +1227,7 @@ 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,
|
||||
|
|
@ -1228,7 +1237,7 @@ pub(crate) struct DashboardApp {
|
|||
}
|
||||
|
||||
impl DashboardApp {
|
||||
fn loading(runtime_command: WorkerRuntimeCommand) -> Self {
|
||||
fn loading(runtime_command: WorkerRuntimeCommand, include_stopped: bool) -> Self {
|
||||
let workspace_root = current_workspace_root();
|
||||
let mut panel = WorkspacePanelViewModel::empty(&workspace_root);
|
||||
panel
|
||||
|
|
@ -1257,6 +1266,7 @@ 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(),
|
||||
|
|
@ -2511,6 +2521,7 @@ 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")]
|
||||
|
|
@ -2524,7 +2535,8 @@ 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).await?;
|
||||
let mut list =
|
||||
load_worker_list(list_selected_name.clone(), MAX_ENTRIES, include_stopped).await?;
|
||||
#[cfg(feature = "e2e-test")]
|
||||
source_timings.push(PanelE2eSourceTiming {
|
||||
source: "pod_metadata_status_probe.initial",
|
||||
|
|
@ -2564,7 +2576,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).await?;
|
||||
list = load_worker_list(list_selected_name.clone(), MAX_ENTRIES, include_stopped).await?;
|
||||
#[cfg(feature = "e2e-test")]
|
||||
source_timings.push(PanelE2eSourceTiming {
|
||||
source: "pod_metadata_status_probe.after_companion_reload",
|
||||
|
|
@ -2622,7 +2634,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).await?;
|
||||
list = load_worker_list(list_selected_name, MAX_ENTRIES, include_stopped).await?;
|
||||
#[cfg(feature = "e2e-test")]
|
||||
source_timings.push(PanelE2eSourceTiming {
|
||||
source: "pod_metadata_status_probe.after_orchestrator_reload",
|
||||
|
|
@ -3420,6 +3432,7 @@ 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)?;
|
||||
|
|
@ -3429,14 +3442,18 @@ async fn load_worker_list(
|
|||
let live = read_reachable_live_worker_infos(&store)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
Ok(WorkerList::from_workspace_sources(
|
||||
let mut list = WorkerList::from_workspace_sources(
|
||||
WorkerVisibilitySource::ResumePicker,
|
||||
stored,
|
||||
live,
|
||||
selected_name,
|
||||
max_entries,
|
||||
¤t_workspace_root(),
|
||||
))
|
||||
);
|
||||
if !include_stopped {
|
||||
list.retain_live_entries();
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
let app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"), false);
|
||||
|
||||
assert!(app.panel.rows.is_empty());
|
||||
assert!(
|
||||
|
|
@ -3241,6 +3241,7 @@ 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(),
|
||||
|
|
@ -3392,7 +3393,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"));
|
||||
let mut app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"), false);
|
||||
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())));
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ 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> },
|
||||
Workers {
|
||||
runtime_id: Option<String>,
|
||||
include_stopped: bool,
|
||||
all: bool,
|
||||
},
|
||||
/// `yoi --backend <url> --runtime-id <id> --worker-id <id>`: open one Worker
|
||||
/// through the selected connection target.
|
||||
OpenWorker {
|
||||
|
|
@ -76,7 +80,7 @@ pub enum LaunchMode {
|
|||
worker_name: Option<String>,
|
||||
},
|
||||
/// `yoi panel`: open the workspace Dashboard from the current workspace.
|
||||
Panel,
|
||||
Panel { include_stopped: bool },
|
||||
}
|
||||
|
||||
pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||
|
|
@ -128,12 +132,34 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
|||
}
|
||||
Err(e) => Err(Box::new(e) 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,
|
||||
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>)
|
||||
}
|
||||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
}
|
||||
}
|
||||
},
|
||||
LaunchMode::OpenWorker {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
|
|
@ -153,8 +179,8 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
|||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::Panel => match target.dashboard() {
|
||||
Ok(dashboard) => dashboard::launch(dashboard.runtime_command).await,
|
||||
LaunchMode::Panel { include_stopped } => match target.dashboard() {
|
||||
Ok(dashboard) => dashboard::launch(dashboard.runtime_command, include_stopped).await,
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -80,20 +80,28 @@ 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)]
|
||||
|
|
@ -134,6 +142,11 @@ 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,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@ 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,
|
||||
|
|
@ -106,8 +112,10 @@ impl TaskStore {
|
|||
}
|
||||
"TaskUpdate" => {
|
||||
if let Ok(p) = serde_json::from_str::<TaskUpdateParams>(arguments)
|
||||
&& let Some(t) = self.tasks.iter_mut().find(|t| t.taskid == p.taskid)
|
||||
&& let Some(task_position) =
|
||||
self.tasks.iter().position(|t| t.taskid == p.taskid)
|
||||
{
|
||||
let t = &mut self.tasks[task_position];
|
||||
if let Some(s) = p.status {
|
||||
t.status = s;
|
||||
}
|
||||
|
|
@ -117,6 +125,9 @@ impl TaskStore {
|
|||
if let Some(d) = p.description {
|
||||
t.description = d;
|
||||
}
|
||||
if !t.status.is_active() {
|
||||
self.tasks.remove(task_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -132,6 +143,10 @@ 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)
|
||||
|
|
@ -175,7 +190,13 @@ 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)
|
||||
Some(
|
||||
snapshot
|
||||
.tasks
|
||||
.into_iter()
|
||||
.filter(|task| task.status.is_active())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -220,7 +241,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn counts_classifies_each_status() {
|
||||
fn counts_tracks_only_active_tasks_after_completion() {
|
||||
let mut s = TaskStore::new();
|
||||
s.apply_tool_call("TaskCreate", r#"{"subject":"a","description":""}"#);
|
||||
s.apply_tool_call("TaskCreate", r#"{"subject":"b","description":""}"#);
|
||||
|
|
@ -230,9 +251,9 @@ mod tests {
|
|||
let c = s.counts();
|
||||
assert_eq!(c.pending, 1);
|
||||
assert_eq!(c.inprogress, 1);
|
||||
assert_eq!(c.completed, 1);
|
||||
assert_eq!(c.completed, 0);
|
||||
assert_eq!(c.deleted, 0);
|
||||
assert_eq!(c.total(), 3);
|
||||
assert_eq!(c.total(), 2);
|
||||
assert_eq!(c.active(), 2);
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +263,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 complete session task list preserved across compaction. \
|
||||
This is the active session task list preserved across compaction. \
|
||||
The following TaskList tool result presents the same state through the tool lane."
|
||||
)
|
||||
}
|
||||
|
|
@ -267,20 +288,19 @@ mod tests {
|
|||
}"#;
|
||||
let text = wrap_snapshot(
|
||||
body,
|
||||
"TaskStore: 2 task(s) (pending: 1, inprogress: 0, completed: 1, deleted: 0)",
|
||||
"TaskStore: 1 active task(s) (pending: 1, inprogress: 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(), 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
|
||||
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
|
||||
// observed in the snapshot.
|
||||
s.apply_tool_call("TaskCreate", r#"{"subject":"new","description":""}"#);
|
||||
assert_eq!(s.tasks()[2].taskid, 8);
|
||||
assert_eq!(s.tasks()[1].taskid, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -304,7 +324,7 @@ mod tests {
|
|||
}
|
||||
]
|
||||
}"#;
|
||||
let text = wrap_snapshot(body, "TaskStore: 1 task(s)");
|
||||
let text = wrap_snapshot(body, "TaskStore: 1 active task(s)");
|
||||
let mut s = TaskStore::new();
|
||||
s.apply_system_message_text(&text);
|
||||
let t = &s.tasks()[0];
|
||||
|
|
@ -329,13 +349,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 complete session task list preserved across compaction. \
|
||||
This is the active 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: 2 task(s) (pending: 0, inprogress: 1, completed: 1, deleted: 0)
|
||||
r#"TaskStore: 1 active task(s) (pending: 0, inprogress: 1)
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -345,12 +365,6 @@ mod snapshot_format_contract {
|
|||
"status": "inprogress",
|
||||
"subject": "first",
|
||||
"description": "first desc"
|
||||
},
|
||||
{
|
||||
"taskid": 2,
|
||||
"status": "completed",
|
||||
"subject": "second",
|
||||
"description": "second desc with\nnewline"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -358,7 +372,7 @@ mod snapshot_format_contract {
|
|||
}
|
||||
|
||||
fn empty_snapshot_fixture() -> &'static str {
|
||||
r#"TaskStore: 0 task(s) (pending: 0, inprogress: 0, completed: 0, deleted: 0)
|
||||
r#"TaskStore: 0 active task(s) (pending: 0, inprogress: 0)
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -384,15 +398,11 @@ mod snapshot_format_contract {
|
|||
downstream.apply_system_message_text(&envelope);
|
||||
|
||||
let tasks = downstream.tasks();
|
||||
assert_eq!(tasks.len(), 2, "TUI parsed wrong number of tasks");
|
||||
assert_eq!(tasks.len(), 1, "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]
|
||||
|
|
|
|||
|
|
@ -126,6 +126,17 @@ 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)
|
||||
|
|
@ -736,6 +747,28 @@ 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();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ license.workspace = true
|
|||
autobins = false
|
||||
|
||||
[[bin]]
|
||||
name = "worker-runtime-rest-server"
|
||||
name = "yoi-runtime"
|
||||
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 = []
|
||||
default = ["ws-server", "fs-store"]
|
||||
fs-store = []
|
||||
http-server = ["dep:axum", "dep:tower", "dep:reqwest"]
|
||||
ws-server = ["http-server", "axum/ws", "dep:futures", "tokio/sync"]
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ From the repository root:
|
|||
|
||||
```bash
|
||||
cargo run -p worker-runtime \
|
||||
--features ws-server,fs-store \
|
||||
--bin worker-runtime-rest-server \
|
||||
--bin yoi-runtime \
|
||||
-- --bind 127.0.0.1:38800
|
||||
```
|
||||
|
||||
|
|
@ -23,8 +22,7 @@ To bind another address explicitly:
|
|||
|
||||
```bash
|
||||
cargo run -p worker-runtime \
|
||||
--features ws-server,fs-store \
|
||||
--bin worker-runtime-rest-server \
|
||||
--bin yoi-runtime \
|
||||
-- --bind 0.0.0.0:38800
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fn main() -> ExitCode {
|
|||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("worker-runtime-rest-server: {error}");
|
||||
eprintln!("yoi-runtime: {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!(
|
||||
"worker-runtime REST server listening on {local_addr}; intended client is a trusted backend/proxy, not a browser"
|
||||
"yoi-runtime 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: worker-runtime-rest-server [OPTIONS]
|
||||
r#"Usage: yoi-runtime [OPTIONS]
|
||||
|
||||
Starts a worker-backed Runtime REST command API for a trusted backend/proxy.
|
||||
Browsers must not connect to this Runtime process directly.
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
self.state.task_store.replace_with(restored.list_active());
|
||||
}
|
||||
|
||||
/// 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())
|
||||
snapshot_overview(&self.state.task_store.list_active())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ 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 {
|
||||
|
|
@ -54,6 +60,8 @@ pub struct TaskSnapshot {
|
|||
pub tasks: Vec<TaskEntry>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_TASK_LIST_LIMIT: usize = 20;
|
||||
|
||||
impl TaskStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
@ -85,6 +93,13 @@ 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()
|
||||
|
|
@ -106,11 +121,12 @@ impl TaskStore {
|
|||
return Err(TaskStoreError::NoFields);
|
||||
}
|
||||
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let task = inner
|
||||
let task_position = inner
|
||||
.tasks
|
||||
.iter_mut()
|
||||
.find(|t| t.taskid == taskid)
|
||||
.iter()
|
||||
.position(|t| t.taskid == taskid)
|
||||
.ok_or(TaskStoreError::Missing(taskid))?;
|
||||
let task = &mut inner.tasks[task_position];
|
||||
if let Some(status) = status {
|
||||
task.status = status;
|
||||
}
|
||||
|
|
@ -120,11 +136,21 @@ impl TaskStore {
|
|||
if let Some(description) = description {
|
||||
task.description = description;
|
||||
}
|
||||
Ok(task.clone())
|
||||
let updated = task.clone();
|
||||
if !updated.status.is_active() {
|
||||
inner.tasks.remove(task_position);
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> TaskSnapshot {
|
||||
TaskSnapshot { tasks: self.list() }
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replay_history(&self, history: &[Item]) {
|
||||
|
|
@ -168,6 +194,10 @@ 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)
|
||||
|
|
@ -237,27 +267,26 @@ pub fn snapshot_overview(tasks: &[TaskEntry]) -> String {
|
|||
.iter()
|
||||
.filter(|t| t.status == TaskStatus::Inprogress)
|
||||
.count();
|
||||
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()
|
||||
)
|
||||
let active = pending + inprogress;
|
||||
format!("TaskStore: {active} active task(s) (pending: {pending}, inprogress: {inprogress})")
|
||||
}
|
||||
|
||||
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: tasks.to_vec(),
|
||||
tasks: active_tasks.clone(),
|
||||
};
|
||||
let json =
|
||||
serde_json::to_string_pretty(&snapshot).unwrap_or_else(|_| String::from("{\"tasks\":[]}"));
|
||||
format!("{}\n\n```json\n{}\n```\n", snapshot_overview(tasks), json)
|
||||
format!(
|
||||
"{}\n\n```json\n{}\n```\n",
|
||||
snapshot_overview(&active_tasks),
|
||||
json
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn parse_compact_snapshot_text(text: &str) -> Option<Vec<TaskEntry>> {
|
||||
|
|
@ -270,7 +299,13 @@ 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)
|
||||
Some(
|
||||
snapshot
|
||||
.tasks
|
||||
.into_iter()
|
||||
.filter(|task| task.status.is_active())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -288,11 +323,9 @@ mod tests {
|
|||
];
|
||||
let store = TaskStore::from_history(&history);
|
||||
let tasks = store.list();
|
||||
assert_eq!(tasks.len(), 2);
|
||||
assert_eq!(tasks.len(), 1);
|
||||
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
|
||||
|
|
@ -300,7 +333,7 @@ mod tests {
|
|||
fn wrap_snapshot_system_message(snapshot: &str) -> String {
|
||||
format!(
|
||||
"[Session TaskStore snapshot]\n\n{snapshot}\n\n\
|
||||
This is the complete session task list preserved across compaction. \
|
||||
This is the active session task list preserved across compaction. \
|
||||
The following TaskList tool result presents the same state through the tool lane."
|
||||
)
|
||||
}
|
||||
|
|
@ -322,11 +355,9 @@ mod tests {
|
|||
];
|
||||
let store = TaskStore::from_history(&history);
|
||||
let tasks = store.list();
|
||||
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");
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0].taskid, 2);
|
||||
assert_eq!(tasks[0].subject, "new");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -365,15 +396,12 @@ mod tests {
|
|||
];
|
||||
let store = TaskStore::from_history(&history);
|
||||
let tasks = store.list();
|
||||
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");
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -415,9 +443,10 @@ 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(&pre.list()),
|
||||
snapshot_overview(&active_tasks),
|
||||
snapshot_text.clone(),
|
||||
);
|
||||
|
||||
|
|
@ -426,7 +455,7 @@ mod tests {
|
|||
.as_text()
|
||||
.and_then(parse_compact_snapshot_text)
|
||||
.expect("system message should parse as snapshot");
|
||||
assert_eq!(extracted, pre.list());
|
||||
assert_eq!(extracted, active_tasks);
|
||||
|
||||
// The synthetic call/result pair shares one call_id and carries the
|
||||
// expected tool name + detailed content.
|
||||
|
|
@ -453,6 +482,6 @@ mod tests {
|
|||
|
||||
// Replaying the full triple reconstructs the same TaskStore.
|
||||
let store = TaskStore::from_history(&[system, call, result]);
|
||||
assert_eq!(store.list(), pre.list());
|
||||
assert_eq!(store.list(), active_tasks);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use async_trait::async_trait;
|
|||
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::store::{TaskEntry, TaskStatus, TaskStore, render_snapshot, snapshot_overview};
|
||||
use super::store::{DEFAULT_TASK_LIST_LIMIT, TaskEntry, TaskStatus, TaskStore, snapshot_overview};
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TaskCreateParams {
|
||||
|
|
@ -17,7 +17,11 @@ struct TaskCreateParams {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TaskListParams {}
|
||||
struct TaskListParams {
|
||||
/// Maximum number of active tasks to return. Defaults to 20.
|
||||
#[serde(default)]
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TaskGetParams {
|
||||
|
|
@ -58,9 +62,7 @@ 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 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 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 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.";
|
||||
|
|
@ -81,7 +83,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();
|
||||
let tasks = self.store.list_active();
|
||||
Ok(task_output(
|
||||
format!(
|
||||
"Created task {} ({})\n{}",
|
||||
|
|
@ -90,7 +92,6 @@ impl Tool for TaskCreateTool {
|
|||
snapshot_overview(&tasks)
|
||||
),
|
||||
&created,
|
||||
&tasks,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -102,12 +103,14 @@ impl Tool for TaskListTool {
|
|||
input_json: &str,
|
||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let _: TaskListParams = serde_json::from_str(input_json)
|
||||
let params: TaskListParams = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid TaskList input: {e}")))?;
|
||||
let tasks = self.store.list();
|
||||
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();
|
||||
Ok(ToolOutput {
|
||||
summary: snapshot_overview(&tasks),
|
||||
content: Some(render_snapshot(&tasks)),
|
||||
summary: list_overview(active_tasks.len(), tasks.len()),
|
||||
content: Some(render_task_list(&tasks)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -150,7 +153,7 @@ impl Tool for TaskUpdateTool {
|
|||
params.description,
|
||||
)
|
||||
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
|
||||
let tasks = self.store.list();
|
||||
let tasks = self.store.list_active();
|
||||
Ok(task_output(
|
||||
format!(
|
||||
"Updated task {} ({})\n{}",
|
||||
|
|
@ -159,21 +162,32 @@ impl Tool for TaskUpdateTool {
|
|||
snapshot_overview(&tasks)
|
||||
),
|
||||
&updated,
|
||||
&tasks,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn task_output(summary: String, task: &TaskEntry, tasks: &[TaskEntry]) -> ToolOutput {
|
||||
let content = serde_json::json!({
|
||||
"task": task,
|
||||
"snapshot": { "tasks": tasks },
|
||||
});
|
||||
fn task_output(summary: String, task: &TaskEntry) -> ToolOutput {
|
||||
ToolOutput {
|
||||
summary,
|
||||
content: Some(serde_json::to_string_pretty(&content).unwrap_or_default()),
|
||||
content: Some(serde_json::to_string_pretty(task).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);
|
||||
|
|
@ -264,6 +278,10 @@ 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
|
||||
|
|
@ -286,10 +304,106 @@ mod tests {
|
|||
assert!(out.content.unwrap().contains("implement tasks"));
|
||||
|
||||
let out = list.execute("{}", Default::default()).await.unwrap();
|
||||
assert!(out.summary.contains("1 task(s)"));
|
||||
assert!(out.summary.contains("1 active task(s)"));
|
||||
let content = out.content.unwrap();
|
||||
assert!(content.contains("\"taskid\": 1"));
|
||||
assert!(content.contains("```json"));
|
||||
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"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -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 complete session task list preserved across compaction. \
|
||||
This is the active 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());
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ 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
|
||||
|
|
@ -25,7 +30,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, features = ["ws-server", "fs-store"] }
|
||||
worker-runtime.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
|
|
|
|||
|
|
@ -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 workspace init --workspace {}` first",
|
||||
"workspace backend local config `{}` does not exist; run `yoi-server init --workspace {}` first",
|
||||
path.display(),
|
||||
workspace_root.display()
|
||||
))),
|
||||
|
|
|
|||
|
|
@ -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 workspace init --workspace {}` first",
|
||||
"workspace is not initialized at {}; run `yoi-server init --workspace {}` first",
|
||||
workspace_root.as_ref().display(),
|
||||
workspace_root.as_ref().display()
|
||||
)))
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ async fn main() -> ExitCode {
|
|||
match run().await {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("yoi-workspace-server: {error}");
|
||||
eprintln!("yoi-server: {error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
|
|
@ -160,7 +160,7 @@ async fn run_init_with_database_path(
|
|||
})?;
|
||||
|
||||
eprintln!(
|
||||
"yoi-workspace-server: initialized workspace `{}` ({}) in server DB `{}`",
|
||||
"yoi-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-workspace-server: serving workspace `{}` from server DB `{}` on http://{}",
|
||||
"yoi-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-workspace-server identity init`".to_string(),
|
||||
"trusted runtimes are registered but server identity is not initialized; run `yoi-server identity init`".to_string(),
|
||||
)));
|
||||
}
|
||||
return Ok(());
|
||||
|
|
@ -617,7 +617,8 @@ 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-workspace-server init --workspace <PATH>`".to_string(),
|
||||
"server DB has no workspace records; run `yoi-server init --workspace <PATH>`"
|
||||
.to_string(),
|
||||
)),
|
||||
[workspace] => Ok(workspace.clone()),
|
||||
_ => Err(CliError(format!(
|
||||
|
|
@ -825,31 +826,31 @@ fn parse_listen(value: &str) -> Result<SocketAddr, CliError> {
|
|||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"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"
|
||||
"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"
|
||||
);
|
||||
}
|
||||
|
||||
fn print_init_help() {
|
||||
println!(
|
||||
"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"
|
||||
"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"
|
||||
);
|
||||
}
|
||||
|
||||
fn print_config_help() {
|
||||
println!(
|
||||
"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"
|
||||
"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"
|
||||
);
|
||||
}
|
||||
|
||||
fn print_skills_help() {
|
||||
println!(
|
||||
"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"
|
||||
"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"
|
||||
);
|
||||
}
|
||||
|
||||
fn print_serve_help() {
|
||||
println!(
|
||||
"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"
|
||||
"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"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use client::{BackendTarget, LocalTarget, Target, TargetKind};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{ParseError, resolve_backend_url};
|
||||
use super::{ParseError, read_client_default_connection, resolve_backend_url};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CliConnectionRequirement {
|
||||
|
|
@ -26,7 +26,6 @@ pub(crate) enum CliCommand {
|
|||
Mcp,
|
||||
MemoryLint,
|
||||
Session,
|
||||
WorkspaceServer,
|
||||
Login,
|
||||
}
|
||||
|
||||
|
|
@ -47,18 +46,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 => CliConnectionRequirement::ConnectionAware,
|
||||
CliCommand::Workers | CliCommand::Login => CliConnectionRequirement::BackendOnly,
|
||||
CliCommand::Resume
|
||||
| CliCommand::Panel
|
||||
| CliCommand::Keys
|
||||
CliCommand::DefaultTui
|
||||
| CliCommand::Workers
|
||||
| CliCommand::Resume
|
||||
| CliCommand::Panel => CliConnectionRequirement::ConnectionAware,
|
||||
CliCommand::Login => CliConnectionRequirement::BackendOnly,
|
||||
CliCommand::Keys
|
||||
| CliCommand::SetupModel
|
||||
| CliCommand::WorkerRuntime
|
||||
| CliCommand::WorkerCleanup
|
||||
|
|
@ -67,15 +66,13 @@ impl CliCommand {
|
|||
| CliCommand::Plugin
|
||||
| CliCommand::Mcp
|
||||
| CliCommand::MemoryLint
|
||||
| CliCommand::Session
|
||||
| CliCommand::WorkspaceServer => CliConnectionRequirement::LocalOnly,
|
||||
| CliCommand::Session => CliConnectionRequirement::LocalOnly,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum ClientDefaultConnection {
|
||||
Local,
|
||||
Backend,
|
||||
|
|
@ -89,7 +86,10 @@ impl Default for ClientDefaultConnection {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum CliConnectionInput<'a> {
|
||||
LocalDefault,
|
||||
DefaultTarget {
|
||||
workspace_id: Option<&'a str>,
|
||||
},
|
||||
LocalTarget,
|
||||
BackendTarget {
|
||||
explicit_backend_url: Option<String>,
|
||||
workspace_id: Option<&'a str>,
|
||||
|
|
@ -114,21 +114,29 @@ impl CliConnectionResolver for ClientConfigCliConnectionResolver {
|
|||
input: CliConnectionInput<'_>,
|
||||
) -> Result<Box<dyn Target>, ParseError> {
|
||||
match (command.connection_requirement(), input) {
|
||||
(CliConnectionRequirement::LocalOnly, CliConnectionInput::LocalDefault) => {
|
||||
Ok(Box::new(LocalTarget::new()))
|
||||
}
|
||||
(
|
||||
CliConnectionRequirement::LocalOnly,
|
||||
CliConnectionInput::DefaultTarget { .. } | CliConnectionInput::LocalTarget,
|
||||
) => 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::LocalDefault) => {
|
||||
(CliConnectionRequirement::BackendOnly, CliConnectionInput::LocalTarget) => {
|
||||
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 {
|
||||
|
|
@ -139,9 +147,19 @@ impl CliConnectionResolver for ClientConfigCliConnectionResolver {
|
|||
resolve_backend_url(explicit_backend_url, workspace_id)?,
|
||||
workspace_id.map(str::to_string),
|
||||
))),
|
||||
(CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalDefault) => {
|
||||
(CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalTarget) => {
|
||||
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),
|
||||
))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -150,7 +168,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::LocalDefault)?;
|
||||
let target = resolver.resolve_connection(command, CliConnectionInput::LocalTarget)?;
|
||||
match target.kind() {
|
||||
TargetKind::Local => Ok(target),
|
||||
TargetKind::Backend => Err(ParseError(format!(
|
||||
|
|
@ -182,6 +200,33 @@ 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,
|
||||
|
|
@ -209,22 +254,25 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn cli_command_connection_requirements_are_explicit() {
|
||||
for command in [
|
||||
CliCommand::DefaultTui,
|
||||
CliCommand::Workers,
|
||||
CliCommand::Resume,
|
||||
CliCommand::Panel,
|
||||
] {
|
||||
assert_eq!(
|
||||
CliCommand::DefaultTui.connection_requirement(),
|
||||
CliConnectionRequirement::ConnectionAware
|
||||
);
|
||||
assert_eq!(
|
||||
CliCommand::Workers.connection_requirement(),
|
||||
CliConnectionRequirement::BackendOnly
|
||||
command.connection_requirement(),
|
||||
CliConnectionRequirement::ConnectionAware,
|
||||
"{} should be connection-aware",
|
||||
command.display_name()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
CliCommand::Login.connection_requirement(),
|
||||
CliConnectionRequirement::BackendOnly
|
||||
);
|
||||
|
||||
for command in [
|
||||
CliCommand::Resume,
|
||||
CliCommand::Panel,
|
||||
CliCommand::Keys,
|
||||
CliCommand::SetupModel,
|
||||
CliCommand::WorkerRuntime,
|
||||
|
|
@ -235,7 +283,6 @@ mod tests {
|
|||
CliCommand::Mcp,
|
||||
CliCommand::MemoryLint,
|
||||
CliCommand::Session,
|
||||
CliCommand::WorkspaceServer,
|
||||
] {
|
||||
assert_eq!(
|
||||
command.connection_requirement(),
|
||||
|
|
@ -251,7 +298,7 @@ mod tests {
|
|||
let resolver = ClientConfigCliConnectionResolver;
|
||||
let err = resolver
|
||||
.resolve_connection(
|
||||
CliCommand::Resume,
|
||||
CliCommand::Keys,
|
||||
CliConnectionInput::BackendTarget {
|
||||
explicit_backend_url: Some("http://127.0.0.1:8787".to_string()),
|
||||
workspace_id: None,
|
||||
|
|
@ -261,20 +308,20 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi resume uses a local connection target and cannot accept Backend target options"
|
||||
"yoi keys uses a local connection target and cannot accept Backend target options"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_connection_resolver_rejects_backend_only_local_default() {
|
||||
fn cli_connection_resolver_rejects_backend_only_local_target() {
|
||||
let resolver = ClientConfigCliConnectionResolver;
|
||||
let err = resolver
|
||||
.resolve_connection(CliCommand::Workers, CliConnectionInput::LocalDefault)
|
||||
.resolve_connection(CliCommand::Login, CliConnectionInput::LocalTarget)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi workers requires a Backend connection target"
|
||||
"yoi login requires a Backend connection target"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +340,8 @@ mod tests {
|
|||
let workers = target
|
||||
.list_workers(client::WorkerListRequest::new(None))
|
||||
.unwrap();
|
||||
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(workers.target.workspace_id, None);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -231,7 +231,7 @@ in
|
|||
tag = imageTag;
|
||||
copyToRoot = runtimeRoot;
|
||||
config = {
|
||||
Entrypoint = [ "/bin/worker-runtime-rest-server" ];
|
||||
Entrypoint = [ "/bin/yoi-runtime" ];
|
||||
Cmd = [
|
||||
"--bind"
|
||||
"0.0.0.0:38800"
|
||||
|
|
@ -259,7 +259,7 @@ in
|
|||
tag = imageTag;
|
||||
copyToRoot = serverRoot;
|
||||
config = {
|
||||
Entrypoint = [ "/bin/yoi-workspace-server" ];
|
||||
Entrypoint = [ "/bin/yoi-server" ];
|
||||
Cmd = [
|
||||
"serve"
|
||||
"--listen"
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ Current image roles:
|
|||
|
||||
```text
|
||||
yoi-runtime:latest
|
||||
worker-runtime-rest-server
|
||||
yoi-runtime
|
||||
|
||||
yoi-server:latest
|
||||
yoi-workspace-server
|
||||
yoi-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 `worker-runtime-rest-server` 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 `yoi-runtime` 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ RUNTIME_BASE_URL=http://127.0.0.1:38800
|
|||
From the Workspace Server host:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server identity init --server-id server-main
|
||||
yoi-server identity init --server-id server-main
|
||||
```
|
||||
|
||||
Show the public identity and copy the `public_key` value:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server identity show --json
|
||||
yoi-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
|
||||
worker-runtime-rest-server identity init --runtime-id runtime-main
|
||||
yoi-runtime identity init --runtime-id runtime-main
|
||||
```
|
||||
|
||||
Show the public identity and copy the `public_key` value:
|
||||
|
||||
```bash
|
||||
worker-runtime-rest-server identity show --json
|
||||
yoi-runtime 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
|
||||
worker-runtime-rest-server identity init \
|
||||
yoi-runtime identity init \
|
||||
--runtime-id runtime-main \
|
||||
--fs-root /var/lib/yoi-runtime
|
||||
|
||||
worker-runtime-rest-server identity show \
|
||||
yoi-runtime 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-workspace-server identity show --json`:
|
||||
On the Runtime host, register the Server public key copied from `yoi-server identity show --json`:
|
||||
|
||||
```bash
|
||||
worker-runtime-rest-server trust-server add \
|
||||
yoi-runtime trust-server add \
|
||||
--server-id server-main \
|
||||
--public-key '<SERVER_PUBLIC_KEY>'
|
||||
```
|
||||
|
|
@ -92,7 +92,7 @@ worker-runtime-rest-server trust-server add \
|
|||
With explicit Runtime storage, keep using the same storage flags:
|
||||
|
||||
```bash
|
||||
worker-runtime-rest-server trust-server add \
|
||||
yoi-runtime trust-server add \
|
||||
--server-id server-main \
|
||||
--public-key '<SERVER_PUBLIC_KEY>' \
|
||||
--fs-root /var/lib/yoi-runtime
|
||||
|
|
@ -101,27 +101,27 @@ worker-runtime-rest-server trust-server add \
|
|||
Verify:
|
||||
|
||||
```bash
|
||||
worker-runtime-rest-server trust-server list --json
|
||||
yoi-runtime 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 `worker-runtime-rest-server identity show --json`:
|
||||
On the Workspace Server host, register the Runtime public key copied from `yoi-runtime identity show --json`:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server trust-runtime add \
|
||||
yoi-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-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.
|
||||
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.
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server trust-runtime list --json
|
||||
yoi-server trust-runtime list --json
|
||||
```
|
||||
|
||||
## 5. Start Runtime and Workspace Server
|
||||
|
|
@ -129,7 +129,7 @@ yoi-workspace-server trust-runtime list --json
|
|||
Start Runtime with the same storage flags used during Runtime identity/trust setup:
|
||||
|
||||
```bash
|
||||
worker-runtime-rest-server \
|
||||
yoi-runtime \
|
||||
--bind 127.0.0.1:38800
|
||||
```
|
||||
|
||||
|
|
@ -137,27 +137,26 @@ For repository builds, the equivalent cargo command is:
|
|||
|
||||
```bash
|
||||
cargo run -p worker-runtime \
|
||||
--features ws-server,fs-store \
|
||||
--bin worker-runtime-rest-server \
|
||||
--bin yoi-runtime \
|
||||
-- --bind 127.0.0.1:38800
|
||||
```
|
||||
|
||||
Start Workspace Server:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server serve --listen 127.0.0.1:8787
|
||||
yoi-server serve --listen 127.0.0.1:8787
|
||||
```
|
||||
|
||||
For repository builds:
|
||||
|
||||
```bash
|
||||
cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787
|
||||
cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787
|
||||
```
|
||||
|
||||
If the Server DB has no workspace record yet, initialize it first:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server init --workspace <WORKSPACE_ROOT>
|
||||
yoi-server init --workspace <WORKSPACE_ROOT>
|
||||
```
|
||||
|
||||
## Smoke checks
|
||||
|
|
@ -165,8 +164,8 @@ yoi-workspace-server init --workspace <WORKSPACE_ROOT>
|
|||
Check both trust stores:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server trust-runtime list --json
|
||||
worker-runtime-rest-server trust-server list --json
|
||||
yoi-server trust-runtime list --json
|
||||
yoi-runtime trust-server list --json
|
||||
```
|
||||
|
||||
Check that Workspace Server can see Runtime workers through the authenticated path. From the CLI:
|
||||
|
|
@ -186,13 +185,13 @@ Identity and trust records are intentionally not overwritten by default.
|
|||
Rotate Server identity:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server identity init --server-id server-main --replace
|
||||
yoi-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
|
||||
worker-runtime-rest-server trust-server add \
|
||||
yoi-runtime trust-server add \
|
||||
--server-id server-main \
|
||||
--public-key '<NEW_SERVER_PUBLIC_KEY>' \
|
||||
--replace
|
||||
|
|
@ -201,13 +200,13 @@ worker-runtime-rest-server trust-server add \
|
|||
Rotate Runtime identity:
|
||||
|
||||
```bash
|
||||
worker-runtime-rest-server identity init --runtime-id runtime-main --replace
|
||||
yoi-runtime identity init --runtime-id runtime-main --replace
|
||||
```
|
||||
|
||||
After Runtime identity rotation, Server must be updated with the new Runtime public key:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server trust-runtime add \
|
||||
yoi-server trust-runtime add \
|
||||
--runtime-id runtime-main \
|
||||
--base-url http://127.0.0.1:38800 \
|
||||
--public-key '<NEW_RUNTIME_PUBLIC_KEY>' \
|
||||
|
|
@ -219,13 +218,13 @@ yoi-workspace-server trust-runtime add \
|
|||
Revoke a trusted Runtime on Server:
|
||||
|
||||
```bash
|
||||
yoi-workspace-server trust-runtime revoke --runtime-id runtime-main
|
||||
yoi-server trust-runtime revoke --runtime-id runtime-main
|
||||
```
|
||||
|
||||
Remove a trusted Server from Runtime:
|
||||
|
||||
```bash
|
||||
worker-runtime-rest-server trust-server revoke --server-id server-main
|
||||
yoi-runtime trust-server revoke --server-id server-main
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
|
@ -235,7 +234,7 @@ worker-runtime-rest-server 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-workspace-server identity init --server-id server-main
|
||||
yoi-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.
|
||||
|
|
@ -245,8 +244,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
|
||||
worker-runtime-rest-server identity show --json
|
||||
worker-runtime-rest-server trust-server list --json
|
||||
yoi-runtime identity show --json
|
||||
yoi-runtime trust-server list --json
|
||||
```
|
||||
|
||||
Also confirm the Runtime process was started with the same `--fs-root` / `--fs-runtime-dir` used for setup.
|
||||
|
|
@ -256,8 +255,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
|
||||
worker-runtime-rest-server identity show --json
|
||||
yoi-workspace-server trust-runtime list --json
|
||||
yoi-runtime identity show --json
|
||||
yoi-server trust-runtime list --json
|
||||
```
|
||||
|
||||
`RUNTIME_ID` is the token audience; mismatches are rejected by Runtime.
|
||||
|
|
|
|||
24
package.nix
24
package.nix
|
|
@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
|
|||
filter = sourceFilter;
|
||||
};
|
||||
|
||||
cargoHash = "sha256-iZaTREhL/aLixn67A1+Gi9opqh2j/yyFuGMyBBvuATM=";
|
||||
cargoHash = "sha256-R33Ty4414wGkqnwCt08zbDQbVu9ggMC5I3ZawgloNT0=";
|
||||
|
||||
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-workspace-server
|
||||
cargo build --offline --profile release -p worker-runtime --bin worker-runtime-rest-server --features ws-server,fs-store
|
||||
cargo build --offline --profile release -p yoi-workspace-server --bin yoi-server
|
||||
cargo build --offline --profile release -p worker-runtime --bin yoi-runtime
|
||||
'';
|
||||
|
||||
# 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-workspace-server | head -n 1)
|
||||
worker_runtime_bin=$(find . -type f -name worker-runtime-rest-server | 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)
|
||||
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-workspace-server -o -name worker-runtime-rest-server \) -print >&2
|
||||
find . -maxdepth 6 -type f \( -name yoi -o -name yoi-server -o -name yoi-runtime \) -print >&2
|
||||
exit 1
|
||||
fi
|
||||
install -Dm755 "$yoi_bin" "$out/bin/yoi"
|
||||
install -Dm755 "$workspace_server_bin" "$out/bin/yoi-workspace-server"
|
||||
install -Dm755 "$worker_runtime_bin" "$out/bin/worker-runtime-rest-server"
|
||||
install -Dm755 "$workspace_server_bin" "$out/bin/yoi-server"
|
||||
install -Dm755 "$worker_runtime_bin" "$out/bin/yoi-runtime"
|
||||
|
||||
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-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 -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 ! -e "$out/bin/yoi-pod"
|
||||
test ! -e "$out/share/yoi/resources"
|
||||
if "$out/bin/yoi" --session not-a-uuid 2>yoi.err; then
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# Workspace Backend local config template.
|
||||
#
|
||||
# `yoi workspace init` copies this packaged template to
|
||||
# `yoi-server 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 workspace config default
|
||||
# yoi-server config default
|
||||
#
|
||||
# Compare the local config with the latest packaged template with:
|
||||
# yoi workspace config diff
|
||||
# yoi-server 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.
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ usage() {
|
|||
Usage: $(basename "$0") <start|stop|restart|status>
|
||||
|
||||
Manage the local Yoi development stack for this checkout:
|
||||
runtime target/debug/worker-runtime-rest-server --bind $RUNTIME_BIND
|
||||
backend target/debug/yoi-workspace-server serve --listen $BACKEND_LISTEN
|
||||
runtime target/debug/yoi-runtime --bind $RUNTIME_BIND
|
||||
backend target/debug/yoi-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 --features ws-server,fs-store --bin worker-runtime-rest-server
|
||||
run_cargo build -p worker-runtime --bin yoi-runtime
|
||||
)
|
||||
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-workspace-server
|
||||
run_cargo build -p yoi-workspace-server --bin yoi-server
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -327,7 +327,7 @@ start_runtime() {
|
|||
fi
|
||||
local port runtime_bin
|
||||
port="$(port_for_addr "$RUNTIME_BIND")"
|
||||
runtime_bin="$ROOT_DIR/target/debug/worker-runtime-rest-server"
|
||||
runtime_bin="$ROOT_DIR/target/debug/yoi-runtime"
|
||||
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-workspace-server"
|
||||
backend_bin="$ROOT_DIR/target/debug/yoi-server"
|
||||
if [[ ! -x "$backend_bin" ]]; then
|
||||
printf 'backend binary not found or not executable: %s\n' "$backend_bin" >&2
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -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-workspace-server` backend.
|
||||
behavior live in the Rust `yoi-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 -- serve --listen 127.0.0.1:8787
|
||||
cargo run -p yoi-workspace-server --bin yoi-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 -- init --workspace .` first when the server
|
||||
`cargo run -p yoi-workspace-server --bin yoi-server -- init --workspace .` first when the server
|
||||
DB has not been initialized.
|
||||
|
||||
## Static build
|
||||
|
|
|
|||
|
|
@ -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 -- serve --listen 127.0.0.1:8787",
|
||||
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user