diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index 52d9a2e6..d09ed546 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -44,6 +44,69 @@ pub enum DiagnosticSeverity { Error, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +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, + /// Reserved boundary for a future remote Workspace Runtime adapter. + RemoteHttp, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeSourceStatus { + Active, + Reserved, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeIdentityAuthority { + /// Public Runtime/Host/Worker ids are registry projections, never raw + /// compatibility-store names, socket addresses, session ids, or paths. + RuntimeRegistryProjection, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct RuntimeSourceSummary { + pub kind: RuntimeSourceKind, + pub status: RuntimeSourceStatus, + pub identity_authority: RuntimeIdentityAuthority, + pub note: String, +} + +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_reserved() -> Self { + Self { + kind: RuntimeSourceKind::EmbeddedWorkerRuntime, + status: RuntimeSourceStatus::Reserved, + identity_authority: RuntimeIdentityAuthority::RuntimeRegistryProjection, + note: "reserved boundary for a future embedded worker-runtime adapter; not connected in this registry foundation".to_string(), + } + } + + pub fn remote_http_reserved() -> Self { + Self { + kind: RuntimeSourceKind::RemoteHttp, + status: RuntimeSourceStatus::Reserved, + identity_authority: RuntimeIdentityAuthority::RuntimeRegistryProjection, + note: "reserved boundary for a future remote Runtime adapter; no HTTP client or REST server is implemented here".to_string(), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RuntimeCapabilitySummary { pub can_list_hosts: bool, @@ -74,6 +137,7 @@ pub struct RuntimeSummary { pub label: String, pub kind: String, pub status: String, + pub source: RuntimeSourceSummary, pub host_ids: Vec, pub capabilities: RuntimeCapabilitySummary, pub diagnostics: Vec, @@ -241,9 +305,16 @@ pub struct WorkerProxyConnectPoint { #[derive(Debug, Clone, PartialEq, Eq)] pub enum RuntimeRegistryError { - InvalidIdentifier { kind: &'static str, value: String }, + InvalidIdentifier { + kind: &'static str, + value: String, + }, + UnknownRuntime(String), UnknownHost(String), - UnknownWorker(String), + UnknownWorker { + runtime_id: String, + worker_id: String, + }, } impl RuntimeRegistryError { @@ -253,8 +324,15 @@ impl RuntimeRegistryError { kind: kind.to_string(), value, }, + Self::UnknownRuntime(runtime_id) => Error::UnknownRuntime(runtime_id), Self::UnknownHost(host_id) => Error::UnknownHost(host_id), - Self::UnknownWorker(worker_id) => Error::UnknownWorker(worker_id), + Self::UnknownWorker { + runtime_id, + worker_id, + } => Error::UnknownWorker { + runtime_id, + worker_id, + }, } } } @@ -317,11 +395,11 @@ pub trait WorkspaceWorkerRuntime: Send + Sync { } #[derive(Clone)] -pub struct WorkerRuntimeRegistry { +pub struct RuntimeRegistry { runtimes: Vec>, } -impl WorkerRuntimeRegistry { +impl RuntimeRegistry { pub fn new(runtimes: Vec>) -> Self { Self { runtimes } } @@ -410,15 +488,25 @@ impl WorkerRuntimeRegistry { } } - pub fn worker(&self, worker_id: &str) -> Result { + pub fn worker( + &self, + runtime_id: &str, + worker_id: &str, + ) -> Result { + validate_backend_identifier("runtime_id", runtime_id)?; validate_backend_identifier("worker_id", worker_id)?; - for runtime in &self.runtimes { - let lookup = runtime.worker(worker_id); - if let Some(worker) = lookup.worker { - return Ok(worker); - } - } - Err(RuntimeRegistryError::UnknownWorker(worker_id.to_string())) + let runtime = self + .runtimes + .iter() + .find(|runtime| runtime.runtime_id() == runtime_id) + .ok_or_else(|| RuntimeRegistryError::UnknownRuntime(runtime_id.to_string()))?; + let lookup = runtime.worker(worker_id); + lookup + .worker + .ok_or_else(|| RuntimeRegistryError::UnknownWorker { + runtime_id: runtime_id.to_string(), + worker_id: worker_id.to_string(), + }) } } @@ -582,6 +670,7 @@ impl WorkspaceWorkerRuntime for LocalWorkerRuntime { 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() @@ -960,6 +1049,7 @@ mod tests { use super::*; use serde_json::json; use std::fs; + use std::sync::Arc; use tempfile::TempDir; fn write_metadata(dir: &Path, worker_name: &str, metadata: &WorkerMetadata) { @@ -991,6 +1081,184 @@ mod tests { host_id_for_workspace("local:test") } + #[derive(Clone)] + struct FixtureRuntime { + runtime_id: String, + host_id: String, + workers: Vec, + } + + impl FixtureRuntime { + fn with_worker(runtime_id: &str, host_id: &str, worker_id: &str, label: &str) -> Self { + Self { + runtime_id: runtime_id.to_string(), + host_id: host_id.to_string(), + workers: vec![WorkerSummary { + runtime_id: runtime_id.to_string(), + worker_id: worker_id.to_string(), + host_id: host_id.to_string(), + label: label.to_string(), + role: None, + profile: None, + workspace: WorkerWorkspaceSummary { + visibility: "opaque".to_string(), + identity: host_id.to_string(), + }, + state: "running".to_string(), + status: "available".to_string(), + last_seen_at: None, + implementation: WorkerImplementationSummary { + kind: "fixture".to_string(), + display_hint: "test fixture".to_string(), + }, + capabilities: WorkerCapabilitySummary { + can_accept_input: false, + can_stream_events: false, + can_stop: false, + can_spawn_followup: false, + can_read_bounded_transcript: false, + }, + diagnostics: Vec::new(), + }], + } + } + } + + impl WorkspaceWorkerRuntime for FixtureRuntime { + fn runtime_id(&self) -> &str { + &self.runtime_id + } + + fn runtime_summary(&self, _limit: usize) -> RuntimeSummary { + RuntimeSummary { + runtime_id: self.runtime_id.clone(), + label: self.runtime_id.clone(), + kind: "fixture".to_string(), + status: "available".to_string(), + source: RuntimeSourceSummary::embedded_worker_runtime_reserved(), + host_ids: vec![self.host_id.clone()], + capabilities: RuntimeCapabilitySummary { + can_list_hosts: true, + can_list_workers: true, + can_get_worker: true, + 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: "none".to_string(), + workspace_scope: "none".to_string(), + max_workers: self.workers.len(), + os: "test".to_string(), + arch: "test".to_string(), + }, + diagnostics: Vec::new(), + } + } + + fn list_hosts(&self, _limit: usize) -> RuntimeList { + RuntimeList::new( + vec![HostSummary { + runtime_id: self.runtime_id.clone(), + host_id: self.host_id.clone(), + label: "fixture host".to_string(), + kind: "fixture".to_string(), + status: "available".to_string(), + observed_at: "unknown".to_string(), + last_seen_at: None, + capabilities: self.runtime_summary(1).capabilities, + diagnostics: Vec::new(), + }], + Vec::new(), + ) + } + + fn list_workers(&self, limit: usize) -> RuntimeList { + RuntimeList::new( + self.workers.iter().take(limit).cloned().collect(), + Vec::new(), + ) + } + + fn worker(&self, worker_id: &str) -> WorkerLookupResult { + WorkerLookupResult { + worker: self + .workers + .iter() + .find(|worker| worker.worker_id == worker_id) + .cloned(), + diagnostics: Vec::new(), + } + } + } + + #[test] + fn registry_worker_lookup_is_scoped_by_runtime_id() { + let registry = RuntimeRegistry::new(vec![ + Arc::new(FixtureRuntime::with_worker( + "runtime-a", + "host-a", + "shared-worker", + "worker from runtime a", + )), + Arc::new(FixtureRuntime::with_worker( + "runtime-b", + "host-b", + "shared-worker", + "worker from runtime b", + )), + ]); + + let from_runtime_b = registry.worker("runtime-b", "shared-worker").unwrap(); + assert_eq!(from_runtime_b.runtime_id, "runtime-b"); + assert_eq!(from_runtime_b.host_id, "host-b"); + assert_eq!(from_runtime_b.label, "worker from runtime b"); + + let from_runtime_a = registry.worker("runtime-a", "shared-worker").unwrap(); + assert_eq!(from_runtime_a.runtime_id, "runtime-a"); + assert_eq!(from_runtime_a.host_id, "host-a"); + assert_eq!(from_runtime_a.label, "worker from runtime a"); + } + + #[test] + fn registry_worker_lookup_reports_unknown_runtime_and_worker_separately() { + let registry = RuntimeRegistry::new(vec![Arc::new(FixtureRuntime::with_worker( + "runtime-a", + "host-a", + "worker-a", + "worker from runtime a", + ))]); + + let unknown_runtime = registry.worker("runtime-missing", "worker-a").unwrap_err(); + assert_eq!( + unknown_runtime, + RuntimeRegistryError::UnknownRuntime("runtime-missing".to_string()) + ); + assert!(matches!( + unknown_runtime.into_error(), + Error::UnknownRuntime(runtime_id) if runtime_id == "runtime-missing" + )); + + let unknown_worker = registry.worker("runtime-a", "worker-missing").unwrap_err(); + assert_eq!( + unknown_worker, + RuntimeRegistryError::UnknownWorker { + runtime_id: "runtime-a".to_string(), + worker_id: "worker-missing".to_string(), + } + ); + assert!(matches!( + unknown_worker.into_error(), + Error::UnknownWorker { runtime_id, worker_id } + if runtime_id == "runtime-a" && worker_id == "worker-missing" + )); + } + #[test] fn local_runtime_reports_host_without_private_paths() { let bridge = LocalWorkerRuntime::new("local:test", "/workspace/project", None); @@ -1011,7 +1279,7 @@ mod tests { fn registry_lists_runtimes_hosts_and_workers() { let temp = TempDir::new().unwrap(); write_metadata(temp.path(), "coder", &metadata(Some("/workspace/project"))); - let registry = WorkerRuntimeRegistry::for_local_pods(LocalWorkerRuntime::new( + let registry = RuntimeRegistry::for_local_pods(LocalWorkerRuntime::new( "local:test", "/workspace/project", Some(temp.path().to_path_buf()), @@ -1019,6 +1287,14 @@ mod tests { 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); @@ -1043,7 +1319,7 @@ mod tests { fn registry_resolves_backend_validated_host_ids() { let temp = TempDir::new().unwrap(); write_metadata(temp.path(), "coder", &metadata(Some("/workspace/project"))); - let registry = WorkerRuntimeRegistry::for_local_pods(LocalWorkerRuntime::new( + let registry = RuntimeRegistry::for_local_pods(LocalWorkerRuntime::new( "local:test", "/workspace/project", Some(temp.path().to_path_buf()), @@ -1133,7 +1409,7 @@ mod tests { "/workspace/project", Some(temp.path().to_path_buf()), ); - let registry = WorkerRuntimeRegistry::for_local_pods(bridge.clone()); + let registry = RuntimeRegistry::for_local_pods(bridge.clone()); let listed = registry.list_workers(100); assert_eq!(listed.items.len(), worker_names.len()); @@ -1149,7 +1425,9 @@ mod tests { assert!(!worker.worker_id.contains('@')); assert!(!worker.worker_id.contains('#')); - let from_registry = registry.worker(&worker.worker_id).unwrap(); + 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); diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index 7a804518..817c6f1e 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -40,8 +40,13 @@ pub enum Error { MissingFrontmatter(String), #[error("unknown local host `{0}`")] UnknownHost(String), - #[error("unknown local worker `{0}`")] - UnknownWorker(String), + #[error("unknown runtime `{0}`")] + UnknownRuntime(String), + #[error("unknown worker `{worker_id}` in runtime `{runtime_id}`")] + UnknownWorker { + runtime_id: String, + worker_id: String, + }, #[error("invalid runtime {kind} `{value}`")] InvalidRuntimeIdentifier { kind: String, value: String }, #[error("runtime `{runtime_id}` does not support `{capability}`")] diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 6f15a305..983f8f9d 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; use crate::hosts::{ - DiagnosticSeverity, HostSummary, LocalWorkerRuntime, RuntimeDiagnostic, RuntimeSummary, - WorkerRuntimeRegistry, WorkerSummary, + DiagnosticSeverity, HostSummary, LocalWorkerRuntime, RuntimeDiagnostic, RuntimeRegistry, + RuntimeSummary, WorkerSummary, }; use crate::identity::WorkspaceIdentity; use crate::records::{ @@ -64,7 +64,7 @@ pub struct WorkspaceApi { config: ServerConfig, store: Arc, records: LocalProjectRecordReader, - runtime: Arc, + runtime: Arc, } impl WorkspaceApi { @@ -78,13 +78,11 @@ impl WorkspaceApi { updated_at: config.workspace_created_at.clone(), }) .await?; - let runtime = Arc::new(WorkerRuntimeRegistry::for_local_pods( - LocalWorkerRuntime::new( - config.workspace_id.clone(), - config.workspace_root.clone(), - config.local_runtime_data_dir.clone(), - ), - )); + let runtime = Arc::new(RuntimeRegistry::for_local_pods(LocalWorkerRuntime::new( + config.workspace_id.clone(), + config.workspace_root.clone(), + config.local_runtime_data_dir.clone(), + ))); Ok(Self { records: LocalProjectRecordReader::new(config.workspace_root.clone()), config, @@ -613,7 +611,8 @@ impl IntoResponse for ApiError { Error::InvalidRecordId(_) | Error::MissingFrontmatter(_) | Error::UnknownHost(_) - | Error::UnknownWorker(_) + | Error::UnknownRuntime(_) + | Error::UnknownWorker { .. } | Error::UnknownRepository(_) => StatusCode::NOT_FOUND, Error::Ticket(_) => StatusCode::NOT_FOUND, Error::RuntimeCapabilityUnsupported { .. } => StatusCode::NOT_IMPLEMENTED, @@ -746,6 +745,15 @@ mod tests { let runtimes = get_json(app.clone(), "/api/runtimes").await; assert_eq!(runtimes["source"], "worker_runtime_registry"); assert_eq!(runtimes["items"][0]["runtime_id"], "local-worker-runtime"); + assert_eq!( + runtimes["items"][0]["source"]["kind"], + "local_compatibility" + ); + assert_eq!( + runtimes["items"][0]["source"]["identity_authority"], + "runtime_registry_projection" + ); + assert!(!runtimes.to_string().contains("/workspace/demo")); assert_eq!(runtimes["items"][0]["host_ids"][0], host_id); let workers = get_json(app.clone(), "/api/workers").await;