From 89024d3b0102b25056c42dec93a93eb190e9cf0f Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 23 Sep 2026 10:00:56 +0900 Subject: [PATCH] feat: migrate workspace worker and companion domains to ServerApi --- crates/client/src/backend_runtime.rs | 54 +- crates/client/src/workspace_product.rs | 53 +- crates/protocol/src/lib.rs | 45 + crates/server-api/Cargo.toml | 4 +- .../examples/generate_companion_api_types.rs | 57 +- .../generate_worker_launch_api_types.rs | 62 +- crates/server-api/src/lib.rs | 872 +++- .../src/repository_openapi_typescript.rs | 230 +- crates/worker-runtime/src/resource.rs | 93 +- crates/worker-runtime/src/worker_source.rs | 259 +- .../worker-runtime/src/workspace_request.rs | 80 +- .../src/feature/builtin/manage_worker.rs | 160 +- .../src/feature/builtin/worker_observation.rs | 33 +- crates/worker/src/lib.rs | 4 +- crates/worker/src/worker.rs | 104 + crates/workspace-server/src/server.rs | 1121 ++-- openapi/server-api.json | 4544 ++++++++++++++++- .../src/lib/generated/companion-api.ts | 38 +- .../src/lib/generated/repository-api.ts | 14 +- .../src/lib/generated/runtime-api.ts | 18 +- .../src/lib/generated/workdir-api.ts | 2 +- .../src/lib/generated/worker-launch-api.ts | 134 +- .../src/lib/workspace/api/workers.ts | 207 +- .../src/lib/workspace/sidebar/types.ts | 44 +- .../routes/w/[workspaceId]/workers/+page.ts | 8 +- .../workers/[workerRef]/+page.ts | 7 +- .../workers/[workerRef]/console/+page.ts | 7 +- .../w/[workspaceId]/workers/new/+page.svelte | 36 +- 28 files changed, 7471 insertions(+), 819 deletions(-) diff --git a/crates/client/src/backend_runtime.rs b/crates/client/src/backend_runtime.rs index 0ab75ab80..78985cf4f 100644 --- a/crates/client/src/backend_runtime.rs +++ b/crates/client/src/backend_runtime.rs @@ -393,15 +393,16 @@ async fn get_backend_worker_launch_options_with_client( target: &BackendWorkerLaunchTarget, api: &BackendApiClient, ) -> Result { - let path = backend_workspace_workers_launch_options_path( - target - .workspace_id - .as_deref() - .expect("validated Backend Workspace scope"), - ); - let response = api.request(HttpMethod::GET, &path)?.send().await?; - let response = api.require_success(response).await?; - Ok(response.json::().await?) + runtime_server_api_client(&target.base_url, api)? + .workspace_worker_launch_options( + target + .workspace_id + .as_deref() + .expect("validated Backend Workspace scope") + .to_string(), + ) + .await + .map_err(Into::into) } pub async fn create_backend_worker( @@ -418,19 +419,17 @@ async fn create_backend_worker_with_client( request: &BackendCreateWorkerRequest, api: &BackendApiClient, ) -> Result { - let path = backend_workspace_workers_path( - target - .workspace_id - .as_deref() - .expect("validated Backend Workspace scope"), - ); - let response = api - .request(HttpMethod::POST, &path)? - .json(request) - .send() - .await?; - let response = api.require_success(response).await?; - Ok(response.json::().await?) + runtime_server_api_client(&target.base_url, api)? + .workspace_worker_create( + target + .workspace_id + .as_deref() + .expect("validated Backend Workspace scope") + .to_string(), + request.clone(), + ) + .await + .map_err(Into::into) } pub async fn list_backend_workers( @@ -680,17 +679,6 @@ fn validate_list_target( Ok(()) } -fn backend_workspace_workers_path(workspace_id: &str) -> String { - format!("/api/w/{}/workers", path_segment_encode(workspace_id)) -} - -fn backend_workspace_workers_launch_options_path(workspace_id: &str) -> String { - format!( - "{}/launch-options", - backend_workspace_workers_path(workspace_id) - ) -} - fn protocol_ws_request( target: &BackendRuntimeTarget, api: &BackendApiClient, diff --git a/crates/client/src/workspace_product.rs b/crates/client/src/workspace_product.rs index 3676e0337..32a360451 100644 --- a/crates/client/src/workspace_product.rs +++ b/crates/client/src/workspace_product.rs @@ -1,6 +1,3 @@ -use reqwest::Method; -use serde::Serialize; -use serde::de::DeserializeOwned; use server_api::{ BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse, CreateWorkspaceWorkerRequest, ListResponse, MemoryDocumentResponse, MemoryStagingListResponse, @@ -417,7 +414,10 @@ impl BackendWorkspaceProductClient { &self, ticket_id: &str, ) -> Result { - let options: WorkerLaunchOptionsResponse = self.get_json("/workers/launch-options")?; + let workspace_id = self.workspace_id.clone(); + let options: WorkerLaunchOptionsResponse = self.generated(move |client| async move { + client.workspace_worker_launch_options(workspace_id).await + })?; let runtime = options .runtimes .iter() @@ -439,8 +439,10 @@ impl BackendWorkspaceProductClient { workdir_attachments: Vec::new(), control_operation_id: None, }; - let response: BrowserCreateWorkerResponse = - self.send_json(Method::POST, "/workers", Some(&request))?; + let workspace_id = self.workspace_id.clone(); + let response: BrowserCreateWorkerResponse = self.generated(move |client| async move { + client.workspace_worker_create(workspace_id, request).await + })?; Ok(format!( "Started Intake Worker {}/{} for Ticket {ticket_id}", response.runtime_id, response.worker_id @@ -448,8 +450,11 @@ impl BackendWorkspaceProductClient { } pub fn start_workspace_orchestrator(&self) -> Result { + let workspace_id = self.workspace_id.clone(); let response: BrowserWorkspaceOrchestratorResponse = - self.send_json::<(), _>(Method::POST, "/orchestrator", None)?; + self.generated(move |client| async move { + client.workspace_orchestrator_start(workspace_id).await + })?; let worker = response.worker.ok_or_else(|| { BackendWorkspaceClientError::InvalidTarget( "Backend accepted the Orchestrator request without returning a Worker".to_string(), @@ -464,39 +469,6 @@ impl BackendWorkspaceProductClient { pub fn default_product_list_limit() -> usize { DEFAULT_PRODUCT_LIST_LIMIT } - - fn get_json(&self, path: &str) -> Result { - self.send_json::<(), R>(Method::GET, path, None) - } - - fn send_json( - &self, - method: Method, - path: &str, - body: Option<&B>, - ) -> Result { - let response = self.request(method, path, body)?.send()?; - self.api.check_status(response.status())?; - response.json().map_err(BackendWorkspaceClientError::Http) - } - - fn request( - &self, - method: Method, - path: &str, - body: Option<&B>, - ) -> Result { - let path = format!( - "/api/w/{}/{}", - encode_path_segment(&self.workspace_id), - path.trim_start_matches('/') - ); - let request = self.api.blocking_request(method, &path)?; - Ok(match body { - Some(body) => request.json(body), - None => request, - }) - } } impl TicketBackend for BackendWorkspaceProductClient { @@ -785,6 +757,7 @@ fn ticket_list_state_query(query: &TicketListQuery) -> String { } } +#[cfg(test)] fn encode_path_segment(value: &str) -> String { let mut encoded = String::with_capacity(value.len()); for byte in value.bytes() { diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index b53729543..3f5fe5ea3 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -126,6 +126,7 @@ pub struct WorkerCommandAcknowledgement { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] #[serde(tag = "kind", content = "state", rename_all = "snake_case")] pub enum WorkerState { Idle, @@ -134,6 +135,7 @@ pub enum WorkerState { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] #[serde(tag = "kind", content = "state", rename_all = "snake_case")] pub enum WorkerBusyState { Run(WorkerRunState), @@ -142,6 +144,7 @@ pub enum WorkerBusyState { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub enum WorkerRunState { Running, @@ -152,6 +155,7 @@ pub enum WorkerRunState { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub enum WorkerMaintenanceState { Compacting, @@ -159,8 +163,13 @@ pub enum WorkerMaintenanceState { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] pub struct WorkerStateSnapshot { /// Highest lifecycle command id observed by this controller instance. + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 9_007_199_254_740_991_u64)) + )] pub last_command_id: u64, pub state: WorkerState, } @@ -453,13 +462,29 @@ impl PasteArtifactAvailability { #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] pub struct PasteArtifactRef { pub artifact_id: String, + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 9_007_199_254_740_991_u64)) + )] pub created_at_ms: u64, pub media_type: PasteArtifactMediaType, /// Availability observed when this immutable reference was committed. /// Reads revalidate storage and integrity rather than trusting this field. pub availability: PasteArtifactAvailability, + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 9_007_199_254_740_991_u64)) + )] pub byte_len: u64, + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 9_007_199_254_740_991_u64)) + )] pub char_count: u64, + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 9_007_199_254_740_991_u64)) + )] pub line_count: u64, pub sha256: String, pub source_entry_id: String, @@ -499,8 +524,16 @@ pub struct UploadedFileRef { pub artifact_id: String, pub file_name: String, pub media_type: String, + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 9_007_199_254_740_991_u64)) + )] pub created_at_ms: u64, pub availability: UploadedFileAvailability, + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 9_007_199_254_740_991_u64)) + )] pub byte_len: u64, pub sha256: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -519,8 +552,20 @@ pub enum Segment { /// `[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage` /// re-broadcast. Paste { + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 4_294_967_295_u32)) + )] id: u32, + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 4_294_967_295_u32)) + )] chars: u32, + #[cfg_attr( + feature = "json-schema", + schemars(range(min = 0, max = 4_294_967_295_u32)) + )] lines: u32, content: String, }, diff --git a/crates/server-api/Cargo.toml b/crates/server-api/Cargo.toml index 47452a8f0..718005b08 100644 --- a/crates/server-api/Cargo.toml +++ b/crates/server-api/Cargo.toml @@ -13,7 +13,7 @@ typescript = ["dep:ts-rs", "protocol/typescript"] api-macros = { workspace = true, features = ["reqwest", "axum", "openapi"] } memory.workspace = true merge-request.workspace = true -protocol.workspace = true +protocol = { workspace = true, features = ["json-schema"] } runtime-api.workspace = true schemars.workspace = true serde = { workspace = true, features = ["derive"] } @@ -36,11 +36,9 @@ name = "generate_workdir_api_types" [[example]] name = "generate_worker_launch_api_types" -required-features = ["typescript"] [[example]] name = "generate_companion_api_types" -required-features = ["typescript"] [[example]] name = "generate_memory_api_types" diff --git a/crates/server-api/examples/generate_companion_api_types.rs b/crates/server-api/examples/generate_companion_api_types.rs index fd402438a..bae574c38 100644 --- a/crates/server-api/examples/generate_companion_api_types.rs +++ b/crates/server-api/examples/generate_companion_api_types.rs @@ -1,6 +1,55 @@ +use std::fs; +use std::path::Path; + +use server_api::repository_openapi_typescript::{ + COMPANION_OUTPUT_PATH, INPUT_PATH, generate_companion_typescript, +}; + fn main() { - print!( - "{}", - server_api::normalized_typescript(server_api::companion_api_typescript()) - ); + if let Err(error) = run() { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), String> { + let mut check = false; + for argument in std::env::args().skip(1) { + match argument.as_str() { + "--check" => check = true, + "--help" | "-h" => { + println!( + "Generate the bounded Companion API TypeScript artifact.\n\nUsage: cargo run -q -p server-api --example generate_companion_api_types -- [--check]" + ); + return Ok(()); + } + other => return Err(format!("unknown argument `{other}`")), + } + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let input_path = root.join(INPUT_PATH); + let output_path = root.join(COMPANION_OUTPUT_PATH); + let input = fs::read_to_string(&input_path) + .map_err(|error| format!("failed to read {}: {error}", input_path.display()))?; + let generated = generate_companion_typescript(&input) + .map_err(|error| format!("failed to generate Companion API types: {error}"))?; + if check { + let checked_in = fs::read_to_string(&output_path) + .map_err(|error| format!("failed to read {}: {error}", output_path.display()))?; + if checked_in != generated { + return Err(format!( + "{} is stale; regenerate it without --check", + output_path.display() + )); + } + println!("{} is current", output_path.display()); + return Ok(()); + } + let temporary_path = output_path.with_extension("ts.tmp"); + fs::write(&temporary_path, generated) + .map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?; + fs::rename(&temporary_path, &output_path) + .map_err(|error| format!("failed to replace {}: {error}", output_path.display()))?; + println!("generated {}", output_path.display()); + Ok(()) } diff --git a/crates/server-api/examples/generate_worker_launch_api_types.rs b/crates/server-api/examples/generate_worker_launch_api_types.rs index 5e326b368..33ea2a545 100644 --- a/crates/server-api/examples/generate_worker_launch_api_types.rs +++ b/crates/server-api/examples/generate_worker_launch_api_types.rs @@ -1,6 +1,60 @@ +use std::fs; +use std::path::Path; + +use server_api::repository_openapi_typescript::{ + INPUT_PATH, WORKER_OUTPUT_PATH, generate_worker_typescript, +}; + fn main() { - print!( - "{}", - server_api::normalized_typescript(server_api::worker_launch_api_typescript()) - ); + if let Err(error) = run() { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), String> { + let check = parse_check_argument()?; + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let input_path = root.join(INPUT_PATH); + let output_path = root.join(WORKER_OUTPUT_PATH); + let input = fs::read_to_string(&input_path) + .map_err(|error| format!("failed to read {}: {error}", input_path.display()))?; + let generated = generate_worker_typescript(&input) + .map_err(|error| format!("failed to generate Worker API types: {error}"))?; + if check { + let checked_in = fs::read_to_string(&output_path) + .map_err(|error| format!("failed to read {}: {error}", output_path.display()))?; + if checked_in != generated { + return Err(format!( + "{} is stale; regenerate it without --check", + output_path.display() + )); + } + println!("{} is current", output_path.display()); + return Ok(()); + } + let temporary_path = output_path.with_extension("ts.tmp"); + fs::write(&temporary_path, generated) + .map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?; + fs::rename(&temporary_path, &output_path) + .map_err(|error| format!("failed to replace {}: {error}", output_path.display()))?; + println!("generated {}", output_path.display()); + Ok(()) +} + +fn parse_check_argument() -> Result { + let mut check = false; + for argument in std::env::args().skip(1) { + match argument.as_str() { + "--check" => check = true, + "--help" | "-h" => { + println!( + "Generate the bounded Worker API TypeScript artifact.\n\nUsage: cargo run -q -p server-api --example generate_worker_launch_api_types -- [--check]" + ); + return Ok(true); + } + other => return Err(format!("unknown argument `{other}`")), + } + } + Ok(check) } diff --git a/crates/server-api/src/lib.rs b/crates/server-api/src/lib.rs index e93c931a3..f5b6ed71c 100644 --- a/crates/server-api/src/lib.rs +++ b/crates/server-api/src/lib.rs @@ -336,6 +336,41 @@ impl_openapi_schema!( WorkspaceRuntimeListResponse, RuntimeWorkerListResponse, WorkerRestoreResponse, + WorkerControlSubject, + WorkerControlRecord, + WorkerControlListResponse, + WorkspaceWorkerDiscoveryQuery, + WorkspaceWorkerSubject, + WorkspaceWorkerDiscoveryItem, + WorkspaceWorkerDiscoveryPage, + CompanionTranscriptQuery, + CompanionLifecycleState, + CompanionMessageDisposition, + CompanionTransportSummary, + CompanionStatusResponse, + CompanionMessageRequest, + CompanionCancelRequest, + CompanionMessageResponse, + CompanionTranscriptRole, + CompanionTranscriptItem, + CompanionTranscriptProjection, + WorkerRemoveRequest, + WorkerRemoveResponse, + WorkerRemoveApiError, + RuntimeResourceKind, + RuntimeResourceOperation, + RuntimeResourceRedactionPolicy, + RuntimeProfileSourceGraphSummary, + RuntimeResourceHandle, + RuntimeResourceFetchRequest, + RuntimeResourceFetchResponse, + RuntimeResourceFetchApiError, + WorkerListResponse, + WorkerSummary, + WorkerLaunchOptionsResponse, + CreateWorkspaceWorkerRequest, + BrowserCreateWorkerResponse, + BrowserWorkspaceOrchestratorResponse, ); #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -1028,6 +1063,377 @@ pub trait ServerApi { #[path] worker_id: String, ) -> Result; + #[get( + "/api/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_worker_list_alias(&self) -> Result; + + #[post( + "/api/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_worker_create_alias( + &self, + #[extension] context: ServerRequestContext, + #[body] request: CreateWorkspaceWorkerRequest, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/orchestrator", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_orchestrator_status( + &self, + #[path] workspace_id: String, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/orchestrator", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_orchestrator_start( + &self, + #[path] workspace_id: String, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/worker-control/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500] + )] + async fn worker_control_list( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/worker-control/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503] + )] + async fn worker_control_spawn( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[body] request: CreateWorkspaceWorkerRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/input", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503] + )] + async fn worker_control_input( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[path] runtime_id: String, + #[path] worker_id: String, + #[body] request: RuntimeWorkerInputRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/cancel", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503] + )] + async fn worker_control_cancel( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[path] runtime_id: String, + #[path] worker_id: String, + #[body] request: RuntimeWorkerLifecycleRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/stop", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504] + )] + async fn worker_control_stop( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[path] runtime_id: String, + #[path] worker_id: String, + #[body] request: RuntimeWorkerLifecycleRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/restore", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504] + )] + async fn worker_control_restore( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[path] runtime_id: String, + #[path] worker_id: String, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/worker-observation/sessions", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + openapi = false + )] + async fn worker_observation_sessions( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/worker-observation/session", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 504], + openapi = false + )] + async fn worker_observation_capture( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[body] request: WorkerObservationSubjectRef, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/worker-discovery/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500] + )] + async fn workspace_worker_discovery( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[query] query: WorkspaceWorkerDiscoveryQuery, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_worker_list( + &self, + #[path] workspace_id: String, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_worker_create( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[body] request: CreateWorkspaceWorkerRequest, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/workers/{worker_ref}", + status = 200, + error_status = 404, + additional_error_statuses = [400, 401, 403, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_worker_detail( + &self, + #[path] workspace_id: String, + #[path] worker_ref: String, + ) -> Result; + + #[get( + "/api/workers/launch-options", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_worker_launch_options_alias( + &self, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/workers/launch-options", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_worker_launch_options( + &self, + #[path] workspace_id: String, + ) -> Result; + + #[post( + "/api/runtime/v1/workspaces/{workspace_id}/resources/fetch", + status = 200, + error_status = 400, + additional_error_statuses = [401, 404, 410, 502, 504] + )] + async fn runtime_resource_fetch( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[body] request: RuntimeResourceFetchRequest, + ) -> Result; + + #[get( + "/api/companion/status", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 500], + bearer_auth = true, + browser_auth = true + )] + async fn companion_status_alias(&self) -> Result; + + #[get( + "/api/w/{workspace_id}/companion/status", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn companion_status( + &self, + #[path] workspace_id: String, + ) -> Result; + + #[get( + "/api/companion/transcript", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 500], + bearer_auth = true, + browser_auth = true + )] + async fn companion_transcript_alias( + &self, + #[query] query: CompanionTranscriptQuery, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/companion/transcript", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn companion_transcript( + &self, + #[path] workspace_id: String, + #[query] query: CompanionTranscriptQuery, + ) -> Result; + + #[post( + "/api/companion/messages", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 500], + bearer_auth = true, + browser_auth = true + )] + async fn companion_message_alias( + &self, + #[body] request: CompanionMessageRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/companion/messages", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn companion_message( + &self, + #[path] workspace_id: String, + #[body] request: CompanionMessageRequest, + ) -> Result; + + #[post( + "/api/companion/cancel", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 500], + bearer_auth = true, + browser_auth = true + )] + async fn companion_cancel_alias( + &self, + #[body] request: CompanionCancelRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/companion/cancel", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn companion_cancel( + &self, + #[path] workspace_id: String, + #[body] request: CompanionCancelRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/workers/remove", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 503] + )] + async fn workspace_worker_remove( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[body] request: WorkerRemoveRequest, + ) -> Result; + #[get( "/api/runtimes", status = 200, @@ -2468,10 +2874,17 @@ pub struct ServerWorkerSource { pub worker_id: String, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ServerRuntimeSource { + pub runtime_id: String, + pub worker_id: Option, +} + #[derive(Clone)] pub struct ServerRequestContext { pub actor: Option, pub worker_source: Option, + pub runtime_source: Option, pub origin: Option, /// Transport headers retained for Server-side authentication adapters. /// @@ -5829,7 +6242,7 @@ pub enum RuntimeTicketWorkerRole { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum RuntimeWorkerSpawnAcceptanceRequirement { SocketReady, RunAccepted { @@ -6556,8 +6969,331 @@ pub struct RuntimeConnectionTestResponse { pub diagnostics: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct WorkerControlSubject { + pub runtime_id: String, + pub worker_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct WorkerControlRecord { + pub subject: WorkerControlSubject, + pub relation: String, + pub origin: String, + pub permissions: Vec, + pub summary: WorkerLaunchWorkerSummary, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct WorkerControlListResponse { + pub workspace_id: String, + pub items: Vec, + pub truncated: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct WorkspaceWorkerDiscoveryQuery { + pub cursor: Option, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] + pub limit: Option, + pub query: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum WorkerObservationSubjectRef { + RuntimeWorker { + runtime_id: String, + worker_id: String, + }, + SubWorker { + name: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorkerObservationSubject { + pub subject: WorkerObservationSubjectRef, + pub display_name: String, + pub relation: String, + pub status: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorkerObservationSessionsResponse { + pub sessions: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct WorkerObservationCaptureResponse { + pub segment_id: String, + pub session: protocol::SessionSnapshot, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CompanionTranscriptQuery { + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] + pub start: Option, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] + pub limit: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct WorkerRemoveRequest { + pub target_runtime_id: String, + pub target_worker_id: String, + pub reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct WorkerRemoveResponse { + pub removed: bool, + pub runtime_id: String, + pub worker_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct WorkerRemoveApiError { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub diagnostics: Option>, + #[serde(skip, default = "default_repository_error_status")] + #[schemars(skip)] + status: u16, +} + +impl WorkerRemoveApiError { + pub fn new( + status: u16, + error: Option, + code: Option, + message: Option, + diagnostics: Option>, + ) -> Self { + Self { + error, + code, + message, + diagnostics, + status, + } + } +} + +impl std::fmt::Display for WorkerRemoveApiError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str( + self.message + .as_deref() + .or(self.error.as_deref()) + .or(self.code.as_deref()) + .unwrap_or("Worker removal failed"), + ) + } +} + +impl std::error::Error for WorkerRemoveApiError {} +impl api_macros::HttpError for WorkerRemoveApiError { + fn status_code(&self) -> u16 { + self.status + } +} +impl api_macros::HttpRequestError for WorkerRemoveApiError { + fn from_request_rejection(status: u16, message: String) -> Self { + Self::new( + status, + Some("invalid_worker_remove".to_string()), + None, + Some(message), + None, + ) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeResourceKind { + ProfileSourceArchive, + RepositorySshAccess, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeResourceOperation { + FetchArchive, + FetchOnce, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeResourceRedactionPolicy { + RuntimeInternalOnly, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct RuntimeProfileSourceGraphSummary { + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] + pub source_count: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] + pub total_source_bytes: u64, + pub entrypoints: BTreeMap, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] + pub import_count: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct RuntimeResourceHandle { + pub kind: RuntimeResourceKind, + pub workspace_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scope_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worker_id: Option, + pub resource_id: String, + pub digest: String, + pub operation: RuntimeResourceOperation, + #[schemars(range(min = -9_007_199_254_740_991_i64, max = 9_007_199_254_740_991_i64))] + pub expires_at_unix_seconds: i64, + pub nonce: String, + pub revision: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] + pub generation: Option, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] + pub max_bytes: u64, + pub content_type: String, + pub redaction: RuntimeResourceRedactionPolicy, + pub audit_correlation_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub profile_source_graph: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct RuntimeResourceFetchRequest { + pub handle: RuntimeResourceHandle, + pub runtime_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worker_id: Option, + pub audit_correlation_id: String, +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct RuntimeResourceFetchResponse { + pub kind: RuntimeResourceKind, + pub resource_id: String, + pub digest: String, + pub content_type: String, + pub bytes: Vec, + pub audit_correlation_id: String, +} + +impl std::fmt::Debug for RuntimeResourceFetchResponse { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("RuntimeResourceFetchResponse") + .field("kind", &self.kind) + .field("resource_id", &self.resource_id) + .field("digest", &self.digest) + .field("content_type", &self.content_type) + .field( + "bytes", + &format_args!("[REDACTED; {} bytes]", self.bytes.len()), + ) + .field("audit_correlation_id", &self.audit_correlation_id) + .finish() + } +} +impl Drop for RuntimeResourceFetchResponse { + fn drop(&mut self) { + self.bytes.fill(0); + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(tag = "code", rename_all = "snake_case")] +pub enum RuntimeResourceFetchApiError { + Expired, + Unauthorized { + message: String, + }, + UnsupportedKind, + MissingResource, + DigestMismatch { + expected: String, + actual: String, + }, + Oversized { + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] + max_bytes: u64, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] + actual_bytes: u64, + }, + ContentTypeMismatch { + expected: String, + actual: String, + }, + Timeout, + Transport { + message: String, + }, + InvalidResponse { + message: String, + }, +} + +impl std::fmt::Display for RuntimeResourceFetchApiError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "Runtime resource fetch failed: {self:?}") + } +} +impl std::error::Error for RuntimeResourceFetchApiError {} +impl api_macros::HttpError for RuntimeResourceFetchApiError { + fn status_code(&self) -> u16 { + match self { + Self::Expired => 410, + Self::Unauthorized { .. } => 401, + Self::MissingResource => 404, + Self::Timeout => 504, + Self::Transport { .. } => 502, + Self::UnsupportedKind + | Self::DigestMismatch { .. } + | Self::Oversized { .. } + | Self::ContentTypeMismatch { .. } + | Self::InvalidResponse { .. } => 400, + } + } +} +impl api_macros::HttpRequestError for RuntimeResourceFetchApiError { + fn from_request_rejection(_status: u16, message: String) -> Self { + Self::InvalidResponse { message } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] pub struct WorkerWorkspaceSummary { pub visibility: String, pub identity: String, @@ -6579,9 +7315,9 @@ pub struct WorkerCapabilitySummary { pub can_spawn_followup: bool, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(tag = "kind", rename_all = "snake_case")] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum WorkspaceWorkerSubject { RuntimeWorker { runtime_id: String, @@ -6592,8 +7328,9 @@ pub enum WorkspaceWorkerSubject { /// Bounded, model-safe projection used by privileged Workspace Worker discovery. /// Runtime placement appears only in the typed subject required by Worker /// control operations; provider and launch internals are intentionally omitted. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] pub struct WorkspaceWorkerDiscoveryItem { pub subject: WorkspaceWorkerSubject, pub resource_key: String, @@ -6604,7 +7341,7 @@ pub struct WorkspaceWorkerDiscoveryItem { } /// Public lifecycle projection for the Workspace Companion endpoint. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum CompanionLifecycleState { @@ -6614,7 +7351,7 @@ pub enum CompanionLifecycleState { } /// Public outcome of a Companion message submission. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum CompanionMessageDisposition { @@ -6623,7 +7360,7 @@ pub enum CompanionMessageDisposition { } /// Public, bounded transport metadata for Companion status. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CompanionTransportSummary { @@ -6632,7 +7369,7 @@ pub struct CompanionTransportSummary { } /// Public Workspace Companion status response. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CompanionStatusResponse { @@ -6644,7 +7381,7 @@ pub struct CompanionStatusResponse { } /// Public Workspace Companion message request. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CompanionMessageRequest { @@ -6652,7 +7389,7 @@ pub struct CompanionMessageRequest { } /// Public Workspace Companion cancellation request. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CompanionCancelRequest { @@ -6661,7 +7398,7 @@ pub struct CompanionCancelRequest { } /// Public Workspace Companion message response. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CompanionMessageResponse { @@ -6670,7 +7407,7 @@ pub struct CompanionMessageResponse { } /// User-visible role accepted in the public Companion transcript. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum CompanionTranscriptRole { @@ -6679,10 +7416,11 @@ pub enum CompanionTranscriptRole { } /// One allowlisted, user-visible Companion transcript item. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CompanionTranscriptItem { + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub sequence: usize, pub role: CompanionTranscriptRole, pub content: String, @@ -6690,14 +7428,18 @@ pub struct CompanionTranscriptItem { } /// Bounded public Companion transcript projection. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CompanionTranscriptProjection { pub state: CompanionLifecycleState, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub start: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub limit: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub total: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub next: Option, pub items: Vec, } @@ -6734,7 +7476,7 @@ pub fn companion_api_typescript() -> String { ) } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct WorkspaceWorkerDiscoveryPage { pub workers: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -6751,7 +7493,7 @@ pub struct WorkerWorkdirAttachmentSummary { pub working_directory: WorkingDirectorySummary, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct RuntimeWorkerWorkdirAttachmentSummary { @@ -6784,7 +7526,6 @@ pub struct WorkerSummary { pub state: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "typescript", ts(optional))] - #[schemars(with = "Option")] pub worker_state: Option, pub last_seen_at: Option, #[serde(default)] @@ -6799,11 +7540,25 @@ pub struct WorkerSummary { pub diagnostics: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkerListResponse { + pub workspace_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] + pub limit: usize, + pub items: Vec, + pub source: String, + #[serde(default)] + pub diagnostics: Vec, +} + /// Runtime-owned Worker summary embedded in Worker launch responses. /// /// This preserves the existing launch wire shape. Workspace-owned Worker list /// and detail responses use [`WorkerSummary`] instead. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkerLaunchWorkerSummary { @@ -6828,7 +7583,7 @@ pub struct WorkerLaunchWorkerSummary { pub diagnostics: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkerLaunchOptionsResponse { @@ -6841,7 +7596,7 @@ pub struct WorkerLaunchOptionsResponse { pub diagnostics: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkerLaunchRuntimeOption { @@ -6854,7 +7609,7 @@ pub struct WorkerLaunchRuntimeOption { pub diagnostics: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkerLaunchProfileCandidate { @@ -6863,7 +7618,7 @@ pub struct WorkerLaunchProfileCandidate { pub description: String, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryRepositoryOption { @@ -6873,7 +7628,7 @@ pub struct WorkingDirectoryRepositoryOption { pub default_selector: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct BrowserWorkerWorkingDirectorySelection { @@ -6884,7 +7639,7 @@ pub struct BrowserWorkerWorkingDirectorySelection { pub relative_cwd: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CreateWorkspaceWorkerTicketAssignmentRequest { @@ -6892,7 +7647,7 @@ pub struct CreateWorkspaceWorkerTicketAssignmentRequest { pub operation_id: String, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct CreateWorkspaceWorkerRequest { @@ -6911,7 +7666,7 @@ pub struct CreateWorkspaceWorkerRequest { pub control_operation_id: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct BrowserCreateWorkerResponse { @@ -6923,7 +7678,7 @@ pub struct BrowserCreateWorkerResponse { pub diagnostics: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct BrowserWorkspaceOrchestratorResponse { @@ -8020,33 +8775,24 @@ pub fn worker_launch_api_typescript() -> String { ) } -#[cfg(all(test, feature = "typescript"))] +#[cfg(test)] mod worker_launch_typescript_tests { #[test] fn generated_worker_launch_api_contract_is_current() { - let expected = super::worker_launch_api_typescript(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../web/workspace/src/lib/generated/worker-launch-api.ts"); + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let input = + std::fs::read_to_string(root.join(crate::repository_openapi_typescript::INPUT_PATH)) + .expect("read Server API OpenAPI artifact"); + let expected = crate::repository_openapi_typescript::generate_worker_typescript(&input) + .expect("generate Worker API TypeScript"); + let path = root.join(crate::repository_openapi_typescript::WORKER_OUTPUT_PATH); let actual = std::fs::read_to_string(&path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); assert_eq!( - normalize(&actual), - normalize(&expected), - "regenerate Worker launch API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts` and format the generated file", + actual, expected, + "regenerate Worker API TypeScript types with `cargo run -q -p server-api --example generate_worker_launch_api_types`", ); } - - fn normalize(value: &str) -> String { - value - .chars() - .filter_map(|character| match character { - character if character.is_whitespace() => None, - ',' => Some(';'), - character => Some(character), - }) - .collect::() - .replace("=|", "=") - } } #[cfg(all(test, feature = "typescript"))] @@ -9606,8 +10352,8 @@ mod tests { let actual = std::fs::read_to_string(&path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); assert_eq!( - normalize_typescript(&actual), - normalize_typescript(&expected), + normalized_typescript(actual), + normalized_typescript(expected), "regenerate auth API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts` and format the generated file", ); } @@ -9680,35 +10426,23 @@ mod tests { assert!(serde_json::from_value::(private_item).is_err()); } - #[cfg(feature = "typescript")] #[test] fn generated_companion_api_contract_is_current() { - let expected = companion_api_typescript(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../web/workspace/src/lib/generated/companion-api.ts"); + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let input = + std::fs::read_to_string(root.join(crate::repository_openapi_typescript::INPUT_PATH)) + .expect("read Server API OpenAPI artifact"); + let expected = crate::repository_openapi_typescript::generate_companion_typescript(&input) + .expect("generate Companion API TypeScript"); + let path = root.join(crate::repository_openapi_typescript::COMPANION_OUTPUT_PATH); let actual = std::fs::read_to_string(&path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); assert_eq!( - normalize_typescript(&actual), - normalize_typescript(&expected), - "regenerate Companion API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_companion_api_types > web/workspace/src/lib/generated/companion-api.ts` and format the generated file", + actual, expected, + "regenerate Companion API TypeScript types with `cargo run -q -p server-api --example generate_companion_api_types`", ); } - #[cfg(feature = "typescript")] - fn normalize_typescript(value: &str) -> String { - value - .chars() - .filter_map(|character| match character { - character if character.is_whitespace() => None, - ',' => Some(';'), - character => Some(character), - }) - .collect::() - .replace(";}", "}") - .replace("=|", "=") - } - #[test] fn workdir_create_request_preserves_optional_operation_fields() { let payload = serde_json::json!({"repository_key": "main"}); diff --git a/crates/server-api/src/repository_openapi_typescript.rs b/crates/server-api/src/repository_openapi_typescript.rs index 58d62758a..dbbd6455a 100644 --- a/crates/server-api/src/repository_openapi_typescript.rs +++ b/crates/server-api/src/repository_openapi_typescript.rs @@ -18,6 +18,17 @@ pub const RUNTIME_GENERATOR_OPTIONS: &str = "runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output"; pub const RUNTIME_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/runtime-api.ts"; +pub const WORKER_GENERATOR_NAME: &str = "yoi-worker-openapi-typescript"; +pub const WORKER_GENERATOR_VERSION: &str = "1"; +pub const WORKER_GENERATOR_OPTIONS: &str = + "worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output"; +pub const WORKER_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/worker-launch-api.ts"; +pub const COMPANION_GENERATOR_NAME: &str = "yoi-companion-openapi-typescript"; +pub const COMPANION_GENERATOR_VERSION: &str = "1"; +pub const COMPANION_GENERATOR_OPTIONS: &str = + "companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output"; +pub const COMPANION_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/companion-api.ts"; + const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0; const SCHEMA_REF_PREFIX: &str = "#/components/schemas/"; @@ -166,6 +177,82 @@ pub fn generate_runtime_typescript(input: &str) -> Result Result { + generate_domain_typescript( + input, + "Worker", + validate_worker_operations, + WORKER_GENERATOR_NAME, + WORKER_GENERATOR_VERSION, + WORKER_GENERATOR_OPTIONS, + WORKER_OUTPUT_PATH, + "generate_worker_launch_api_types", + ) +} + +pub fn generate_companion_typescript(input: &str) -> Result { + generate_domain_typescript( + input, + "Companion", + validate_companion_operations, + COMPANION_GENERATOR_NAME, + COMPANION_GENERATOR_VERSION, + COMPANION_GENERATOR_OPTIONS, + COMPANION_OUTPUT_PATH, + "generate_companion_api_types", + ) +} + +#[allow(clippy::too_many_arguments)] +fn generate_domain_typescript( + input: &str, + domain: &str, + validate_operations: fn(&Map) -> Result, GenerationError>, + generator_name: &str, + generator_version: &str, + generator_options: &str, + output_path: &str, + example_name: &str, +) -> Result { + let document: Value = serde_json::from_str(input) + .map_err(|error| GenerationError::invalid(format!("invalid OpenAPI JSON: {error}")))?; + let root = object(&document, "OpenAPI document")?; + if root.get("openapi").and_then(Value::as_str) != Some("3.1.0") { + return Err(GenerationError::invalid(format!( + "{domain} TypeScript generation requires OpenAPI 3.1.0" + ))); + } + let info = object_field(root, "info", "OpenAPI document")?; + let contract_version = required_string(info, "version", "OpenAPI info")?; + let contract_digest = required_string(info, "x-yoi-source-digest", "OpenAPI info")?; + validate_contract_digest(contract_digest)?; + let schemas = object_field( + object_field(root, "components", "OpenAPI document")?, + "schemas", + "OpenAPI components", + )?; + let paths = object_field(root, "paths", "OpenAPI document")?; + let roots = validate_operations(paths)?; + let closure = schema_closure(schemas, &roots)?; + let mut output = format!( + "// @generated by {generator_name}@{generator_version}. Do not edit.\n\ +// Input: {INPUT_PATH}\n\ +// Input contract: {contract_version} ({contract_digest})\n\ +// Options: {generator_options}\n\ +// Output: {output_path}\n\ +// Regenerate/check: cargo run -q -p server-api --example {example_name} -- --check\n\n" + ); + for name in closure { + let schema = schemas + .get(&name) + .ok_or_else(|| GenerationError::invalid(format!("missing schema `{name}`")))?; + let declaration = render_schema(schema, &format!("schema `{name}`"))?; + output.push_str(&format!("export type {name} = {declaration};\n\n")); + } + output.pop(); + Ok(output) +} + fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> { let Some(hex) = digest.strip_prefix("sha256:") else { return Err(GenerationError::invalid( @@ -180,6 +267,141 @@ fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> { Ok(()) } +fn validate_worker_operations( + paths: &Map, +) -> Result, GenerationError> { + let operations = [ + OperationContract { + path: "/api/workers", + method: "get", + operation_id: "workspace_worker_list_alias", + request: None, + successes: &[("200", "WorkerListResponse")], + }, + OperationContract { + path: "/api/workers", + method: "post", + operation_id: "workspace_worker_create_alias", + request: Some("CreateWorkspaceWorkerRequest"), + successes: &[("200", "BrowserCreateWorkerResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/orchestrator", + method: "get", + operation_id: "workspace_orchestrator_status", + request: None, + successes: &[("200", "BrowserWorkspaceOrchestratorResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/orchestrator", + method: "post", + operation_id: "workspace_orchestrator_start", + request: None, + successes: &[("200", "BrowserWorkspaceOrchestratorResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/workers", + method: "get", + operation_id: "workspace_worker_list", + request: None, + successes: &[("200", "WorkerListResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/workers", + method: "post", + operation_id: "workspace_worker_create", + request: Some("CreateWorkspaceWorkerRequest"), + successes: &[("200", "BrowserCreateWorkerResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/workers/{worker_ref}", + method: "get", + operation_id: "workspace_worker_detail", + request: None, + successes: &[("200", "WorkerSummary")], + }, + OperationContract { + path: "/api/workers/launch-options", + method: "get", + operation_id: "workspace_worker_launch_options_alias", + request: None, + successes: &[("200", "WorkerLaunchOptionsResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/workers/launch-options", + method: "get", + operation_id: "workspace_worker_launch_options", + request: None, + successes: &[("200", "WorkerLaunchOptionsResponse")], + }, + ]; + validate_operation_contracts(paths, &operations) +} + +fn validate_companion_operations( + paths: &Map, +) -> Result, GenerationError> { + let operations = [ + OperationContract { + path: "/api/companion/status", + method: "get", + operation_id: "companion_status_alias", + request: None, + successes: &[("200", "CompanionStatusResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/companion/status", + method: "get", + operation_id: "companion_status", + request: None, + successes: &[("200", "CompanionStatusResponse")], + }, + OperationContract { + path: "/api/companion/transcript", + method: "get", + operation_id: "companion_transcript_alias", + request: None, + successes: &[("200", "CompanionTranscriptProjection")], + }, + OperationContract { + path: "/api/w/{workspace_id}/companion/transcript", + method: "get", + operation_id: "companion_transcript", + request: None, + successes: &[("200", "CompanionTranscriptProjection")], + }, + OperationContract { + path: "/api/companion/messages", + method: "post", + operation_id: "companion_message_alias", + request: Some("CompanionMessageRequest"), + successes: &[("200", "CompanionMessageResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/companion/messages", + method: "post", + operation_id: "companion_message", + request: Some("CompanionMessageRequest"), + successes: &[("200", "CompanionMessageResponse")], + }, + OperationContract { + path: "/api/companion/cancel", + method: "post", + operation_id: "companion_cancel_alias", + request: Some("CompanionCancelRequest"), + successes: &[("200", "CompanionMessageResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/companion/cancel", + method: "post", + operation_id: "companion_cancel", + request: Some("CompanionCancelRequest"), + successes: &[("200", "CompanionMessageResponse")], + }, + ]; + validate_operation_contracts(paths, &operations) +} + fn validate_repository_operations( paths: &Map, ) -> Result, GenerationError> { @@ -948,9 +1170,7 @@ fn literal_matches_type(kind: &str, value: &Value) -> bool { fn tagged_object_discriminator(value: &Value) -> Option<(String, String)> { let schema = value.as_object()?; - if schema.get("type")?.as_str()? != "object" - || schema.get("additionalProperties") != Some(&Value::Bool(false)) - { + if schema.get("type")?.as_str()? != "object" { return None; } let required = schema.get("required")?.as_array()?; @@ -1447,7 +1667,9 @@ mod tests { ))); assert!(output.contains("export type RuntimeCleanupPlanResponse =")); assert!(output.contains("export type RuntimeWorkerLifecycleResult =")); - assert!(output.contains("export type RuntimeWorkerSpawnAcceptanceRequirement = \"socket_ready\" | { run_accepted:")); + assert!(output.contains( + "export type RuntimeWorkerSpawnAcceptanceRequirement = { kind: \"socket_ready\"; } | { expected_segments: number; kind: \"run_accepted\"; };" + )); assert!(!output.contains("export type UnmigratedDomain =")); let mut document: Value = serde_json::from_str(OPENAPI).unwrap(); diff --git a/crates/worker-runtime/src/resource.rs b/crates/worker-runtime/src/resource.rs index 9f76ce34d..43189039f 100644 --- a/crates/worker-runtime/src/resource.rs +++ b/crates/worker-runtime/src/resource.rs @@ -1,7 +1,7 @@ use crate::auth::BACKEND_RESOURCE_FETCH_PERMISSION; use crate::identity::WorkerId; use crate::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveRef, sha256_hex}; -use crate::workspace_request::{RuntimeWorkspaceRequest, RuntimeWorkspaceRequestClient}; +use crate::workspace_request::RuntimeWorkspaceRequestClient; use async_trait::async_trait; use serde::{Deserialize, Serialize}; @@ -219,11 +219,6 @@ impl BackendResourceClient for HttpBackendResourceClient { &self, request: BackendResourceFetchRequest, ) -> Result { - let body = serde_json::to_vec(&request).map_err(|error| { - BackendResourceError::InvalidResponse { - message: error.to_string(), - } - })?; let endpoint = reqwest::Url::parse(&self.endpoint).map_err(|error| { BackendResourceError::Transport { message: error.to_string(), @@ -248,17 +243,17 @@ impl BackendResourceClient for HttpBackendResourceClient { .to_string(), } })?; - if !endpoint_suffix.starts_with('/') { + let expected_path = format!( + "/api/runtime/v1/workspaces/{}/resources/fetch", + client.workspace_id() + ); + if endpoint_suffix != expected_path { return Err(BackendResourceError::Unauthorized { message: "Workspace resource endpoint does not match its request client" .to_string(), }); } let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::CONTENT_TYPE, - reqwest::header::HeaderValue::from_static("application/json"), - ); if let Some(token) = self.bearer_token.as_deref() { let value = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}")) .map_err(|error| BackendResourceError::Transport { @@ -266,41 +261,57 @@ impl BackendResourceClient for HttpBackendResourceClient { })?; headers.insert(reqwest::header::AUTHORIZATION, value); } - let response = client - .execute(RuntimeWorkspaceRequest { - method: reqwest::Method::POST, - path_and_query: endpoint_suffix.to_string(), - body, + let request: server_api::RuntimeResourceFetchRequest = + serde_json::from_value(serde_json::to_value(&request).map_err(|error| { + BackendResourceError::InvalidResponse { + message: error.to_string(), + } + })?) + .map_err(|error| BackendResourceError::InvalidResponse { + message: error.to_string(), + })?; + let generated = client + .server_api_client( + BACKEND_RESOURCE_FETCH_PERMISSION, + None, headers, - permission: BACKEND_RESOURCE_FETCH_PERMISSION.to_string(), - worker_id: None, - timeout: Some(self.request_timeout), - max_response_bytes: 8 * 1024 * 1024, - }) + self.request_timeout, + 8 * 1024 * 1024, + ) + .map_err(|error| BackendResourceError::Transport { + message: error.to_string(), + })?; + match generated + .runtime_resource_fetch(client.workspace_id().to_string(), request) .await - .map_err(|error| { - if error.is_timeout() { - BackendResourceError::Timeout - } else { - BackendResourceError::Transport { + { + Ok(response) => { + serde_json::from_value(serde_json::to_value(&response).map_err(|error| { + BackendResourceError::InvalidResponse { message: error.to_string(), } - } - })?; - if response.status.is_success() { - serde_json::from_slice::(&response.body).map_err(|err| { - BackendResourceError::InvalidResponse { - message: err.to_string(), - } - }) - } else { - let status = response.status; - match serde_json::from_slice::(&response.body) { - Ok(error) => Err(error), - Err(err) => Err(BackendResourceError::Transport { - message: format!("backend resource fetch failed with HTTP {status}: {err}"), - }), + })?) + .map_err(|error| BackendResourceError::InvalidResponse { + message: error.to_string(), + }) } + Err(server_api::client_support::ClientError::Public { error, .. }) => { + serde_json::from_value(serde_json::to_value(error).map_err(|error| { + BackendResourceError::InvalidResponse { + message: error.to_string(), + } + })?) + .map_err(|error| BackendResourceError::InvalidResponse { + message: error.to_string(), + }) + .and_then(Err) + } + Err(server_api::client_support::ClientError::Failure( + server_api::client_support::ClientFailure::Timeout, + )) => Err(BackendResourceError::Timeout), + Err(error) => Err(BackendResourceError::Transport { + message: error.to_string(), + }), } } } diff --git a/crates/worker-runtime/src/worker_source.rs b/crates/worker-runtime/src/worker_source.rs index dff664a8f..68f68eb54 100644 --- a/crates/worker-runtime/src/worker_source.rs +++ b/crates/worker-runtime/src/worker_source.rs @@ -5,6 +5,7 @@ use crate::workspace_request::{RuntimeWorkspaceRequest, RuntimeWorkspaceRequestC use worker::{ WorkspaceClient, WorkspaceClientError, WorkspacePromptCatalogResolution, WorkspacePromptProjection, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse, + WorkspaceServerOperation, }; use crate::auth::{ @@ -254,45 +255,53 @@ fn execute_remote_worker_remove_http( fn execute_remote_worker_remove_http_blocking( request: RemoteWorkerRemoveHttpRequest, ) -> Result { - let path = format!( - "/api/w/{}/workers/remove", - request.request_client.workspace_id() - ); - let body = serde_json::to_vec(&serde_json::json!({ - "target_runtime_id": request.target_runtime_id, - "target_worker_id": request.target_worker_id, - "reason": request.reason, - })) - .map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?; let mut headers = reqwest::header::HeaderMap::new(); headers.insert( crate::auth::WORKER_MUTATION_SOURCE_PROOF_HEADER, reqwest::header::HeaderValue::from_str(&request.token) .map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?, ); - headers.insert( - reqwest::header::CONTENT_TYPE, - reqwest::header::HeaderValue::from_static("application/json"), - ); - let response = request + let client = request .request_client - .execute_blocking(RuntimeWorkspaceRequest { - method: reqwest::Method::POST, - path_and_query: path, - body, + .server_api_client( + WORKSPACE_REQUEST_PERMISSION, + Some(request.source_worker_id), headers, - permission: WORKSPACE_REQUEST_PERMISSION.to_string(), - worker_id: Some(request.source_worker_id), - timeout: Some(Duration::from_secs(5)), - max_response_bytes: 8 * 1024 * 1024, - }) + Duration::from_secs(5), + 8 * 1024 * 1024, + ) .map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?; - let body = String::from_utf8(response.body) + let workspace_id = request.request_client.workspace_id().to_string(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() .map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?; - Ok(WorkspaceResponse { - status: response.status.as_u16(), - body, - }) + let result = runtime.block_on(client.workspace_worker_remove( + workspace_id, + server_api::WorkerRemoveRequest { + target_runtime_id: request.target_runtime_id, + target_worker_id: request.target_worker_id, + reason: request.reason, + }, + )); + match result { + Ok(response) => Ok(WorkspaceResponse { + status: 200, + body: serde_json::to_string(&response) + .map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?, + }), + Err(server_api::client_support::ClientError::Public { status, error }) => { + Ok(WorkspaceResponse { + status: status.as_u16(), + body: serde_json::to_string(&error).map_err(|error| { + RuntimeWorkerMutationForwardError::Transport(error.to_string()) + })?, + }) + } + Err(error) => Err(RuntimeWorkerMutationForwardError::Transport( + error.to_string(), + )), + } } #[derive(Clone)] @@ -432,6 +441,26 @@ impl std::fmt::Debug for RuntimeOwnedWorkspaceClient { } } +fn generated_workspace_response( + response: Result>, +) -> Result { + match response { + Ok(response) => Ok(WorkspaceResponse { + status: 200, + body: serde_json::to_string(&response) + .map_err(|error| WorkspaceClientError::Request(error.to_string()))?, + }), + Err(server_api::client_support::ClientError::Public { status, error }) => { + Ok(WorkspaceResponse { + status: status.as_u16(), + body: serde_json::to_string(&error) + .map_err(|error| WorkspaceClientError::Request(error.to_string()))?, + }) + } + Err(error) => Err(WorkspaceClientError::Request(error.to_string())), + } +} + impl WorkspaceClient for RuntimeOwnedWorkspaceClient { fn workspace_id(&self) -> Option<&str> { Some(self.request_client.workspace_id()) @@ -452,36 +481,143 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient { self.execute_with_permission(request, WORKSPACE_REQUEST_PERMISSION) } + fn execute_server_operation( + &self, + operation: WorkspaceServerOperation, + ) -> Result { + let request_client = self.request_client.clone(); + let workspace_id = self.workspace_id.clone(); + let worker_id = self.worker_id.clone(); + let timeout = self.request_timeout.unwrap_or(Duration::from_secs(5)); + std::thread::Builder::new() + .name("yoi-workspace-server-api".to_string()) + .spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| WorkspaceClientError::Request(error.to_string()))?; + let client = request_client + .server_api_client( + WORKSPACE_REQUEST_PERMISSION, + Some(worker_id), + reqwest::header::HeaderMap::new(), + timeout, + 8 * 1024 * 1024, + ) + .map_err(|error| WorkspaceClientError::Request(error.to_string()))?; + runtime.block_on(async move { + match operation { + WorkspaceServerOperation::WorkerControlList => { + generated_workspace_response( + client.worker_control_list(workspace_id).await, + ) + } + WorkspaceServerOperation::WorkerControlSpawn(request) => { + generated_workspace_response( + client.worker_control_spawn(workspace_id, request).await, + ) + } + WorkspaceServerOperation::WorkerControlInput { + runtime_id, + worker_id, + request, + } => generated_workspace_response( + client + .worker_control_input(workspace_id, runtime_id, worker_id, request) + .await, + ), + WorkspaceServerOperation::WorkerControlCancel { + runtime_id, + worker_id, + request, + } => generated_workspace_response( + client + .worker_control_cancel(workspace_id, runtime_id, worker_id, request) + .await, + ), + WorkspaceServerOperation::WorkerControlStop { + runtime_id, + worker_id, + request, + } => generated_workspace_response( + client + .worker_control_stop(workspace_id, runtime_id, worker_id, request) + .await, + ), + WorkspaceServerOperation::WorkerControlRestore { + runtime_id, + worker_id, + } => generated_workspace_response( + client + .worker_control_restore(workspace_id, runtime_id, worker_id) + .await, + ), + WorkspaceServerOperation::WorkerObservationSessions => { + generated_workspace_response( + client.worker_observation_sessions(workspace_id).await, + ) + } + WorkspaceServerOperation::WorkerObservationCapture(request) => { + generated_workspace_response( + client + .worker_observation_capture(workspace_id, request) + .await, + ) + } + } + }) + }) + .map_err(|error| WorkspaceClientError::Request(error.to_string()))? + .join() + .map_err(|_| { + WorkspaceClientError::Request( + "Workspace Server API request thread panicked".to_string(), + ) + })? + } + fn list_workspace_workers( &self, request: worker::WorkspaceWorkerDiscoveryRequest, ) -> Result { - let mut path = format!( - "/api/w/{}/worker-discovery/workers?limit={}", - self.workspace_id, request.limit - ); - if let Some(cursor) = request.cursor.as_deref() { - path.push_str("&cursor="); - path.push_str(&percent_encode_query(cursor)); - } - if let Some(query) = request.query.as_deref() { - path.push_str("&query="); - path.push_str(&percent_encode_query(query)); - } - let response = self.execute_with_permission( - WorkspaceRequest::get(path), - WORKSPACE_WORKER_DISCOVERY_PERMISSION, - )?; - if !(200..300).contains(&response.status) { - return Err(WorkspaceClientError::Request(format!( - "Workspace Worker discovery failed with HTTP {}: {}", - response.status, response.body - ))); - } - serde_json::from_str(&response.body).map_err(|error| { - WorkspaceClientError::Request(format!( - "invalid Workspace Worker discovery response: {error}" - )) + let request_client = self.request_client.clone(); + let workspace_id = self.workspace_id.clone(); + let worker_id = self.worker_id.clone(); + let timeout = self.request_timeout.unwrap_or(Duration::from_secs(5)); + let query = server_api::WorkspaceWorkerDiscoveryQuery { + cursor: request.cursor, + limit: Some(request.limit), + query: request.query, + }; + let result = std::thread::Builder::new() + .name("yoi-worker-discovery-http".to_string()) + .spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| error.to_string())?; + let client = request_client + .server_api_client( + WORKSPACE_WORKER_DISCOVERY_PERMISSION, + Some(worker_id), + reqwest::header::HeaderMap::new(), + timeout, + 8 * 1024 * 1024, + ) + .map_err(|error| error.to_string())?; + runtime + .block_on(client.workspace_worker_discovery(workspace_id, query)) + .map_err(|error| error.to_string()) + }) + .map_err(|error| WorkspaceClientError::Request(error.to_string()))? + .join() + .map_err(|_| { + WorkspaceClientError::Request( + "Workspace Worker discovery HTTP thread panicked".to_string(), + ) + })?; + result.map_err(|error| { + WorkspaceClientError::Request(format!("Workspace Worker discovery failed: {error}")) }) } @@ -587,19 +723,6 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient { } } -fn percent_encode_query(value: &str) -> String { - let mut encoded = String::with_capacity(value.len()); - for byte in value.bytes() { - if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') { - encoded.push(char::from(byte)); - } else { - use std::fmt::Write as _; - let _ = write!(encoded, "%{byte:02X}"); - } - } - encoded -} - #[derive(Debug, thiserror::Error)] pub enum RuntimeWorkerMutationForwardError { #[error(transparent)] diff --git a/crates/worker-runtime/src/workspace_request.rs b/crates/worker-runtime/src/workspace_request.rs index 507e69c71..a075144a3 100644 --- a/crates/worker-runtime/src/workspace_request.rs +++ b/crates/worker-runtime/src/workspace_request.rs @@ -62,9 +62,57 @@ impl RuntimeWorkspaceRequestError { timeout, } } +} - pub(crate) fn is_timeout(&self) -> bool { - matches!(self, Self::Transport { timeout: true, .. }) +#[derive(Clone)] +pub(crate) struct RuntimeWorkspaceRequestAuthorizer { + workspace_id: String, + runtime_id: String, + request_source: Option<(RuntimeRequestSourceSigner, String)>, + permission: String, + worker_id: Option, + extra_headers: HeaderMap, +} + +impl server_api::client_support::RequestAuthorizer for RuntimeWorkspaceRequestAuthorizer { + fn authorize( + &self, + request: server_api::client_support::AuthorizerRequest<'_>, + ) -> Result { + let mut headers = self.extra_headers.clone(); + headers.insert( + RUNTIME_ID_HEADER, + reqwest::header::HeaderValue::from_str(&self.runtime_id) + .map_err(|_| server_api::client_support::AuthorizationError::new())?, + ); + if let Some(worker_id) = self.worker_id.as_deref() { + headers.insert( + WORKER_ID_HEADER, + reqwest::header::HeaderValue::from_str(worker_id) + .map_err(|_| server_api::client_support::AuthorizationError::new())?, + ); + } + if let Some((signer, audience)) = self.request_source.as_ref() { + let proof = signer + .issue( + audience, + &self.workspace_id, + self.worker_id.as_deref(), + &self.permission, + request.method.as_str(), + request.path_and_query, + request.body, + unix_now_seconds(), + DEFAULT_REQUEST_PROOF_TTL_SECONDS, + ) + .map_err(|_| server_api::client_support::AuthorizationError::new())?; + headers.insert( + RUNTIME_REQUEST_SOURCE_PROOF_HEADER, + reqwest::header::HeaderValue::from_str(&proof) + .map_err(|_| server_api::client_support::AuthorizationError::new())?, + ); + } + Ok(headers) } } @@ -117,6 +165,34 @@ impl RuntimeWorkspaceRequestClient { && self.base_url.trim_end_matches('/') == base_url.trim_end_matches('/') } + pub(crate) fn server_api_client( + &self, + permission: impl Into, + worker_id: Option, + extra_headers: HeaderMap, + timeout: Duration, + response_body_limit: usize, + ) -> Result< + server_api::ServerApiClient, + RuntimeWorkspaceRequestError, + > { + let authorizer = RuntimeWorkspaceRequestAuthorizer { + workspace_id: self.workspace_id.clone(), + runtime_id: self.runtime_id.clone(), + request_source: self.request_source.clone(), + permission: permission.into(), + worker_id, + extra_headers, + }; + server_api::ServerApiClient::builder(&self.base_url) + .map_err(|error| RuntimeWorkspaceRequestError::InvalidRequest(error.to_string()))? + .authorizer(authorizer) + .request_timeout(timeout) + .response_body_limit(response_body_limit) + .build() + .map_err(|error| RuntimeWorkspaceRequestError::InvalidRequest(error.to_string())) + } + pub(crate) async fn execute( &self, request: RuntimeWorkspaceRequest, diff --git a/crates/worker/src/feature/builtin/manage_worker.rs b/crates/worker/src/feature/builtin/manage_worker.rs index 1f9ced49b..f9cc88202 100644 --- a/crates/worker/src/feature/builtin/manage_worker.rs +++ b/crates/worker/src/feature/builtin/manage_worker.rs @@ -19,10 +19,7 @@ use crate::feature::{ ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration, }; use crate::spawn::registry::{SpawnedWorkerRegistry, SubWorkerStopSummary}; -use crate::worker::{ - WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod, - WorkspaceResponse, -}; +use crate::worker::{WorkspaceClient, WorkspaceClientError, WorkspaceResponse}; const FEATURE_ID: &str = "worker"; const FEATURE_NAME: &str = "Worker"; @@ -76,16 +73,22 @@ pub trait WorkerControlService: Send + Sync { &self, request: WorkerLifecycleSpawnRequest, ) -> Result; + async fn restore_worker( + &self, + runtime_id: String, + worker_id: String, + ) -> Result { + let _ = (runtime_id, worker_id); + Err(WorkspaceClientError::Unavailable( + "Runtime Worker restore is unavailable".to_string(), + )) + } fn remove_runtime_worker( &self, runtime_id: &str, worker_id: &str, reason: &str, ) -> Result; - async fn execute_runtime( - &self, - request: WorkspaceRequest, - ) -> Result; async fn ensure_permission( &self, subject: &super::worker_observation::WorkerObservationSubjectRef, @@ -144,11 +147,9 @@ impl WorkerControlService for WorkspaceWorkerControlService { async fn list_workers(&self) -> Result { let response = if self.runtime_worker_control { - self.execute_runtime(WorkspaceRequest::get(format!( - "/api/w/{}/worker-control/workers", - self.workspace_id - ))) - .await? + self.client.execute_server_operation( + crate::worker::WorkspaceServerOperation::WorkerControlList, + )? } else { WorkspaceResponse { status: 200, @@ -210,19 +211,17 @@ impl WorkerControlService for WorkspaceWorkerControlService { WorkerControlInputKind::User => "user", WorkerControlInputKind::Notify => "notify", }; - self.execute_runtime(WorkspaceRequest::json( - WorkspaceRequestMethod::Post, - format!( - "/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/input", - self.workspace_id - ), - serde_json::json!({ - "kind": kind, - "content": content, - }) - .to_string(), - )) - .await + self.client.execute_server_operation( + crate::worker::WorkspaceServerOperation::WorkerControlInput { + runtime_id, + worker_id, + request: server_api::RuntimeWorkerInputRequest { + kind: Some(kind.to_string()), + content, + segments: None, + }, + }, + ) } WorkerControlSubject::SubWorker { name } => { if kind != WorkerControlInputKind::User { @@ -273,19 +272,29 @@ impl WorkerControlService for WorkspaceWorkerControlService { runtime_id, worker_id, } => { - let action = match kind { - WorkerControlStopKind::Cancel => "cancel", - WorkerControlStopKind::Stop => "stop", + let operation = match kind { + WorkerControlStopKind::Cancel => { + crate::worker::WorkspaceServerOperation::WorkerControlCancel { + runtime_id, + worker_id, + request: server_api::RuntimeWorkerLifecycleRequest { + reason, + ticket_assignment: None, + }, + } + } + WorkerControlStopKind::Stop => { + crate::worker::WorkspaceServerOperation::WorkerControlStop { + runtime_id, + worker_id, + request: server_api::RuntimeWorkerLifecycleRequest { + reason, + ticket_assignment: None, + }, + } + } }; - self.execute_runtime(WorkspaceRequest::json( - WorkspaceRequestMethod::Post, - format!( - "/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/{action}", - self.workspace_id - ), - serde_json::json!({ "reason": reason }).to_string(), - )) - .await + self.client.execute_server_operation(operation) } WorkerControlSubject::SubWorker { name } => { if kind != WorkerControlStopKind::Stop { @@ -329,12 +338,29 @@ impl WorkerControlService for WorkspaceWorkerControlService { ) -> Result { WorkspaceWorkerLifecycleService { client: self.client.clone(), - workspace_id: self.workspace_id.clone(), } .spawn(request) .await } + async fn restore_worker( + &self, + runtime_id: String, + worker_id: String, + ) -> Result { + if !self.runtime_worker_control { + return Err(WorkspaceClientError::Unavailable( + "Runtime Worker control is not enabled for this Worker".to_string(), + )); + } + self.client.execute_server_operation( + crate::worker::WorkspaceServerOperation::WorkerControlRestore { + runtime_id, + worker_id, + }, + ) + } + fn remove_runtime_worker( &self, runtime_id: &str, @@ -350,18 +376,6 @@ impl WorkerControlService for WorkspaceWorkerControlService { .execute_worker_remove(runtime_id, worker_id, reason) } - async fn execute_runtime( - &self, - request: WorkspaceRequest, - ) -> Result { - if !self.runtime_worker_control { - return Err(WorkspaceClientError::Request( - "Runtime Worker control is not enabled for this Worker".to_string(), - )); - } - self.client.execute(request) - } - async fn ensure_permission( &self, subject: &super::worker_observation::WorkerObservationSubjectRef, @@ -391,10 +405,9 @@ impl WorkerControlService for WorkspaceWorkerControlService { "Runtime Worker control is not enabled for this Worker".to_string(), )); } - let response = self.client.execute(WorkspaceRequest::get(format!( - "/api/w/{}/worker-control/workers", - self.workspace_id - )))?; + let response = self.client.execute_server_operation( + crate::worker::WorkspaceServerOperation::WorkerControlList, + )?; if !response.is_success() { return Err(WorkspaceClientError::Request(format!( "Workspace control request returned {}: {}", @@ -459,7 +472,6 @@ pub struct WorkerLifecycleSpawnRequest { struct WorkspaceWorkerLifecycleService { client: Arc, - workspace_id: String, } fn workspace_worker_create_request( @@ -505,12 +517,9 @@ impl WorkerLifecycleService for WorkspaceWorkerLifecycleService { request: WorkerLifecycleSpawnRequest, ) -> Result { let body = workspace_worker_create_request(request)?; - self.client.execute(WorkspaceRequest::json( - WorkspaceRequestMethod::Post, - format!("/api/w/{}/worker-control/workers", self.workspace_id), - serde_json::to_string(&body) - .map_err(|error| WorkspaceClientError::Request(error.to_string()))?, - )) + self.client.execute_server_operation( + crate::worker::WorkspaceServerOperation::WorkerControlSpawn(body), + ) } } @@ -639,20 +648,17 @@ impl FeatureModule for ManageWorkerFeature { } fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { - let workspace_id = self - .client + self.client .workspace_id() .filter(|value| !value.trim().is_empty()) .ok_or_else(|| { FeatureInstallError::InvalidDescriptor( "worker feature requires a Workspace id".to_string(), ) - })? - .to_string(); + })?; let lifecycle: Arc = Arc::new(WorkspaceWorkerLifecycleService { client: self.client.clone(), - workspace_id: workspace_id.clone(), }); context.services().provide( ServiceDeclaration::new( @@ -948,14 +954,7 @@ impl Tool for WorkspaceWorkerTool { let input = parse::(input_json, "WorkerRestore")?; let (runtime_id, worker_id) = runtime_subject_ids(&input.subject, self.operation)?; self.control - .execute_runtime(WorkspaceRequest::json( - WorkspaceRequestMethod::Post, - format!( - "/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/restore", - self.control.workspace_id() - ), - "{}", - )) + .restore_worker(runtime_id, worker_id) .await .map_err(control_tool_error)? } @@ -1191,7 +1190,7 @@ mod tests { use std::sync::Mutex; use super::*; - use crate::worker::{WorkspaceClientError, WorkspaceResponse}; + use crate::worker::{WorkspaceClientError, WorkspaceRequest, WorkspaceResponse}; #[derive(Debug, Default)] struct RecordingWorkspaceClient { @@ -1361,15 +1360,6 @@ mod tests { )) } - async fn execute_runtime( - &self, - _request: WorkspaceRequest, - ) -> Result { - Err(WorkspaceClientError::Unavailable( - "Runtime Worker control is disabled in this test".to_string(), - )) - } - async fn ensure_permission( &self, _subject: &crate::feature::builtin::WorkerObservationSubjectRef, diff --git a/crates/worker/src/feature/builtin/worker_observation.rs b/crates/worker/src/feature/builtin/worker_observation.rs index bda05e091..65c895683 100644 --- a/crates/worker/src/feature/builtin/worker_observation.rs +++ b/crates/worker/src/feature/builtin/worker_observation.rs @@ -125,17 +125,16 @@ impl WorkerObservationProvider for WorkspaceClientWorkerObservationProvider { async fn list_worker_sessions( &self, ) -> Result, WorkerObservationError> { - let workspace_id = self.client.workspace_id().ok_or_else(|| { + let _workspace_id = self.client.workspace_id().ok_or_else(|| { WorkerObservationError::Unavailable( "Workspace observation requires a scoped Workspace client".to_string(), ) })?; let response = self .client - .execute(crate::worker::WorkspaceRequest::get(format!( - "/api/w/{}/worker-observation/sessions", - workspace_id - ))) + .execute_server_operation( + crate::worker::WorkspaceServerOperation::WorkerObservationSessions, + ) .map_err(workspace_client_error)?; let body = workspace_response_body(response)?; serde_json::from_str::(&body) @@ -147,20 +146,28 @@ impl WorkerObservationProvider for WorkspaceClientWorkerObservationProvider { &self, subject: &WorkerObservationSubjectRef, ) -> Result { - let body = serde_json::to_string(subject) - .map_err(|error| WorkerObservationError::Unavailable(error.to_string()))?; - let workspace_id = self.client.workspace_id().ok_or_else(|| { + let request = match subject { + WorkerObservationSubjectRef::RuntimeWorker { + runtime_id, + worker_id, + } => server_api::WorkerObservationSubjectRef::RuntimeWorker { + runtime_id: runtime_id.clone(), + worker_id: worker_id.clone(), + }, + WorkerObservationSubjectRef::SubWorker { name } => { + server_api::WorkerObservationSubjectRef::SubWorker { name: name.clone() } + } + }; + let _workspace_id = self.client.workspace_id().ok_or_else(|| { WorkerObservationError::Unavailable( "Workspace observation requires a scoped Workspace client".to_string(), ) })?; let response = self .client - .execute(crate::worker::WorkspaceRequest::json( - crate::worker::WorkspaceRequestMethod::Post, - format!("/api/w/{}/worker-observation/session", workspace_id), - body, - )) + .execute_server_operation( + crate::worker::WorkspaceServerOperation::WorkerObservationCapture(request), + ) .map_err(workspace_client_error)?; let body = workspace_response_body(response)?; let response = serde_json::from_str::(&body) diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index c83ef56ae..a5b88a79f 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -60,6 +60,6 @@ pub use worker::{ LocalWorkingDirectory, Worker, WorkerError, WorkerFilesystemAuthority, WorkerRunResult, WorkerWorkspaceContext, WorkspaceClient, WorkspaceClientError, WorkspaceId, WorkspaceIdError, WorkspacePromptCatalogResolution, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse, - WorkspaceWorkerDiscoveryRequest, apply_worker_manifest, marker_workspace_client, - unavailable_workspace_client, + WorkspaceServerOperation, WorkspaceWorkerDiscoveryRequest, apply_worker_manifest, + marker_workspace_client, unavailable_workspace_client, }; diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index da0471de8..a71e36823 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -544,6 +544,98 @@ pub struct WorkspaceWorkerDiscoveryRequest { pub query: Option, } +#[derive(Debug, Clone)] +pub enum WorkspaceServerOperation { + WorkerControlList, + WorkerControlSpawn(server_api::CreateWorkspaceWorkerRequest), + WorkerControlInput { + runtime_id: String, + worker_id: String, + request: server_api::RuntimeWorkerInputRequest, + }, + WorkerControlCancel { + runtime_id: String, + worker_id: String, + request: server_api::RuntimeWorkerLifecycleRequest, + }, + WorkerControlStop { + runtime_id: String, + worker_id: String, + request: server_api::RuntimeWorkerLifecycleRequest, + }, + WorkerControlRestore { + runtime_id: String, + worker_id: String, + }, + WorkerObservationSessions, + WorkerObservationCapture(server_api::WorkerObservationSubjectRef), +} + +fn workspace_server_json_request( + path: String, + body: &T, +) -> Result { + Ok(WorkspaceRequest::json( + WorkspaceRequestMethod::Post, + path, + serde_json::to_string(body) + .map_err(|error| WorkspaceClientError::Request(error.to_string()))?, + )) +} + +fn workspace_server_operation_request( + workspace_id: &str, + operation: WorkspaceServerOperation, +) -> Result { + let base = format!("/api/w/{workspace_id}"); + match operation { + WorkspaceServerOperation::WorkerControlList => Ok(WorkspaceRequest::get(format!( + "{base}/worker-control/workers" + ))), + WorkspaceServerOperation::WorkerControlSpawn(request) => { + workspace_server_json_request(format!("{base}/worker-control/workers"), &request) + } + WorkspaceServerOperation::WorkerControlInput { + runtime_id, + worker_id, + request, + } => workspace_server_json_request( + format!("{base}/worker-control/workers/{runtime_id}/{worker_id}/input"), + &request, + ), + WorkspaceServerOperation::WorkerControlCancel { + runtime_id, + worker_id, + request, + } => workspace_server_json_request( + format!("{base}/worker-control/workers/{runtime_id}/{worker_id}/cancel"), + &request, + ), + WorkspaceServerOperation::WorkerControlStop { + runtime_id, + worker_id, + request, + } => workspace_server_json_request( + format!("{base}/worker-control/workers/{runtime_id}/{worker_id}/stop"), + &request, + ), + WorkspaceServerOperation::WorkerControlRestore { + runtime_id, + worker_id, + } => Ok(WorkspaceRequest::json( + WorkspaceRequestMethod::Post, + format!("{base}/worker-control/workers/{runtime_id}/{worker_id}/restore"), + "".to_string(), + )), + WorkspaceServerOperation::WorkerObservationSessions => Ok(WorkspaceRequest::get(format!( + "{base}/worker-observation/sessions" + ))), + WorkspaceServerOperation::WorkerObservationCapture(request) => { + workspace_server_json_request(format!("{base}/worker-observation/session"), &request) + } + } +} + /// Path-free Workspace operation authority injected by Runtime/host code. /// /// Workers receive this trait object rather than a Backend URL. The concrete @@ -556,6 +648,18 @@ pub trait WorkspaceClient: std::fmt::Debug + Send + Sync { fn execute(&self, request: WorkspaceRequest) -> Result; + fn execute_server_operation( + &self, + operation: WorkspaceServerOperation, + ) -> Result { + let workspace_id = self.workspace_id().ok_or_else(|| { + WorkspaceClientError::Unavailable( + "Workspace Server operation requires Workspace identity".to_string(), + ) + })?; + self.execute(workspace_server_operation_request(workspace_id, operation)?) + } + /// Lists Workspace-visible Workers through dedicated Runtime-owned source /// proof. Implementations must not fall back to generic Workspace request /// authority. diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 510541fe8..9ea774b3e 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -3641,12 +3641,14 @@ fn build_server_auth_router(api: ServerAuthApi) -> Router { fn server_request_context( actor: Option, worker_source: Option, + runtime_source: Option, origin: Option, headers: &HeaderMap, ) -> server_api::ServerRequestContext { server_api::ServerRequestContext { actor, worker_source, + runtime_source, origin, transport_headers: headers .iter() @@ -3684,7 +3686,7 @@ fn contract_request_headers( async fn attach_server_origin_context(mut request: Request, next: Next) -> Response { let actor = request.extensions().get::().cloned(); let origin = request_origin(request.headers()); - let context = server_request_context(actor, None, origin, request.headers()); + let context = server_request_context(actor, None, None, origin, request.headers()); request.extensions_mut().insert(context); next.run(request).await } @@ -3716,8 +3718,21 @@ async fn attach_server_request_context( runtime_id: source.runtime_id.clone(), worker_id: source.worker_id.clone(), }); + let runtime_source = request + .extensions() + .get::() + .map(|source| server_api::ServerRuntimeSource { + runtime_id: source.runtime_id.clone(), + worker_id: source.worker_id.clone(), + }); let origin = request_origin(request.headers()); - let context = server_request_context(actor, worker_source, origin, request.headers()); + let context = server_request_context( + actor, + worker_source, + runtime_source, + origin, + request.headers(), + ); request.extensions_mut().insert(context); next.run(request).await } @@ -4047,6 +4062,174 @@ fn generated_workspace_contract_router(service: ServerApiContractService) -> Rou )) .merge(server_api::server_api_axum::ticket_queue(service.clone())) .merge(server_api::server_api_axum::ticket_close(service.clone())) + .merge(server_api::server_api_axum::workspace_worker_list_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_worker_create_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_orchestrator_status( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_orchestrator_start( + service.clone(), + )) + .merge(server_api::server_api_axum::worker_control_list( + service.clone(), + )) + .merge(server_api::server_api_axum::worker_control_spawn( + service.clone(), + )) + .merge(server_api::server_api_axum::worker_control_input( + service.clone(), + )) + .merge(server_api::server_api_axum::worker_control_cancel( + service.clone(), + )) + .merge(server_api::server_api_axum::worker_control_stop( + service.clone(), + )) + .merge(server_api::server_api_axum::worker_control_restore( + service.clone(), + )) + .merge(server_api::server_api_axum::worker_observation_sessions( + service.clone(), + )) + .merge(server_api::server_api_axum::worker_observation_capture( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_worker_discovery( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_worker_list( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_worker_create( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_worker_detail( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_worker_launch_options_alias(service.clone())) + .merge(server_api::server_api_axum::workspace_worker_launch_options(service.clone())) + .merge(server_api::server_api_axum::runtime_resource_fetch( + service.clone(), + )) + .merge(server_api::server_api_axum::companion_status_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::companion_status( + service.clone(), + )) + .merge(server_api::server_api_axum::companion_transcript_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::companion_transcript( + service.clone(), + )) + .merge(server_api::server_api_axum::companion_message_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::companion_message( + service.clone(), + )) + .merge(server_api::server_api_axum::companion_cancel_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::companion_cancel( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_worker_remove( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_list_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_list(service.clone())) + .merge(server_api::server_api_axum::runtime_create(service.clone())) + .merge(server_api::server_api_axum::runtime_detail(service.clone())) + .merge(server_api::server_api_axum::runtime_update(service.clone())) + .merge(server_api::server_api_axum::runtime_remove(service.clone())) + .merge(server_api::server_api_axum::runtime_trust_key_reveal( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_trust_key_revoke( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_connection_test( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_list_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_create_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_list( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_create( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_config_bundle_sync_alias(service.clone())) + .merge(server_api::server_api_axum::runtime_config_bundle_sync( + service.clone(), + )) + .merge( + server_api::server_api_axum::runtime_config_bundle_availability_alias(service.clone()), + ) + .merge(server_api::server_api_axum::runtime_config_bundle_availability(service.clone())) + .merge(server_api::server_api_axum::runtime_worker_detail_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_detail( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_restore_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_restore( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_pin( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_unpin( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_cleanup_plan( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_cleanup_execute( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_input_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_input( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_attachment_upload_grant(service.clone())) + .merge( + server_api::server_api_axum::runtime_worker_attachment_upload_cancel(service.clone()), + ) + .merge(server_api::server_api_axum::runtime_worker_attachment_delete(service.clone())) + .merge(server_api::server_api_axum::runtime_worker_completions_alias(service.clone())) + .merge(server_api::server_api_axum::runtime_worker_completions( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_stop_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_stop( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_cancel_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_worker_cancel( + service.clone(), + )) .merge(server_api::server_api_axum::objective_list_alias( service.clone(), )) @@ -5048,6 +5231,480 @@ impl server_api::ServerApi for ServerApiContractService { }) } + async fn workspace_worker_list_alias( + &self, + ) -> std::result::Result { + let Json(response) = list_workers(State(self.workspace_api()?.clone())) + .await + .map_err(ApiError::into_repository_api_error)?; + project_contract_dto(&response) + } + + async fn workspace_worker_create_alias( + &self, + context: server_api::ServerRequestContext, + request: server_api::CreateWorkspaceWorkerRequest, + ) -> std::result::Result + { + create_workspace_worker( + State(self.workspace_api()?.clone()), + contract_request_headers(&context)?, + Json(request), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn workspace_orchestrator_status( + &self, + workspace_id: String, + ) -> std::result::Result< + server_api::BrowserWorkspaceOrchestratorResponse, + server_api::RepositoryApiError, + > { + scoped_workspace_orchestrator_status( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn workspace_orchestrator_start( + &self, + workspace_id: String, + ) -> std::result::Result< + server_api::BrowserWorkspaceOrchestratorResponse, + server_api::RepositoryApiError, + > { + scoped_start_workspace_orchestrator( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn worker_control_list( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + ) -> std::result::Result + { + let Json(response) = list_known_workers( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + contract_request_headers(&context)?, + ) + .await + .map_err(ApiError::into_repository_api_error)?; + project_contract_dto(&response) + } + + async fn worker_control_spawn( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::CreateWorkspaceWorkerRequest, + ) -> std::result::Result + { + spawn_known_worker( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + contract_request_headers(&context)?, + Json(request), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn worker_control_input( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + runtime_id: String, + worker_id: String, + request: server_api::RuntimeWorkerInputRequest, + ) -> std::result::Result + { + let request = project_server_dto(&request) + .map_err(|error| ApiError::from(error).into_repository_api_error())?; + let Json(response) = send_known_worker_input( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRuntimeWorkerPath { + workspace_id, + worker: RuntimeWorkerRef::new(runtime_id, worker_id), + }), + contract_request_headers(&context)?, + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + project_contract_dto(&response) + } + + async fn worker_control_cancel( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + runtime_id: String, + worker_id: String, + request: server_api::RuntimeWorkerLifecycleRequest, + ) -> std::result::Result + { + let request = project_server_dto(&request) + .map_err(|error| ApiError::from(error).into_repository_api_error())?; + let Json(response) = cancel_known_worker( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRuntimeWorkerPath { + workspace_id, + worker: RuntimeWorkerRef::new(runtime_id, worker_id), + }), + contract_request_headers(&context)?, + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + project_contract_dto(&response) + } + + async fn worker_control_stop( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + runtime_id: String, + worker_id: String, + request: server_api::RuntimeWorkerLifecycleRequest, + ) -> std::result::Result + { + let request = project_server_dto(&request) + .map_err(|error| ApiError::from(error).into_repository_api_error())?; + let Json(response) = stop_known_worker( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRuntimeWorkerPath { + workspace_id, + worker: RuntimeWorkerRef::new(runtime_id, worker_id), + }), + contract_request_headers(&context)?, + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + project_contract_dto(&response) + } + + async fn worker_control_restore( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + runtime_id: String, + worker_id: String, + ) -> std::result::Result + { + restore_known_worker( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRuntimeWorkerPath { + workspace_id, + worker: RuntimeWorkerRef::new(runtime_id, worker_id), + }), + contract_request_headers(&context)?, + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn worker_observation_sessions( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + ) -> std::result::Result< + server_api::WorkerObservationSessionsResponse, + server_api::RepositoryApiError, + > { + let Json(response) = scoped_list_worker_observation_sessions( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + contract_request_headers(&context)?, + ) + .await + .map_err(ApiError::into_repository_api_error)?; + serde_json::from_value(response) + .map_err(|error| Error::RegistryInconsistency(error.to_string())) + .map_err(|error| ApiError::from(error).into_repository_api_error()) + } + + async fn worker_observation_capture( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::WorkerObservationSubjectRef, + ) -> std::result::Result< + server_api::WorkerObservationCaptureResponse, + server_api::RepositoryApiError, + > { + let request = project_server_dto(&request) + .map_err(|error| ApiError::from(error).into_repository_api_error())?; + let Json(response) = scoped_capture_worker_observation_session( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + contract_request_headers(&context)?, + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + serde_json::from_value(response) + .map_err(|error| Error::RegistryInconsistency(error.to_string())) + .map_err(|error| ApiError::from(error).into_repository_api_error()) + } + + async fn workspace_worker_discovery( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + query: server_api::WorkspaceWorkerDiscoveryQuery, + ) -> std::result::Result + { + discover_workspace_workers( + self.workspace_api()?.clone(), + workspace_id, + query, + context.runtime_source.is_some(), + ) + .await + .map_err(ApiError::into_repository_api_error) + } + + async fn workspace_worker_list( + &self, + workspace_id: String, + ) -> std::result::Result { + let Json(response) = scoped_list_workers( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + project_contract_dto(&response) + } + + async fn workspace_worker_create( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::CreateWorkspaceWorkerRequest, + ) -> std::result::Result + { + scoped_create_workspace_worker( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + contract_request_headers(&context)?, + Json(request), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn workspace_worker_detail( + &self, + workspace_id: String, + worker_ref: String, + ) -> std::result::Result { + scoped_get_workspace_worker( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspaceWorkerReferencePath { + workspace_id, + worker_ref, + }), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn workspace_worker_launch_options_alias( + &self, + ) -> std::result::Result + { + get_worker_launch_options(State(self.workspace_api()?.clone())) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn workspace_worker_launch_options( + &self, + workspace_id: String, + ) -> std::result::Result + { + scoped_get_worker_launch_options( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn runtime_resource_fetch( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::RuntimeResourceFetchRequest, + ) -> std::result::Result< + server_api::RuntimeResourceFetchResponse, + server_api::RuntimeResourceFetchApiError, + > { + runtime_resource_fetch_contract( + self.workspace_api() + .map_err(|_| server_api::RuntimeResourceFetchApiError::MissingResource)?, + context, + workspace_id, + request, + ) + .await + } + + async fn companion_status_alias( + &self, + ) -> std::result::Result + { + get_companion_status(State(self.workspace_api()?.clone())) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn companion_status( + &self, + workspace_id: String, + ) -> std::result::Result + { + scoped_get_companion_status( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn companion_transcript_alias( + &self, + query: server_api::CompanionTranscriptQuery, + ) -> std::result::Result< + server_api::CompanionTranscriptProjection, + server_api::RepositoryApiError, + > { + get_companion_transcript( + State(self.workspace_api()?.clone()), + Query(TranscriptQuery { + start: query.start, + limit: query.limit, + }), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn companion_transcript( + &self, + workspace_id: String, + query: server_api::CompanionTranscriptQuery, + ) -> std::result::Result< + server_api::CompanionTranscriptProjection, + server_api::RepositoryApiError, + > { + scoped_get_companion_transcript( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + Query(TranscriptQuery { + start: query.start, + limit: query.limit, + }), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn companion_message_alias( + &self, + request: server_api::CompanionMessageRequest, + ) -> std::result::Result + { + post_companion_message(State(self.workspace_api()?.clone()), Json(request)) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn companion_message( + &self, + workspace_id: String, + request: server_api::CompanionMessageRequest, + ) -> std::result::Result + { + scoped_post_companion_message( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + Json(request), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn companion_cancel_alias( + &self, + request: server_api::CompanionCancelRequest, + ) -> std::result::Result + { + post_companion_cancel(State(self.workspace_api()?.clone()), Json(request)) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn companion_cancel( + &self, + workspace_id: String, + request: server_api::CompanionCancelRequest, + ) -> std::result::Result + { + scoped_post_companion_cancel( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + Json(request), + ) + .await + .map(|Json(response)| response) + .map_err(ApiError::into_repository_api_error) + } + + async fn workspace_worker_remove( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::WorkerRemoveRequest, + ) -> std::result::Result + { + worker_remove_contract( + self.workspace_api() + .map_err(|error| worker_remove_repository_error(error))?, + context, + workspace_id, + request, + ) + .await + } + async fn runtime_list_alias( &self, ) -> std::result::Result { @@ -7414,95 +8071,10 @@ fn build_inner_router(api: WorkspaceApi) -> Router { "/api/w/{workspace_id}/runtime-config", get(get_latest_workspace_runtime_config), ) - .route( - "/api/workers", - get(list_workers).post(create_workspace_worker), - ) - .route( - "/api/w/{workspace_id}/orchestrator", - get(scoped_workspace_orchestrator_status) - .post(scoped_start_workspace_orchestrator), - ) - .route( - "/api/w/{workspace_id}/worker-control/workers", - get(list_known_workers).post(spawn_known_worker), - ) - .route( - "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/input", - post(send_known_worker_input), - ) - .route( - "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/cancel", - post(cancel_known_worker), - ) - .route( - "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/stop", - post(stop_known_worker), - ) - .route( - "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/restore", - post(restore_known_worker), - ) - .route( - "/api/w/{workspace_id}/worker-observation/sessions", - get(scoped_list_worker_observation_sessions), - ) - .route( - "/api/w/{workspace_id}/worker-observation/session", - post(scoped_capture_worker_observation_session), - ) - .route( - "/api/w/{workspace_id}/worker-discovery/workers", - get(scoped_discover_workspace_workers), - ) - .route( - "/api/w/{workspace_id}/workers", - get(scoped_list_workers).post(scoped_create_workspace_worker), - ) - .route( - "/api/w/{workspace_id}/workers/{worker_ref}", - get(scoped_get_workspace_worker), - ) .route( "/api/w/{workspace_id}/protocol/ws", get(scoped_workspace_protocol_ws), ) - .route( - "/api/workers/launch-options", - get(get_worker_launch_options), - ) - .route( - "/api/w/{workspace_id}/workers/launch-options", - get(scoped_get_worker_launch_options), - ) - .route( - "/api/runtime/v1/workspaces/{workspace_id}/resources/fetch", - post(scoped_post_internal_runtime_resource_fetch), - ) - .route("/api/companion/status", get(get_companion_status)) - .route( - "/api/w/{workspace_id}/companion/status", - get(scoped_get_companion_status), - ) - .route("/api/companion/transcript", get(get_companion_transcript)) - .route( - "/api/w/{workspace_id}/companion/transcript", - get(scoped_get_companion_transcript), - ) - .route("/api/companion/messages", post(post_companion_message)) - .route( - "/api/w/{workspace_id}/companion/messages", - post(scoped_post_companion_message), - ) - .route("/api/companion/cancel", post(post_companion_cancel)) - .route( - "/api/w/{workspace_id}/companion/cancel", - post(scoped_post_companion_cancel), - ) - .route( - "/api/w/{workspace_id}/workers/remove", - post(scoped_worker_remove_source_boundary), - ) .route( "/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachment-uploads/{upload_id}", put(scoped_upload_runtime_worker_file) @@ -13952,92 +14524,109 @@ fn worker_retention_error_response( } } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct WorkerRemoveBoundaryRequest { - target_runtime_id: String, - target_worker_id: String, - reason: String, +fn worker_remove_repository_error( + error: server_api::RepositoryApiError, +) -> server_api::WorkerRemoveApiError { + server_api::WorkerRemoveApiError::new( + StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + Some(error.error), + None, + Some(error.message), + Some(error.diagnostics), + ) } -async fn scoped_worker_remove_source_boundary( - State(api): State, - AxumPath(path): AxumPath, - headers: HeaderMap, - Json(request): Json, -) -> Response { - if let Err(error) = validate_workspace_scope(&api, &path.workspace_id) { - return error.into_response(); +async fn worker_remove_contract( + api: &WorkspaceApi, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::WorkerRemoveRequest, +) -> std::result::Result { + if workspace_id != api.config.workspace_id { + return Err(server_api::WorkerRemoveApiError::new( + StatusCode::NOT_FOUND.as_u16(), + Some("workspace_not_found".to_owned()), + None, + Some("Workspace was not found".to_owned()), + None, + )); } - let proof = match crate::worker_source::presented_worker_remove_source(&headers, None) { - Ok(proof) => proof, - Err(error) => { - return ( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({ "error": error.to_string() })), + let headers = contract_request_headers(&context).map_err(worker_remove_repository_error)?; + let proof = + crate::worker_source::presented_worker_remove_source(&headers, None).map_err(|error| { + server_api::WorkerRemoveApiError::new( + StatusCode::UNAUTHORIZED.as_u16(), + Some(error.to_string()), + None, + None, + None, ) - .into_response(); - } - }; - match crate::worker_source::verify_worker_remove_source( - &api, + })?; + let source = crate::worker_source::verify_worker_remove_source( + api, proof, &request.target_runtime_id, &request.target_worker_id, ) .await - { - Ok(source) => { - let executor = WorkerRemovalService::new(&api); - match executor - .execute_async( - source, - &request.target_runtime_id, - &request.target_worker_id, - &request.reason, - ) - .await - { - Ok(response) => ( - StatusCode::from_u16(response.status) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - [(CONTENT_TYPE, "application/json")], - response.body, - ) - .into_response(), - Err(_) => { - let response = worker_remove_error_response( - StatusCode::INTERNAL_SERVER_ERROR, - "worker_remove_failed", - "Worker removal failed before lifecycle execution", - ); - ( - StatusCode::from_u16(response.status) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - [(CONTENT_TYPE, "application/json")], - response.body, - ) - .into_response() - } + .map_err(|error| { + let status = match error { + crate::worker_source::WorkerMutationSourceProofError::Replay => StatusCode::CONFLICT, + crate::worker_source::WorkerMutationSourceProofError::Authority(_) => { + StatusCode::INTERNAL_SERVER_ERROR } - } - Err(error) => { - let status = match error { - crate::worker_source::WorkerMutationSourceProofError::Replay => { - StatusCode::CONFLICT - } - crate::worker_source::WorkerMutationSourceProofError::Authority(_) => { - StatusCode::INTERNAL_SERVER_ERROR - } - _ => StatusCode::FORBIDDEN, - }; - ( - status, - Json(serde_json::json!({ "error": error.to_string() })), + _ => StatusCode::FORBIDDEN, + }; + server_api::WorkerRemoveApiError::new( + status.as_u16(), + Some(error.to_string()), + None, + None, + None, + ) + })?; + let response = WorkerRemovalService::new(api) + .execute_async( + source, + &request.target_runtime_id, + &request.target_worker_id, + &request.reason, + ) + .await + .unwrap_or_else(|_| { + worker_remove_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "worker_remove_failed", + "Worker removal failed before lifecycle execution", ) - .into_response() - } + }); + if (200..300).contains(&response.status) { + return serde_json::from_str(&response.body).map_err(|error| { + server_api::WorkerRemoveApiError::new( + StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + Some("worker_remove_invalid_response".to_owned()), + None, + Some(error.to_string()), + None, + ) + }); } + let body: serde_json::Value = serde_json::from_str(&response.body).unwrap_or_default(); + Err(server_api::WorkerRemoveApiError::new( + response.status, + body.get("error") + .and_then(serde_json::Value::as_str) + .map(str::to_owned), + body.get("code") + .and_then(serde_json::Value::as_str) + .map(str::to_owned), + body.get("message") + .and_then(serde_json::Value::as_str) + .map(str::to_owned), + body.get("diagnostics") + .cloned() + .and_then(|value| serde_json::from_value(value).ok()), + )) } async fn scoped_get_workspace_worker( @@ -14069,73 +14658,43 @@ async fn scoped_get_workspace_worker( }) } -#[derive(Debug, Deserialize)] -struct WorkspaceWorkerDiscoveryQuery { - cursor: Option, - limit: Option, - query: Option, -} - -async fn scoped_discover_workspace_workers( - State(api): State, - AxumPath(path): AxumPath, - Query(query): Query, - source: Option>, -) -> Response { - let Some(Extension(_source)) = source else { - return ( - StatusCode::FORBIDDEN, - Json(serde_json::json!({ - "error": "workspace_worker_discovery_source_required" - })), - ) - .into_response(); - }; - - if let Err(error) = validate_workspace_scope(&api, &path.workspace_id) { - return error.into_response(); +async fn discover_workspace_workers( + api: WorkspaceApi, + workspace_id: String, + query: server_api::WorkspaceWorkerDiscoveryQuery, + verified_runtime_source: bool, +) -> std::result::Result { + if !verified_runtime_source { + return Err(ApiError::from(Error::WorkspacePermissionDenied( + "workspace_worker_discovery_source_required".to_string(), + ))); } + validate_workspace_scope(&api, &workspace_id)?; let limit = query.limit.unwrap_or(50).clamp(1, 100); - let cursor = query.cursor.as_deref(); + let cursor = query.cursor.clone(); let query = match query.query.as_deref().map(str::trim) { Some("") | None => None, Some(query) if query.len() <= 128 => Some(query), Some(_) => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "error": "invalid_workspace_worker_discovery_query" - })), - ) - .into_response(); + return Err(ApiError::from(Error::InvalidInput( + "invalid_workspace_worker_discovery_query".to_string(), + ))); } }; let filter_fingerprint = workspace_worker_discovery_filter_fingerprint(query); - let offset = match query_cursor_offset(cursor, filter_fingerprint) { - Ok(offset) => offset, - Err(()) => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "error": "invalid_workspace_worker_discovery_cursor" - })), - ) - .into_response(); - } - }; - - let workers = match workers_response(api) { - Ok(workers) => workers, - Err(error) => return error.into_response(), - }; - Json(workspace_worker_discovery_page( + let offset = query_cursor_offset(cursor.as_deref(), filter_fingerprint).map_err(|()| { + ApiError::from(Error::InvalidInput( + "invalid_workspace_worker_discovery_cursor".to_string(), + )) + })?; + let workers = workers_response(api)?; + Ok(workspace_worker_discovery_page( workers.items, query, offset, limit, filter_fingerprint, )) - .into_response() } fn workspace_worker_discovery_page( @@ -20094,101 +20653,89 @@ fn finalize_spawned_worker_workdir_attachments( Ok(()) } -async fn scoped_post_internal_runtime_resource_fetch( - State(api): State, - AxumPath(workspace_id): AxumPath, - headers: HeaderMap, - request: Request, +async fn runtime_resource_fetch_contract( + api: &WorkspaceApi, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::RuntimeResourceFetchRequest, ) -> std::result::Result< - Json, - (StatusCode, Json), + server_api::RuntimeResourceFetchResponse, + server_api::RuntimeResourceFetchApiError, > { if workspace_id != api.workspace_id() { - return Err(( - StatusCode::NOT_FOUND, - Json(BackendResourceError::MissingResource), - )); + return Err(server_api::RuntimeResourceFetchApiError::MissingResource); } - let proof = headers - .get(worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER) - .and_then(|value| value.to_str().ok()) - .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| { - ( - StatusCode::UNAUTHORIZED, - Json(BackendResourceError::Unauthorized { - message: "Runtime request proof is required".to_owned(), - }), - ) - })?; - let verified_source = request - .extensions() - .get::() - .cloned(); - let method = request.method().as_str().to_owned(); - let path = signed_request_target(request.uri()).to_owned(); - let body = axum::body::to_bytes(request.into_body(), 16 * 1024 * 1024) - .await - .map_err(|error| { - ( - StatusCode::BAD_REQUEST, - Json(BackendResourceError::InvalidResponse { - message: error.to_string(), - }), - ) - })?; - let source = if let Some(source) = verified_source { + let source = if let Some(source) = context.runtime_source { source } else { - crate::worker_source::verify_runtime_request_source_proof( - &api, + let proof = context + .transport_headers + .iter() + .find(|(name, _)| { + name.eq_ignore_ascii_case(worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER) + }) + .and_then(|(_, value)| std::str::from_utf8(value).ok()) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| server_api::RuntimeResourceFetchApiError::Unauthorized { + message: "Runtime request proof is required".to_owned(), + })?; + let body = serde_json::to_vec(&request).map_err(|error| { + server_api::RuntimeResourceFetchApiError::InvalidResponse { + message: error.to_string(), + } + })?; + let path = format!("/api/runtime/v1/workspaces/{workspace_id}/resources/fetch"); + let source = crate::worker_source::verify_runtime_request_source_proof( + api, proof, &workspace_id, worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION, - &method, + "POST", &path, &worker_runtime::auth::request_body_digest(&body), ) .await - .map_err(|_| { - ( - StatusCode::UNAUTHORIZED, - Json(BackendResourceError::Unauthorized { - message: "Runtime request proof is invalid".to_owned(), - }), - ) - })? + .map_err(|_| server_api::RuntimeResourceFetchApiError::Unauthorized { + message: "Runtime request proof is invalid".to_owned(), + })?; + server_api::ServerRuntimeSource { + runtime_id: source.runtime_id, + worker_id: source.worker_id, + } }; if source.worker_id.is_some() { - return Err(( - StatusCode::UNAUTHORIZED, - Json(BackendResourceError::Unauthorized { - message: "Worker-scoped proof cannot fetch Runtime resources".to_owned(), - }), - )); + return Err(server_api::RuntimeResourceFetchApiError::Unauthorized { + message: "Worker-scoped proof cannot fetch Runtime resources".to_owned(), + }); } - let request: BackendResourceFetchRequest = serde_json::from_slice(&body).map_err(|error| { - ( - StatusCode::BAD_REQUEST, - Json(BackendResourceError::InvalidResponse { - message: error.to_string(), - }), - ) - })?; if request.runtime_id != source.runtime_id { - return Err(( - StatusCode::UNAUTHORIZED, - Json(BackendResourceError::Unauthorized { - message: "Runtime request proof subject does not match the request".to_owned(), - }), - )); + return Err(server_api::RuntimeResourceFetchApiError::Unauthorized { + message: "Runtime request proof subject does not match the request".to_owned(), + }); } - api.resource_broker + let request: BackendResourceFetchRequest = project_server_dto(&request).map_err(|error| { + server_api::RuntimeResourceFetchApiError::InvalidResponse { + message: error.to_string(), + } + })?; + let response = api + .resource_broker .fetch_resource(request) - .map(Json) - .map_err(|error| (backend_resource_error_status(&error), Json(error))) + .map_err(|error| { + project_contract_dto(&error).unwrap_or_else(|projection_error| { + server_api::RuntimeResourceFetchApiError::InvalidResponse { + message: projection_error.message, + } + }) + })?; + project_contract_dto(&response).map_err(|error| { + server_api::RuntimeResourceFetchApiError::InvalidResponse { + message: error.message, + } + }) } +#[cfg(test)] fn backend_resource_error_status(error: &BackendResourceError) -> StatusCode { match error { BackendResourceError::Expired => StatusCode::GONE, diff --git a/openapi/server-api.json b/openapi/server-api.json index 3a47d8674..0e072f255 100644 --- a/openapi/server-api.json +++ b/openapi/server-api.json @@ -310,6 +310,41 @@ ], "type": "object" }, + "BrowserCreateWorkerResponse": { + "additionalProperties": false, + "properties": { + "console_href": { + "type": "string" + }, + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "runtime_id": { + "type": "string" + }, + "worker": { + "$ref": "#/components/schemas/WorkerLaunchWorkerSummary" + }, + "worker_id": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "runtime_id", + "worker_id", + "console_href", + "worker", + "diagnostics" + ], + "type": "object" + }, "BrowserEditTicketRequest": { "additionalProperties": false, "properties": { @@ -454,6 +489,67 @@ ], "type": "object" }, + "BrowserWorkerWorkingDirectorySelection": { + "additionalProperties": false, + "properties": { + "alias": { + "description": "Stable Worker-local routing alias.", + "type": "string" + }, + "relative_cwd": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "working_directory_id": { + "type": "string" + } + }, + "required": [ + "alias", + "working_directory_id" + ], + "type": "object" + }, + "BrowserWorkspaceOrchestratorResponse": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "disposition": { + "type": "string" + }, + "online": { + "type": "boolean" + }, + "worker": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkerLaunchWorkerSummary" + }, + { + "type": "null" + } + ] + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "online", + "disposition", + "diagnostics" + ], + "type": "object" + }, "CancelTicketImplementationRequest": { "additionalProperties": false, "properties": { @@ -814,6 +910,227 @@ ], "type": "string" }, + "CompanionCancelRequest": { + "additionalProperties": false, + "description": "Public Workspace Companion cancellation request.", + "properties": { + "reason": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "CompanionLifecycleState": { + "description": "Public lifecycle projection for the Workspace Companion endpoint.", + "enum": [ + "idle", + "running", + "stopped" + ], + "type": "string" + }, + "CompanionMessageDisposition": { + "description": "Public outcome of a Companion message submission.", + "enum": [ + "accepted", + "rejected" + ], + "type": "string" + }, + "CompanionMessageRequest": { + "additionalProperties": false, + "description": "Public Workspace Companion message request.", + "properties": { + "content": { + "type": "string" + } + }, + "required": [ + "content" + ], + "type": "object" + }, + "CompanionMessageResponse": { + "additionalProperties": false, + "description": "Public Workspace Companion message response.", + "properties": { + "message": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/CompanionMessageDisposition" + } + }, + "required": [ + "state", + "message" + ], + "type": "object" + }, + "CompanionStatusResponse": { + "additionalProperties": false, + "description": "Public Workspace Companion status response.", + "properties": { + "diagnostics": { + "default": [], + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "state": { + "$ref": "#/components/schemas/CompanionLifecycleState" + }, + "transport": { + "$ref": "#/components/schemas/CompanionTransportSummary" + }, + "worker": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkspaceWorkerDiscoveryItem" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "state", + "transport" + ], + "type": "object" + }, + "CompanionTranscriptItem": { + "additionalProperties": false, + "description": "One allowlisted, user-visible Companion transcript item.", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/CompanionTranscriptRole" + }, + "sequence": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "sequence", + "role", + "content", + "created_at" + ], + "type": "object" + }, + "CompanionTranscriptProjection": { + "additionalProperties": false, + "description": "Bounded public Companion transcript projection.", + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/CompanionTranscriptItem" + }, + "type": "array" + }, + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "next": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "start": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "state": { + "$ref": "#/components/schemas/CompanionLifecycleState" + }, + "total": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "state", + "start", + "limit", + "total", + "items" + ], + "type": "object" + }, + "CompanionTranscriptQuery": { + "additionalProperties": false, + "properties": { + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "start": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, + "CompanionTranscriptRole": { + "description": "User-visible role accepted in the public Companion transcript.", + "enum": [ + "user", + "assistant" + ], + "type": "string" + }, + "CompanionTransportSummary": { + "additionalProperties": false, + "description": "Public, bounded transport metadata for Companion status.", + "properties": { + "available": { + "type": "boolean" + }, + "mode": { + "type": "string" + } + }, + "required": [ + "mode", + "available" + ], + "type": "object" + }, "CompiledFlowDefinition": { "additionalProperties": false, "properties": { @@ -1369,6 +1686,78 @@ ], "type": "object" }, + "CreateWorkspaceWorkerRequest": { + "additionalProperties": false, + "properties": { + "control_operation_id": { + "default": null, + "description": "Backend idempotency key used only for authenticated Worker-owned spawn/control.", + "type": [ + "string", + "null" + ] + }, + "display_name": { + "type": "string" + }, + "initial_submit": { + "default": [], + "items": { + "$ref": "#/components/schemas/Segment" + }, + "type": "array" + }, + "profile": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "runtime_id": { + "type": "string" + }, + "ticket_assignment": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateWorkspaceWorkerTicketAssignmentRequest" + }, + { + "type": "null" + } + ], + "default": null + }, + "workdir_attachments": { + "default": [], + "items": { + "$ref": "#/components/schemas/BrowserWorkerWorkingDirectorySelection" + }, + "type": "array" + } + }, + "required": [ + "runtime_id", + "display_name" + ], + "type": "object" + }, + "CreateWorkspaceWorkerTicketAssignmentRequest": { + "additionalProperties": false, + "properties": { + "operation_id": { + "type": "string" + }, + "ticket_id": { + "type": "string" + } + }, + "required": [ + "ticket_id", + "operation_id" + ], + "type": "object" + }, "CurrentWorkerWorkdirAttachRequest": { "additionalProperties": false, "properties": { @@ -5286,6 +5675,78 @@ ], "type": "object" }, + "PasteArtifactAvailability": { + "enum": [ + "available", + "unavailable", + "integrity_failed" + ], + "type": "string" + }, + "PasteArtifactMediaType": { + "description": "One typed piece of a user submission.\n\n`Method::Submit` and `Event::UserMessage` carry `Vec`. Dumb\nclients (CLI piping, scripts) only need to produce a single\n`Segment::Text`; richer clients (TUI / GUI) construct typed atoms\n(paste chips, file refs) and\nsend them through directly so the Worker side never has to re-parse a\nflattened string.\n\nForward compat: payloads with unknown `kind` deserialize to\n`Segment::Unknown`. Worker treats this the same as known-but-unresolved\nvariants — emits an alert and inserts a `[unknown input segment]`\nplaceholder into the LLM context so neither user nor LLM is blind to\nthe dropped intent.", + "enum": [ + "text_plain_utf8" + ], + "type": "string" + }, + "PasteArtifactRef": { + "description": "Session-owned reference to a large pasted-input artifact.\n\nThe reference contains only bounded integrity and provenance metadata. The\nartifact body remains in session storage and is available to the model only\nthrough the scoped paste-artifact tools installed by Worker.", + "properties": { + "artifact_id": { + "type": "string" + }, + "availability": { + "$ref": "#/components/schemas/PasteArtifactAvailability", + "description": "Availability observed when this immutable reference was committed.\nReads revalidate storage and integrity rather than trusting this field." + }, + "byte_len": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "char_count": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "created_at_ms": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "line_count": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "media_type": { + "$ref": "#/components/schemas/PasteArtifactMediaType" + }, + "sha256": { + "type": "string" + }, + "source_entry_id": { + "type": "string" + } + }, + "required": [ + "artifact_id", + "created_at_ms", + "media_type", + "availability", + "byte_len", + "char_count", + "line_count", + "sha256", + "source_entry_id" + ], + "type": "object" + }, "ProfileSettingsResponse": { "additionalProperties": false, "description": "Read-only Profile catalog projected from one active Workspace config revision.", @@ -7099,6 +7560,42 @@ } ] }, + "RuntimeProfileSourceGraphSummary": { + "additionalProperties": false, + "properties": { + "entrypoints": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "import_count": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "source_count": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "total_source_bytes": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "source_count", + "total_source_bytes", + "entrypoints", + "import_count" + ], + "type": "object" + }, "RuntimePublicIdentityBundle": { "additionalProperties": false, "properties": { @@ -7178,6 +7675,355 @@ ], "type": "string" }, + "RuntimeResourceFetchApiError": { + "oneOf": [ + { + "properties": { + "code": { + "const": "expired", + "type": "string" + } + }, + "required": [ + "code" + ], + "type": "object" + }, + { + "properties": { + "code": { + "const": "unauthorized", + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "properties": { + "code": { + "const": "unsupported_kind", + "type": "string" + } + }, + "required": [ + "code" + ], + "type": "object" + }, + { + "properties": { + "code": { + "const": "missing_resource", + "type": "string" + } + }, + "required": [ + "code" + ], + "type": "object" + }, + { + "properties": { + "actual": { + "type": "string" + }, + "code": { + "const": "digest_mismatch", + "type": "string" + }, + "expected": { + "type": "string" + } + }, + "required": [ + "code", + "expected", + "actual" + ], + "type": "object" + }, + { + "properties": { + "actual_bytes": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "code": { + "const": "oversized", + "type": "string" + }, + "max_bytes": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "code", + "max_bytes", + "actual_bytes" + ], + "type": "object" + }, + { + "properties": { + "actual": { + "type": "string" + }, + "code": { + "const": "content_type_mismatch", + "type": "string" + }, + "expected": { + "type": "string" + } + }, + "required": [ + "code", + "expected", + "actual" + ], + "type": "object" + }, + { + "properties": { + "code": { + "const": "timeout", + "type": "string" + } + }, + "required": [ + "code" + ], + "type": "object" + }, + { + "properties": { + "code": { + "const": "transport", + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "properties": { + "code": { + "const": "invalid_response", + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + ] + }, + "RuntimeResourceFetchRequest": { + "additionalProperties": false, + "properties": { + "audit_correlation_id": { + "type": "string" + }, + "handle": { + "$ref": "#/components/schemas/RuntimeResourceHandle" + }, + "runtime_id": { + "type": "string" + }, + "worker_id": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "handle", + "runtime_id", + "audit_correlation_id" + ], + "type": "object" + }, + "RuntimeResourceFetchResponse": { + "additionalProperties": false, + "properties": { + "audit_correlation_id": { + "type": "string" + }, + "bytes": { + "items": { + "format": "uint8", + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "content_type": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/RuntimeResourceKind" + }, + "resource_id": { + "type": "string" + } + }, + "required": [ + "kind", + "resource_id", + "digest", + "content_type", + "bytes", + "audit_correlation_id" + ], + "type": "object" + }, + "RuntimeResourceHandle": { + "additionalProperties": false, + "properties": { + "audit_correlation_id": { + "type": "string" + }, + "content_type": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "expires_at_unix_seconds": { + "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "generation": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "kind": { + "$ref": "#/components/schemas/RuntimeResourceKind" + }, + "max_bytes": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "nonce": { + "type": "string" + }, + "operation": { + "$ref": "#/components/schemas/RuntimeResourceOperation" + }, + "profile_source_graph": { + "anyOf": [ + { + "$ref": "#/components/schemas/RuntimeProfileSourceGraphSummary" + }, + { + "type": "null" + } + ] + }, + "redaction": { + "$ref": "#/components/schemas/RuntimeResourceRedactionPolicy" + }, + "resource_id": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "runtime_id": { + "type": [ + "string", + "null" + ] + }, + "scope_id": { + "type": [ + "string", + "null" + ] + }, + "worker_id": { + "type": [ + "string", + "null" + ] + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "kind", + "workspace_id", + "resource_id", + "digest", + "operation", + "expires_at_unix_seconds", + "nonce", + "revision", + "max_bytes", + "content_type", + "redaction", + "audit_correlation_id" + ], + "type": "object" + }, + "RuntimeResourceKind": { + "enum": [ + "profile_source_archive", + "repository_ssh_access" + ], + "type": "string" + }, + "RuntimeResourceOperation": { + "enum": [ + "fetch_archive", + "fetch_once" + ], + "type": "string" + }, + "RuntimeResourceRedactionPolicy": { + "enum": [ + "runtime_internal_only" + ], + "type": "string" + }, "RuntimeSourceKind": { "enum": [ "embedded_worker_runtime", @@ -7727,7 +8573,16 @@ "worker_id": { "type": "string" }, - "worker_state": true, + "worker_state": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkerStateSnapshot" + }, + { + "type": "null" + } + ] + }, "workspace": { "$ref": "#/components/schemas/WorkerWorkspaceSummary" } @@ -7765,31 +8620,35 @@ "RuntimeWorkerSpawnAcceptanceRequirement": { "oneOf": [ { - "enum": [ - "socket_ready" + "additionalProperties": false, + "properties": { + "kind": { + "const": "socket_ready", + "type": "string" + } + }, + "required": [ + "kind" ], - "type": "string" + "type": "object" }, { "additionalProperties": false, "properties": { - "run_accepted": { - "properties": { - "expected_segments": { - "format": "uint", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "expected_segments" - ], - "type": "object" + "expected_segments": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "kind": { + "const": "run_accepted", + "type": "string" } }, "required": [ - "run_accepted" + "kind", + "expected_segments" ], "type": "object" } @@ -8080,6 +8939,22 @@ ], "type": "object" }, + "RuntimeWorkerWorkdirAttachmentSummary": { + "additionalProperties": false, + "properties": { + "alias": { + "type": "string" + }, + "working_directory": { + "$ref": "#/components/schemas/RuntimeWorkingDirectorySummary" + } + }, + "required": [ + "alias", + "working_directory" + ], + "type": "object" + }, "RuntimeWorkersQuery": { "additionalProperties": false, "properties": { @@ -8102,6 +8977,270 @@ ], "type": "string" }, + "RuntimeWorkingDirectoryCleanupTarget": { + "additionalProperties": false, + "description": "Runtime-internal Workdir cleanup authority. This transport intentionally\nretains the Backend-generated Repository id and is never a Workspace public\nprojection.", + "properties": { + "kind": { + "type": "string" + }, + "repository_id": { + "type": "string" + }, + "working_directory_id": { + "type": "string" + } + }, + "required": [ + "kind", + "working_directory_id", + "repository_id" + ], + "type": "object" + }, + "RuntimeWorkingDirectorySummary": { + "additionalProperties": false, + "description": "Runtime-internal Workdir inventory transport. Workspace REST and model-facing\nsurfaces must project this through [`WorkingDirectorySummary`] so the UUID is\nreplaced with `repository_key`.", + "properties": { + "cleanliness": { + "type": [ + "string", + "null" + ] + }, + "cleanup_target": { + "anyOf": [ + { + "$ref": "#/components/schemas/RuntimeWorkingDirectoryCleanupTarget" + }, + { + "type": "null" + } + ] + }, + "creation_ref": { + "type": [ + "string", + "null" + ] + }, + "creation_selector": { + "type": [ + "string", + "null" + ] + }, + "creation_tree": { + "type": [ + "string", + "null" + ] + }, + "current_ref": { + "type": [ + "string", + "null" + ] + }, + "current_selector": { + "type": [ + "string", + "null" + ] + }, + "current_tree": { + "type": [ + "string", + "null" + ] + }, + "display_name": { + "description": "Optional human-facing label, never a routing key.", + "type": [ + "string", + "null" + ] + }, + "materializer_kind": { + "$ref": "#/components/schemas/WorkingDirectoryMaterializerKind" + }, + "observed_at_epoch_seconds": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "occupied_by": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkingDirectoryOccupancy" + }, + { + "type": "null" + } + ] + }, + "repository_id": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/WorkingDirectoryStatusKind" + }, + "working_directory_id": { + "type": "string" + } + }, + "required": [ + "working_directory_id", + "repository_id", + "materializer_kind", + "status" + ], + "type": "object" + }, + "Segment": { + "oneOf": [ + { + "description": "Free-form text. The fallback every client can produce.", + "properties": { + "content": { + "type": "string" + }, + "kind": { + "const": "text", + "type": "string" + } + }, + "required": [ + "kind", + "content" + ], + "type": "object" + }, + { + "description": "Bracketed-paste capture from a TUI-style client. `id`, `chars`\nand `lines` carry the metadata needed to re-render a\n`[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`\nre-broadcast.", + "properties": { + "chars": { + "format": "uint32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "content": { + "type": "string" + }, + "id": { + "format": "uint32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "kind": { + "const": "paste", + "type": "string" + }, + "lines": { + "format": "uint32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "kind", + "id", + "chars", + "lines", + "content" + ], + "type": "object" + }, + { + "description": "Internal reference produced when Worker stores a large `Paste` before\ncommitting input. Clients may receive this in history/event projections;\nthe body is intentionally absent.", + "properties": { + "artifact": { + "$ref": "#/components/schemas/PasteArtifactRef" + }, + "kind": { + "const": "paste_artifact", + "type": "string" + } + }, + "required": [ + "kind", + "artifact" + ], + "type": "object" + }, + { + "description": "Client-local file uploaded into the owning Worker session before submit.\nThe Session Log stores only this immutable reference, never file bytes or\nthe client's local path.", + "properties": { + "file": { + "$ref": "#/components/schemas/UploadedFileRef" + }, + "kind": { + "const": "uploaded_file", + "type": "string" + } + }, + "required": [ + "kind", + "file" + ], + "type": "object" + }, + { + "description": "`@` file-system reference. Worker resolves readable files to\n`[File: ]` attachments and readable normal directories to shallow\n`[Dir: ]` listings; the flattened user text keeps the literal\n`@` placeholder either way.", + "properties": { + "kind": { + "const": "file_ref", + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + }, + { + "description": "Source-qualified request for the host to start a Flow before committing\nthis user input. Runtime resolves it through Workspace authority and\nreplaces it with the entered state's instructions before Worker-side\ninput resolution.", + "properties": { + "kind": { + "const": "flow", + "type": "string" + }, + "selector": { + "type": "string" + } + }, + "required": [ + "kind", + "selector" + ], + "type": "object" + }, + { + "description": "Unknown variant from a newer client. Worker treats this as an\nunresolved input — surfaces an alert and inserts a placeholder.\nRound-trip is lossy: re-serializing yields `{\"kind\":\"unknown\"}`.", + "properties": { + "kind": { + "const": "unknown", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + } + ] + }, "ServerJsonSafeU64": { "format": "uint64", "maximum": 9007199254740991, @@ -11022,6 +12161,63 @@ ], "type": "object" }, + "UploadedFileAvailability": { + "description": "Availability recorded for an uploaded client-local file.", + "enum": [ + "available", + "unavailable", + "integrity_failed" + ], + "type": "string" + }, + "UploadedFileRef": { + "description": "Session-owned immutable reference to a client-local uploaded file.\n\nUpload transports return an unbound reference. Worker fills\n`source_entry_id` immediately before the containing user input is committed;\ncommitted Session Log and public snapshot records therefore always retain\nthe durable source-entry identity without storing the file body.", + "properties": { + "artifact_id": { + "type": "string" + }, + "availability": { + "$ref": "#/components/schemas/UploadedFileAvailability" + }, + "byte_len": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "created_at_ms": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "file_name": { + "type": "string" + }, + "media_type": { + "type": "string" + }, + "sha256": { + "type": "string" + }, + "source_entry_id": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "artifact_id", + "file_name", + "media_type", + "created_at_ms", + "availability", + "byte_len", + "sha256" + ], + "type": "object" + }, "UsageAudit": { "properties": { "cache_creation_input_tokens": { @@ -11688,6 +12884,42 @@ ], "type": "string" }, + "WorkerBusyState": { + "oneOf": [ + { + "properties": { + "kind": { + "const": "run", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/WorkerRunState" + } + }, + "required": [ + "kind", + "state" + ], + "type": "object" + }, + { + "properties": { + "kind": { + "const": "maintenance", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/WorkerMaintenanceState" + } + }, + "required": [ + "kind", + "state" + ], + "type": "object" + } + ] + }, "WorkerCapabilitySummary": { "properties": { "can_spawn_followup": { @@ -11703,6 +12935,76 @@ ], "type": "object" }, + "WorkerControlListResponse": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/WorkerControlRecord" + }, + "type": "array" + }, + "truncated": { + "type": "boolean" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "items", + "truncated" + ], + "type": "object" + }, + "WorkerControlRecord": { + "additionalProperties": false, + "properties": { + "origin": { + "type": "string" + }, + "permissions": { + "items": { + "type": "string" + }, + "type": "array" + }, + "relation": { + "type": "string" + }, + "subject": { + "$ref": "#/components/schemas/WorkerControlSubject" + }, + "summary": { + "$ref": "#/components/schemas/WorkerLaunchWorkerSummary" + } + }, + "required": [ + "subject", + "relation", + "origin", + "permissions", + "summary" + ], + "type": "object" + }, + "WorkerControlSubject": { + "additionalProperties": false, + "properties": { + "runtime_id": { + "type": "string" + }, + "worker_id": { + "type": "string" + } + }, + "required": [ + "runtime_id", + "worker_id" + ], + "type": "object" + }, "WorkerFileDeleteResponse": { "additionalProperties": false, "properties": { @@ -11753,6 +13055,209 @@ ], "type": "object" }, + "WorkerLaunchOptionsResponse": { + "additionalProperties": false, + "properties": { + "default_profile": { + "type": [ + "string", + "null" + ] + }, + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "profiles": { + "items": { + "$ref": "#/components/schemas/WorkerLaunchProfileCandidate" + }, + "type": "array" + }, + "repositories": { + "items": { + "$ref": "#/components/schemas/WorkingDirectoryRepositoryOption" + }, + "type": "array" + }, + "runtimes": { + "items": { + "$ref": "#/components/schemas/WorkerLaunchRuntimeOption" + }, + "type": "array" + }, + "working_directories": { + "items": { + "$ref": "#/components/schemas/WorkingDirectorySummary" + }, + "type": "array" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "runtimes", + "profiles", + "repositories", + "working_directories", + "diagnostics" + ], + "type": "object" + }, + "WorkerLaunchProfileCandidate": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "id", + "label", + "description" + ], + "type": "object" + }, + "WorkerLaunchRuntimeOption": { + "additionalProperties": false, + "properties": { + "built_in": { + "type": "boolean" + }, + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "display_name": { + "type": "string" + }, + "runtime_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "worker_creation_available": { + "type": "boolean" + }, + "working_directory_required": { + "type": "boolean" + } + }, + "required": [ + "runtime_id", + "display_name", + "built_in", + "worker_creation_available", + "working_directory_required", + "status", + "diagnostics" + ], + "type": "object" + }, + "WorkerLaunchWorkerSummary": { + "additionalProperties": false, + "description": "Runtime-owned Worker summary embedded in Worker launch responses.\n\nThis preserves the existing launch wire shape. Workspace-owned Worker list\nand detail responses use [`WorkerSummary`] instead.", + "properties": { + "capabilities": { + "$ref": "#/components/schemas/WorkerCapabilitySummary" + }, + "diagnostics": { + "default": [], + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "display_name": { + "type": "string" + }, + "host_id": { + "type": "string" + }, + "implementation": { + "$ref": "#/components/schemas/WorkerImplementationSummary" + }, + "label": { + "type": "string" + }, + "last_seen_at": { + "type": [ + "string", + "null" + ] + }, + "pinned": { + "type": "boolean" + }, + "profile": { + "type": [ + "string", + "null" + ] + }, + "retention_state": { + "type": "string" + }, + "runtime_id": { + "type": "string" + }, + "singleton_key": { + "type": [ + "string", + "null" + ] + }, + "state": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "workdir_attachments": { + "items": { + "$ref": "#/components/schemas/RuntimeWorkerWorkdirAttachmentSummary" + }, + "type": "array" + }, + "worker_id": { + "type": "string" + }, + "workspace": { + "$ref": "#/components/schemas/WorkerWorkspaceSummary" + } + }, + "required": [ + "runtime_id", + "worker_id", + "host_id", + "display_name", + "label", + "tags", + "workspace", + "state", + "pinned", + "retention_state", + "implementation", + "capabilities" + ], + "type": "object" + }, "WorkerLifecycleAudit": { "properties": { "consolidation": { @@ -11840,6 +13345,49 @@ ], "type": "string" }, + "WorkerListResponse": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "default": [], + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "items": { + "items": { + "$ref": "#/components/schemas/WorkerSummary" + }, + "type": "array" + }, + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "source": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "limit", + "items", + "source" + ], + "type": "object" + }, + "WorkerMaintenanceState": { + "enum": [ + "compacting" + ], + "type": "string" + }, "WorkerOperationState": { "enum": [ "accepted", @@ -11848,6 +13396,79 @@ ], "type": "string" }, + "WorkerRemoveApiError": { + "additionalProperties": false, + "properties": { + "code": { + "type": [ + "string", + "null" + ] + }, + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": [ + "array", + "null" + ] + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "message": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "WorkerRemoveRequest": { + "additionalProperties": false, + "properties": { + "reason": { + "type": "string" + }, + "target_runtime_id": { + "type": "string" + }, + "target_worker_id": { + "type": "string" + } + }, + "required": [ + "target_runtime_id", + "target_worker_id", + "reason" + ], + "type": "object" + }, + "WorkerRemoveResponse": { + "additionalProperties": false, + "properties": { + "removed": { + "type": "boolean" + }, + "runtime_id": { + "type": "string" + }, + "worker_id": { + "type": "string" + } + }, + "required": [ + "removed", + "runtime_id", + "worker_id" + ], + "type": "object" + }, "WorkerRestoreResponse": { "additionalProperties": false, "properties": { @@ -11939,6 +13560,66 @@ ], "type": "object" }, + "WorkerRunState": { + "enum": [ + "running", + "pausing", + "paused", + "cancelling" + ], + "type": "string" + }, + "WorkerState": { + "oneOf": [ + { + "properties": { + "kind": { + "const": "idle", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + { + "properties": { + "kind": { + "const": "busy", + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/WorkerBusyState" + } + }, + "required": [ + "kind", + "state" + ], + "type": "object" + } + ] + }, + "WorkerStateSnapshot": { + "properties": { + "last_command_id": { + "description": "Highest lifecycle command id observed by this controller instance.", + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "state": { + "$ref": "#/components/schemas/WorkerState" + } + }, + "required": [ + "last_command_id", + "state" + ], + "type": "object" + }, "WorkerSummary": { "description": "Workspace-authoritative Worker projection.\n\n`resource_key` is required here even though Runtime-internal Worker summaries\ndo not carry one. The Workspace Server must resolve it from Workspace\nauthority before constructing this response.", "properties": { @@ -12017,7 +13698,16 @@ "worker_id": { "type": "string" }, - "worker_state": true, + "worker_state": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkerStateSnapshot" + }, + { + "type": "null" + } + ] + }, "workspace": { "$ref": "#/components/schemas/WorkerWorkspaceSummary" } @@ -12053,6 +13743,7 @@ "type": "object" }, "WorkerWorkspaceSummary": { + "additionalProperties": false, "properties": { "identity": { "type": "string" @@ -12292,6 +13983,24 @@ ], "type": "object" }, + "WorkingDirectoryRepositoryOption": { + "additionalProperties": false, + "properties": { + "default_selector": { + "type": [ + "string", + "null" + ] + }, + "repository_key": { + "type": "string" + } + }, + "required": [ + "repository_key" + ], + "type": "object" + }, "WorkingDirectorySource": { "description": "Public Workdir source identity. Provider routing details and host paths are intentionally absent.", "oneOf": [ @@ -13572,6 +15281,111 @@ ], "type": "object" }, + "WorkspaceWorkerDiscoveryItem": { + "additionalProperties": false, + "description": "Bounded, model-safe projection used by privileged Workspace Worker discovery.\nRuntime placement appears only in the typed subject required by Worker\ncontrol operations; provider and launch internals are intentionally omitted.", + "properties": { + "display_name": { + "type": "string" + }, + "profile": { + "type": [ + "string", + "null" + ] + }, + "resource_key": { + "type": "string" + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "subject": { + "$ref": "#/components/schemas/WorkspaceWorkerSubject" + } + }, + "required": [ + "subject", + "resource_key", + "display_name" + ], + "type": "object" + }, + "WorkspaceWorkerDiscoveryPage": { + "properties": { + "next_cursor": { + "type": [ + "string", + "null" + ] + }, + "workers": { + "items": { + "$ref": "#/components/schemas/WorkspaceWorkerDiscoveryItem" + }, + "type": "array" + } + }, + "required": [ + "workers" + ], + "type": "object" + }, + "WorkspaceWorkerDiscoveryQuery": { + "additionalProperties": false, + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "WorkspaceWorkerSubject": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "runtime_worker", + "type": "string" + }, + "runtime_id": { + "type": "string" + }, + "worker_id": { + "type": "string" + } + }, + "required": [ + "kind", + "runtime_id", + "worker_id" + ], + "type": "object" + } + ] + }, "WriteRequest": { "additionalProperties": false, "properties": { @@ -13645,7 +15459,7 @@ "info": { "title": "Yoi Server API", "version": "0.1.0", - "x-yoi-source-digest": "sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718" + "x-yoi-source-digest": "sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07" }, "openapi": "3.1.0", "paths": { @@ -14063,6 +15877,298 @@ } } }, + "/api/companion/cancel": { + "post": { + "operationId": "companion_cancel_alias", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionCancelRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionMessageResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/companion/messages": { + "post": { + "operationId": "companion_message_alias", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionMessageRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionMessageResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/companion/status": { + "get": { + "operationId": "companion_status_alias", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionStatusResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/companion/transcript": { + "get": { + "operationId": "companion_transcript_alias", + "parameters": [ + { + "explode": true, + "in": "query", + "name": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/CompanionTranscriptQuery" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionTranscriptProjection" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/hosts": { "get": { "operationId": "host_list_alias", @@ -14612,6 +16718,105 @@ ] } }, + "/api/runtime/v1/workspaces/{workspace_id}/resources/fetch": { + "post": { + "operationId": "runtime_resource_fetch", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeResourceFetchRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeResourceFetchResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeResourceFetchApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeResourceFetchApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeResourceFetchApiError" + } + } + }, + "description": "Error response" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeResourceFetchApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeResourceFetchApiError" + } + } + }, + "description": "Error response" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeResourceFetchApiError" + } + } + }, + "description": "Error response" + } + } + } + }, "/api/runtimes": { "get": { "operationId": "runtime_list_alias", @@ -16113,6 +18318,384 @@ } } }, + "/api/w/{workspace_id}/companion/cancel": { + "post": { + "operationId": "companion_cancel", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionCancelRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionMessageResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/companion/messages": { + "post": { + "operationId": "companion_message", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionMessageRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionMessageResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/companion/status": { + "get": { + "operationId": "companion_status", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionStatusResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/companion/transcript": { + "get": { + "operationId": "companion_transcript", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": true, + "in": "query", + "name": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/CompanionTranscriptQuery" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompanionTranscriptProjection" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/w/{workspace_id}/config/projections/prompts": { "get": { "operationId": "workspace_prompt_projection", @@ -19088,6 +21671,228 @@ ] } }, + "/api/w/{workspace_id}/orchestrator": { + "get": { + "operationId": "workspace_orchestrator_status", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrowserWorkspaceOrchestratorResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "post": { + "operationId": "workspace_orchestrator_start", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrowserWorkspaceOrchestratorResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/w/{workspace_id}/repositories": { "get": { "operationId": "repository_list", @@ -30380,6 +33185,1305 @@ } } }, + "/api/w/{workspace_id}/worker-control/workers": { + "get": { + "operationId": "worker_control_list", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerControlListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + } + }, + "post": { + "operationId": "worker_control_spawn", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkspaceWorkerRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrowserCreateWorkerResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + } + } + }, + "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/cancel": { + "post": { + "operationId": "worker_control_cancel", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "runtime_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "worker_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeWorkerLifecycleRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeWorkerLifecycleResult" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + } + } + }, + "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/input": { + "post": { + "operationId": "worker_control_input", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "runtime_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "worker_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeWorkerInputRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeWorkerInputResult" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + } + } + }, + "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/restore": { + "post": { + "operationId": "worker_control_restore", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "runtime_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "worker_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRestoreResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + } + } + }, + "/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/stop": { + "post": { + "operationId": "worker_control_stop", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "runtime_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "worker_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeWorkerLifecycleRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuntimeWorkerLifecycleResult" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + } + } + }, + "/api/w/{workspace_id}/worker-discovery/workers": { + "get": { + "operationId": "workspace_worker_discovery", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": true, + "in": "query", + "name": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/WorkspaceWorkerDiscoveryQuery" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceWorkerDiscoveryPage" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + } + } + }, + "/api/w/{workspace_id}/workers": { + "get": { + "operationId": "workspace_worker_list", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "post": { + "operationId": "workspace_worker_create", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkspaceWorkerRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrowserCreateWorkerResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/workers/launch-options": { + "get": { + "operationId": "workspace_worker_launch_options", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerLaunchOptionsResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/workers/remove": { + "post": { + "operationId": "workspace_worker_remove", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerRemoveApiError" + } + } + }, + "description": "Error response" + } + } + } + }, "/api/w/{workspace_id}/workers/self/workdir-attachments": { "post": { "operationId": "current_worker_workdir_attach", @@ -30752,6 +34856,123 @@ ] } }, + "/api/w/{workspace_id}/workers/{worker_ref}": { + "get": { + "operationId": "workspace_worker_detail", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "worker_ref", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerSummary" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/w/{workspace_id}/working-directories": { "get": { "operationId": "workspace_workdir_list", @@ -31333,6 +35554,289 @@ ] } }, + "/api/workers": { + "get": { + "operationId": "workspace_worker_list_alias", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "post": { + "operationId": "workspace_worker_create_alias", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkspaceWorkerRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrowserCreateWorkerResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/workers/launch-options": { + "get": { + "operationId": "workspace_worker_launch_options_alias", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkerLaunchOptionsResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/workspace": { "get": { "operationId": "workspace_current", diff --git a/web/workspace/src/lib/generated/companion-api.ts b/web/workspace/src/lib/generated/companion-api.ts index 76cc6231c..2f2719bea 100644 --- a/web/workspace/src/lib/generated/companion-api.ts +++ b/web/workspace/src/lib/generated/companion-api.ts @@ -1,30 +1,36 @@ -// Generated by `cargo run -p server-api --features typescript --example generate_companion_api_types`. -// Do not edit manually. +// @generated by yoi-companion-openapi-typescript@1. Do not edit. +// Input: openapi/server-api.json +// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07) +// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output +// Output: web/workspace/src/lib/generated/companion-api.ts +// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check -export type DiagnosticSeverity = "info" | "warning" | "error"; - -export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; - -export type WorkspaceWorkerSubject = { "kind": "runtime_worker", runtime_id: string, worker_id: string, }; - -export type WorkspaceWorkerDiscoveryItem = { subject: WorkspaceWorkerSubject, resource_key: string, display_name: string, profile: string | null, status?: string | null, }; +export type CompanionCancelRequest = { reason?: string | null; }; export type CompanionLifecycleState = "idle" | "running" | "stopped"; export type CompanionMessageDisposition = "accepted" | "rejected"; -export type CompanionTransportSummary = { mode: string, available: boolean, }; +export type CompanionMessageRequest = { content: string; }; -export type CompanionStatusResponse = { state: CompanionLifecycleState, worker: WorkspaceWorkerDiscoveryItem | null, transport: CompanionTransportSummary, diagnostics: Array, }; +export type CompanionMessageResponse = { message: string; state: CompanionMessageDisposition; }; -export type CompanionMessageRequest = { content: string, }; +export type CompanionStatusResponse = { diagnostics?: Array; state: CompanionLifecycleState; transport: CompanionTransportSummary; worker?: WorkspaceWorkerDiscoveryItem | null; }; -export type CompanionCancelRequest = { reason?: string | null, }; +export type CompanionTranscriptItem = { content: string; created_at: string; role: CompanionTranscriptRole; sequence: number; }; -export type CompanionMessageResponse = { state: CompanionMessageDisposition, message: string, }; +export type CompanionTranscriptProjection = { items: Array; limit: number; next?: number | null; start: number; state: CompanionLifecycleState; total: number; }; export type CompanionTranscriptRole = "user" | "assistant"; -export type CompanionTranscriptItem = { sequence: number, role: CompanionTranscriptRole, content: string, created_at: string, }; +export type CompanionTransportSummary = { available: boolean; mode: string; }; -export type CompanionTranscriptProjection = { state: CompanionLifecycleState, start: number, limit: number, total: number, next: number | null, items: Array, }; +export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; }; + +export type DiagnosticSeverity = "info" | "warning" | "error"; + +export type RepositoryApiError = { diagnostics?: Array; error: string; message: string; }; + +export type WorkspaceWorkerDiscoveryItem = { display_name: string; profile?: string | null; resource_key: string; status?: string | null; subject: WorkspaceWorkerSubject; }; + +export type WorkspaceWorkerSubject = { kind: "runtime_worker"; runtime_id: string; worker_id: string; }; diff --git a/web/workspace/src/lib/generated/repository-api.ts b/web/workspace/src/lib/generated/repository-api.ts index b9d58d0e4..0b2531745 100644 --- a/web/workspace/src/lib/generated/repository-api.ts +++ b/web/workspace/src/lib/generated/repository-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-repository-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718) +// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07) // Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output // Output: web/workspace/src/lib/generated/repository-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check @@ -55,11 +55,21 @@ export type RepositorySshHostTrust = { created_at: string; current_revision: num export type RepositorySummary = { default_selector?: string | null; diagnostics?: Array | null; git?: GitRepositorySummary | null; kind: string; observed_at?: string | null; observed_status: RepositoryObservedStatus; provider: string; record_authority: string; repository_key: string; source: RepositorySource; source_fingerprint: string; source_revision: number; }; +export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; }; + export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; }; export type WorkerImplementationSummary = { display_hint: string; kind: string; }; -export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; }; +export type WorkerMaintenanceState = "compacting"; + +export type WorkerRunState = "running" | "pausing" | "paused" | "cancelling"; + +export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; }; + +export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; + +export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; diff --git a/web/workspace/src/lib/generated/runtime-api.ts b/web/workspace/src/lib/generated/runtime-api.ts index bdc01ad91..ed23ced4c 100644 --- a/web/workspace/src/lib/generated/runtime-api.ts +++ b/web/workspace/src/lib/generated/runtime-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-runtime-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718) +// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07) // Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/runtime-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check @@ -105,11 +105,11 @@ export type RuntimeWorkerLifecycleResult = { diagnostics?: Array; ru export type RuntimeWorkerListResponse = { diagnostics?: Array; items: Array; limit: number; source: string; workspace_id: string; }; -export type RuntimeWorkerShowResponse = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; updated_at: string; workdir_attachments?: Array; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; }; +export type RuntimeWorkerShowResponse = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; updated_at: string; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; export type RuntimeWorkerSpawnAcceptanceEvidence = { detail: string; kind: string; }; -export type RuntimeWorkerSpawnAcceptanceRequirement = "socket_ready" | { run_accepted: { expected_segments: number; }; }; +export type RuntimeWorkerSpawnAcceptanceRequirement = { kind: "socket_ready"; } | { expected_segments: number; kind: "run_accepted"; }; export type RuntimeWorkerSpawnIntent = { kind: "workspace_companion"; } | { kind: "workspace_orchestrator"; } | { kind: "workspace_coding"; } | { kind: "ticket_role"; role: RuntimeTicketWorkerRole; ticket_id: string; }; @@ -127,12 +127,16 @@ export type RuntimeWorkerTicketAssignmentRequest = { operation_id: string; ticke export type UpdateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; }; +export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; }; + export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; }; export type WorkerFileDeleteResponse = { deleted: boolean; }; export type WorkerImplementationSummary = { display_hint: string; kind: string; }; +export type WorkerMaintenanceState = "compacting"; + export type WorkerOperationState = "accepted" | "unsupported" | "rejected"; export type WorkerRestoreResponse = { result: WorkerRestoreResult; runtime_id: string; worker_id: string; workspace_id: string; }; @@ -143,7 +147,13 @@ export type WorkerRestoreState = "accepted" | "rejected" | "rolled_back" | "reco export type WorkerRetentionResponse = { pinned: boolean; retention_state: string; runtime_id: string; worker_id: string; workspace_id: string; }; -export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; }; +export type WorkerRunState = "running" | "pausing" | "paused" | "cancelling"; + +export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; }; + +export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; + +export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; diff --git a/web/workspace/src/lib/generated/workdir-api.ts b/web/workspace/src/lib/generated/workdir-api.ts index 135e935f6..ac955be89 100644 --- a/web/workspace/src/lib/generated/workdir-api.ts +++ b/web/workspace/src/lib/generated/workdir-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-workdir-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718) +// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07) // Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/workdir-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check diff --git a/web/workspace/src/lib/generated/worker-launch-api.ts b/web/workspace/src/lib/generated/worker-launch-api.ts index ab24375d9..3521b3766 100644 --- a/web/workspace/src/lib/generated/worker-launch-api.ts +++ b/web/workspace/src/lib/generated/worker-launch-api.ts @@ -1,68 +1,84 @@ -// Generated from server-api. Do not edit by hand. -// Regenerate: cargo run -q -p server-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts +// @generated by yoi-worker-openapi-typescript@1. Do not edit. +// Input: openapi/server-api.json +// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07) +// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output +// Output: web/workspace/src/lib/generated/worker-launch-api.ts +// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check -import type { Segment } from "./protocol"; +export type BrowserCreateWorkerResponse = { console_href: string; diagnostics: Array; runtime_id: string; worker: WorkerLaunchWorkerSummary; worker_id: string; workspace_id: string; }; + +export type BrowserWorkerWorkingDirectorySelection = { alias: string; relative_cwd?: string | null; working_directory_id: string; }; + +export type BrowserWorkspaceOrchestratorResponse = { diagnostics: Array; disposition: string; online: boolean; worker?: WorkerLaunchWorkerSummary | null; workspace_id: string; }; + +export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; initial_submit?: Array; profile?: string | null; runtime_id: string; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array; }; + +export type CreateWorkspaceWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; }; + +export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; }; export type DiagnosticSeverity = "info" | "warning" | "error"; -export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; +export type PasteArtifactAvailability = "available" | "unavailable" | "integrity_failed"; + +export type PasteArtifactMediaType = "text_plain_utf8"; + +export type PasteArtifactRef = { artifact_id: string; availability: PasteArtifactAvailability; byte_len: number; char_count: number; created_at_ms: number; line_count: number; media_type: PasteArtifactMediaType; sha256: string; source_entry_id: string; }; + +export type RepositoryApiError = { diagnostics?: Array; error: string; message: string; }; + +export type RuntimeWorkerWorkdirAttachmentSummary = { alias: string; working_directory: RuntimeWorkingDirectorySummary; }; + +export type RuntimeWorkingDirectoryCleanupTarget = { kind: string; repository_id: string; working_directory_id: string; }; + +export type RuntimeWorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: RuntimeWorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; repository_id: string; status: WorkingDirectoryStatusKind; working_directory_id: string; }; + +export type Segment = { content: string; kind: "text"; } | { chars: number; content: string; id: number; kind: "paste"; lines: number; } | { artifact: PasteArtifactRef; kind: "paste_artifact"; } | { file: UploadedFileRef; kind: "uploaded_file"; } | { kind: "file_ref"; path: string; } | { kind: "flow"; selector: string; } | { kind: "unknown"; }; + +export type UploadedFileAvailability = "available" | "unavailable" | "integrity_failed"; + +export type UploadedFileRef = { artifact_id: string; availability: UploadedFileAvailability; byte_len: number; created_at_ms: number; file_name: string; media_type: string; sha256: string; source_entry_id?: string | null; }; + +export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; }; + +export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; }; + +export type WorkerImplementationSummary = { display_hint: string; kind: string; }; + +export type WorkerLaunchOptionsResponse = { default_profile?: string | null; diagnostics: Array; profiles: Array; repositories: Array; runtimes: Array; working_directories: Array; workspace_id: string; }; + +export type WorkerLaunchProfileCandidate = { description: string; id: string; label: string; }; + +export type WorkerLaunchRuntimeOption = { built_in: boolean; diagnostics: Array; display_name: string; runtime_id: string; status: string; worker_creation_available: boolean; working_directory_required: boolean; }; + +export type WorkerLaunchWorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned: boolean; profile?: string | null; retention_state: string; runtime_id: string; singleton_key?: string | null; state: string; tags: Array; workdir_attachments?: Array; worker_id: string; workspace: WorkerWorkspaceSummary; }; + +export type WorkerListResponse = { diagnostics?: Array; items: Array; limit: number; source: string; workspace_id: string; }; + +export type WorkerMaintenanceState = "compacting"; + +export type WorkerRunState = "running" | "pausing" | "paused" | "cancelling"; + +export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; }; + +export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; + +export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; + +export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; + +export type WorkerWorkspaceSummary = { identity: string; visibility: string; workspace_id?: string | null; }; + +export type WorkingDirectoryCleanupTarget = { kind: string; repository_key: string; working_directory_id: string; }; export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external"; +export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; }; + +export type WorkingDirectoryRepositoryOption = { default_selector?: string | null; repository_key: string; }; + +export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; kind: "external_grant"; }; + export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown"; -export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_key: string, }; - -export type RuntimeWorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_id: string, }; - -export type RuntimeWorkingDirectorySummary = { working_directory_id: string, -/** - * Optional human-facing label, never a routing key. - */ -display_name?: string | null, repository_id: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: RuntimeWorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, }; - -export type WorkingDirectoryOccupancy = { runtime_id: string, worker_id: string, display_name: string, linked_at: string, }; - -export type WorkingDirectorySource = { "kind": "repository", repository_key: string, } | { "kind": "external_grant", grant_id: string, }; - -export type WorkingDirectorySummary = { working_directory_id: string, -/** - * Optional human-facing label, never a routing key. - */ -display_name?: string | null, source: WorkingDirectorySource, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, }; - -export type WorkerWorkspaceSummary = { visibility: string, identity: string, workspace_id?: string | null, }; - -export type WorkerImplementationSummary = { kind: string, display_hint: string, }; - -export type WorkerCapabilitySummary = { can_stop: boolean, can_spawn_followup: boolean, }; - -export type RuntimeWorkerWorkdirAttachmentSummary = { alias: string, working_directory: RuntimeWorkingDirectorySummary, }; - -export type WorkerLaunchWorkerSummary = { runtime_id: string, worker_id: string, host_id: string, display_name: string, label: string, profile: string | null, singleton_key: string | null, tags: Array, workspace: WorkerWorkspaceSummary, state: string, last_seen_at: string | null, pinned: boolean, retention_state: string, implementation: WorkerImplementationSummary, capabilities: WorkerCapabilitySummary, workdir_attachments?: Array, diagnostics: Array, }; - -export type WorkerLaunchRuntimeOption = { runtime_id: string, display_name: string, built_in: boolean, worker_creation_available: boolean, working_directory_required: boolean, status: string, diagnostics: Array, }; - -export type WorkerLaunchProfileCandidate = { id: string, label: string, description: string, }; - -export type WorkingDirectoryRepositoryOption = { repository_key: string, default_selector?: string | null, }; - -export type WorkerLaunchOptionsResponse = { workspace_id: string, runtimes: Array, default_profile: string | null, profiles: Array, repositories: Array, working_directories: Array, diagnostics: Array, }; - -export type BrowserWorkerWorkingDirectorySelection = { -/** - * Stable Worker-local routing alias. - */ -alias: string, working_directory_id: string, relative_cwd: string | null, }; - -export type CreateWorkspaceWorkerTicketAssignmentRequest = { ticket_id: string, operation_id: string, }; - -export type CreateWorkspaceWorkerRequest = { runtime_id: string, display_name: string, profile: string | null, ticket_assignment: CreateWorkspaceWorkerTicketAssignmentRequest | null, initial_submit: Array, workdir_attachments: Array, -/** - * Backend idempotency key used only for authenticated Worker-owned spawn/control. - */ -control_operation_id: string | null, }; - -export type BrowserCreateWorkerResponse = { workspace_id: string, runtime_id: string, worker_id: string, console_href: string, worker: WorkerLaunchWorkerSummary, diagnostics: Array, }; - -export type BrowserWorkspaceOrchestratorResponse = { workspace_id: string, online: boolean, disposition: string, worker?: WorkerLaunchWorkerSummary | null, diagnostics: Array, }; +export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; }; diff --git a/web/workspace/src/lib/workspace/api/workers.ts b/web/workspace/src/lib/workspace/api/workers.ts index f085c4727..66d66371a 100644 --- a/web/workspace/src/lib/workspace/api/workers.ts +++ b/web/workspace/src/lib/workspace/api/workers.ts @@ -6,15 +6,20 @@ import type { CreateWorkspaceWorkerTicketAssignmentRequest, Diagnostic, DiagnosticSeverity, + RepositoryApiError, + RuntimeWorkerWorkdirAttachmentSummary, RuntimeWorkingDirectoryCleanupTarget, RuntimeWorkingDirectorySummary, - RuntimeWorkerWorkdirAttachmentSummary, WorkerCapabilitySummary, WorkerImplementationSummary, WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary, + WorkerListResponse, + WorkerStateSnapshot, + WorkerSummary, + WorkerWorkdirAttachmentSummary, WorkerWorkspaceSummary, WorkingDirectoryRepositoryOption, } from "$lib/generated/worker-launch-api"; @@ -45,8 +50,14 @@ function exact( } } +const MAX_STRING_LENGTH = 65_536; +const MAX_COLLECTION_LENGTH = 4_096; + function string(value: unknown, label: string): string { if (typeof value !== "string") throw new Error(`${label} must be a string`); + if (value.length > MAX_STRING_LENGTH) { + throw new Error(`${label} exceeds its length limit`); + } return value; } @@ -56,8 +67,8 @@ function boolean(value: unknown, label: string): boolean { } function number(value: unknown, label: string): number { - if (typeof value !== "number" || !Number.isFinite(value)) { - throw new Error(`${label} must be a finite number`); + if (typeof value !== "number" || !Number.isSafeInteger(value)) { + throw new Error(`${label} must be a safe integer`); } return value; } @@ -72,6 +83,9 @@ function array( parse: (item: unknown, label: string) => T, ): T[] { if (!Array.isArray(value)) throw new Error(`${label} must be an array`); + if (value.length > MAX_COLLECTION_LENGTH) { + throw new Error(`${label} exceeds its item limit`); + } return value.map((item, index) => parse(item, `${label}[${index}]`)); } @@ -165,6 +179,18 @@ function repositoryOption( }; } +export function parseWorkerApiError(value: unknown): RepositoryApiError { + const item = record(value, "Worker API error"); + exact(item, ["error", "message", "diagnostics"], "Worker API error"); + return { + error: string(item.error, "error"), + message: string(item.message, "message"), + diagnostics: item.diagnostics === undefined + ? [] + : array(item.diagnostics, "diagnostics", diagnostic), + }; +} + export function parseWorkerLaunchOptionsResponse( value: unknown, ): WorkerLaunchOptionsResponse { @@ -237,6 +263,181 @@ function capabilitySummary( }; } +function workerStateSnapshot( + value: unknown, + label: string, +): WorkerStateSnapshot { + const snapshot = record(value, label); + exact(snapshot, ["last_command_id", "state"], label); + const state = record(snapshot.state, `${label}.state`); + const kind = string(state.kind, `${label}.state.kind`); + if (kind === "idle") { + exact(state, ["kind"], `${label}.state`); + return { + last_command_id: unsignedInteger( + snapshot.last_command_id, + `${label}.last_command_id`, + ), + state: { kind: "idle" }, + }; + } + if (kind !== "busy") throw new Error(`${label}.state.kind is invalid`); + exact(state, ["kind", "state"], `${label}.state`); + const busy = record(state.state, `${label}.state.state`); + exact(busy, ["kind", "state"], `${label}.state.state`); + const busyKind = string(busy.kind, `${label}.state.state.kind`); + const busyState = string(busy.state, `${label}.state.state.state`); + if ( + (busyKind === "run" && + ["running", "pausing", "paused", "cancelling"].includes(busyState)) || + (busyKind === "maintenance" && busyState === "compacting") + ) { + return { + last_command_id: unsignedInteger( + snapshot.last_command_id, + `${label}.last_command_id`, + ), + state: { + kind: "busy", + state: busyKind === "run" + ? { + kind: "run", + state: busyState as "running" | "pausing" | "paused" | "cancelling", + } + : { kind: "maintenance", state: "compacting" }, + }, + }; + } + throw new Error(`${label}.state.state is invalid`); +} + +function workerWorkdirAttachment( + value: unknown, + label: string, +): WorkerWorkdirAttachmentSummary { + const item = record(value, label); + exact(item, ["alias", "working_directory"], label); + return { + alias: string(item.alias, `${label}.alias`), + working_directory: parseWorkingDirectorySummary(item.working_directory), + }; +} + +export type ParsedWorkerSummary = + & Omit< + WorkerSummary, + "display_name" | "tags" | "diagnostics" + > + & { + display_name: string; + tags: string[]; + diagnostics: Diagnostic[]; + }; + +export type ParsedWorkerListResponse = Omit & { + items: ParsedWorkerSummary[]; +}; + +export function parseWorkerSummary( + value: unknown, + label = "Worker summary", +): ParsedWorkerSummary { + const item = record(value, label); + exact( + item, + [ + "runtime_id", + "worker_id", + "resource_key", + "host_id", + "display_name", + "label", + "profile", + "singleton_key", + "tags", + "workspace", + "state", + "worker_state", + "last_seen_at", + "pinned", + "retention_state", + "implementation", + "capabilities", + "workdir_attachments", + "diagnostics", + ], + label, + ); + return { + runtime_id: string(item.runtime_id, `${label}.runtime_id`), + worker_id: string(item.worker_id, `${label}.worker_id`), + resource_key: string(item.resource_key, `${label}.resource_key`), + host_id: string(item.host_id, `${label}.host_id`), + display_name: item.display_name === undefined + ? "" + : string(item.display_name, `${label}.display_name`), + label: string(item.label, `${label}.label`), + profile: optional(item.profile, `${label}.profile`, string), + singleton_key: optional( + item.singleton_key, + `${label}.singleton_key`, + string, + ), + tags: item.tags === undefined + ? [] + : array(item.tags, `${label}.tags`, string), + workspace: workspaceSummary(item.workspace, `${label}.workspace`), + state: string(item.state, `${label}.state`), + worker_state: optional( + item.worker_state, + `${label}.worker_state`, + workerStateSnapshot, + ), + last_seen_at: optional(item.last_seen_at, `${label}.last_seen_at`, string), + pinned: item.pinned === undefined + ? false + : boolean(item.pinned, `${label}.pinned`), + retention_state: item.retention_state === undefined + ? "" + : string(item.retention_state, `${label}.retention_state`), + implementation: implementationSummary( + item.implementation, + `${label}.implementation`, + ), + capabilities: capabilitySummary(item.capabilities, `${label}.capabilities`), + workdir_attachments: item.workdir_attachments === undefined + ? undefined + : array( + item.workdir_attachments, + `${label}.workdir_attachments`, + workerWorkdirAttachment, + ), + diagnostics: item.diagnostics === undefined + ? [] + : array(item.diagnostics, `${label}.diagnostics`, diagnostic), + }; +} + +export function parseWorkerListResponse( + value: unknown, +): ParsedWorkerListResponse { + const item = record(value, "Worker list response"); + exact( + item, + ["workspace_id", "limit", "items", "source", "diagnostics"], + "Worker list response", + ); + return { + workspace_id: string(item.workspace_id, "workspace_id"), + limit: unsignedInteger(item.limit, "limit"), + items: array(item.items, "items", parseWorkerSummary), + source: string(item.source, "source"), + diagnostics: item.diagnostics === undefined + ? [] + : array(item.diagnostics, "diagnostics", diagnostic), + }; +} + function runtimeCleanupTarget( value: unknown, label: string, diff --git a/web/workspace/src/lib/workspace/sidebar/types.ts b/web/workspace/src/lib/workspace/sidebar/types.ts index db44802f9..d17a338d9 100644 --- a/web/workspace/src/lib/workspace/sidebar/types.ts +++ b/web/workspace/src/lib/workspace/sidebar/types.ts @@ -9,10 +9,14 @@ import type { BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse, BrowserWorkerWorkingDirectorySelection as SharedBrowserWorkerWorkingDirectorySelection, + Diagnostic as SharedDiagnostic, + WorkerCapabilitySummary as SharedWorkerCapabilitySummary, WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption, - WorkerLaunchWorkerSummary as WorkerSummary, + WorkerLaunchWorkerSummary, + WorkerSummary as SharedWorkerSummary, + WorkerWorkdirAttachmentSummary as SharedWorkerWorkdirAttachmentSummary, WorkingDirectoryRepositoryOption as SharedWorkingDirectoryRepositoryOption, } from "$lib/generated/worker-launch-api"; import type { @@ -53,11 +57,7 @@ export type { }; export type WorkspaceResponse = SharedWorkspaceResponse; -export type Diagnostic = { - code: string; - severity: string; - message: string; -}; +export type Diagnostic = SharedDiagnostic; export type Runtime = { runtime_id: string; @@ -78,35 +78,17 @@ export type Runtime = { }; }; -export type WorkerCapabilities = { - can_stop: boolean; - can_spawn_followup: boolean; -}; +export type WorkerCapabilities = SharedWorkerCapabilitySummary; -export type WorkerWorkdirAttachment = { - alias: string; - working_directory: WorkingDirectorySummary; -}; +export type WorkerWorkdirAttachment = SharedWorkerWorkdirAttachmentSummary; -export type Worker = { - runtime_id: string; - worker_id: string; - resource_key: string; - host_id: string; +export type Worker = Omit< + SharedWorkerSummary, + "display_name" | "tags" | "worker_state" | "diagnostics" +> & { display_name: string; - label: string; - profile?: string | null; - singleton_key?: string | null; tags: string[]; - workspace: { visibility: string; identity: string }; - state: string; worker_state?: WorkerStateSnapshot | null; - pinned?: boolean; - retention_state?: string; - last_seen_at?: string | null; - implementation: { kind: string; display_hint: string }; - capabilities: WorkerCapabilities; - workdir_attachments?: WorkerWorkdirAttachment[]; diagnostics: Diagnostic[]; }; @@ -121,7 +103,7 @@ export type WorkerRestoreState = export type WorkerRestoreResult = { state: WorkerRestoreState; - worker?: WorkerSummary | null; + worker?: WorkerLaunchWorkerSummary | null; diagnostics: Diagnostic[]; }; diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts b/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts index 905b91f3e..e35ae80ad 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts @@ -1,16 +1,18 @@ import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; +import { parseWorkerListResponse } from "$lib/workspace/api/workers"; import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers"; import type { - ListResponse, RuntimeCleanupPlanResponse, - Worker, } from "$lib/workspace/sidebar/types"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch, params }) => { - const workers = await loadJson>( + const workers = await loadJson( fetch, workspaceApiPath(params.workspaceId, "/workers"), + undefined, + parseWorkerListResponse, + { diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 }, ); const runtimeIds = Array.from( new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []), diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/+page.ts index 70302703f..37e5c9f79 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/+page.ts @@ -1,20 +1,23 @@ import { redirect } from "@sveltejs/kit"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; +import { parseWorkerSummary } from "$lib/workspace/api/workers"; import { canonicalResourceReference, resourceKey, } from "$lib/workspace/resource-links"; -import type { Worker } from "$lib/workspace/sidebar/types"; import type { PageLoad } from "./$types"; export const load = (async ({ fetch, params }) => { const reference = resourceKey(params.workerRef); - const result = await loadJson( + const result = await loadJson( fetch, workspaceApiPath( params.workspaceId, `/workers/${encodeURIComponent(reference)}`, ), + undefined, + parseWorkerSummary, + { diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 }, ); if (result.data?.resource_key) { const canonical = canonicalResourceReference( diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.ts b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.ts index 04be0654b..fd694c17a 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.ts @@ -1,20 +1,23 @@ import { redirect } from "@sveltejs/kit"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; +import { parseWorkerSummary } from "$lib/workspace/api/workers"; import { canonicalResourceReference, resourceKey, } from "$lib/workspace/resource-links"; -import type { Worker } from "$lib/workspace/sidebar/types"; import type { PageLoad } from "./$types"; export const load = (async ({ fetch, params }) => { const reference = resourceKey(params.workerRef); - const result = await loadJson( + const result = await loadJson( fetch, workspaceApiPath( params.workspaceId, `/workers/${encodeURIComponent(reference)}`, ), + undefined, + parseWorkerSummary, + { diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 }, ); if (result.data?.resource_key) { const canonical = canonicalResourceReference( diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte index 134894910..db55d1e10 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte @@ -1,13 +1,14 @@