ui: remove legacy local worker projection

This commit is contained in:
Keisuke Hirata 2026-06-27 18:31:17 +09:00
parent 62420b7cc4
commit 135667417b
No known key found for this signature in database
9 changed files with 108 additions and 847 deletions

2
Cargo.lock generated
View File

@ -6037,8 +6037,6 @@ dependencies = [
"axum", "axum",
"chrono", "chrono",
"futures", "futures",
"manifest",
"pod-store",
"project-record", "project-record",
"protocol", "protocol",
"reqwest", "reqwest",

View File

@ -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"] }

View File

@ -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());

View File

@ -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));
}
} }

View File

@ -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);

View File

@ -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,

View File

@ -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) {

View File

@ -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>

View File

@ -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;