feat: migrate workspace worker and companion domains to ServerApi

This commit is contained in:
2026-09-23 10:00:56 +09:00
parent ad12d77ad4
commit 89024d3b01
28 changed files with 7471 additions and 819 deletions
+21 -33
View File
@@ -393,15 +393,16 @@ async fn get_backend_worker_launch_options_with_client(
target: &BackendWorkerLaunchTarget, target: &BackendWorkerLaunchTarget,
api: &BackendApiClient, api: &BackendApiClient,
) -> Result<BackendWorkerLaunchOptions, BackendRuntimeClientError> { ) -> Result<BackendWorkerLaunchOptions, BackendRuntimeClientError> {
let path = backend_workspace_workers_launch_options_path( runtime_server_api_client(&target.base_url, api)?
target .workspace_worker_launch_options(
.workspace_id target
.as_deref() .workspace_id
.expect("validated Backend Workspace scope"), .as_deref()
); .expect("validated Backend Workspace scope")
let response = api.request(HttpMethod::GET, &path)?.send().await?; .to_string(),
let response = api.require_success(response).await?; )
Ok(response.json::<BackendWorkerLaunchOptions>().await?) .await
.map_err(Into::into)
} }
pub async fn create_backend_worker( pub async fn create_backend_worker(
@@ -418,19 +419,17 @@ async fn create_backend_worker_with_client(
request: &BackendCreateWorkerRequest, request: &BackendCreateWorkerRequest,
api: &BackendApiClient, api: &BackendApiClient,
) -> Result<BackendCreateWorkerResponse, BackendRuntimeClientError> { ) -> Result<BackendCreateWorkerResponse, BackendRuntimeClientError> {
let path = backend_workspace_workers_path( runtime_server_api_client(&target.base_url, api)?
target .workspace_worker_create(
.workspace_id target
.as_deref() .workspace_id
.expect("validated Backend Workspace scope"), .as_deref()
); .expect("validated Backend Workspace scope")
let response = api .to_string(),
.request(HttpMethod::POST, &path)? request.clone(),
.json(request) )
.send() .await
.await?; .map_err(Into::into)
let response = api.require_success(response).await?;
Ok(response.json::<BackendCreateWorkerResponse>().await?)
} }
pub async fn list_backend_workers( pub async fn list_backend_workers(
@@ -680,17 +679,6 @@ fn validate_list_target(
Ok(()) 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( fn protocol_ws_request(
target: &BackendRuntimeTarget, target: &BackendRuntimeTarget,
api: &BackendApiClient, api: &BackendApiClient,
+13 -40
View File
@@ -1,6 +1,3 @@
use reqwest::Method;
use serde::Serialize;
use serde::de::DeserializeOwned;
use server_api::{ use server_api::{
BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse, BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse,
CreateWorkspaceWorkerRequest, ListResponse, MemoryDocumentResponse, MemoryStagingListResponse, CreateWorkspaceWorkerRequest, ListResponse, MemoryDocumentResponse, MemoryStagingListResponse,
@@ -417,7 +414,10 @@ impl BackendWorkspaceProductClient {
&self, &self,
ticket_id: &str, ticket_id: &str,
) -> Result<String, BackendWorkspaceClientError> { ) -> Result<String, BackendWorkspaceClientError> {
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 let runtime = options
.runtimes .runtimes
.iter() .iter()
@@ -439,8 +439,10 @@ impl BackendWorkspaceProductClient {
workdir_attachments: Vec::new(), workdir_attachments: Vec::new(),
control_operation_id: None, control_operation_id: None,
}; };
let response: BrowserCreateWorkerResponse = let workspace_id = self.workspace_id.clone();
self.send_json(Method::POST, "/workers", Some(&request))?; let response: BrowserCreateWorkerResponse = self.generated(move |client| async move {
client.workspace_worker_create(workspace_id, request).await
})?;
Ok(format!( Ok(format!(
"Started Intake Worker {}/{} for Ticket {ticket_id}", "Started Intake Worker {}/{} for Ticket {ticket_id}",
response.runtime_id, response.worker_id response.runtime_id, response.worker_id
@@ -448,8 +450,11 @@ impl BackendWorkspaceProductClient {
} }
pub fn start_workspace_orchestrator(&self) -> Result<String, BackendWorkspaceClientError> { pub fn start_workspace_orchestrator(&self) -> Result<String, BackendWorkspaceClientError> {
let workspace_id = self.workspace_id.clone();
let response: BrowserWorkspaceOrchestratorResponse = 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(|| { let worker = response.worker.ok_or_else(|| {
BackendWorkspaceClientError::InvalidTarget( BackendWorkspaceClientError::InvalidTarget(
"Backend accepted the Orchestrator request without returning a Worker".to_string(), "Backend accepted the Orchestrator request without returning a Worker".to_string(),
@@ -464,39 +469,6 @@ impl BackendWorkspaceProductClient {
pub fn default_product_list_limit() -> usize { pub fn default_product_list_limit() -> usize {
DEFAULT_PRODUCT_LIST_LIMIT DEFAULT_PRODUCT_LIST_LIMIT
} }
fn get_json<R: DeserializeOwned>(&self, path: &str) -> Result<R, BackendWorkspaceClientError> {
self.send_json::<(), R>(Method::GET, path, None)
}
fn send_json<B: Serialize + ?Sized, R: DeserializeOwned>(
&self,
method: Method,
path: &str,
body: Option<&B>,
) -> Result<R, BackendWorkspaceClientError> {
let response = self.request(method, path, body)?.send()?;
self.api.check_status(response.status())?;
response.json().map_err(BackendWorkspaceClientError::Http)
}
fn request<B: Serialize + ?Sized>(
&self,
method: Method,
path: &str,
body: Option<&B>,
) -> Result<reqwest::blocking::RequestBuilder, BackendWorkspaceClientError> {
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 { 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 { fn encode_path_segment(value: &str) -> String {
let mut encoded = String::with_capacity(value.len()); let mut encoded = String::with_capacity(value.len());
for byte in value.bytes() { for byte in value.bytes() {
+45
View File
@@ -126,6 +126,7 @@ pub struct WorkerCommandAcknowledgement {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[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")] #[serde(tag = "kind", content = "state", rename_all = "snake_case")]
pub enum WorkerState { pub enum WorkerState {
Idle, Idle,
@@ -134,6 +135,7 @@ pub enum WorkerState {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[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")] #[serde(tag = "kind", content = "state", rename_all = "snake_case")]
pub enum WorkerBusyState { pub enum WorkerBusyState {
Run(WorkerRunState), Run(WorkerRunState),
@@ -142,6 +144,7 @@ pub enum WorkerBusyState {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkerRunState { pub enum WorkerRunState {
Running, Running,
@@ -152,6 +155,7 @@ pub enum WorkerRunState {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkerMaintenanceState { pub enum WorkerMaintenanceState {
Compacting, Compacting,
@@ -159,8 +163,13 @@ pub enum WorkerMaintenanceState {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct WorkerStateSnapshot { pub struct WorkerStateSnapshot {
/// Highest lifecycle command id observed by this controller instance. /// 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 last_command_id: u64,
pub state: WorkerState, pub state: WorkerState,
} }
@@ -453,13 +462,29 @@ impl PasteArtifactAvailability {
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct PasteArtifactRef { pub struct PasteArtifactRef {
pub artifact_id: String, 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 created_at_ms: u64,
pub media_type: PasteArtifactMediaType, pub media_type: PasteArtifactMediaType,
/// Availability observed when this immutable reference was committed. /// Availability observed when this immutable reference was committed.
/// Reads revalidate storage and integrity rather than trusting this field. /// Reads revalidate storage and integrity rather than trusting this field.
pub availability: PasteArtifactAvailability, pub availability: PasteArtifactAvailability,
#[cfg_attr(
feature = "json-schema",
schemars(range(min = 0, max = 9_007_199_254_740_991_u64))
)]
pub byte_len: 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, 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 line_count: u64,
pub sha256: String, pub sha256: String,
pub source_entry_id: String, pub source_entry_id: String,
@@ -499,8 +524,16 @@ pub struct UploadedFileRef {
pub artifact_id: String, pub artifact_id: String,
pub file_name: String, pub file_name: String,
pub media_type: 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 created_at_ms: u64,
pub availability: UploadedFileAvailability, 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 byte_len: u64,
pub sha256: String, pub sha256: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[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` /// `[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`
/// re-broadcast. /// re-broadcast.
Paste { Paste {
#[cfg_attr(
feature = "json-schema",
schemars(range(min = 0, max = 4_294_967_295_u32))
)]
id: u32, id: u32,
#[cfg_attr(
feature = "json-schema",
schemars(range(min = 0, max = 4_294_967_295_u32))
)]
chars: u32, chars: u32,
#[cfg_attr(
feature = "json-schema",
schemars(range(min = 0, max = 4_294_967_295_u32))
)]
lines: u32, lines: u32,
content: String, content: String,
}, },
+1 -3
View File
@@ -13,7 +13,7 @@ typescript = ["dep:ts-rs", "protocol/typescript"]
api-macros = { workspace = true, features = ["reqwest", "axum", "openapi"] } api-macros = { workspace = true, features = ["reqwest", "axum", "openapi"] }
memory.workspace = true memory.workspace = true
merge-request.workspace = true merge-request.workspace = true
protocol.workspace = true protocol = { workspace = true, features = ["json-schema"] }
runtime-api.workspace = true runtime-api.workspace = true
schemars.workspace = true schemars.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
@@ -36,11 +36,9 @@ name = "generate_workdir_api_types"
[[example]] [[example]]
name = "generate_worker_launch_api_types" name = "generate_worker_launch_api_types"
required-features = ["typescript"]
[[example]] [[example]]
name = "generate_companion_api_types" name = "generate_companion_api_types"
required-features = ["typescript"]
[[example]] [[example]]
name = "generate_memory_api_types" name = "generate_memory_api_types"
@@ -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() { fn main() {
print!( if let Err(error) = run() {
"{}", eprintln!("{error}");
server_api::normalized_typescript(server_api::companion_api_typescript()) 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(())
} }
@@ -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() { fn main() {
print!( if let Err(error) = run() {
"{}", eprintln!("{error}");
server_api::normalized_typescript(server_api::worker_launch_api_typescript()) 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<bool, 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 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)
} }
File diff suppressed because it is too large Load Diff
@@ -18,6 +18,17 @@ pub const RUNTIME_GENERATOR_OPTIONS: &str =
"runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output"; "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 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 MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
const MIN_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/"; const SCHEMA_REF_PREFIX: &str = "#/components/schemas/";
@@ -166,6 +177,82 @@ pub fn generate_runtime_typescript(input: &str) -> Result<String, GenerationErro
Ok(output) Ok(output)
} }
pub fn generate_worker_typescript(input: &str) -> Result<String, GenerationError> {
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<String, GenerationError> {
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<String, Value>) -> Result<BTreeSet<String>, GenerationError>,
generator_name: &str,
generator_version: &str,
generator_options: &str,
output_path: &str,
example_name: &str,
) -> Result<String, GenerationError> {
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> { fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> {
let Some(hex) = digest.strip_prefix("sha256:") else { let Some(hex) = digest.strip_prefix("sha256:") else {
return Err(GenerationError::invalid( return Err(GenerationError::invalid(
@@ -180,6 +267,141 @@ fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> {
Ok(()) Ok(())
} }
fn validate_worker_operations(
paths: &Map<String, Value>,
) -> Result<BTreeSet<String>, 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<String, Value>,
) -> Result<BTreeSet<String>, 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( fn validate_repository_operations(
paths: &Map<String, Value>, paths: &Map<String, Value>,
) -> Result<BTreeSet<String>, GenerationError> { ) -> Result<BTreeSet<String>, GenerationError> {
@@ -948,9 +1170,7 @@ fn literal_matches_type(kind: &str, value: &Value) -> bool {
fn tagged_object_discriminator(value: &Value) -> Option<(String, String)> { fn tagged_object_discriminator(value: &Value) -> Option<(String, String)> {
let schema = value.as_object()?; let schema = value.as_object()?;
if schema.get("type")?.as_str()? != "object" if schema.get("type")?.as_str()? != "object" {
|| schema.get("additionalProperties") != Some(&Value::Bool(false))
{
return None; return None;
} }
let required = schema.get("required")?.as_array()?; let required = schema.get("required")?.as_array()?;
@@ -1447,7 +1667,9 @@ mod tests {
))); )));
assert!(output.contains("export type RuntimeCleanupPlanResponse =")); assert!(output.contains("export type RuntimeCleanupPlanResponse ="));
assert!(output.contains("export type RuntimeWorkerLifecycleResult =")); 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 =")); assert!(!output.contains("export type UnmigratedDomain ="));
let mut document: Value = serde_json::from_str(OPENAPI).unwrap(); let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
+52 -41
View File
@@ -1,7 +1,7 @@
use crate::auth::BACKEND_RESOURCE_FETCH_PERMISSION; use crate::auth::BACKEND_RESOURCE_FETCH_PERMISSION;
use crate::identity::WorkerId; use crate::identity::WorkerId;
use crate::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveRef, sha256_hex}; 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 async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -219,11 +219,6 @@ impl BackendResourceClient for HttpBackendResourceClient {
&self, &self,
request: BackendResourceFetchRequest, request: BackendResourceFetchRequest,
) -> Result<BackendResourceFetchResponse, BackendResourceError> { ) -> Result<BackendResourceFetchResponse, BackendResourceError> {
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| { let endpoint = reqwest::Url::parse(&self.endpoint).map_err(|error| {
BackendResourceError::Transport { BackendResourceError::Transport {
message: error.to_string(), message: error.to_string(),
@@ -248,17 +243,17 @@ impl BackendResourceClient for HttpBackendResourceClient {
.to_string(), .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 { return Err(BackendResourceError::Unauthorized {
message: "Workspace resource endpoint does not match its request client" message: "Workspace resource endpoint does not match its request client"
.to_string(), .to_string(),
}); });
} }
let mut headers = reqwest::header::HeaderMap::new(); 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() { if let Some(token) = self.bearer_token.as_deref() {
let value = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}")) let value = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
.map_err(|error| BackendResourceError::Transport { .map_err(|error| BackendResourceError::Transport {
@@ -266,41 +261,57 @@ impl BackendResourceClient for HttpBackendResourceClient {
})?; })?;
headers.insert(reqwest::header::AUTHORIZATION, value); headers.insert(reqwest::header::AUTHORIZATION, value);
} }
let response = client let request: server_api::RuntimeResourceFetchRequest =
.execute(RuntimeWorkspaceRequest { serde_json::from_value(serde_json::to_value(&request).map_err(|error| {
method: reqwest::Method::POST, BackendResourceError::InvalidResponse {
path_and_query: endpoint_suffix.to_string(), message: error.to_string(),
body, }
})?)
.map_err(|error| BackendResourceError::InvalidResponse {
message: error.to_string(),
})?;
let generated = client
.server_api_client(
BACKEND_RESOURCE_FETCH_PERMISSION,
None,
headers, headers,
permission: BACKEND_RESOURCE_FETCH_PERMISSION.to_string(), self.request_timeout,
worker_id: None, 8 * 1024 * 1024,
timeout: Some(self.request_timeout), )
max_response_bytes: 8 * 1024 * 1024, .map_err(|error| BackendResourceError::Transport {
}) message: error.to_string(),
})?;
match generated
.runtime_resource_fetch(client.workspace_id().to_string(), request)
.await .await
.map_err(|error| { {
if error.is_timeout() { Ok(response) => {
BackendResourceError::Timeout serde_json::from_value(serde_json::to_value(&response).map_err(|error| {
} else { BackendResourceError::InvalidResponse {
BackendResourceError::Transport {
message: error.to_string(), message: error.to_string(),
} }
} })?)
})?; .map_err(|error| BackendResourceError::InvalidResponse {
if response.status.is_success() { message: error.to_string(),
serde_json::from_slice::<BackendResourceFetchResponse>(&response.body).map_err(|err| { })
BackendResourceError::InvalidResponse {
message: err.to_string(),
}
})
} else {
let status = response.status;
match serde_json::from_slice::<BackendResourceError>(&response.body) {
Ok(error) => Err(error),
Err(err) => Err(BackendResourceError::Transport {
message: format!("backend resource fetch failed with HTTP {status}: {err}"),
}),
} }
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(),
}),
} }
} }
} }
+191 -68
View File
@@ -5,6 +5,7 @@ use crate::workspace_request::{RuntimeWorkspaceRequest, RuntimeWorkspaceRequestC
use worker::{ use worker::{
WorkspaceClient, WorkspaceClientError, WorkspacePromptCatalogResolution, WorkspaceClient, WorkspaceClientError, WorkspacePromptCatalogResolution,
WorkspacePromptProjection, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse, WorkspacePromptProjection, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse,
WorkspaceServerOperation,
}; };
use crate::auth::{ use crate::auth::{
@@ -254,45 +255,53 @@ fn execute_remote_worker_remove_http(
fn execute_remote_worker_remove_http_blocking( fn execute_remote_worker_remove_http_blocking(
request: RemoteWorkerRemoveHttpRequest, request: RemoteWorkerRemoveHttpRequest,
) -> Result<WorkspaceResponse, RuntimeWorkerMutationForwardError> { ) -> Result<WorkspaceResponse, RuntimeWorkerMutationForwardError> {
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(); let mut headers = reqwest::header::HeaderMap::new();
headers.insert( headers.insert(
crate::auth::WORKER_MUTATION_SOURCE_PROOF_HEADER, crate::auth::WORKER_MUTATION_SOURCE_PROOF_HEADER,
reqwest::header::HeaderValue::from_str(&request.token) reqwest::header::HeaderValue::from_str(&request.token)
.map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?, .map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?,
); );
headers.insert( let client = request
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("application/json"),
);
let response = request
.request_client .request_client
.execute_blocking(RuntimeWorkspaceRequest { .server_api_client(
method: reqwest::Method::POST, WORKSPACE_REQUEST_PERMISSION,
path_and_query: path, Some(request.source_worker_id),
body,
headers, headers,
permission: WORKSPACE_REQUEST_PERMISSION.to_string(), Duration::from_secs(5),
worker_id: Some(request.source_worker_id), 8 * 1024 * 1024,
timeout: Some(Duration::from_secs(5)), )
max_response_bytes: 8 * 1024 * 1024,
})
.map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?; .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()))?; .map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?;
Ok(WorkspaceResponse { let result = runtime.block_on(client.workspace_worker_remove(
status: response.status.as_u16(), workspace_id,
body, 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)] #[derive(Clone)]
@@ -432,6 +441,26 @@ impl std::fmt::Debug for RuntimeOwnedWorkspaceClient {
} }
} }
fn generated_workspace_response<T: serde::Serialize>(
response: Result<T, server_api::client_support::ClientError<server_api::RepositoryApiError>>,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
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 { impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
fn workspace_id(&self) -> Option<&str> { fn workspace_id(&self) -> Option<&str> {
Some(self.request_client.workspace_id()) Some(self.request_client.workspace_id())
@@ -452,36 +481,143 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
self.execute_with_permission(request, WORKSPACE_REQUEST_PERMISSION) self.execute_with_permission(request, WORKSPACE_REQUEST_PERMISSION)
} }
fn execute_server_operation(
&self,
operation: WorkspaceServerOperation,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
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( fn list_workspace_workers(
&self, &self,
request: worker::WorkspaceWorkerDiscoveryRequest, request: worker::WorkspaceWorkerDiscoveryRequest,
) -> Result<server_api::WorkspaceWorkerDiscoveryPage, WorkspaceClientError> { ) -> Result<server_api::WorkspaceWorkerDiscoveryPage, WorkspaceClientError> {
let mut path = format!( let request_client = self.request_client.clone();
"/api/w/{}/worker-discovery/workers?limit={}", let workspace_id = self.workspace_id.clone();
self.workspace_id, request.limit let worker_id = self.worker_id.clone();
); let timeout = self.request_timeout.unwrap_or(Duration::from_secs(5));
if let Some(cursor) = request.cursor.as_deref() { let query = server_api::WorkspaceWorkerDiscoveryQuery {
path.push_str("&cursor="); cursor: request.cursor,
path.push_str(&percent_encode_query(cursor)); limit: Some(request.limit),
} query: request.query,
if let Some(query) = request.query.as_deref() { };
path.push_str("&query="); let result = std::thread::Builder::new()
path.push_str(&percent_encode_query(query)); .name("yoi-worker-discovery-http".to_string())
} .spawn(move || {
let response = self.execute_with_permission( let runtime = tokio::runtime::Builder::new_current_thread()
WorkspaceRequest::get(path), .enable_all()
WORKSPACE_WORKER_DISCOVERY_PERMISSION, .build()
)?; .map_err(|error| error.to_string())?;
if !(200..300).contains(&response.status) { let client = request_client
return Err(WorkspaceClientError::Request(format!( .server_api_client(
"Workspace Worker discovery failed with HTTP {}: {}", WORKSPACE_WORKER_DISCOVERY_PERMISSION,
response.status, response.body Some(worker_id),
))); reqwest::header::HeaderMap::new(),
} timeout,
serde_json::from_str(&response.body).map_err(|error| { 8 * 1024 * 1024,
WorkspaceClientError::Request(format!( )
"invalid Workspace Worker discovery response: {error}" .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)] #[derive(Debug, thiserror::Error)]
pub enum RuntimeWorkerMutationForwardError { pub enum RuntimeWorkerMutationForwardError {
#[error(transparent)] #[error(transparent)]
+78 -2
View File
@@ -62,9 +62,57 @@ impl RuntimeWorkspaceRequestError {
timeout, timeout,
} }
} }
}
pub(crate) fn is_timeout(&self) -> bool { #[derive(Clone)]
matches!(self, Self::Transport { timeout: true, .. }) pub(crate) struct RuntimeWorkspaceRequestAuthorizer {
workspace_id: String,
runtime_id: String,
request_source: Option<(RuntimeRequestSourceSigner, String)>,
permission: String,
worker_id: Option<String>,
extra_headers: HeaderMap,
}
impl server_api::client_support::RequestAuthorizer for RuntimeWorkspaceRequestAuthorizer {
fn authorize(
&self,
request: server_api::client_support::AuthorizerRequest<'_>,
) -> Result<HeaderMap, server_api::client_support::AuthorizationError> {
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('/') && self.base_url.trim_end_matches('/') == base_url.trim_end_matches('/')
} }
pub(crate) fn server_api_client(
&self,
permission: impl Into<String>,
worker_id: Option<String>,
extra_headers: HeaderMap,
timeout: Duration,
response_body_limit: usize,
) -> Result<
server_api::ServerApiClient<RuntimeWorkspaceRequestAuthorizer>,
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( pub(crate) async fn execute(
&self, &self,
request: RuntimeWorkspaceRequest, request: RuntimeWorkspaceRequest,
@@ -19,10 +19,7 @@ use crate::feature::{
ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration, ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration,
}; };
use crate::spawn::registry::{SpawnedWorkerRegistry, SubWorkerStopSummary}; use crate::spawn::registry::{SpawnedWorkerRegistry, SubWorkerStopSummary};
use crate::worker::{ use crate::worker::{WorkspaceClient, WorkspaceClientError, WorkspaceResponse};
WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod,
WorkspaceResponse,
};
const FEATURE_ID: &str = "worker"; const FEATURE_ID: &str = "worker";
const FEATURE_NAME: &str = "Worker"; const FEATURE_NAME: &str = "Worker";
@@ -76,16 +73,22 @@ pub trait WorkerControlService: Send + Sync {
&self, &self,
request: WorkerLifecycleSpawnRequest, request: WorkerLifecycleSpawnRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError>; ) -> Result<WorkspaceResponse, WorkspaceClientError>;
async fn restore_worker(
&self,
runtime_id: String,
worker_id: String,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
let _ = (runtime_id, worker_id);
Err(WorkspaceClientError::Unavailable(
"Runtime Worker restore is unavailable".to_string(),
))
}
fn remove_runtime_worker( fn remove_runtime_worker(
&self, &self,
runtime_id: &str, runtime_id: &str,
worker_id: &str, worker_id: &str,
reason: &str, reason: &str,
) -> Result<WorkspaceResponse, WorkspaceClientError>; ) -> Result<WorkspaceResponse, WorkspaceClientError>;
async fn execute_runtime(
&self,
request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError>;
async fn ensure_permission( async fn ensure_permission(
&self, &self,
subject: &super::worker_observation::WorkerObservationSubjectRef, subject: &super::worker_observation::WorkerObservationSubjectRef,
@@ -144,11 +147,9 @@ impl WorkerControlService for WorkspaceWorkerControlService {
async fn list_workers(&self) -> Result<WorkspaceResponse, WorkspaceClientError> { async fn list_workers(&self) -> Result<WorkspaceResponse, WorkspaceClientError> {
let response = if self.runtime_worker_control { let response = if self.runtime_worker_control {
self.execute_runtime(WorkspaceRequest::get(format!( self.client.execute_server_operation(
"/api/w/{}/worker-control/workers", crate::worker::WorkspaceServerOperation::WorkerControlList,
self.workspace_id )?
)))
.await?
} else { } else {
WorkspaceResponse { WorkspaceResponse {
status: 200, status: 200,
@@ -210,19 +211,17 @@ impl WorkerControlService for WorkspaceWorkerControlService {
WorkerControlInputKind::User => "user", WorkerControlInputKind::User => "user",
WorkerControlInputKind::Notify => "notify", WorkerControlInputKind::Notify => "notify",
}; };
self.execute_runtime(WorkspaceRequest::json( self.client.execute_server_operation(
WorkspaceRequestMethod::Post, crate::worker::WorkspaceServerOperation::WorkerControlInput {
format!( runtime_id,
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/input", worker_id,
self.workspace_id request: server_api::RuntimeWorkerInputRequest {
), kind: Some(kind.to_string()),
serde_json::json!({ content,
"kind": kind, segments: None,
"content": content, },
}) },
.to_string(), )
))
.await
} }
WorkerControlSubject::SubWorker { name } => { WorkerControlSubject::SubWorker { name } => {
if kind != WorkerControlInputKind::User { if kind != WorkerControlInputKind::User {
@@ -273,19 +272,29 @@ impl WorkerControlService for WorkspaceWorkerControlService {
runtime_id, runtime_id,
worker_id, worker_id,
} => { } => {
let action = match kind { let operation = match kind {
WorkerControlStopKind::Cancel => "cancel", WorkerControlStopKind::Cancel => {
WorkerControlStopKind::Stop => "stop", 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( self.client.execute_server_operation(operation)
WorkspaceRequestMethod::Post,
format!(
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/{action}",
self.workspace_id
),
serde_json::json!({ "reason": reason }).to_string(),
))
.await
} }
WorkerControlSubject::SubWorker { name } => { WorkerControlSubject::SubWorker { name } => {
if kind != WorkerControlStopKind::Stop { if kind != WorkerControlStopKind::Stop {
@@ -329,12 +338,29 @@ impl WorkerControlService for WorkspaceWorkerControlService {
) -> Result<WorkspaceResponse, WorkspaceClientError> { ) -> Result<WorkspaceResponse, WorkspaceClientError> {
WorkspaceWorkerLifecycleService { WorkspaceWorkerLifecycleService {
client: self.client.clone(), client: self.client.clone(),
workspace_id: self.workspace_id.clone(),
} }
.spawn(request) .spawn(request)
.await .await
} }
async fn restore_worker(
&self,
runtime_id: String,
worker_id: String,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
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( fn remove_runtime_worker(
&self, &self,
runtime_id: &str, runtime_id: &str,
@@ -350,18 +376,6 @@ impl WorkerControlService for WorkspaceWorkerControlService {
.execute_worker_remove(runtime_id, worker_id, reason) .execute_worker_remove(runtime_id, worker_id, reason)
} }
async fn execute_runtime(
&self,
request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
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( async fn ensure_permission(
&self, &self,
subject: &super::worker_observation::WorkerObservationSubjectRef, subject: &super::worker_observation::WorkerObservationSubjectRef,
@@ -391,10 +405,9 @@ impl WorkerControlService for WorkspaceWorkerControlService {
"Runtime Worker control is not enabled for this Worker".to_string(), "Runtime Worker control is not enabled for this Worker".to_string(),
)); ));
} }
let response = self.client.execute(WorkspaceRequest::get(format!( let response = self.client.execute_server_operation(
"/api/w/{}/worker-control/workers", crate::worker::WorkspaceServerOperation::WorkerControlList,
self.workspace_id )?;
)))?;
if !response.is_success() { if !response.is_success() {
return Err(WorkspaceClientError::Request(format!( return Err(WorkspaceClientError::Request(format!(
"Workspace control request returned {}: {}", "Workspace control request returned {}: {}",
@@ -459,7 +472,6 @@ pub struct WorkerLifecycleSpawnRequest {
struct WorkspaceWorkerLifecycleService { struct WorkspaceWorkerLifecycleService {
client: Arc<dyn WorkspaceClient>, client: Arc<dyn WorkspaceClient>,
workspace_id: String,
} }
fn workspace_worker_create_request( fn workspace_worker_create_request(
@@ -505,12 +517,9 @@ impl WorkerLifecycleService for WorkspaceWorkerLifecycleService {
request: WorkerLifecycleSpawnRequest, request: WorkerLifecycleSpawnRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> { ) -> Result<WorkspaceResponse, WorkspaceClientError> {
let body = workspace_worker_create_request(request)?; let body = workspace_worker_create_request(request)?;
self.client.execute(WorkspaceRequest::json( self.client.execute_server_operation(
WorkspaceRequestMethod::Post, crate::worker::WorkspaceServerOperation::WorkerControlSpawn(body),
format!("/api/w/{}/worker-control/workers", self.workspace_id), )
serde_json::to_string(&body)
.map_err(|error| WorkspaceClientError::Request(error.to_string()))?,
))
} }
} }
@@ -639,20 +648,17 @@ impl FeatureModule for ManageWorkerFeature {
} }
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
let workspace_id = self self.client
.client
.workspace_id() .workspace_id()
.filter(|value| !value.trim().is_empty()) .filter(|value| !value.trim().is_empty())
.ok_or_else(|| { .ok_or_else(|| {
FeatureInstallError::InvalidDescriptor( FeatureInstallError::InvalidDescriptor(
"worker feature requires a Workspace id".to_string(), "worker feature requires a Workspace id".to_string(),
) )
})? })?;
.to_string();
let lifecycle: Arc<dyn WorkerLifecycleService> = let lifecycle: Arc<dyn WorkerLifecycleService> =
Arc::new(WorkspaceWorkerLifecycleService { Arc::new(WorkspaceWorkerLifecycleService {
client: self.client.clone(), client: self.client.clone(),
workspace_id: workspace_id.clone(),
}); });
context.services().provide( context.services().provide(
ServiceDeclaration::new( ServiceDeclaration::new(
@@ -948,14 +954,7 @@ impl Tool for WorkspaceWorkerTool {
let input = parse::<WorkerTargetInput>(input_json, "WorkerRestore")?; let input = parse::<WorkerTargetInput>(input_json, "WorkerRestore")?;
let (runtime_id, worker_id) = runtime_subject_ids(&input.subject, self.operation)?; let (runtime_id, worker_id) = runtime_subject_ids(&input.subject, self.operation)?;
self.control self.control
.execute_runtime(WorkspaceRequest::json( .restore_worker(runtime_id, worker_id)
WorkspaceRequestMethod::Post,
format!(
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/restore",
self.control.workspace_id()
),
"{}",
))
.await .await
.map_err(control_tool_error)? .map_err(control_tool_error)?
} }
@@ -1191,7 +1190,7 @@ mod tests {
use std::sync::Mutex; use std::sync::Mutex;
use super::*; use super::*;
use crate::worker::{WorkspaceClientError, WorkspaceResponse}; use crate::worker::{WorkspaceClientError, WorkspaceRequest, WorkspaceResponse};
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct RecordingWorkspaceClient { struct RecordingWorkspaceClient {
@@ -1361,15 +1360,6 @@ mod tests {
)) ))
} }
async fn execute_runtime(
&self,
_request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
Err(WorkspaceClientError::Unavailable(
"Runtime Worker control is disabled in this test".to_string(),
))
}
async fn ensure_permission( async fn ensure_permission(
&self, &self,
_subject: &crate::feature::builtin::WorkerObservationSubjectRef, _subject: &crate::feature::builtin::WorkerObservationSubjectRef,
@@ -125,17 +125,16 @@ impl WorkerObservationProvider for WorkspaceClientWorkerObservationProvider {
async fn list_worker_sessions( async fn list_worker_sessions(
&self, &self,
) -> Result<Vec<WorkerObservationSubject>, WorkerObservationError> { ) -> Result<Vec<WorkerObservationSubject>, WorkerObservationError> {
let workspace_id = self.client.workspace_id().ok_or_else(|| { let _workspace_id = self.client.workspace_id().ok_or_else(|| {
WorkerObservationError::Unavailable( WorkerObservationError::Unavailable(
"Workspace observation requires a scoped Workspace client".to_string(), "Workspace observation requires a scoped Workspace client".to_string(),
) )
})?; })?;
let response = self let response = self
.client .client
.execute(crate::worker::WorkspaceRequest::get(format!( .execute_server_operation(
"/api/w/{}/worker-observation/sessions", crate::worker::WorkspaceServerOperation::WorkerObservationSessions,
workspace_id )
)))
.map_err(workspace_client_error)?; .map_err(workspace_client_error)?;
let body = workspace_response_body(response)?; let body = workspace_response_body(response)?;
serde_json::from_str::<WorkspaceWorkerObservationListResponse>(&body) serde_json::from_str::<WorkspaceWorkerObservationListResponse>(&body)
@@ -147,20 +146,28 @@ impl WorkerObservationProvider for WorkspaceClientWorkerObservationProvider {
&self, &self,
subject: &WorkerObservationSubjectRef, subject: &WorkerObservationSubjectRef,
) -> Result<WorkerSessionCapture, WorkerObservationError> { ) -> Result<WorkerSessionCapture, WorkerObservationError> {
let body = serde_json::to_string(subject) let request = match subject {
.map_err(|error| WorkerObservationError::Unavailable(error.to_string()))?; WorkerObservationSubjectRef::RuntimeWorker {
let workspace_id = self.client.workspace_id().ok_or_else(|| { 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( WorkerObservationError::Unavailable(
"Workspace observation requires a scoped Workspace client".to_string(), "Workspace observation requires a scoped Workspace client".to_string(),
) )
})?; })?;
let response = self let response = self
.client .client
.execute(crate::worker::WorkspaceRequest::json( .execute_server_operation(
crate::worker::WorkspaceRequestMethod::Post, crate::worker::WorkspaceServerOperation::WorkerObservationCapture(request),
format!("/api/w/{}/worker-observation/session", workspace_id), )
body,
))
.map_err(workspace_client_error)?; .map_err(workspace_client_error)?;
let body = workspace_response_body(response)?; let body = workspace_response_body(response)?;
let response = serde_json::from_str::<WorkspaceWorkerObservationCaptureResponse>(&body) let response = serde_json::from_str::<WorkspaceWorkerObservationCaptureResponse>(&body)
+2 -2
View File
@@ -60,6 +60,6 @@ pub use worker::{
LocalWorkingDirectory, Worker, WorkerError, WorkerFilesystemAuthority, WorkerRunResult, LocalWorkingDirectory, Worker, WorkerError, WorkerFilesystemAuthority, WorkerRunResult,
WorkerWorkspaceContext, WorkspaceClient, WorkspaceClientError, WorkspaceId, WorkspaceIdError, WorkerWorkspaceContext, WorkspaceClient, WorkspaceClientError, WorkspaceId, WorkspaceIdError,
WorkspacePromptCatalogResolution, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse, WorkspacePromptCatalogResolution, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse,
WorkspaceWorkerDiscoveryRequest, apply_worker_manifest, marker_workspace_client, WorkspaceServerOperation, WorkspaceWorkerDiscoveryRequest, apply_worker_manifest,
unavailable_workspace_client, marker_workspace_client, unavailable_workspace_client,
}; };
+104
View File
@@ -544,6 +544,98 @@ pub struct WorkspaceWorkerDiscoveryRequest {
pub query: Option<String>, pub query: Option<String>,
} }
#[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<T: serde::Serialize>(
path: String,
body: &T,
) -> Result<WorkspaceRequest, WorkspaceClientError> {
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<WorkspaceRequest, WorkspaceClientError> {
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. /// Path-free Workspace operation authority injected by Runtime/host code.
/// ///
/// Workers receive this trait object rather than a Backend URL. The concrete /// 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) fn execute(&self, request: WorkspaceRequest)
-> Result<WorkspaceResponse, WorkspaceClientError>; -> Result<WorkspaceResponse, WorkspaceClientError>;
fn execute_server_operation(
&self,
operation: WorkspaceServerOperation,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
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 /// Lists Workspace-visible Workers through dedicated Runtime-owned source
/// proof. Implementations must not fall back to generic Workspace request /// proof. Implementations must not fall back to generic Workspace request
/// authority. /// authority.
File diff suppressed because it is too large Load Diff
+4524 -20
View File
File diff suppressed because it is too large Load Diff
@@ -1,30 +1,36 @@
// Generated by `cargo run -p server-api --features typescript --example generate_companion_api_types`. // @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Do not edit manually. // 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 CompanionCancelRequest = { reason?: string | null; };
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 CompanionLifecycleState = "idle" | "running" | "stopped"; export type CompanionLifecycleState = "idle" | "running" | "stopped";
export type CompanionMessageDisposition = "accepted" | "rejected"; 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<Diagnostic>, }; export type CompanionMessageResponse = { message: string; state: CompanionMessageDisposition; };
export type CompanionMessageRequest = { content: string, }; export type CompanionStatusResponse = { diagnostics?: Array<Diagnostic>; 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<CompanionTranscriptItem>; limit: number; next?: number | null; start: number; state: CompanionLifecycleState; total: number; };
export type CompanionTranscriptRole = "user" | "assistant"; 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<CompanionTranscriptItem>, }; export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
export type DiagnosticSeverity = "info" | "warning" | "error";
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; 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; };
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit. // @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json // 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 // 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 // Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check // 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<RepositoryDiagnostic> | 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 RepositorySummary = { default_selector?: string | null; diagnostics?: Array<RepositoryDiagnostic> | 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 WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
export type WorkerImplementationSummary = { display_hint: string; kind: string; }; export type WorkerImplementationSummary = { display_hint: string; kind: string; };
export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; 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<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; 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<Diagnostic>; 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<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; };
+14 -4
View File
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit. // @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json // 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 // Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts // Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check // Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -105,11 +105,11 @@ export type RuntimeWorkerLifecycleResult = { diagnostics?: Array<Diagnostic>; ru
export type RuntimeWorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; }; export type RuntimeWorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
export type RuntimeWorkerShowResponse = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; 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<string>; updated_at: string; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; }; export type RuntimeWorkerShowResponse = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; 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<string>; updated_at: string; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type RuntimeWorkerSpawnAcceptanceEvidence = { detail: string; kind: string; }; 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; }; 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 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 WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
export type WorkerFileDeleteResponse = { deleted: boolean; }; export type WorkerFileDeleteResponse = { deleted: boolean; };
export type WorkerImplementationSummary = { display_hint: string; kind: string; }; export type WorkerImplementationSummary = { display_hint: string; kind: string; };
export type WorkerMaintenanceState = "compacting";
export type WorkerOperationState = "accepted" | "unsupported" | "rejected"; export type WorkerOperationState = "accepted" | "unsupported" | "rejected";
export type WorkerRestoreResponse = { result: WorkerRestoreResult; runtime_id: string; worker_id: string; workspace_id: string; }; 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 WorkerRetentionResponse = { pinned: boolean; retention_state: string; runtime_id: string; worker_id: string; workspace_id: string; };
export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; 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<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; 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<Diagnostic>; 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<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; };
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit. // @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json // 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 // Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/workdir-api.ts // Output: web/workspace/src/lib/generated/workdir-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check // Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
@@ -1,68 +1,84 @@
// Generated from server-api. Do not edit by hand. // @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Regenerate: cargo run -q -p server-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts // 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<Diagnostic>; 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<Diagnostic>; disposition: string; online: boolean; worker?: WorkerLaunchWorkerSummary | null; workspace_id: string; };
export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; initial_submit?: Array<Segment>; profile?: string | null; runtime_id: string; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array<BrowserWorkerWorkingDirectorySelection>; };
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 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<Diagnostic>; 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<Diagnostic>; profiles: Array<WorkerLaunchProfileCandidate>; repositories: Array<WorkingDirectoryRepositoryOption>; runtimes: Array<WorkerLaunchRuntimeOption>; working_directories: Array<WorkingDirectorySummary>; workspace_id: string; };
export type WorkerLaunchProfileCandidate = { description: string; id: string; label: string; };
export type WorkerLaunchRuntimeOption = { built_in: boolean; diagnostics: Array<Diagnostic>; display_name: string; runtime_id: string; status: string; worker_creation_available: boolean; working_directory_required: boolean; };
export type WorkerLaunchWorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; 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<string>; workdir_attachments?: Array<RuntimeWorkerWorkdirAttachmentSummary>; worker_id: string; workspace: WorkerWorkspaceSummary; };
export type WorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; 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<Diagnostic>; 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<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; 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 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 WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_key: string, }; 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; };
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<string>, workspace: WorkerWorkspaceSummary, state: string, last_seen_at: string | null, pinned: boolean, retention_state: string, implementation: WorkerImplementationSummary, capabilities: WorkerCapabilitySummary, workdir_attachments?: Array<RuntimeWorkerWorkdirAttachmentSummary>, diagnostics: Array<Diagnostic>, };
export type WorkerLaunchRuntimeOption = { runtime_id: string, display_name: string, built_in: boolean, worker_creation_available: boolean, working_directory_required: boolean, status: string, diagnostics: Array<Diagnostic>, };
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<WorkerLaunchRuntimeOption>, default_profile: string | null, profiles: Array<WorkerLaunchProfileCandidate>, repositories: Array<WorkingDirectoryRepositoryOption>, working_directories: Array<WorkingDirectorySummary>, diagnostics: Array<Diagnostic>, };
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<Segment>, workdir_attachments: Array<BrowserWorkerWorkingDirectorySelection>,
/**
* 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<Diagnostic>, };
export type BrowserWorkspaceOrchestratorResponse = { workspace_id: string, online: boolean, disposition: string, worker?: WorkerLaunchWorkerSummary | null, diagnostics: Array<Diagnostic>, };
+204 -3
View File
@@ -6,15 +6,20 @@ import type {
CreateWorkspaceWorkerTicketAssignmentRequest, CreateWorkspaceWorkerTicketAssignmentRequest,
Diagnostic, Diagnostic,
DiagnosticSeverity, DiagnosticSeverity,
RepositoryApiError,
RuntimeWorkerWorkdirAttachmentSummary,
RuntimeWorkingDirectoryCleanupTarget, RuntimeWorkingDirectoryCleanupTarget,
RuntimeWorkingDirectorySummary, RuntimeWorkingDirectorySummary,
RuntimeWorkerWorkdirAttachmentSummary,
WorkerCapabilitySummary, WorkerCapabilitySummary,
WorkerImplementationSummary, WorkerImplementationSummary,
WorkerLaunchOptionsResponse, WorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate, WorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption, WorkerLaunchRuntimeOption,
WorkerLaunchWorkerSummary, WorkerLaunchWorkerSummary,
WorkerListResponse,
WorkerStateSnapshot,
WorkerSummary,
WorkerWorkdirAttachmentSummary,
WorkerWorkspaceSummary, WorkerWorkspaceSummary,
WorkingDirectoryRepositoryOption, WorkingDirectoryRepositoryOption,
} from "$lib/generated/worker-launch-api"; } 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 { function string(value: unknown, label: string): string {
if (typeof value !== "string") throw new Error(`${label} must be a 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; return value;
} }
@@ -56,8 +67,8 @@ function boolean(value: unknown, label: string): boolean {
} }
function number(value: unknown, label: string): number { function number(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) { if (typeof value !== "number" || !Number.isSafeInteger(value)) {
throw new Error(`${label} must be a finite number`); throw new Error(`${label} must be a safe integer`);
} }
return value; return value;
} }
@@ -72,6 +83,9 @@ function array<T>(
parse: (item: unknown, label: string) => T, parse: (item: unknown, label: string) => T,
): T[] { ): T[] {
if (!Array.isArray(value)) throw new Error(`${label} must be an array`); 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}]`)); 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( export function parseWorkerLaunchOptionsResponse(
value: unknown, value: unknown,
): WorkerLaunchOptionsResponse { ): 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<WorkerListResponse, "items"> & {
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( function runtimeCleanupTarget(
value: unknown, value: unknown,
label: string, label: string,
@@ -9,10 +9,14 @@ import type {
BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse, BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse,
BrowserWorkerWorkingDirectorySelection BrowserWorkerWorkingDirectorySelection
as SharedBrowserWorkerWorkingDirectorySelection, as SharedBrowserWorkerWorkingDirectorySelection,
Diagnostic as SharedDiagnostic,
WorkerCapabilitySummary as SharedWorkerCapabilitySummary,
WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse, WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate, WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption, WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption,
WorkerLaunchWorkerSummary as WorkerSummary, WorkerLaunchWorkerSummary,
WorkerSummary as SharedWorkerSummary,
WorkerWorkdirAttachmentSummary as SharedWorkerWorkdirAttachmentSummary,
WorkingDirectoryRepositoryOption as SharedWorkingDirectoryRepositoryOption, WorkingDirectoryRepositoryOption as SharedWorkingDirectoryRepositoryOption,
} from "$lib/generated/worker-launch-api"; } from "$lib/generated/worker-launch-api";
import type { import type {
@@ -53,11 +57,7 @@ export type {
}; };
export type WorkspaceResponse = SharedWorkspaceResponse; export type WorkspaceResponse = SharedWorkspaceResponse;
export type Diagnostic = { export type Diagnostic = SharedDiagnostic;
code: string;
severity: string;
message: string;
};
export type Runtime = { export type Runtime = {
runtime_id: string; runtime_id: string;
@@ -78,35 +78,17 @@ export type Runtime = {
}; };
}; };
export type WorkerCapabilities = { export type WorkerCapabilities = SharedWorkerCapabilitySummary;
can_stop: boolean;
can_spawn_followup: boolean;
};
export type WorkerWorkdirAttachment = { export type WorkerWorkdirAttachment = SharedWorkerWorkdirAttachmentSummary;
alias: string;
working_directory: WorkingDirectorySummary;
};
export type Worker = { export type Worker = Omit<
runtime_id: string; SharedWorkerSummary,
worker_id: string; "display_name" | "tags" | "worker_state" | "diagnostics"
resource_key: string; > & {
host_id: string;
display_name: string; display_name: string;
label: string;
profile?: string | null;
singleton_key?: string | null;
tags: string[]; tags: string[];
workspace: { visibility: string; identity: string };
state: string;
worker_state?: WorkerStateSnapshot | null; 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[]; diagnostics: Diagnostic[];
}; };
@@ -121,7 +103,7 @@ export type WorkerRestoreState =
export type WorkerRestoreResult = { export type WorkerRestoreResult = {
state: WorkerRestoreState; state: WorkerRestoreState;
worker?: WorkerSummary | null; worker?: WorkerLaunchWorkerSummary | null;
diagnostics: Diagnostic[]; diagnostics: Diagnostic[];
}; };
@@ -1,16 +1,18 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseWorkerListResponse } from "$lib/workspace/api/workers";
import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers"; import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers";
import type { import type {
ListResponse,
RuntimeCleanupPlanResponse, RuntimeCleanupPlanResponse,
Worker,
} from "$lib/workspace/sidebar/types"; } from "$lib/workspace/sidebar/types";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => { export const load: PageLoad = async ({ fetch, params }) => {
const workers = await loadJson<ListResponse<Worker>>( const workers = await loadJson(
fetch, fetch,
workspaceApiPath(params.workspaceId, "/workers"), workspaceApiPath(params.workspaceId, "/workers"),
undefined,
parseWorkerListResponse,
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
); );
const runtimeIds = Array.from( const runtimeIds = Array.from(
new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []), new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []),
@@ -1,20 +1,23 @@
import { redirect } from "@sveltejs/kit"; import { redirect } from "@sveltejs/kit";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseWorkerSummary } from "$lib/workspace/api/workers";
import { import {
canonicalResourceReference, canonicalResourceReference,
resourceKey, resourceKey,
} from "$lib/workspace/resource-links"; } from "$lib/workspace/resource-links";
import type { Worker } from "$lib/workspace/sidebar/types";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load = (async ({ fetch, params }) => { export const load = (async ({ fetch, params }) => {
const reference = resourceKey(params.workerRef); const reference = resourceKey(params.workerRef);
const result = await loadJson<Worker>( const result = await loadJson(
fetch, fetch,
workspaceApiPath( workspaceApiPath(
params.workspaceId, params.workspaceId,
`/workers/${encodeURIComponent(reference)}`, `/workers/${encodeURIComponent(reference)}`,
), ),
undefined,
parseWorkerSummary,
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
); );
if (result.data?.resource_key) { if (result.data?.resource_key) {
const canonical = canonicalResourceReference( const canonical = canonicalResourceReference(
@@ -1,20 +1,23 @@
import { redirect } from "@sveltejs/kit"; import { redirect } from "@sveltejs/kit";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseWorkerSummary } from "$lib/workspace/api/workers";
import { import {
canonicalResourceReference, canonicalResourceReference,
resourceKey, resourceKey,
} from "$lib/workspace/resource-links"; } from "$lib/workspace/resource-links";
import type { Worker } from "$lib/workspace/sidebar/types";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load = (async ({ fetch, params }) => { export const load = (async ({ fetch, params }) => {
const reference = resourceKey(params.workerRef); const reference = resourceKey(params.workerRef);
const result = await loadJson<Worker>( const result = await loadJson(
fetch, fetch,
workspaceApiPath( workspaceApiPath(
params.workspaceId, params.workspaceId,
`/workers/${encodeURIComponent(reference)}`, `/workers/${encodeURIComponent(reference)}`,
), ),
undefined,
parseWorkerSummary,
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
); );
if (result.data?.resource_key) { if (result.data?.resource_key) {
const canonical = canonicalResourceReference( const canonical = canonicalResourceReference(
@@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { workspaceApiPath } from '$lib/workspace/api/http'; import { readBoundedJson, workspaceApiPath } from '$lib/workspace/api/http';
import { import {
parseWorkingDirectoryCreateResponse, parseWorkingDirectoryCreateResponse,
validateWorkingDirectoryCreateRequest, validateWorkingDirectoryCreateRequest,
} from '$lib/workspace/api/workdirs'; } from '$lib/workspace/api/workdirs';
import { import {
parseBrowserCreateWorkerResponse, parseBrowserCreateWorkerResponse,
parseWorkerApiError,
parseWorkerLaunchOptionsResponse, parseWorkerLaunchOptionsResponse,
} from '$lib/workspace/api/workers'; } from '$lib/workspace/api/workers';
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision'; import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
@@ -29,12 +30,6 @@
diagnostics: Diagnostic[]; diagnostics: Diagnostic[];
}; };
type ErrorPayload = {
error?: { message?: string; code?: string } | string;
message?: string;
diagnostics?: Diagnostic[];
};
function workdirOptionLabel(directory: WorkingDirectorySummary): string { function workdirOptionLabel(directory: WorkingDirectorySummary): string {
const repositoryKey = directory.source.kind === 'repository' const repositoryKey = directory.source.kind === 'repository'
? directory.source.repository_key ? directory.source.repository_key
@@ -171,7 +166,9 @@
if (!response.ok) { if (!response.ok) {
throw new Error(`worker launch options failed (${response.status})`); throw new Error(`worker launch options failed (${response.status})`);
} }
const payload = parseWorkerLaunchOptionsResponse(await response.json()); const payload = parseWorkerLaunchOptionsResponse(
await readBoundedJson(response, 8 * 1024 * 1024),
);
options = payload; options = payload;
const form = defaultWorkerLaunchForm(payload, { const form = defaultWorkerLaunchForm(payload, {
runtime_id: runtimeId, runtime_id: runtimeId,
@@ -313,7 +310,9 @@
submitError = await responseDisplayError(response, 'worker create failed'); submitError = await responseDisplayError(response, 'worker create failed');
return; return;
} }
const payload = parseBrowserCreateWorkerResponse(await response.json()); const payload = parseBrowserCreateWorkerResponse(
await readBoundedJson(response, 8 * 1024 * 1024),
);
await goto(payload.console_href); await goto(payload.console_href);
} catch (err) { } catch (err) {
submitError = exceptionDisplayError(err, 'worker create failed'); submitError = exceptionDisplayError(err, 'worker create failed');
@@ -324,18 +323,13 @@
async function responseDisplayError(response: Response, fallback: string): Promise<DisplayError> { async function responseDisplayError(response: Response, fallback: string): Promise<DisplayError> {
try { try {
const payload = (await response.json()) as ErrorPayload; const payload = parseWorkerApiError(
const diagnostics = Array.isArray(payload.diagnostics) ? payload.diagnostics : []; await readBoundedJson(response, 1024 * 1024),
if (typeof payload.error === 'object' && payload.error?.message) { );
return { return {
message: `${payload.error.code ?? 'request_failed'}: ${payload.error.message}`, message: `${payload.error}: ${payload.message}`,
diagnostics, diagnostics: payload.diagnostics ?? [],
}; };
}
if (payload.message) {
const code = typeof payload.error === 'string' ? payload.error : 'request_failed';
return { message: `${code}: ${payload.message}`, diagnostics };
}
} catch { } catch {
// fall through // fall through
} }