ui: remove legacy local worker projection
This commit is contained in:
parent
62420b7cc4
commit
135667417b
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -6037,8 +6037,6 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"manifest",
|
|
||||||
"pod-store",
|
|
||||||
"project-record",
|
"project-record",
|
||||||
"protocol",
|
"protocol",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ publish = false
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
axum = { workspace = true, features = ["ws"] }
|
axum = { workspace = true, features = ["ws"] }
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
manifest = { workspace = true }
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
pod-store = { workspace = true }
|
|
||||||
protocol = { workspace = true }
|
protocol = { workspace = true }
|
||||||
project-record.workspace = true
|
project-record.workspace = true
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "native-tls"] }
|
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "native-tls"] }
|
||||||
|
|
|
||||||
|
|
@ -564,14 +564,12 @@ fn diagnostic(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::hosts::{EmbeddedWorkerRuntime, LocalWorkerRuntime, RuntimeRegistry};
|
use crate::hosts::{EmbeddedWorkerRuntime, RuntimeRegistry};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn companion_spawns_visible_worker_and_records_providerless_turn() {
|
fn companion_spawns_visible_worker_and_records_providerless_turn() {
|
||||||
let registry = RuntimeRegistry::for_workspace(
|
let registry =
|
||||||
LocalWorkerRuntime::new("local:test", "/workspace/project", None),
|
RuntimeRegistry::for_workspace(EmbeddedWorkerRuntime::new_memory("local:test"));
|
||||||
EmbeddedWorkerRuntime::new_memory("local:test"),
|
|
||||||
);
|
|
||||||
let registry = Arc::new(registry);
|
let registry = Arc::new(registry);
|
||||||
let companion = CompanionConsole::new(registry.clone());
|
let companion = CompanionConsole::new(registry.clone());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,12 @@
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use pod_store::WorkerMetadata;
|
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use reqwest::blocking::{Client as BlockingHttpClient, RequestBuilder};
|
use reqwest::blocking::{Client as BlockingHttpClient, RequestBuilder};
|
||||||
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
|
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{
|
use std::{sync::Arc, time::Duration};
|
||||||
collections::BTreeSet,
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use worker_runtime::catalog::{
|
use worker_runtime::catalog::{
|
||||||
CapabilityRequest, ConfigBundleRef, CreateWorkerRequest, ProfileSelector,
|
CapabilityRequest, ConfigBundleRef, CreateWorkerRequest, ProfileSelector,
|
||||||
WorkerDetail as EmbeddedWorkerDetail, WorkerIntent, WorkerStatus as EmbeddedWorkerStatus,
|
WorkerDetail as EmbeddedWorkerDetail, WorkerIntent, WorkerStatus as EmbeddedWorkerStatus,
|
||||||
|
|
@ -38,9 +30,7 @@ use worker_runtime::observation::{
|
||||||
TranscriptProjection as EmbeddedTranscriptProjection, TranscriptQuery, TranscriptRole,
|
TranscriptProjection as EmbeddedTranscriptProjection, TranscriptQuery, TranscriptRole,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCAL_RUNTIME_ID: &str = "local-worker-runtime";
|
|
||||||
const EMBEDDED_RUNTIME_ID: &str = "embedded-worker-runtime";
|
const EMBEDDED_RUNTIME_ID: &str = "embedded-worker-runtime";
|
||||||
const LOCAL_HOST_KIND: &str = "local-worker-host";
|
|
||||||
const EMBEDDED_HOST_KIND: &str = "embedded-worker-runtime-host";
|
const EMBEDDED_HOST_KIND: &str = "embedded-worker-runtime-host";
|
||||||
const REMOTE_HOST_KIND: &str = "remote-worker-runtime-host";
|
const REMOTE_HOST_KIND: &str = "remote-worker-runtime-host";
|
||||||
const MAX_DIAGNOSTICS: usize = 16;
|
const MAX_DIAGNOSTICS: usize = 16;
|
||||||
|
|
@ -77,11 +67,7 @@ pub enum DiagnosticSeverity {
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum RuntimeSourceKind {
|
pub enum RuntimeSourceKind {
|
||||||
/// Compatibility projection over the existing local Pod metadata store.
|
|
||||||
LocalCompatibility,
|
|
||||||
/// Reserved boundary for the future in-process worker-runtime Runtime adapter.
|
|
||||||
EmbeddedWorkerRuntime,
|
EmbeddedWorkerRuntime,
|
||||||
/// Reserved boundary for a future remote Workspace Runtime adapter.
|
|
||||||
RemoteHttp,
|
RemoteHttp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +82,7 @@ pub enum RuntimeSourceStatus {
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum RuntimeIdentityAuthority {
|
pub enum RuntimeIdentityAuthority {
|
||||||
/// Public Runtime/Host/Worker ids are registry projections, never raw
|
/// Public Runtime/Host/Worker ids are registry projections, never raw
|
||||||
/// compatibility-store names, socket addresses, session ids, or paths.
|
/// socket addresses, session ids, credentials, or paths.
|
||||||
RuntimeRegistryProjection,
|
RuntimeRegistryProjection,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,15 +95,6 @@ pub struct RuntimeSourceSummary {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeSourceSummary {
|
impl RuntimeSourceSummary {
|
||||||
pub fn local_compatibility() -> Self {
|
|
||||||
Self {
|
|
||||||
kind: RuntimeSourceKind::LocalCompatibility,
|
|
||||||
status: RuntimeSourceStatus::Active,
|
|
||||||
identity_authority: RuntimeIdentityAuthority::RuntimeRegistryProjection,
|
|
||||||
note: "read-only compatibility projection over local Worker metadata; no socket, session, or path authority is exposed".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn embedded_worker_runtime() -> Self {
|
pub fn embedded_worker_runtime() -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: RuntimeSourceKind::EmbeddedWorkerRuntime,
|
kind: RuntimeSourceKind::EmbeddedWorkerRuntime,
|
||||||
|
|
@ -170,7 +147,6 @@ pub struct RuntimeCapabilitySummary {
|
||||||
pub has_git: bool,
|
pub has_git: bool,
|
||||||
pub supports_worktrees: bool,
|
pub supports_worktrees: bool,
|
||||||
pub supports_backend_internal_tools: bool,
|
pub supports_backend_internal_tools: bool,
|
||||||
pub local_pod_inspection: String,
|
|
||||||
pub workspace_scope: String,
|
pub workspace_scope: String,
|
||||||
pub max_workers: usize,
|
pub max_workers: usize,
|
||||||
pub os: String,
|
pub os: String,
|
||||||
|
|
@ -674,15 +650,8 @@ impl RuntimeRegistry {
|
||||||
Self { runtimes }
|
Self { runtimes }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_local_pods(runtime: LocalWorkerRuntime) -> Self {
|
pub fn for_workspace(embedded_runtime: EmbeddedWorkerRuntime) -> Self {
|
||||||
Self::new(vec![Arc::new(runtime)])
|
Self::new(vec![Arc::new(embedded_runtime)])
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_workspace(
|
|
||||||
local_runtime: LocalWorkerRuntime,
|
|
||||||
embedded_runtime: EmbeddedWorkerRuntime,
|
|
||||||
) -> Self {
|
|
||||||
Self::new(vec![Arc::new(local_runtime), Arc::new(embedded_runtime)])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register<R>(&mut self, runtime: R)
|
pub fn register<R>(&mut self, runtime: R)
|
||||||
|
|
@ -1894,300 +1863,6 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct LocalWorkerRuntime {
|
|
||||||
runtime_id: String,
|
|
||||||
host_id: String,
|
|
||||||
workspace_root: PathBuf,
|
|
||||||
data_dir: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type LocalRuntimeBridge = LocalWorkerRuntime;
|
|
||||||
|
|
||||||
impl LocalWorkerRuntime {
|
|
||||||
pub fn new(
|
|
||||||
workspace_id: impl AsRef<str>,
|
|
||||||
workspace_root: impl Into<PathBuf>,
|
|
||||||
data_dir: Option<PathBuf>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
runtime_id: LOCAL_RUNTIME_ID.to_string(),
|
|
||||||
host_id: host_id_for_workspace(workspace_id.as_ref()),
|
|
||||||
workspace_root: workspace_root.into(),
|
|
||||||
data_dir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pod_root(&self) -> Option<PathBuf> {
|
|
||||||
self.data_dir.as_ref().map(|dir| dir.join("pods"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn worker_names(&self, pod_root: &Path) -> Result<BTreeSet<String>, RuntimeDiagnostic> {
|
|
||||||
let entries = fs::read_dir(pod_root).map_err(|err| {
|
|
||||||
diagnostic(
|
|
||||||
"local_pod_registry_unreadable",
|
|
||||||
DiagnosticSeverity::Warning,
|
|
||||||
format!("local Worker registry could not be read: {err}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut worker_names = BTreeSet::new();
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let Ok(file_type) = entry.file_type() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if !file_type.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some(name) = entry.file_name().to_str().map(|name| name.to_string()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
worker_names.insert(name);
|
|
||||||
}
|
|
||||||
Ok(worker_names)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_worker(&self, pod_root: &Path, worker_name: &str) -> WorkerReadOutcome {
|
|
||||||
let metadata_path = pod_root.join(worker_name).join("metadata.json");
|
|
||||||
let data = match fs::read_to_string(metadata_path) {
|
|
||||||
Ok(data) => data,
|
|
||||||
Err(err) => {
|
|
||||||
return WorkerReadOutcome::Diagnostic(diagnostic(
|
|
||||||
"local_pod_metadata_unreadable",
|
|
||||||
DiagnosticSeverity::Warning,
|
|
||||||
format!("local Worker metadata could not be read: {err}"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let metadata: WorkerMetadata = match serde_json::from_str(&data) {
|
|
||||||
Ok(metadata) => metadata,
|
|
||||||
Err(err) => {
|
|
||||||
return WorkerReadOutcome::Diagnostic(diagnostic(
|
|
||||||
"local_pod_metadata_invalid",
|
|
||||||
DiagnosticSeverity::Warning,
|
|
||||||
format!("local Worker metadata could not be parsed: {err}"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(metadata_workspace_root) = metadata.workspace_root.as_ref() else {
|
|
||||||
return WorkerReadOutcome::Diagnostic(diagnostic(
|
|
||||||
"local_pod_workspace_root_missing",
|
|
||||||
DiagnosticSeverity::Warning,
|
|
||||||
"local Worker metadata did not include a workspace identity and was not included"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
if !same_workspace_root(metadata_workspace_root, &self.workspace_root) {
|
|
||||||
return WorkerReadOutcome::Diagnostic(diagnostic(
|
|
||||||
"local_pod_workspace_not_visible",
|
|
||||||
DiagnosticSeverity::Info,
|
|
||||||
"local Worker metadata belongs to another workspace and was not included"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = safe_display_hint(&metadata.worker_name);
|
|
||||||
let role = manifest_hint_string(&metadata.resolved_manifest_snapshot, "role");
|
|
||||||
let profile =
|
|
||||||
manifest_hint_string(&metadata.resolved_manifest_snapshot, "profile_selector");
|
|
||||||
let worker_id = worker_id_for_pod(worker_name);
|
|
||||||
let status = match metadata
|
|
||||||
.active
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|active| active.segment_id.as_ref())
|
|
||||||
{
|
|
||||||
Some(_) => "active:segment".to_string(),
|
|
||||||
None if metadata.active.is_some() => "active:pending_segment".to_string(),
|
|
||||||
None => "metadata_only".to_string(),
|
|
||||||
};
|
|
||||||
let state = if metadata.active.is_some() {
|
|
||||||
"active".to_string()
|
|
||||||
} else {
|
|
||||||
"metadata_only".to_string()
|
|
||||||
};
|
|
||||||
let last_seen_at = None;
|
|
||||||
WorkerReadOutcome::Worker(WorkerSummary {
|
|
||||||
runtime_id: self.runtime_id.clone(),
|
|
||||||
worker_id,
|
|
||||||
host_id: self.host_id.clone(),
|
|
||||||
label,
|
|
||||||
role,
|
|
||||||
profile,
|
|
||||||
workspace: WorkerWorkspaceSummary {
|
|
||||||
visibility: "current_workspace".to_string(),
|
|
||||||
identity: "runtime_workspace".to_string(),
|
|
||||||
},
|
|
||||||
state,
|
|
||||||
status,
|
|
||||||
last_seen_at,
|
|
||||||
implementation: WorkerImplementationSummary {
|
|
||||||
kind: "local_pod".to_string(),
|
|
||||||
display_hint: safe_display_hint(worker_name),
|
|
||||||
},
|
|
||||||
capabilities: WorkerCapabilitySummary {
|
|
||||||
can_accept_input: true,
|
|
||||||
can_stream_events: false,
|
|
||||||
can_stop: false,
|
|
||||||
can_spawn_followup: false,
|
|
||||||
can_read_bounded_transcript: false,
|
|
||||||
},
|
|
||||||
diagnostics: vec![diagnostic(
|
|
||||||
"worker_actions_not_implemented",
|
|
||||||
DiagnosticSeverity::Info,
|
|
||||||
"runtime overview is available; worker control and stream/proxy operations are not yet implemented"
|
|
||||||
.to_string(),
|
|
||||||
)],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkspaceWorkerRuntime for LocalWorkerRuntime {
|
|
||||||
fn runtime_id(&self) -> &str {
|
|
||||||
&self.runtime_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn runtime_summary(&self, limit: usize) -> RuntimeSummary {
|
|
||||||
let host_list = self.list_hosts(1);
|
|
||||||
RuntimeSummary {
|
|
||||||
runtime_id: self.runtime_id.clone(),
|
|
||||||
label: "Local Worker runtime".to_string(),
|
|
||||||
kind: "local_pod".to_string(),
|
|
||||||
status: "available".to_string(),
|
|
||||||
source: RuntimeSourceSummary::local_compatibility(),
|
|
||||||
host_ids: host_list
|
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.take(limit)
|
|
||||||
.map(|host| host.host_id.clone())
|
|
||||||
.collect(),
|
|
||||||
capabilities: local_runtime_capabilities(limit, self.pod_root().is_some()),
|
|
||||||
diagnostics: host_list.diagnostics,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_hosts(&self, limit: usize) -> RuntimeList<HostSummary> {
|
|
||||||
if limit == 0 {
|
|
||||||
return RuntimeList::new(Vec::new(), Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut diagnostics = Vec::new();
|
|
||||||
let inspection_available = self.pod_root().is_some();
|
|
||||||
if !inspection_available {
|
|
||||||
diagnostics.push(diagnostic(
|
|
||||||
"local_pod_registry_unavailable",
|
|
||||||
DiagnosticSeverity::Warning,
|
|
||||||
"local Worker data directory is not configured; worker discovery is unavailable"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let item = HostSummary {
|
|
||||||
runtime_id: self.runtime_id.clone(),
|
|
||||||
host_id: self.host_id.clone(),
|
|
||||||
label: label_for_workspace(&self.workspace_root),
|
|
||||||
kind: LOCAL_HOST_KIND.to_string(),
|
|
||||||
status: if inspection_available {
|
|
||||||
"available"
|
|
||||||
} else {
|
|
||||||
"degraded"
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
observed_at: Utc::now().to_rfc3339(),
|
|
||||||
last_seen_at: None,
|
|
||||||
capabilities: local_runtime_capabilities(limit, inspection_available),
|
|
||||||
diagnostics: diagnostics.clone(),
|
|
||||||
};
|
|
||||||
RuntimeList::new(vec![item], diagnostics)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_workers(&self, limit: usize) -> RuntimeList<WorkerSummary> {
|
|
||||||
let Some(pod_root) = self.pod_root() else {
|
|
||||||
return RuntimeList::new(
|
|
||||||
Vec::new(),
|
|
||||||
vec![diagnostic(
|
|
||||||
"local_pod_registry_unavailable",
|
|
||||||
DiagnosticSeverity::Warning,
|
|
||||||
"local Worker data directory is not configured; worker discovery is unavailable"
|
|
||||||
.to_string(),
|
|
||||||
)],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
if limit == 0 {
|
|
||||||
return RuntimeList::new(Vec::new(), Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut workers = Vec::new();
|
|
||||||
let mut diagnostics = Vec::new();
|
|
||||||
let worker_names = match self.worker_names(&pod_root) {
|
|
||||||
Ok(worker_names) => worker_names,
|
|
||||||
Err(diag) => return RuntimeList::new(Vec::new(), vec![diag]),
|
|
||||||
};
|
|
||||||
|
|
||||||
for worker_name in worker_names {
|
|
||||||
if workers.len() >= limit {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match self.read_worker(&pod_root, &worker_name) {
|
|
||||||
WorkerReadOutcome::Worker(worker) => workers.push(worker),
|
|
||||||
WorkerReadOutcome::Diagnostic(diag) => {
|
|
||||||
if diagnostics.len() < MAX_DIAGNOSTICS {
|
|
||||||
diagnostics.push(diag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RuntimeList::new(workers, diagnostics)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn worker(&self, worker_id: &str) -> WorkerLookupResult {
|
|
||||||
let Some(pod_root) = self.pod_root() else {
|
|
||||||
return WorkerLookupResult {
|
|
||||||
worker: None,
|
|
||||||
diagnostics: vec![diagnostic(
|
|
||||||
"local_pod_registry_unavailable",
|
|
||||||
DiagnosticSeverity::Warning,
|
|
||||||
"local Worker data directory is not configured; worker discovery is unavailable"
|
|
||||||
.to_string(),
|
|
||||||
)],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
let worker_names = match self.worker_names(&pod_root) {
|
|
||||||
Ok(worker_names) => worker_names,
|
|
||||||
Err(diag) => {
|
|
||||||
return WorkerLookupResult {
|
|
||||||
worker: None,
|
|
||||||
diagnostics: vec![diag],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for worker_name in worker_names {
|
|
||||||
if worker_id_for_pod(&worker_name) != worker_id {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return match self.read_worker(&pod_root, &worker_name) {
|
|
||||||
WorkerReadOutcome::Worker(worker) => WorkerLookupResult {
|
|
||||||
worker: Some(worker),
|
|
||||||
diagnostics: Vec::new(),
|
|
||||||
},
|
|
||||||
WorkerReadOutcome::Diagnostic(diag) => WorkerLookupResult {
|
|
||||||
worker: None,
|
|
||||||
diagnostics: vec![diag],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
WorkerLookupResult {
|
|
||||||
worker: None,
|
|
||||||
diagnostics: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum WorkerReadOutcome {
|
|
||||||
Worker(WorkerSummary),
|
|
||||||
Diagnostic(RuntimeDiagnostic),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn embedded_runtime_capabilities(limit: usize, available: bool) -> RuntimeCapabilitySummary {
|
fn embedded_runtime_capabilities(limit: usize, available: bool) -> RuntimeCapabilitySummary {
|
||||||
RuntimeCapabilitySummary {
|
RuntimeCapabilitySummary {
|
||||||
can_list_hosts: true,
|
can_list_hosts: true,
|
||||||
|
|
@ -2203,7 +1878,6 @@ fn embedded_runtime_capabilities(limit: usize, available: bool) -> RuntimeCapabi
|
||||||
has_git: false,
|
has_git: false,
|
||||||
supports_worktrees: false,
|
supports_worktrees: false,
|
||||||
supports_backend_internal_tools: true,
|
supports_backend_internal_tools: true,
|
||||||
local_pod_inspection: "not_applicable".to_string(),
|
|
||||||
workspace_scope: "backend_internal".to_string(),
|
workspace_scope: "backend_internal".to_string(),
|
||||||
max_workers: limit,
|
max_workers: limit,
|
||||||
os: std::env::consts::OS.to_string(),
|
os: std::env::consts::OS.to_string(),
|
||||||
|
|
@ -2472,7 +2146,6 @@ fn remote_runtime_capabilities(limit: usize, available: bool) -> RuntimeCapabili
|
||||||
has_git: false,
|
has_git: false,
|
||||||
supports_worktrees: false,
|
supports_worktrees: false,
|
||||||
supports_backend_internal_tools: false,
|
supports_backend_internal_tools: false,
|
||||||
local_pod_inspection: "unavailable".to_string(),
|
|
||||||
workspace_scope: "remote_runtime_backend_private".to_string(),
|
workspace_scope: "remote_runtime_backend_private".to_string(),
|
||||||
max_workers: limit,
|
max_workers: limit,
|
||||||
os: "remote".to_string(),
|
os: "remote".to_string(),
|
||||||
|
|
@ -2532,37 +2205,6 @@ fn remote_http_status_diagnostic(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_runtime_capabilities(
|
|
||||||
limit: usize,
|
|
||||||
inspection_available: bool,
|
|
||||||
) -> RuntimeCapabilitySummary {
|
|
||||||
RuntimeCapabilitySummary {
|
|
||||||
can_list_hosts: true,
|
|
||||||
can_list_workers: inspection_available,
|
|
||||||
can_get_worker: inspection_available,
|
|
||||||
can_spawn_worker: false,
|
|
||||||
can_stop_worker: false,
|
|
||||||
can_accept_input: false,
|
|
||||||
can_stream_events: false,
|
|
||||||
can_read_bounded_transcript: false,
|
|
||||||
has_workspace_fs: false,
|
|
||||||
has_shell: false,
|
|
||||||
has_git: false,
|
|
||||||
supports_worktrees: false,
|
|
||||||
supports_backend_internal_tools: false,
|
|
||||||
local_pod_inspection: if inspection_available {
|
|
||||||
"available"
|
|
||||||
} else {
|
|
||||||
"unavailable"
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
workspace_scope: "current_workspace".to_string(),
|
|
||||||
max_workers: limit,
|
|
||||||
os: std::env::consts::OS.to_string(),
|
|
||||||
arch: std::env::consts::ARCH.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diagnostic(
|
fn diagnostic(
|
||||||
code: impl Into<String>,
|
code: impl Into<String>,
|
||||||
severity: DiagnosticSeverity,
|
severity: DiagnosticSeverity,
|
||||||
|
|
@ -2594,14 +2236,6 @@ fn operation_failed_or_unknown_worker(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn host_id_for_workspace(workspace_id: &str) -> String {
|
|
||||||
bounded_backend_identifier("local-", workspace_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn worker_id_for_pod(worker_name: &str) -> String {
|
|
||||||
bounded_backend_identifier("local-worker-", worker_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bounded_backend_identifier(prefix: &str, value: &str) -> String {
|
fn bounded_backend_identifier(prefix: &str, value: &str) -> String {
|
||||||
let digest = digest_hex(value.as_bytes(), ID_DIGEST_HEX_LEN);
|
let digest = digest_hex(value.as_bytes(), ID_DIGEST_HEX_LEN);
|
||||||
let mut body = sanitize_identifier_body(value);
|
let mut body = sanitize_identifier_body(value);
|
||||||
|
|
@ -2680,27 +2314,6 @@ fn validate_backend_identifier(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn label_for_workspace(workspace_root: &Path) -> String {
|
|
||||||
workspace_root
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.filter(|name| !name.is_empty())
|
|
||||||
.unwrap_or("workspace")
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn same_workspace_root(left: &Path, right: &Path) -> bool {
|
|
||||||
lexical_path_string(left) == lexical_path_string(right)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lexical_path_string(path: &Path) -> String {
|
|
||||||
let mut value = path.to_string_lossy().replace('\\', "/");
|
|
||||||
while value.len() > 1 && value.ends_with('/') {
|
|
||||||
value.pop();
|
|
||||||
}
|
|
||||||
value
|
|
||||||
}
|
|
||||||
|
|
||||||
fn safe_display_hint(value: &str) -> String {
|
fn safe_display_hint(value: &str) -> String {
|
||||||
value
|
value
|
||||||
.chars()
|
.chars()
|
||||||
|
|
@ -2722,27 +2335,6 @@ fn worker_spawn_intent_label(intent: &WorkerSpawnIntent) -> &'static str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manifest_hint_string(snapshot: &Option<Value>, key: &str) -> Option<String> {
|
|
||||||
let value = snapshot.as_ref()?;
|
|
||||||
let candidate = match key {
|
|
||||||
"role" => value
|
|
||||||
.get("role")
|
|
||||||
.or_else(|| value.get("role_claim").and_then(|role| role.get("role"))),
|
|
||||||
"profile_selector" => value
|
|
||||||
.get("profile_selector")
|
|
||||||
.or_else(|| value.get("profile")),
|
|
||||||
_ => value.get(key),
|
|
||||||
}?;
|
|
||||||
let text = candidate.as_str().map(ToOwned::to_owned).or_else(|| {
|
|
||||||
candidate
|
|
||||||
.get("name")
|
|
||||||
.and_then(|name| name.as_str())
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
})?;
|
|
||||||
let safe = safe_display_hint(&text);
|
|
||||||
(!safe.is_empty()).then_some(safe)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn placeholder_worker(host_id: impl Into<String>) -> WorkerSummary {
|
pub fn placeholder_worker(host_id: impl Into<String>) -> WorkerSummary {
|
||||||
let host_id = host_id.into();
|
let host_id = host_id.into();
|
||||||
WorkerSummary {
|
WorkerSummary {
|
||||||
|
|
@ -2795,32 +2387,10 @@ pub fn placeholder_spawn_response(host_id: impl Into<String>) -> WorkerSpawnResu
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::fs;
|
|
||||||
use std::io::{Read as _, Write as _};
|
use std::io::{Read as _, Write as _};
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
fn write_metadata(dir: &Path, worker_name: &str, metadata: &WorkerMetadata) {
|
|
||||||
let pod_dir = dir.join("pods").join(worker_name);
|
|
||||||
fs::create_dir_all(&pod_dir).unwrap();
|
|
||||||
fs::write(
|
|
||||||
pod_dir.join("metadata.json"),
|
|
||||||
serde_json::to_vec(metadata).unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn metadata(workspace_root: Option<&str>) -> WorkerMetadata {
|
|
||||||
let mut metadata = WorkerMetadata::new("coder", None);
|
|
||||||
metadata.workspace_root = workspace_root.map(PathBuf::from);
|
|
||||||
metadata.resolved_manifest_snapshot = Some(json!({
|
|
||||||
"role": "coder",
|
|
||||||
"profile_selector": "builtin:coder"
|
|
||||||
}));
|
|
||||||
metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_config_bundle() -> ConfigBundle {
|
fn test_config_bundle() -> ConfigBundle {
|
||||||
ConfigBundle {
|
ConfigBundle {
|
||||||
|
|
@ -2848,15 +2418,6 @@ mod tests {
|
||||||
.with_computed_digest()
|
.with_computed_digest()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_valid_generated_id(id: &str) {
|
|
||||||
assert!(id.len() <= MAX_IDENTIFIER_LEN, "id too long: {id}");
|
|
||||||
validate_backend_identifier("test_id", id).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn host_id() -> String {
|
|
||||||
host_id_for_workspace("local:test")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct FixtureRuntime {
|
struct FixtureRuntime {
|
||||||
runtime_id: String,
|
runtime_id: String,
|
||||||
|
|
@ -2927,7 +2488,6 @@ mod tests {
|
||||||
has_git: false,
|
has_git: false,
|
||||||
supports_worktrees: false,
|
supports_worktrees: false,
|
||||||
supports_backend_internal_tools: false,
|
supports_backend_internal_tools: false,
|
||||||
local_pod_inspection: "none".to_string(),
|
|
||||||
workspace_scope: "none".to_string(),
|
workspace_scope: "none".to_string(),
|
||||||
max_workers: self.workers.len(),
|
max_workers: self.workers.len(),
|
||||||
os: "test".to_string(),
|
os: "test".to_string(),
|
||||||
|
|
@ -3035,140 +2595,10 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn local_runtime_reports_host_without_private_paths() {
|
|
||||||
let bridge = LocalWorkerRuntime::new("local:test", "/workspace/project", None);
|
|
||||||
let hosts = bridge.list_hosts(10);
|
|
||||||
assert_eq!(hosts.items.len(), 1);
|
|
||||||
let host = &hosts.items[0];
|
|
||||||
assert_eq!(host.runtime_id, LOCAL_RUNTIME_ID);
|
|
||||||
assert_eq!(host.host_id, host_id());
|
|
||||||
assert_valid_generated_id(&host.host_id);
|
|
||||||
assert_eq!(host.capabilities.local_pod_inspection, "unavailable");
|
|
||||||
assert_eq!(host.capabilities.workspace_scope, "current_workspace");
|
|
||||||
let json = serde_json::to_string(host).unwrap();
|
|
||||||
assert!(!json.contains("/workspace/project"));
|
|
||||||
assert!(!json.contains("metadata.json"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn registry_lists_runtimes_hosts_and_workers() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
write_metadata(temp.path(), "coder", &metadata(Some("/workspace/project")));
|
|
||||||
let registry = RuntimeRegistry::for_local_pods(LocalWorkerRuntime::new(
|
|
||||||
"local:test",
|
|
||||||
"/workspace/project",
|
|
||||||
Some(temp.path().to_path_buf()),
|
|
||||||
));
|
|
||||||
|
|
||||||
let runtimes = registry.list_runtimes(10);
|
|
||||||
assert_eq!(runtimes.items[0].runtime_id, LOCAL_RUNTIME_ID);
|
|
||||||
assert_eq!(
|
|
||||||
runtimes.items[0].source.kind,
|
|
||||||
RuntimeSourceKind::LocalCompatibility
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
runtimes.items[0].source.identity_authority,
|
|
||||||
RuntimeIdentityAuthority::RuntimeRegistryProjection
|
|
||||||
);
|
|
||||||
assert_eq!(runtimes.items[0].host_ids, vec![host_id()]);
|
|
||||||
|
|
||||||
let hosts = registry.list_hosts(10);
|
|
||||||
assert_eq!(hosts.items[0].host_id, host_id());
|
|
||||||
assert_valid_generated_id(&hosts.items[0].host_id);
|
|
||||||
|
|
||||||
let workers = registry.list_workers(10);
|
|
||||||
assert_eq!(workers.items.len(), 1);
|
|
||||||
let worker = &workers.items[0];
|
|
||||||
assert_eq!(worker.runtime_id, LOCAL_RUNTIME_ID);
|
|
||||||
assert_eq!(worker.worker_id, worker_id_for_pod("coder"));
|
|
||||||
assert_valid_generated_id(&worker.worker_id);
|
|
||||||
assert_eq!(worker.host_id, host_id());
|
|
||||||
assert_eq!(worker.workspace.visibility, "current_workspace");
|
|
||||||
assert_eq!(worker.implementation.display_hint, "coder");
|
|
||||||
let json = serde_json::to_string(worker).unwrap();
|
|
||||||
assert!(!json.contains("/workspace/project"));
|
|
||||||
assert!(!json.contains("metadata.json"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn registry_resolves_backend_validated_host_ids() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
write_metadata(temp.path(), "coder", &metadata(Some("/workspace/project")));
|
|
||||||
let registry = RuntimeRegistry::for_local_pods(LocalWorkerRuntime::new(
|
|
||||||
"local:test",
|
|
||||||
"/workspace/project",
|
|
||||||
Some(temp.path().to_path_buf()),
|
|
||||||
));
|
|
||||||
|
|
||||||
let workers = registry.list_workers_for_host(&host_id(), 10).unwrap();
|
|
||||||
assert_eq!(workers.items.len(), 1);
|
|
||||||
assert!(matches!(
|
|
||||||
registry.list_workers_for_host("../secret", 10),
|
|
||||||
Err(RuntimeRegistryError::InvalidIdentifier { .. })
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
registry.list_workers_for_host("local-missing", 10),
|
|
||||||
Err(RuntimeRegistryError::UnknownHost(_))
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn local_runtime_excludes_other_workspace_metadata() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
write_metadata(temp.path(), "other", &metadata(Some("/workspace/other")));
|
|
||||||
write_metadata(temp.path(), "missing", &metadata(None));
|
|
||||||
let bridge = LocalWorkerRuntime::new(
|
|
||||||
"local:test",
|
|
||||||
"/workspace/project",
|
|
||||||
Some(temp.path().to_path_buf()),
|
|
||||||
);
|
|
||||||
let workers = bridge.list_workers(10);
|
|
||||||
assert!(workers.items.is_empty());
|
|
||||||
assert!(
|
|
||||||
workers
|
|
||||||
.diagnostics
|
|
||||||
.iter()
|
|
||||||
.any(|diag| diag.code == "local_pod_workspace_not_visible")
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
workers
|
|
||||||
.diagnostics
|
|
||||||
.iter()
|
|
||||||
.any(|diag| diag.code == "local_pod_workspace_root_missing")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn local_runtime_worker_detail_is_safe_and_bounded() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
write_metadata(temp.path(), "coder", &metadata(Some("/workspace/project")));
|
|
||||||
let bridge = LocalWorkerRuntime::new(
|
|
||||||
"local:test",
|
|
||||||
"/workspace/project",
|
|
||||||
Some(temp.path().to_path_buf()),
|
|
||||||
);
|
|
||||||
let worker_id = worker_id_for_pod("coder");
|
|
||||||
let worker = bridge.worker(&worker_id).worker.unwrap();
|
|
||||||
assert_eq!(worker.label, "coder");
|
|
||||||
assert_eq!(worker.workspace.identity, "runtime_workspace");
|
|
||||||
assert!(
|
|
||||||
bridge
|
|
||||||
.worker(&worker_id_for_pod("missing"))
|
|
||||||
.worker
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn embedded_runtime_registers_routes_input_and_transcript_without_internal_leaks() {
|
fn embedded_runtime_registers_routes_input_and_transcript_without_internal_leaks() {
|
||||||
let temp = TempDir::new().unwrap();
|
let registry =
|
||||||
let mut registry = RuntimeRegistry::for_local_pods(LocalWorkerRuntime::new(
|
RuntimeRegistry::for_workspace(EmbeddedWorkerRuntime::new_memory("local:test"));
|
||||||
"local:test",
|
|
||||||
"/workspace/project",
|
|
||||||
Some(temp.path().to_path_buf()),
|
|
||||||
));
|
|
||||||
registry.register(EmbeddedWorkerRuntime::new_memory("local:test"));
|
|
||||||
|
|
||||||
let runtimes = registry.list_runtimes(10);
|
let runtimes = registry.list_runtimes(10);
|
||||||
let embedded_summary = runtimes
|
let embedded_summary = runtimes
|
||||||
|
|
@ -3242,19 +2672,6 @@ mod tests {
|
||||||
assert_eq!(transcript.items[0].role, "user");
|
assert_eq!(transcript.items[0].role, "user");
|
||||||
assert_eq!(transcript.items[0].content, "hello embedded runtime");
|
assert_eq!(transcript.items[0].content, "hello embedded runtime");
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
registry.send_input(
|
|
||||||
LOCAL_RUNTIME_ID,
|
|
||||||
&worker.worker_id,
|
|
||||||
WorkerInputRequest {
|
|
||||||
kind: WorkerInputKind::User,
|
|
||||||
content: "wrong runtime".to_string(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Err(RuntimeRegistryError::UnknownWorker { runtime_id, worker_id })
|
|
||||||
if runtime_id == LOCAL_RUNTIME_ID && worker_id == worker.worker_id
|
|
||||||
));
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&(embedded_summary, worker, transcript)).unwrap();
|
let json = serde_json::to_string(&(embedded_summary, worker, transcript)).unwrap();
|
||||||
for forbidden in [
|
for forbidden in [
|
||||||
"/workspace/project",
|
"/workspace/project",
|
||||||
|
|
@ -3627,61 +3044,4 @@ mod tests {
|
||||||
"last_event_id": 0
|
"last_event_id": 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn generated_worker_ids_are_opaque_bounded_unique_and_resolvable() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
let long_a = format!("{}-A", "TicketWorkerWithVeryLongName".repeat(8));
|
|
||||||
let long_b = format!("{}-B", "TicketWorkerWithVeryLongName".repeat(8));
|
|
||||||
let worker_names = vec![
|
|
||||||
"00001KVWECEQG-Coder.Worker".to_string(),
|
|
||||||
"foo.bar".to_string(),
|
|
||||||
"foo-bar".to_string(),
|
|
||||||
"Ticket#Worker@Reviewer".to_string(),
|
|
||||||
long_a,
|
|
||||||
long_b,
|
|
||||||
];
|
|
||||||
for worker_name in &worker_names {
|
|
||||||
write_metadata(
|
|
||||||
temp.path(),
|
|
||||||
worker_name,
|
|
||||||
&metadata(Some("/workspace/project")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let bridge = LocalWorkerRuntime::new(
|
|
||||||
"local:test",
|
|
||||||
"/workspace/project",
|
|
||||||
Some(temp.path().to_path_buf()),
|
|
||||||
);
|
|
||||||
let registry = RuntimeRegistry::for_local_pods(bridge.clone());
|
|
||||||
|
|
||||||
let listed = registry.list_workers(100);
|
|
||||||
assert_eq!(listed.items.len(), worker_names.len());
|
|
||||||
let mut ids = BTreeSet::new();
|
|
||||||
for worker in listed.items {
|
|
||||||
assert_valid_generated_id(&worker.worker_id);
|
|
||||||
assert!(
|
|
||||||
ids.insert(worker.worker_id.clone()),
|
|
||||||
"duplicate id: {}",
|
|
||||||
worker.worker_id
|
|
||||||
);
|
|
||||||
assert!(!worker.worker_id.contains('.'));
|
|
||||||
assert!(!worker.worker_id.contains('@'));
|
|
||||||
assert!(!worker.worker_id.contains('#'));
|
|
||||||
|
|
||||||
let from_registry = registry
|
|
||||||
.worker(&worker.runtime_id, &worker.worker_id)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(from_registry.worker_id, worker.worker_id);
|
|
||||||
let from_runtime = bridge.worker(&worker.worker_id).worker.unwrap();
|
|
||||||
assert_eq!(from_runtime.worker_id, worker.worker_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dotted = worker_id_for_pod("foo.bar");
|
|
||||||
let dashed = worker_id_for_pod("foo-bar");
|
|
||||||
assert_ne!(dotted, dashed);
|
|
||||||
assert!(ids.contains(&dotted));
|
|
||||||
assert!(ids.contains(&dashed));
|
|
||||||
assert!(ids.iter().any(|id| id.len() == MAX_IDENTIFIER_LEN));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ use crate::companion::{
|
||||||
};
|
};
|
||||||
use crate::hosts::{
|
use crate::hosts::{
|
||||||
ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EmbeddedWorkerRuntime,
|
ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EmbeddedWorkerRuntime,
|
||||||
HostSummary, LocalWorkerRuntime, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic,
|
HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic, RuntimeRegistry,
|
||||||
RuntimeRegistry, RuntimeSummary, WorkerInputRequest, WorkerInputResult, WorkerLifecycleRequest,
|
RuntimeSummary, WorkerInputRequest, WorkerInputResult, WorkerLifecycleRequest,
|
||||||
WorkerLifecycleResult, WorkerSpawnRequest, WorkerSpawnResult, WorkerSummary,
|
WorkerLifecycleResult, WorkerSpawnRequest, WorkerSpawnResult, WorkerSummary,
|
||||||
WorkerTranscriptProjection,
|
WorkerTranscriptProjection,
|
||||||
};
|
};
|
||||||
|
|
@ -53,7 +53,6 @@ pub struct ServerConfig {
|
||||||
pub static_assets_dir: Option<PathBuf>,
|
pub static_assets_dir: Option<PathBuf>,
|
||||||
pub auth: AuthConfig,
|
pub auth: AuthConfig,
|
||||||
pub max_records: usize,
|
pub max_records: usize,
|
||||||
pub local_runtime_data_dir: Option<PathBuf>,
|
|
||||||
pub runtime_event_sources: Vec<RuntimeObservationSourceConfig>,
|
pub runtime_event_sources: Vec<RuntimeObservationSourceConfig>,
|
||||||
pub remote_runtime_sources: Vec<RemoteRuntimeConfig>,
|
pub remote_runtime_sources: Vec<RemoteRuntimeConfig>,
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +70,6 @@ impl ServerConfig {
|
||||||
token_configured: false,
|
token_configured: false,
|
||||||
},
|
},
|
||||||
max_records: 200,
|
max_records: 200,
|
||||||
local_runtime_data_dir: manifest::paths::data_dir(),
|
|
||||||
runtime_event_sources: Vec::new(),
|
runtime_event_sources: Vec::new(),
|
||||||
remote_runtime_sources: Vec::new(),
|
remote_runtime_sources: Vec::new(),
|
||||||
}
|
}
|
||||||
|
|
@ -99,14 +97,9 @@ impl WorkspaceApi {
|
||||||
updated_at: config.workspace_created_at.clone(),
|
updated_at: config.workspace_created_at.clone(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let mut runtime = RuntimeRegistry::for_workspace(
|
let mut runtime = RuntimeRegistry::for_workspace(EmbeddedWorkerRuntime::new_memory(
|
||||||
LocalWorkerRuntime::new(
|
|
||||||
config.workspace_id.clone(),
|
config.workspace_id.clone(),
|
||||||
config.workspace_root.clone(),
|
));
|
||||||
config.local_runtime_data_dir.clone(),
|
|
||||||
),
|
|
||||||
EmbeddedWorkerRuntime::new_memory(config.workspace_id.clone()),
|
|
||||||
);
|
|
||||||
for remote_config in config.remote_runtime_sources.iter().cloned() {
|
for remote_config in config.remote_runtime_sources.iter().cloned() {
|
||||||
runtime
|
runtime
|
||||||
.register(RemoteWorkerRuntime::new(remote_config).map_err(|err| err.into_error())?);
|
.register(RemoteWorkerRuntime::new(remote_config).map_err(|err| err.into_error())?);
|
||||||
|
|
@ -1049,7 +1042,6 @@ mod tests {
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
|
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
|
||||||
config.static_assets_dir = Some(static_dir);
|
config.static_assets_dir = Some(static_dir);
|
||||||
config.local_runtime_data_dir = Some(dir.path().join("data"));
|
|
||||||
let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap();
|
let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap();
|
||||||
let app = build_router(api);
|
let app = build_router(api);
|
||||||
|
|
||||||
|
|
@ -1109,28 +1101,24 @@ mod tests {
|
||||||
|
|
||||||
let hosts = get_json(app.clone(), "/api/hosts").await;
|
let hosts = get_json(app.clone(), "/api/hosts").await;
|
||||||
assert_eq!(hosts["source"], "worker_runtime_registry");
|
assert_eq!(hosts["source"], "worker_runtime_registry");
|
||||||
assert_eq!(hosts["items"][0]["runtime_id"], "local-worker-runtime");
|
assert_eq!(hosts["items"][0]["runtime_id"], "embedded-worker-runtime");
|
||||||
let host_id = hosts["items"][0]["host_id"].as_str().unwrap().to_string();
|
let host_id = hosts["items"][0]["host_id"].as_str().unwrap().to_string();
|
||||||
assert!(host_id.starts_with("local-"));
|
assert_eq!(hosts["items"][0]["kind"], "embedded-worker-runtime-host");
|
||||||
assert!(host_id.len() <= 120);
|
|
||||||
assert_ne!(host_id, TEST_REPOSITORY_ID);
|
|
||||||
assert_eq!(hosts["items"][0]["kind"], "local-worker-host");
|
|
||||||
assert_eq!(
|
|
||||||
hosts["items"][0]["capabilities"]["local_pod_inspection"],
|
|
||||||
"available"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hosts["items"][0]["capabilities"]["workspace_scope"],
|
hosts["items"][0]["capabilities"]["workspace_scope"],
|
||||||
"current_workspace"
|
"backend_internal"
|
||||||
);
|
);
|
||||||
assert!(!hosts.to_string().contains("metadata.json"));
|
assert!(!hosts.to_string().contains("metadata.json"));
|
||||||
|
|
||||||
let runtimes = get_json(app.clone(), "/api/runtimes").await;
|
let runtimes = get_json(app.clone(), "/api/runtimes").await;
|
||||||
assert_eq!(runtimes["source"], "worker_runtime_registry");
|
assert_eq!(runtimes["source"], "worker_runtime_registry");
|
||||||
assert_eq!(runtimes["items"][0]["runtime_id"], "local-worker-runtime");
|
assert_eq!(
|
||||||
|
runtimes["items"][0]["runtime_id"],
|
||||||
|
"embedded-worker-runtime"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtimes["items"][0]["source"]["kind"],
|
runtimes["items"][0]["source"]["kind"],
|
||||||
"local_compatibility"
|
"embedded_worker_runtime"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtimes["items"][0]["source"]["identity_authority"],
|
runtimes["items"][0]["source"]["identity_authority"],
|
||||||
|
|
@ -1147,10 +1135,6 @@ mod tests {
|
||||||
.expect("companion worker is visible through runtime worker API");
|
.expect("companion worker is visible through runtime worker API");
|
||||||
assert_eq!(companion_worker["runtime_id"], "embedded-worker-runtime");
|
assert_eq!(companion_worker["runtime_id"], "embedded-worker-runtime");
|
||||||
assert_eq!(companion_worker["capabilities"]["can_stop"], false);
|
assert_eq!(companion_worker["capabilities"]["can_stop"], false);
|
||||||
assert_eq!(
|
|
||||||
workers["diagnostics"][0]["code"],
|
|
||||||
"local_pod_registry_unreadable"
|
|
||||||
);
|
|
||||||
|
|
||||||
let companion_status = get_json(app.clone(), "/api/companion/status").await;
|
let companion_status = get_json(app.clone(), "/api/companion/status").await;
|
||||||
assert_eq!(companion_status["state"], "ready");
|
assert_eq!(companion_status["state"], "ready");
|
||||||
|
|
@ -1186,7 +1170,13 @@ mod tests {
|
||||||
assert_eq!(companion_transcript["items"][1]["role"], "assistant");
|
assert_eq!(companion_transcript["items"][1]["role"], "assistant");
|
||||||
|
|
||||||
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
|
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
|
||||||
assert!(host_workers["items"].as_array().unwrap().is_empty());
|
assert!(
|
||||||
|
host_workers["items"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|worker| worker["role"] == "workspace_companion")
|
||||||
|
);
|
||||||
|
|
||||||
let runs_response = app
|
let runs_response = app
|
||||||
.clone()
|
.clone()
|
||||||
|
|
@ -1270,8 +1260,7 @@ mod tests {
|
||||||
async fn embedded_runtime_api_routes_by_runtime_and_worker_ids_without_leaking_internals() {
|
async fn embedded_runtime_api_routes_by_runtime_and_worker_ids_without_leaking_internals() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
|
let config = ServerConfig::local_dev(dir.path(), test_identity());
|
||||||
config.local_runtime_data_dir = Some(dir.path().join("data"));
|
|
||||||
let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap();
|
let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap();
|
||||||
let app = build_router(api);
|
let app = build_router(api);
|
||||||
|
|
||||||
|
|
@ -1362,7 +1351,7 @@ mod tests {
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("POST")
|
.method("POST")
|
||||||
.uri(format!(
|
.uri(format!(
|
||||||
"/api/runtimes/local-worker-runtime/workers/{worker_id}/input"
|
"/api/runtimes/unknown-runtime/workers/{worker_id}/input"
|
||||||
))
|
))
|
||||||
.header(CONTENT_TYPE, "application/json")
|
.header(CONTENT_TYPE, "application/json")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
|
|
@ -1418,7 +1407,6 @@ mod tests {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
|
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
|
||||||
config.local_runtime_data_dir = Some(dir.path().join("data"));
|
|
||||||
config
|
config
|
||||||
.runtime_event_sources
|
.runtime_event_sources
|
||||||
.push(RuntimeObservationSourceConfig {
|
.push(RuntimeObservationSourceConfig {
|
||||||
|
|
@ -1630,7 +1618,6 @@ mod tests {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
|
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
|
||||||
config.local_runtime_data_dir = Some(dir.path().join("data"));
|
|
||||||
let runtime_id = source.runtime_id.clone();
|
let runtime_id = source.runtime_id.clone();
|
||||||
let worker_id = source.worker_id.clone();
|
let worker_id = source.worker_id.clone();
|
||||||
config.runtime_event_sources.push(source);
|
config.runtime_event_sources.push(source);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
|
||||||
filter = sourceFilter;
|
filter = sourceFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoHash = "sha256-fdmGo/HE80wRSLE/u20YXS63G/vvHx43uoc9BivZUxQ=";
|
cargoHash = "sha256-TPXVkDfy61HCWTfSr0boLKlFbvc6zdpRKQRUDXuPppU=";
|
||||||
|
|
||||||
depsExtraArgs = {
|
depsExtraArgs = {
|
||||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@
|
||||||
|
|
||||||
--radius-soft: 8px;
|
--radius-soft: 8px;
|
||||||
--radius-panel: 12px;
|
--radius-panel: 12px;
|
||||||
--interactive-hover: color-mix(in oklch, var(--bg-subtle) 88%, white 12%);
|
--interactive-hover: oklch(94.5% 0 0);
|
||||||
--interactive-selected: color-mix(in oklch, var(--bg-subtle) 88%, var(--accent) 12%);
|
--interactive-selected: oklch(93% 0.02 230);
|
||||||
--font-sans:
|
--font-sans:
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
"Segoe UI", sans-serif;
|
"Segoe UI", sans-serif;
|
||||||
|
|
@ -81,6 +81,8 @@
|
||||||
--success: oklch(77% 0.12 145);
|
--success: oklch(77% 0.12 145);
|
||||||
--warning: oklch(82% 0.13 85);
|
--warning: oklch(82% 0.13 85);
|
||||||
--danger: oklch(76% 0.14 25);
|
--danger: oklch(76% 0.14 25);
|
||||||
|
--interactive-hover: oklch(29% 0 0);
|
||||||
|
--interactive-selected: oklch(30% 0.025 230);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -598,9 +600,9 @@
|
||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
padding: 0.75rem 0.9rem;
|
padding: 0.75rem 0.9rem;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: #ecfeff;
|
background: var(--bg-raised);
|
||||||
border: 1px solid #a5f3fc;
|
border: 1px solid var(--line);
|
||||||
color: #0f766e;
|
color: var(--text-muted);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
|
@ -610,28 +612,28 @@
|
||||||
.console-status small {
|
.console-status small {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
color: #475569;
|
color: var(--text-muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-status[data-state='busy'] {
|
.console-status[data-state='busy'] {
|
||||||
background: #fff7ed;
|
background: var(--bg-raised);
|
||||||
border-color: #fed7aa;
|
border-color: var(--line);
|
||||||
color: #c2410c;
|
color: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-status[data-state='error'],
|
.console-status[data-state='error'],
|
||||||
.console-status[data-state='timeout'] {
|
.console-status[data-state='timeout'] {
|
||||||
background: #fef2f2;
|
background: var(--bg-raised);
|
||||||
border-color: #fecaca;
|
border-color: var(--line);
|
||||||
color: #b91c1c;
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-status[data-state='cancelled'],
|
.console-status[data-state='cancelled'],
|
||||||
.console-status[data-state='rejected'] {
|
.console-status[data-state='rejected'] {
|
||||||
background: #f8fafc;
|
background: var(--bg-raised);
|
||||||
border-color: #cbd5e1;
|
border-color: var(--line);
|
||||||
color: #475569;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-transport {
|
.console-transport {
|
||||||
|
|
@ -647,7 +649,7 @@
|
||||||
|
|
||||||
.console-transport dt {
|
.console-transport dt {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #64748b;
|
color: var(--text-muted);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
|
@ -656,14 +658,14 @@
|
||||||
|
|
||||||
.console-transport dd {
|
.console-transport dd {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #0f172a;
|
color: var(--text-strong);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-transport p {
|
.console-transport p {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #475569;
|
color: var(--text-muted);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -676,20 +678,20 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.55rem 0.7rem;
|
padding: 0.55rem 0.7rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #eff6ff;
|
background: var(--bg-raised);
|
||||||
color: #1d4ed8;
|
color: var(--accent);
|
||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diagnostic.warning,
|
.diagnostic.warning,
|
||||||
.diagnostic.warn {
|
.diagnostic.warn {
|
||||||
background: #fff7ed;
|
background: var(--bg-raised);
|
||||||
color: #c2410c;
|
color: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.diagnostic.error {
|
.diagnostic.error {
|
||||||
background: #fef2f2;
|
background: var(--bg-raised);
|
||||||
color: #b91c1c;
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcript-card,
|
.transcript-card,
|
||||||
|
|
@ -711,20 +713,20 @@
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
padding: 0.85rem 1rem;
|
padding: 0.85rem 1rem;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid #dbeafe;
|
border: 1px solid var(--line);
|
||||||
background: #f8fafc;
|
background: var(--bg-raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcript-item.user {
|
.transcript-item.user {
|
||||||
margin-left: min(8vw, 4rem);
|
margin-left: min(8vw, 4rem);
|
||||||
background: #eff6ff;
|
background: var(--bg-raised);
|
||||||
border-color: #bfdbfe;
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcript-item.assistant {
|
.transcript-item.assistant {
|
||||||
margin-right: min(8vw, 4rem);
|
margin-right: min(8vw, 4rem);
|
||||||
background: #f0fdf4;
|
background: var(--bg-raised);
|
||||||
border-color: #bbf7d0;
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-meta {
|
.message-meta {
|
||||||
|
|
@ -732,12 +734,12 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
color: #64748b;
|
color: var(--text-muted);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-meta strong {
|
.message-meta strong {
|
||||||
color: #0f172a;
|
color: var(--text-strong);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
@ -750,37 +752,37 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
color: #1f2937;
|
color: var(--code);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: #f8fafc;
|
background: var(--bg-raised);
|
||||||
color: #64748b;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-card label {
|
.composer-card label {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #0f172a;
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-card textarea {
|
.composer-card textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 8rem;
|
min-height: 8rem;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid var(--line);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 0.85rem 1rem;
|
padding: 0.85rem 1rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: #0f172a;
|
color: var(--text-strong);
|
||||||
background: #ffffff;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-card textarea:disabled {
|
.composer-card textarea:disabled {
|
||||||
background: #f8fafc;
|
background: var(--bg-raised);
|
||||||
color: #64748b;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-actions {
|
.composer-actions {
|
||||||
|
|
@ -793,7 +795,7 @@
|
||||||
|
|
||||||
.composer-actions span {
|
.composer-actions span {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
color: #64748b;
|
color: var(--text-muted);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -801,15 +803,15 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0.65rem 1rem;
|
padding: 0.65rem 1rem;
|
||||||
background: #2563eb;
|
background: var(--accent);
|
||||||
color: #ffffff;
|
color: var(--bg);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-actions button.secondary {
|
.composer-actions button.secondary {
|
||||||
background: #e2e8f0;
|
background: var(--bg-raised);
|
||||||
color: #334155;
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-actions button:disabled {
|
.composer-actions button:disabled {
|
||||||
|
|
@ -824,10 +826,10 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--line);
|
||||||
padding: 0.4rem 0.75rem;
|
padding: 0.4rem 0.75rem;
|
||||||
background: #ffffff;
|
background: var(--bg);
|
||||||
color: #1d4ed8;
|
color: var(--accent);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
@ -836,8 +838,8 @@
|
||||||
|
|
||||||
.secondary-button:hover,
|
.secondary-button:hover,
|
||||||
.inline-link:hover {
|
.inline-link:hover {
|
||||||
border-color: #bfdbfe;
|
border-color: var(--line-strong);
|
||||||
background: #eff6ff;
|
background: var(--bg-raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
.worker-console-shell {
|
.worker-console-shell {
|
||||||
|
|
@ -847,10 +849,10 @@
|
||||||
.console-status-pill {
|
.console-status-pill {
|
||||||
min-width: 14rem;
|
min-width: 14rem;
|
||||||
padding: 0.75rem 0.9rem;
|
padding: 0.75rem 0.9rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: #ecfeff;
|
background: var(--bg-raised);
|
||||||
border: 1px solid #a5f3fc;
|
color: var(--text-muted);
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
|
|
@ -859,9 +861,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-status-pill.warn {
|
.console-status-pill.warn {
|
||||||
background: #fff7ed;
|
color: var(--warning);
|
||||||
border-color: #fed7aa;
|
|
||||||
color: #c2410c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-grid {
|
.console-grid {
|
||||||
|
|
@ -891,26 +891,18 @@
|
||||||
.worker-transcript li {
|
.worker-transcript li {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--line);
|
||||||
border-left: 4px solid #cbd5e1;
|
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 0.75rem 0.85rem;
|
padding: 0.75rem 0.85rem;
|
||||||
background: #ffffff;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.worker-transcript li.user {
|
|
||||||
border-left-color: #2563eb;
|
|
||||||
background: #eff6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.worker-transcript li.assistant {
|
|
||||||
border-left-color: #10b981;
|
|
||||||
background: #f0fdf4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.worker-transcript li.error-line {
|
.worker-transcript li.error-line {
|
||||||
border-left-color: #dc2626;
|
border-color: var(--danger);
|
||||||
background: #fef2f2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-heading {
|
.message-heading {
|
||||||
|
|
@ -918,13 +910,13 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
color: #0f172a;
|
color: var(--text-strong);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-heading small {
|
.message-heading small {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #64748b;
|
color: var(--text-muted);
|
||||||
font-size: 0.74rem;
|
font-size: 0.74rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
@ -935,12 +927,12 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
color: #1f2937;
|
color: var(--code);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-detail,
|
.message-detail,
|
||||||
.metadata-details {
|
.metadata-details {
|
||||||
color: #475569;
|
color: var(--text-muted);
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -967,7 +959,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-side-card dt {
|
.console-side-card dt {
|
||||||
color: #64748b;
|
color: var(--text-muted);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
|
@ -976,15 +968,16 @@
|
||||||
|
|
||||||
.console-side-card dd {
|
.console-side-card dd {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #0f172a;
|
color: var(--text-strong);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.degrade-note {
|
.degrade-note {
|
||||||
border: 1px solid #fde68a;
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 0.65rem 0.8rem;
|
padding: 0.65rem 0.8rem;
|
||||||
background: #fffbeb;
|
background: var(--bg-raised);
|
||||||
|
color: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-composer {
|
.console-composer {
|
||||||
|
|
@ -993,7 +986,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-composer label {
|
.console-composer label {
|
||||||
color: #0f172a;
|
color: var(--text-strong);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1001,16 +994,16 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 7rem;
|
min-height: 7rem;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid var(--line);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 0.85rem 1rem;
|
padding: 0.85rem 1rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: #0f172a;
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-composer textarea:disabled {
|
.console-composer textarea:disabled {
|
||||||
background: #f8fafc;
|
background: var(--bg-raised);
|
||||||
color: #64748b;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
@media (max-width: 960px) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||||
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||||
import type {
|
import type {
|
||||||
Diagnostic,
|
|
||||||
Host,
|
Host,
|
||||||
ListResponse,
|
ListResponse,
|
||||||
ObjectiveDetail,
|
ObjectiveDetail,
|
||||||
|
|
@ -28,17 +27,6 @@
|
||||||
objectiveId = null
|
objectiveId = null
|
||||||
}: { view?: WorkspaceView; repositoryId?: string; objectiveId?: string | null } = $props();
|
}: { view?: WorkspaceView; repositoryId?: string; objectiveId?: string | null } = $props();
|
||||||
|
|
||||||
const endpoints = [
|
|
||||||
{ label: 'Workspace', path: '/api/workspace' },
|
|
||||||
{ label: 'Tickets', path: '/api/tickets' },
|
|
||||||
{ label: 'Objectives', path: '/api/objectives' },
|
|
||||||
{ label: 'Repositories', path: '/api/repositories' },
|
|
||||||
{ label: 'Repository tickets', path: '/api/repositories/local/tickets' },
|
|
||||||
{ label: 'Runs', path: '/api/runs' },
|
|
||||||
{ label: 'Hosts', path: '/api/hosts' },
|
|
||||||
{ label: 'Workers', path: '/api/workers' }
|
|
||||||
];
|
|
||||||
|
|
||||||
let workspace = $state<WorkspaceResponse | null>(null);
|
let workspace = $state<WorkspaceResponse | null>(null);
|
||||||
let hosts = $state<ListResponse<Host> | null>(null);
|
let hosts = $state<ListResponse<Host> | null>(null);
|
||||||
let workers = $state<ListResponse<Worker> | null>(null);
|
let workers = $state<ListResponse<Worker> | null>(null);
|
||||||
|
|
@ -149,10 +137,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function diagnosticsFor(...groups: Array<Diagnostic[] | undefined>): Diagnostic[] {
|
|
||||||
return groups.flatMap((group) => group ?? []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function routeFromView(view: WorkspaceView, objectiveId: string | null): RouteState {
|
function routeFromView(view: WorkspaceView, objectiveId: string | null): RouteState {
|
||||||
if (view === 'repository') {
|
if (view === 'repository') {
|
||||||
return { page: 'repository' };
|
return { page: 'repository' };
|
||||||
|
|
@ -265,21 +249,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{@const repositoryDiagnostics = diagnosticsFor(repository?.git.diagnostics, repositoryTickets?.diagnostics)}
|
|
||||||
{#if repositoryDiagnostics.length > 0}
|
|
||||||
<section class="card diagnostics">
|
|
||||||
<h2>Repository diagnostics</h2>
|
|
||||||
<ul>
|
|
||||||
{#each repositoryDiagnostics as diagnostic}
|
|
||||||
<li>
|
|
||||||
<strong>{diagnostic.severity}</strong>
|
|
||||||
<code>{diagnostic.code}</code>
|
|
||||||
<span>{diagnostic.message}</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
{:else if route.page === 'objectives' || route.page === 'objective'}
|
{:else if route.page === 'objectives' || route.page === 'objective'}
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Objectives</h2>
|
<h2>Objectives</h2>
|
||||||
|
|
@ -391,26 +360,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid">
|
|
||||||
<div class="card">
|
|
||||||
<h2>Read API surface</h2>
|
|
||||||
<ul>
|
|
||||||
{#each endpoints as endpoint}
|
|
||||||
<li><code>{endpoint.path}</code> — {endpoint.label}</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>Reserved seams</h2>
|
|
||||||
<p>
|
|
||||||
Event streams remain represented as extension-point state in the backend
|
|
||||||
response. Hosts and Workers are read-only local observations; no
|
|
||||||
scheduler, lifecycle control, or hosted multi-tenant behavior is
|
|
||||||
implemented in this slice.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="grid runtime">
|
<section class="grid runtime">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
@ -435,10 +384,6 @@
|
||||||
<dt>Kind</dt>
|
<dt>Kind</dt>
|
||||||
<dd>{host.kind}</dd>
|
<dd>{host.kind}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<dt>Local inspection</dt>
|
|
||||||
<dd>{host.capabilities.local_pod_inspection}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<dt>Runtime</dt>
|
<dt>Runtime</dt>
|
||||||
<dd><code>{host.runtime_id}</code></dd>
|
<dd><code>{host.runtime_id}</code></dd>
|
||||||
|
|
@ -509,23 +454,6 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#if hosts || workers}
|
|
||||||
{@const diagnostics = diagnosticsFor(hosts?.diagnostics, workers?.diagnostics)}
|
|
||||||
{#if diagnostics.length > 0}
|
|
||||||
<section class="card diagnostics">
|
|
||||||
<h2>Diagnostics</h2>
|
|
||||||
<ul>
|
|
||||||
{#each diagnostics as diagnostic}
|
|
||||||
<li>
|
|
||||||
<strong>{diagnostic.severity}</strong>
|
|
||||||
<code>{diagnostic.code}</code>
|
|
||||||
<span>{diagnostic.message}</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ export type RuntimeCapabilities = {
|
||||||
has_git: boolean;
|
has_git: boolean;
|
||||||
supports_worktrees: boolean;
|
supports_worktrees: boolean;
|
||||||
supports_backend_internal_tools: boolean;
|
supports_backend_internal_tools: boolean;
|
||||||
local_pod_inspection: string;
|
|
||||||
workspace_scope: string;
|
workspace_scope: string;
|
||||||
os: string;
|
os: string;
|
||||||
arch: string;
|
arch: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user