feat: migrate workspace worker and companion domains to ServerApi
This commit is contained in:
@@ -393,15 +393,16 @@ async fn get_backend_worker_launch_options_with_client(
|
||||
target: &BackendWorkerLaunchTarget,
|
||||
api: &BackendApiClient,
|
||||
) -> Result<BackendWorkerLaunchOptions, BackendRuntimeClientError> {
|
||||
let path = backend_workspace_workers_launch_options_path(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope"),
|
||||
);
|
||||
let response = api.request(HttpMethod::GET, &path)?.send().await?;
|
||||
let response = api.require_success(response).await?;
|
||||
Ok(response.json::<BackendWorkerLaunchOptions>().await?)
|
||||
runtime_server_api_client(&target.base_url, api)?
|
||||
.workspace_worker_launch_options(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope")
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn create_backend_worker(
|
||||
@@ -418,19 +419,17 @@ async fn create_backend_worker_with_client(
|
||||
request: &BackendCreateWorkerRequest,
|
||||
api: &BackendApiClient,
|
||||
) -> Result<BackendCreateWorkerResponse, BackendRuntimeClientError> {
|
||||
let path = backend_workspace_workers_path(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope"),
|
||||
);
|
||||
let response = api
|
||||
.request(HttpMethod::POST, &path)?
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
let response = api.require_success(response).await?;
|
||||
Ok(response.json::<BackendCreateWorkerResponse>().await?)
|
||||
runtime_server_api_client(&target.base_url, api)?
|
||||
.workspace_worker_create(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope")
|
||||
.to_string(),
|
||||
request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn list_backend_workers(
|
||||
@@ -680,17 +679,6 @@ fn validate_list_target(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backend_workspace_workers_path(workspace_id: &str) -> String {
|
||||
format!("/api/w/{}/workers", path_segment_encode(workspace_id))
|
||||
}
|
||||
|
||||
fn backend_workspace_workers_launch_options_path(workspace_id: &str) -> String {
|
||||
format!(
|
||||
"{}/launch-options",
|
||||
backend_workspace_workers_path(workspace_id)
|
||||
)
|
||||
}
|
||||
|
||||
fn protocol_ws_request(
|
||||
target: &BackendRuntimeTarget,
|
||||
api: &BackendApiClient,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use server_api::{
|
||||
BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse,
|
||||
CreateWorkspaceWorkerRequest, ListResponse, MemoryDocumentResponse, MemoryStagingListResponse,
|
||||
@@ -417,7 +414,10 @@ impl BackendWorkspaceProductClient {
|
||||
&self,
|
||||
ticket_id: &str,
|
||||
) -> Result<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
|
||||
.runtimes
|
||||
.iter()
|
||||
@@ -439,8 +439,10 @@ impl BackendWorkspaceProductClient {
|
||||
workdir_attachments: Vec::new(),
|
||||
control_operation_id: None,
|
||||
};
|
||||
let response: BrowserCreateWorkerResponse =
|
||||
self.send_json(Method::POST, "/workers", Some(&request))?;
|
||||
let workspace_id = self.workspace_id.clone();
|
||||
let response: BrowserCreateWorkerResponse = self.generated(move |client| async move {
|
||||
client.workspace_worker_create(workspace_id, request).await
|
||||
})?;
|
||||
Ok(format!(
|
||||
"Started Intake Worker {}/{} for Ticket {ticket_id}",
|
||||
response.runtime_id, response.worker_id
|
||||
@@ -448,8 +450,11 @@ impl BackendWorkspaceProductClient {
|
||||
}
|
||||
|
||||
pub fn start_workspace_orchestrator(&self) -> Result<String, BackendWorkspaceClientError> {
|
||||
let workspace_id = self.workspace_id.clone();
|
||||
let response: BrowserWorkspaceOrchestratorResponse =
|
||||
self.send_json::<(), _>(Method::POST, "/orchestrator", None)?;
|
||||
self.generated(move |client| async move {
|
||||
client.workspace_orchestrator_start(workspace_id).await
|
||||
})?;
|
||||
let worker = response.worker.ok_or_else(|| {
|
||||
BackendWorkspaceClientError::InvalidTarget(
|
||||
"Backend accepted the Orchestrator request without returning a Worker".to_string(),
|
||||
@@ -464,39 +469,6 @@ impl BackendWorkspaceProductClient {
|
||||
pub fn default_product_list_limit() -> usize {
|
||||
DEFAULT_PRODUCT_LIST_LIMIT
|
||||
}
|
||||
|
||||
fn get_json<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 {
|
||||
@@ -785,6 +757,7 @@ fn ticket_list_state_query(query: &TicketListQuery) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn encode_path_segment(value: &str) -> String {
|
||||
let mut encoded = String::with_capacity(value.len());
|
||||
for byte in value.bytes() {
|
||||
|
||||
@@ -126,6 +126,7 @@ pub struct WorkerCommandAcknowledgement {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "kind", content = "state", rename_all = "snake_case")]
|
||||
pub enum WorkerState {
|
||||
Idle,
|
||||
@@ -134,6 +135,7 @@ pub enum WorkerState {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "kind", content = "state", rename_all = "snake_case")]
|
||||
pub enum WorkerBusyState {
|
||||
Run(WorkerRunState),
|
||||
@@ -142,6 +144,7 @@ pub enum WorkerBusyState {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkerRunState {
|
||||
Running,
|
||||
@@ -152,6 +155,7 @@ pub enum WorkerRunState {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkerMaintenanceState {
|
||||
Compacting,
|
||||
@@ -159,8 +163,13 @@ pub enum WorkerMaintenanceState {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct WorkerStateSnapshot {
|
||||
/// Highest lifecycle command id observed by this controller instance.
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 9_007_199_254_740_991_u64))
|
||||
)]
|
||||
pub last_command_id: u64,
|
||||
pub state: WorkerState,
|
||||
}
|
||||
@@ -453,13 +462,29 @@ impl PasteArtifactAvailability {
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct PasteArtifactRef {
|
||||
pub artifact_id: String,
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 9_007_199_254_740_991_u64))
|
||||
)]
|
||||
pub created_at_ms: u64,
|
||||
pub media_type: PasteArtifactMediaType,
|
||||
/// Availability observed when this immutable reference was committed.
|
||||
/// Reads revalidate storage and integrity rather than trusting this field.
|
||||
pub availability: PasteArtifactAvailability,
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 9_007_199_254_740_991_u64))
|
||||
)]
|
||||
pub byte_len: u64,
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 9_007_199_254_740_991_u64))
|
||||
)]
|
||||
pub char_count: u64,
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 9_007_199_254_740_991_u64))
|
||||
)]
|
||||
pub line_count: u64,
|
||||
pub sha256: String,
|
||||
pub source_entry_id: String,
|
||||
@@ -499,8 +524,16 @@ pub struct UploadedFileRef {
|
||||
pub artifact_id: String,
|
||||
pub file_name: String,
|
||||
pub media_type: String,
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 9_007_199_254_740_991_u64))
|
||||
)]
|
||||
pub created_at_ms: u64,
|
||||
pub availability: UploadedFileAvailability,
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 9_007_199_254_740_991_u64))
|
||||
)]
|
||||
pub byte_len: u64,
|
||||
pub sha256: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -519,8 +552,20 @@ pub enum Segment {
|
||||
/// `[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`
|
||||
/// re-broadcast.
|
||||
Paste {
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 4_294_967_295_u32))
|
||||
)]
|
||||
id: u32,
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 4_294_967_295_u32))
|
||||
)]
|
||||
chars: u32,
|
||||
#[cfg_attr(
|
||||
feature = "json-schema",
|
||||
schemars(range(min = 0, max = 4_294_967_295_u32))
|
||||
)]
|
||||
lines: u32,
|
||||
content: String,
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ typescript = ["dep:ts-rs", "protocol/typescript"]
|
||||
api-macros = { workspace = true, features = ["reqwest", "axum", "openapi"] }
|
||||
memory.workspace = true
|
||||
merge-request.workspace = true
|
||||
protocol.workspace = true
|
||||
protocol = { workspace = true, features = ["json-schema"] }
|
||||
runtime-api.workspace = true
|
||||
schemars.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -36,11 +36,9 @@ name = "generate_workdir_api_types"
|
||||
|
||||
[[example]]
|
||||
name = "generate_worker_launch_api_types"
|
||||
required-features = ["typescript"]
|
||||
|
||||
[[example]]
|
||||
name = "generate_companion_api_types"
|
||||
required-features = ["typescript"]
|
||||
|
||||
[[example]]
|
||||
name = "generate_memory_api_types"
|
||||
|
||||
@@ -1,6 +1,55 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use server_api::repository_openapi_typescript::{
|
||||
COMPANION_OUTPUT_PATH, INPUT_PATH, generate_companion_typescript,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
print!(
|
||||
"{}",
|
||||
server_api::normalized_typescript(server_api::companion_api_typescript())
|
||||
);
|
||||
if let Err(error) = run() {
|
||||
eprintln!("{error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let mut check = false;
|
||||
for argument in std::env::args().skip(1) {
|
||||
match argument.as_str() {
|
||||
"--check" => check = true,
|
||||
"--help" | "-h" => {
|
||||
println!(
|
||||
"Generate the bounded Companion API TypeScript artifact.\n\nUsage: cargo run -q -p server-api --example generate_companion_api_types -- [--check]"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
other => return Err(format!("unknown argument `{other}`")),
|
||||
}
|
||||
}
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let input_path = root.join(INPUT_PATH);
|
||||
let output_path = root.join(COMPANION_OUTPUT_PATH);
|
||||
let input = fs::read_to_string(&input_path)
|
||||
.map_err(|error| format!("failed to read {}: {error}", input_path.display()))?;
|
||||
let generated = generate_companion_typescript(&input)
|
||||
.map_err(|error| format!("failed to generate Companion API types: {error}"))?;
|
||||
if check {
|
||||
let checked_in = fs::read_to_string(&output_path)
|
||||
.map_err(|error| format!("failed to read {}: {error}", output_path.display()))?;
|
||||
if checked_in != generated {
|
||||
return Err(format!(
|
||||
"{} is stale; regenerate it without --check",
|
||||
output_path.display()
|
||||
));
|
||||
}
|
||||
println!("{} is current", output_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
let temporary_path = output_path.with_extension("ts.tmp");
|
||||
fs::write(&temporary_path, generated)
|
||||
.map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?;
|
||||
fs::rename(&temporary_path, &output_path)
|
||||
.map_err(|error| format!("failed to replace {}: {error}", output_path.display()))?;
|
||||
println!("generated {}", output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,60 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use server_api::repository_openapi_typescript::{
|
||||
INPUT_PATH, WORKER_OUTPUT_PATH, generate_worker_typescript,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
print!(
|
||||
"{}",
|
||||
server_api::normalized_typescript(server_api::worker_launch_api_typescript())
|
||||
);
|
||||
if let Err(error) = run() {
|
||||
eprintln!("{error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let check = parse_check_argument()?;
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let input_path = root.join(INPUT_PATH);
|
||||
let output_path = root.join(WORKER_OUTPUT_PATH);
|
||||
let input = fs::read_to_string(&input_path)
|
||||
.map_err(|error| format!("failed to read {}: {error}", input_path.display()))?;
|
||||
let generated = generate_worker_typescript(&input)
|
||||
.map_err(|error| format!("failed to generate Worker API types: {error}"))?;
|
||||
if check {
|
||||
let checked_in = fs::read_to_string(&output_path)
|
||||
.map_err(|error| format!("failed to read {}: {error}", output_path.display()))?;
|
||||
if checked_in != generated {
|
||||
return Err(format!(
|
||||
"{} is stale; regenerate it without --check",
|
||||
output_path.display()
|
||||
));
|
||||
}
|
||||
println!("{} is current", output_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
let temporary_path = output_path.with_extension("ts.tmp");
|
||||
fs::write(&temporary_path, generated)
|
||||
.map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?;
|
||||
fs::rename(&temporary_path, &output_path)
|
||||
.map_err(|error| format!("failed to replace {}: {error}", output_path.display()))?;
|
||||
println!("generated {}", output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_check_argument() -> Result<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)
|
||||
}
|
||||
|
||||
+803
-69
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";
|
||||
pub const RUNTIME_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/runtime-api.ts";
|
||||
|
||||
pub const WORKER_GENERATOR_NAME: &str = "yoi-worker-openapi-typescript";
|
||||
pub const WORKER_GENERATOR_VERSION: &str = "1";
|
||||
pub const WORKER_GENERATOR_OPTIONS: &str =
|
||||
"worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
||||
pub const WORKER_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/worker-launch-api.ts";
|
||||
pub const COMPANION_GENERATOR_NAME: &str = "yoi-companion-openapi-typescript";
|
||||
pub const COMPANION_GENERATOR_VERSION: &str = "1";
|
||||
pub const COMPANION_GENERATOR_OPTIONS: &str =
|
||||
"companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
||||
pub const COMPANION_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/companion-api.ts";
|
||||
|
||||
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
|
||||
const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0;
|
||||
const SCHEMA_REF_PREFIX: &str = "#/components/schemas/";
|
||||
@@ -166,6 +177,82 @@ pub fn generate_runtime_typescript(input: &str) -> Result<String, GenerationErro
|
||||
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> {
|
||||
let Some(hex) = digest.strip_prefix("sha256:") else {
|
||||
return Err(GenerationError::invalid(
|
||||
@@ -180,6 +267,141 @@ fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_worker_operations(
|
||||
paths: &Map<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(
|
||||
paths: &Map<String, Value>,
|
||||
) -> 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)> {
|
||||
let schema = value.as_object()?;
|
||||
if schema.get("type")?.as_str()? != "object"
|
||||
|| schema.get("additionalProperties") != Some(&Value::Bool(false))
|
||||
{
|
||||
if schema.get("type")?.as_str()? != "object" {
|
||||
return None;
|
||||
}
|
||||
let required = schema.get("required")?.as_array()?;
|
||||
@@ -1447,7 +1667,9 @@ mod tests {
|
||||
)));
|
||||
assert!(output.contains("export type RuntimeCleanupPlanResponse ="));
|
||||
assert!(output.contains("export type RuntimeWorkerLifecycleResult ="));
|
||||
assert!(output.contains("export type RuntimeWorkerSpawnAcceptanceRequirement = \"socket_ready\" | { run_accepted:"));
|
||||
assert!(output.contains(
|
||||
"export type RuntimeWorkerSpawnAcceptanceRequirement = { kind: \"socket_ready\"; } | { expected_segments: number; kind: \"run_accepted\"; };"
|
||||
));
|
||||
assert!(!output.contains("export type UnmigratedDomain ="));
|
||||
|
||||
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::auth::BACKEND_RESOURCE_FETCH_PERMISSION;
|
||||
use crate::identity::WorkerId;
|
||||
use crate::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveRef, sha256_hex};
|
||||
use crate::workspace_request::{RuntimeWorkspaceRequest, RuntimeWorkspaceRequestClient};
|
||||
use crate::workspace_request::RuntimeWorkspaceRequestClient;
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -219,11 +219,6 @@ impl BackendResourceClient for HttpBackendResourceClient {
|
||||
&self,
|
||||
request: BackendResourceFetchRequest,
|
||||
) -> Result<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| {
|
||||
BackendResourceError::Transport {
|
||||
message: error.to_string(),
|
||||
@@ -248,17 +243,17 @@ impl BackendResourceClient for HttpBackendResourceClient {
|
||||
.to_string(),
|
||||
}
|
||||
})?;
|
||||
if !endpoint_suffix.starts_with('/') {
|
||||
let expected_path = format!(
|
||||
"/api/runtime/v1/workspaces/{}/resources/fetch",
|
||||
client.workspace_id()
|
||||
);
|
||||
if endpoint_suffix != expected_path {
|
||||
return Err(BackendResourceError::Unauthorized {
|
||||
message: "Workspace resource endpoint does not match its request client"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
if let Some(token) = self.bearer_token.as_deref() {
|
||||
let value = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
|
||||
.map_err(|error| BackendResourceError::Transport {
|
||||
@@ -266,41 +261,57 @@ impl BackendResourceClient for HttpBackendResourceClient {
|
||||
})?;
|
||||
headers.insert(reqwest::header::AUTHORIZATION, value);
|
||||
}
|
||||
let response = client
|
||||
.execute(RuntimeWorkspaceRequest {
|
||||
method: reqwest::Method::POST,
|
||||
path_and_query: endpoint_suffix.to_string(),
|
||||
body,
|
||||
let request: server_api::RuntimeResourceFetchRequest =
|
||||
serde_json::from_value(serde_json::to_value(&request).map_err(|error| {
|
||||
BackendResourceError::InvalidResponse {
|
||||
message: error.to_string(),
|
||||
}
|
||||
})?)
|
||||
.map_err(|error| BackendResourceError::InvalidResponse {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let generated = client
|
||||
.server_api_client(
|
||||
BACKEND_RESOURCE_FETCH_PERMISSION,
|
||||
None,
|
||||
headers,
|
||||
permission: BACKEND_RESOURCE_FETCH_PERMISSION.to_string(),
|
||||
worker_id: None,
|
||||
timeout: Some(self.request_timeout),
|
||||
max_response_bytes: 8 * 1024 * 1024,
|
||||
})
|
||||
self.request_timeout,
|
||||
8 * 1024 * 1024,
|
||||
)
|
||||
.map_err(|error| BackendResourceError::Transport {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
match generated
|
||||
.runtime_resource_fetch(client.workspace_id().to_string(), request)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
if error.is_timeout() {
|
||||
BackendResourceError::Timeout
|
||||
} else {
|
||||
BackendResourceError::Transport {
|
||||
{
|
||||
Ok(response) => {
|
||||
serde_json::from_value(serde_json::to_value(&response).map_err(|error| {
|
||||
BackendResourceError::InvalidResponse {
|
||||
message: error.to_string(),
|
||||
}
|
||||
}
|
||||
})?;
|
||||
if response.status.is_success() {
|
||||
serde_json::from_slice::<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}"),
|
||||
}),
|
||||
})?)
|
||||
.map_err(|error| BackendResourceError::InvalidResponse {
|
||||
message: error.to_string(),
|
||||
})
|
||||
}
|
||||
Err(server_api::client_support::ClientError::Public { error, .. }) => {
|
||||
serde_json::from_value(serde_json::to_value(error).map_err(|error| {
|
||||
BackendResourceError::InvalidResponse {
|
||||
message: error.to_string(),
|
||||
}
|
||||
})?)
|
||||
.map_err(|error| BackendResourceError::InvalidResponse {
|
||||
message: error.to_string(),
|
||||
})
|
||||
.and_then(Err)
|
||||
}
|
||||
Err(server_api::client_support::ClientError::Failure(
|
||||
server_api::client_support::ClientFailure::Timeout,
|
||||
)) => Err(BackendResourceError::Timeout),
|
||||
Err(error) => Err(BackendResourceError::Transport {
|
||||
message: error.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::workspace_request::{RuntimeWorkspaceRequest, RuntimeWorkspaceRequestC
|
||||
use worker::{
|
||||
WorkspaceClient, WorkspaceClientError, WorkspacePromptCatalogResolution,
|
||||
WorkspacePromptProjection, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse,
|
||||
WorkspaceServerOperation,
|
||||
};
|
||||
|
||||
use crate::auth::{
|
||||
@@ -254,45 +255,53 @@ fn execute_remote_worker_remove_http(
|
||||
fn execute_remote_worker_remove_http_blocking(
|
||||
request: RemoteWorkerRemoveHttpRequest,
|
||||
) -> Result<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();
|
||||
headers.insert(
|
||||
crate::auth::WORKER_MUTATION_SOURCE_PROOF_HEADER,
|
||||
reqwest::header::HeaderValue::from_str(&request.token)
|
||||
.map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?,
|
||||
);
|
||||
headers.insert(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
let response = request
|
||||
let client = request
|
||||
.request_client
|
||||
.execute_blocking(RuntimeWorkspaceRequest {
|
||||
method: reqwest::Method::POST,
|
||||
path_and_query: path,
|
||||
body,
|
||||
.server_api_client(
|
||||
WORKSPACE_REQUEST_PERMISSION,
|
||||
Some(request.source_worker_id),
|
||||
headers,
|
||||
permission: WORKSPACE_REQUEST_PERMISSION.to_string(),
|
||||
worker_id: Some(request.source_worker_id),
|
||||
timeout: Some(Duration::from_secs(5)),
|
||||
max_response_bytes: 8 * 1024 * 1024,
|
||||
})
|
||||
Duration::from_secs(5),
|
||||
8 * 1024 * 1024,
|
||||
)
|
||||
.map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?;
|
||||
let body = String::from_utf8(response.body)
|
||||
let workspace_id = request.request_client.workspace_id().to_string();
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?;
|
||||
Ok(WorkspaceResponse {
|
||||
status: response.status.as_u16(),
|
||||
body,
|
||||
})
|
||||
let result = runtime.block_on(client.workspace_worker_remove(
|
||||
workspace_id,
|
||||
server_api::WorkerRemoveRequest {
|
||||
target_runtime_id: request.target_runtime_id,
|
||||
target_worker_id: request.target_worker_id,
|
||||
reason: request.reason,
|
||||
},
|
||||
));
|
||||
match result {
|
||||
Ok(response) => Ok(WorkspaceResponse {
|
||||
status: 200,
|
||||
body: serde_json::to_string(&response)
|
||||
.map_err(|error| RuntimeWorkerMutationForwardError::Transport(error.to_string()))?,
|
||||
}),
|
||||
Err(server_api::client_support::ClientError::Public { status, error }) => {
|
||||
Ok(WorkspaceResponse {
|
||||
status: status.as_u16(),
|
||||
body: serde_json::to_string(&error).map_err(|error| {
|
||||
RuntimeWorkerMutationForwardError::Transport(error.to_string())
|
||||
})?,
|
||||
})
|
||||
}
|
||||
Err(error) => Err(RuntimeWorkerMutationForwardError::Transport(
|
||||
error.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -432,6 +441,26 @@ impl std::fmt::Debug for RuntimeOwnedWorkspaceClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn generated_workspace_response<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 {
|
||||
fn workspace_id(&self) -> Option<&str> {
|
||||
Some(self.request_client.workspace_id())
|
||||
@@ -452,36 +481,143 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
|
||||
self.execute_with_permission(request, WORKSPACE_REQUEST_PERMISSION)
|
||||
}
|
||||
|
||||
fn execute_server_operation(
|
||||
&self,
|
||||
operation: WorkspaceServerOperation,
|
||||
) -> Result<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(
|
||||
&self,
|
||||
request: worker::WorkspaceWorkerDiscoveryRequest,
|
||||
) -> Result<server_api::WorkspaceWorkerDiscoveryPage, WorkspaceClientError> {
|
||||
let mut path = format!(
|
||||
"/api/w/{}/worker-discovery/workers?limit={}",
|
||||
self.workspace_id, request.limit
|
||||
);
|
||||
if let Some(cursor) = request.cursor.as_deref() {
|
||||
path.push_str("&cursor=");
|
||||
path.push_str(&percent_encode_query(cursor));
|
||||
}
|
||||
if let Some(query) = request.query.as_deref() {
|
||||
path.push_str("&query=");
|
||||
path.push_str(&percent_encode_query(query));
|
||||
}
|
||||
let response = self.execute_with_permission(
|
||||
WorkspaceRequest::get(path),
|
||||
WORKSPACE_WORKER_DISCOVERY_PERMISSION,
|
||||
)?;
|
||||
if !(200..300).contains(&response.status) {
|
||||
return Err(WorkspaceClientError::Request(format!(
|
||||
"Workspace Worker discovery failed with HTTP {}: {}",
|
||||
response.status, response.body
|
||||
)));
|
||||
}
|
||||
serde_json::from_str(&response.body).map_err(|error| {
|
||||
WorkspaceClientError::Request(format!(
|
||||
"invalid Workspace Worker discovery response: {error}"
|
||||
))
|
||||
let request_client = self.request_client.clone();
|
||||
let workspace_id = self.workspace_id.clone();
|
||||
let worker_id = self.worker_id.clone();
|
||||
let timeout = self.request_timeout.unwrap_or(Duration::from_secs(5));
|
||||
let query = server_api::WorkspaceWorkerDiscoveryQuery {
|
||||
cursor: request.cursor,
|
||||
limit: Some(request.limit),
|
||||
query: request.query,
|
||||
};
|
||||
let result = std::thread::Builder::new()
|
||||
.name("yoi-worker-discovery-http".to_string())
|
||||
.spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let client = request_client
|
||||
.server_api_client(
|
||||
WORKSPACE_WORKER_DISCOVERY_PERMISSION,
|
||||
Some(worker_id),
|
||||
reqwest::header::HeaderMap::new(),
|
||||
timeout,
|
||||
8 * 1024 * 1024,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
runtime
|
||||
.block_on(client.workspace_worker_discovery(workspace_id, query))
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
.map_err(|error| WorkspaceClientError::Request(error.to_string()))?
|
||||
.join()
|
||||
.map_err(|_| {
|
||||
WorkspaceClientError::Request(
|
||||
"Workspace Worker discovery HTTP thread panicked".to_string(),
|
||||
)
|
||||
})?;
|
||||
result.map_err(|error| {
|
||||
WorkspaceClientError::Request(format!("Workspace Worker discovery failed: {error}"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -587,19 +723,6 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn percent_encode_query(value: &str) -> String {
|
||||
let mut encoded = String::with_capacity(value.len());
|
||||
for byte in value.bytes() {
|
||||
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
|
||||
encoded.push(char::from(byte));
|
||||
} else {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(encoded, "%{byte:02X}");
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RuntimeWorkerMutationForwardError {
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -62,9 +62,57 @@ impl RuntimeWorkspaceRequestError {
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_timeout(&self) -> bool {
|
||||
matches!(self, Self::Transport { timeout: true, .. })
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RuntimeWorkspaceRequestAuthorizer {
|
||||
workspace_id: String,
|
||||
runtime_id: String,
|
||||
request_source: Option<(RuntimeRequestSourceSigner, String)>,
|
||||
permission: String,
|
||||
worker_id: Option<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('/')
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
request: RuntimeWorkspaceRequest,
|
||||
|
||||
@@ -19,10 +19,7 @@ use crate::feature::{
|
||||
ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration,
|
||||
};
|
||||
use crate::spawn::registry::{SpawnedWorkerRegistry, SubWorkerStopSummary};
|
||||
use crate::worker::{
|
||||
WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod,
|
||||
WorkspaceResponse,
|
||||
};
|
||||
use crate::worker::{WorkspaceClient, WorkspaceClientError, WorkspaceResponse};
|
||||
|
||||
const FEATURE_ID: &str = "worker";
|
||||
const FEATURE_NAME: &str = "Worker";
|
||||
@@ -76,16 +73,22 @@ pub trait WorkerControlService: Send + Sync {
|
||||
&self,
|
||||
request: WorkerLifecycleSpawnRequest,
|
||||
) -> Result<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(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
worker_id: &str,
|
||||
reason: &str,
|
||||
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
||||
async fn execute_runtime(
|
||||
&self,
|
||||
request: WorkspaceRequest,
|
||||
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
||||
async fn ensure_permission(
|
||||
&self,
|
||||
subject: &super::worker_observation::WorkerObservationSubjectRef,
|
||||
@@ -144,11 +147,9 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
||||
|
||||
async fn list_workers(&self) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||
let response = if self.runtime_worker_control {
|
||||
self.execute_runtime(WorkspaceRequest::get(format!(
|
||||
"/api/w/{}/worker-control/workers",
|
||||
self.workspace_id
|
||||
)))
|
||||
.await?
|
||||
self.client.execute_server_operation(
|
||||
crate::worker::WorkspaceServerOperation::WorkerControlList,
|
||||
)?
|
||||
} else {
|
||||
WorkspaceResponse {
|
||||
status: 200,
|
||||
@@ -210,19 +211,17 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
||||
WorkerControlInputKind::User => "user",
|
||||
WorkerControlInputKind::Notify => "notify",
|
||||
};
|
||||
self.execute_runtime(WorkspaceRequest::json(
|
||||
WorkspaceRequestMethod::Post,
|
||||
format!(
|
||||
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/input",
|
||||
self.workspace_id
|
||||
),
|
||||
serde_json::json!({
|
||||
"kind": kind,
|
||||
"content": content,
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.await
|
||||
self.client.execute_server_operation(
|
||||
crate::worker::WorkspaceServerOperation::WorkerControlInput {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
request: server_api::RuntimeWorkerInputRequest {
|
||||
kind: Some(kind.to_string()),
|
||||
content,
|
||||
segments: None,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
WorkerControlSubject::SubWorker { name } => {
|
||||
if kind != WorkerControlInputKind::User {
|
||||
@@ -273,19 +272,29 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
} => {
|
||||
let action = match kind {
|
||||
WorkerControlStopKind::Cancel => "cancel",
|
||||
WorkerControlStopKind::Stop => "stop",
|
||||
let operation = match kind {
|
||||
WorkerControlStopKind::Cancel => {
|
||||
crate::worker::WorkspaceServerOperation::WorkerControlCancel {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
request: server_api::RuntimeWorkerLifecycleRequest {
|
||||
reason,
|
||||
ticket_assignment: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
WorkerControlStopKind::Stop => {
|
||||
crate::worker::WorkspaceServerOperation::WorkerControlStop {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
request: server_api::RuntimeWorkerLifecycleRequest {
|
||||
reason,
|
||||
ticket_assignment: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
self.execute_runtime(WorkspaceRequest::json(
|
||||
WorkspaceRequestMethod::Post,
|
||||
format!(
|
||||
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/{action}",
|
||||
self.workspace_id
|
||||
),
|
||||
serde_json::json!({ "reason": reason }).to_string(),
|
||||
))
|
||||
.await
|
||||
self.client.execute_server_operation(operation)
|
||||
}
|
||||
WorkerControlSubject::SubWorker { name } => {
|
||||
if kind != WorkerControlStopKind::Stop {
|
||||
@@ -329,12 +338,29 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
||||
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||
WorkspaceWorkerLifecycleService {
|
||||
client: self.client.clone(),
|
||||
workspace_id: self.workspace_id.clone(),
|
||||
}
|
||||
.spawn(request)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn restore_worker(
|
||||
&self,
|
||||
runtime_id: String,
|
||||
worker_id: String,
|
||||
) -> Result<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(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
@@ -350,18 +376,6 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
||||
.execute_worker_remove(runtime_id, worker_id, reason)
|
||||
}
|
||||
|
||||
async fn execute_runtime(
|
||||
&self,
|
||||
request: WorkspaceRequest,
|
||||
) -> Result<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(
|
||||
&self,
|
||||
subject: &super::worker_observation::WorkerObservationSubjectRef,
|
||||
@@ -391,10 +405,9 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
||||
"Runtime Worker control is not enabled for this Worker".to_string(),
|
||||
));
|
||||
}
|
||||
let response = self.client.execute(WorkspaceRequest::get(format!(
|
||||
"/api/w/{}/worker-control/workers",
|
||||
self.workspace_id
|
||||
)))?;
|
||||
let response = self.client.execute_server_operation(
|
||||
crate::worker::WorkspaceServerOperation::WorkerControlList,
|
||||
)?;
|
||||
if !response.is_success() {
|
||||
return Err(WorkspaceClientError::Request(format!(
|
||||
"Workspace control request returned {}: {}",
|
||||
@@ -459,7 +472,6 @@ pub struct WorkerLifecycleSpawnRequest {
|
||||
|
||||
struct WorkspaceWorkerLifecycleService {
|
||||
client: Arc<dyn WorkspaceClient>,
|
||||
workspace_id: String,
|
||||
}
|
||||
|
||||
fn workspace_worker_create_request(
|
||||
@@ -505,12 +517,9 @@ impl WorkerLifecycleService for WorkspaceWorkerLifecycleService {
|
||||
request: WorkerLifecycleSpawnRequest,
|
||||
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||
let body = workspace_worker_create_request(request)?;
|
||||
self.client.execute(WorkspaceRequest::json(
|
||||
WorkspaceRequestMethod::Post,
|
||||
format!("/api/w/{}/worker-control/workers", self.workspace_id),
|
||||
serde_json::to_string(&body)
|
||||
.map_err(|error| WorkspaceClientError::Request(error.to_string()))?,
|
||||
))
|
||||
self.client.execute_server_operation(
|
||||
crate::worker::WorkspaceServerOperation::WorkerControlSpawn(body),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,20 +648,17 @@ impl FeatureModule for ManageWorkerFeature {
|
||||
}
|
||||
|
||||
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
|
||||
let workspace_id = self
|
||||
.client
|
||||
self.client
|
||||
.workspace_id()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| {
|
||||
FeatureInstallError::InvalidDescriptor(
|
||||
"worker feature requires a Workspace id".to_string(),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
})?;
|
||||
let lifecycle: Arc<dyn WorkerLifecycleService> =
|
||||
Arc::new(WorkspaceWorkerLifecycleService {
|
||||
client: self.client.clone(),
|
||||
workspace_id: workspace_id.clone(),
|
||||
});
|
||||
context.services().provide(
|
||||
ServiceDeclaration::new(
|
||||
@@ -948,14 +954,7 @@ impl Tool for WorkspaceWorkerTool {
|
||||
let input = parse::<WorkerTargetInput>(input_json, "WorkerRestore")?;
|
||||
let (runtime_id, worker_id) = runtime_subject_ids(&input.subject, self.operation)?;
|
||||
self.control
|
||||
.execute_runtime(WorkspaceRequest::json(
|
||||
WorkspaceRequestMethod::Post,
|
||||
format!(
|
||||
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/restore",
|
||||
self.control.workspace_id()
|
||||
),
|
||||
"{}",
|
||||
))
|
||||
.restore_worker(runtime_id, worker_id)
|
||||
.await
|
||||
.map_err(control_tool_error)?
|
||||
}
|
||||
@@ -1191,7 +1190,7 @@ mod tests {
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::*;
|
||||
use crate::worker::{WorkspaceClientError, WorkspaceResponse};
|
||||
use crate::worker::{WorkspaceClientError, WorkspaceRequest, WorkspaceResponse};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RecordingWorkspaceClient {
|
||||
@@ -1361,15 +1360,6 @@ mod tests {
|
||||
))
|
||||
}
|
||||
|
||||
async fn execute_runtime(
|
||||
&self,
|
||||
_request: WorkspaceRequest,
|
||||
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||
Err(WorkspaceClientError::Unavailable(
|
||||
"Runtime Worker control is disabled in this test".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn ensure_permission(
|
||||
&self,
|
||||
_subject: &crate::feature::builtin::WorkerObservationSubjectRef,
|
||||
|
||||
@@ -125,17 +125,16 @@ impl WorkerObservationProvider for WorkspaceClientWorkerObservationProvider {
|
||||
async fn list_worker_sessions(
|
||||
&self,
|
||||
) -> 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(
|
||||
"Workspace observation requires a scoped Workspace client".to_string(),
|
||||
)
|
||||
})?;
|
||||
let response = self
|
||||
.client
|
||||
.execute(crate::worker::WorkspaceRequest::get(format!(
|
||||
"/api/w/{}/worker-observation/sessions",
|
||||
workspace_id
|
||||
)))
|
||||
.execute_server_operation(
|
||||
crate::worker::WorkspaceServerOperation::WorkerObservationSessions,
|
||||
)
|
||||
.map_err(workspace_client_error)?;
|
||||
let body = workspace_response_body(response)?;
|
||||
serde_json::from_str::<WorkspaceWorkerObservationListResponse>(&body)
|
||||
@@ -147,20 +146,28 @@ impl WorkerObservationProvider for WorkspaceClientWorkerObservationProvider {
|
||||
&self,
|
||||
subject: &WorkerObservationSubjectRef,
|
||||
) -> Result<WorkerSessionCapture, WorkerObservationError> {
|
||||
let body = serde_json::to_string(subject)
|
||||
.map_err(|error| WorkerObservationError::Unavailable(error.to_string()))?;
|
||||
let workspace_id = self.client.workspace_id().ok_or_else(|| {
|
||||
let request = match subject {
|
||||
WorkerObservationSubjectRef::RuntimeWorker {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
} => server_api::WorkerObservationSubjectRef::RuntimeWorker {
|
||||
runtime_id: runtime_id.clone(),
|
||||
worker_id: worker_id.clone(),
|
||||
},
|
||||
WorkerObservationSubjectRef::SubWorker { name } => {
|
||||
server_api::WorkerObservationSubjectRef::SubWorker { name: name.clone() }
|
||||
}
|
||||
};
|
||||
let _workspace_id = self.client.workspace_id().ok_or_else(|| {
|
||||
WorkerObservationError::Unavailable(
|
||||
"Workspace observation requires a scoped Workspace client".to_string(),
|
||||
)
|
||||
})?;
|
||||
let response = self
|
||||
.client
|
||||
.execute(crate::worker::WorkspaceRequest::json(
|
||||
crate::worker::WorkspaceRequestMethod::Post,
|
||||
format!("/api/w/{}/worker-observation/session", workspace_id),
|
||||
body,
|
||||
))
|
||||
.execute_server_operation(
|
||||
crate::worker::WorkspaceServerOperation::WorkerObservationCapture(request),
|
||||
)
|
||||
.map_err(workspace_client_error)?;
|
||||
let body = workspace_response_body(response)?;
|
||||
let response = serde_json::from_str::<WorkspaceWorkerObservationCaptureResponse>(&body)
|
||||
|
||||
@@ -60,6 +60,6 @@ pub use worker::{
|
||||
LocalWorkingDirectory, Worker, WorkerError, WorkerFilesystemAuthority, WorkerRunResult,
|
||||
WorkerWorkspaceContext, WorkspaceClient, WorkspaceClientError, WorkspaceId, WorkspaceIdError,
|
||||
WorkspacePromptCatalogResolution, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse,
|
||||
WorkspaceWorkerDiscoveryRequest, apply_worker_manifest, marker_workspace_client,
|
||||
unavailable_workspace_client,
|
||||
WorkspaceServerOperation, WorkspaceWorkerDiscoveryRequest, apply_worker_manifest,
|
||||
marker_workspace_client, unavailable_workspace_client,
|
||||
};
|
||||
|
||||
@@ -544,6 +544,98 @@ pub struct WorkspaceWorkerDiscoveryRequest {
|
||||
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.
|
||||
///
|
||||
/// Workers receive this trait object rather than a Backend URL. The concrete
|
||||
@@ -556,6 +648,18 @@ pub trait WorkspaceClient: std::fmt::Debug + Send + Sync {
|
||||
fn execute(&self, request: WorkspaceRequest)
|
||||
-> Result<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
|
||||
/// proof. Implementations must not fall back to generic Workspace request
|
||||
/// authority.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+4524
-20
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`.
|
||||
// Do not edit manually.
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
|
||||
|
||||
export type WorkspaceWorkerSubject = { "kind": "runtime_worker", runtime_id: string, worker_id: string, };
|
||||
|
||||
export type WorkspaceWorkerDiscoveryItem = { subject: WorkspaceWorkerSubject, resource_key: string, display_name: string, profile: string | null, status?: string | null, };
|
||||
export type CompanionCancelRequest = { reason?: string | null; };
|
||||
|
||||
export type CompanionLifecycleState = "idle" | "running" | "stopped";
|
||||
|
||||
export type CompanionMessageDisposition = "accepted" | "rejected";
|
||||
|
||||
export type CompanionTransportSummary = { mode: string, available: boolean, };
|
||||
export type CompanionMessageRequest = { content: string; };
|
||||
|
||||
export type CompanionStatusResponse = { state: CompanionLifecycleState, worker: WorkspaceWorkerDiscoveryItem | null, transport: CompanionTransportSummary, diagnostics: Array<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 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.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
@@ -55,11 +55,21 @@ export type RepositorySshHostTrust = { created_at: string; current_revision: num
|
||||
|
||||
export type RepositorySummary = { default_selector?: string | null; diagnostics?: Array<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 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; };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||
@@ -105,11 +105,11 @@ export type RuntimeWorkerLifecycleResult = { diagnostics?: Array<Diagnostic>; ru
|
||||
|
||||
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 RuntimeWorkerSpawnAcceptanceRequirement = "socket_ready" | { run_accepted: { expected_segments: number; }; };
|
||||
export type RuntimeWorkerSpawnAcceptanceRequirement = { kind: "socket_ready"; } | { expected_segments: number; kind: "run_accepted"; };
|
||||
|
||||
export type RuntimeWorkerSpawnIntent = { kind: "workspace_companion"; } | { kind: "workspace_orchestrator"; } | { kind: "workspace_coding"; } | { kind: "ticket_role"; role: RuntimeTicketWorkerRole; ticket_id: string; };
|
||||
|
||||
@@ -127,12 +127,16 @@ export type RuntimeWorkerTicketAssignmentRequest = { operation_id: string; ticke
|
||||
|
||||
export type UpdateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; };
|
||||
|
||||
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
|
||||
|
||||
export type WorkerFileDeleteResponse = { deleted: boolean; };
|
||||
|
||||
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
|
||||
|
||||
export type WorkerMaintenanceState = "compacting";
|
||||
|
||||
export type WorkerOperationState = "accepted" | "unsupported" | "rejected";
|
||||
|
||||
export type WorkerRestoreResponse = { result: WorkerRestoreResult; runtime_id: string; worker_id: string; workspace_id: string; };
|
||||
@@ -143,7 +147,13 @@ export type WorkerRestoreState = "accepted" | "rejected" | "rolled_back" | "reco
|
||||
|
||||
export type WorkerRetentionResponse = { pinned: boolean; retention_state: string; runtime_id: string; worker_id: string; workspace_id: string; };
|
||||
|
||||
export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<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; };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/workdir-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
|
||||
|
||||
@@ -1,68 +1,84 @@
|
||||
// Generated from server-api. Do not edit by hand.
|
||||
// Regenerate: cargo run -q -p server-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
|
||||
import type { Segment } from "./protocol";
|
||||
export type BrowserCreateWorkerResponse = { console_href: string; diagnostics: Array<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 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 WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; };
|
||||
|
||||
export type WorkingDirectoryRepositoryOption = { default_selector?: string | null; repository_key: string; };
|
||||
|
||||
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; kind: "external_grant"; };
|
||||
|
||||
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
|
||||
|
||||
export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_key: string, };
|
||||
|
||||
export type RuntimeWorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_id: string, };
|
||||
|
||||
export type RuntimeWorkingDirectorySummary = { working_directory_id: string,
|
||||
/**
|
||||
* Optional human-facing label, never a routing key.
|
||||
*/
|
||||
display_name?: string | null, repository_id: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: RuntimeWorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
|
||||
export type WorkingDirectoryOccupancy = { runtime_id: string, worker_id: string, display_name: string, linked_at: string, };
|
||||
|
||||
export type WorkingDirectorySource = { "kind": "repository", repository_key: string, } | { "kind": "external_grant", grant_id: string, };
|
||||
|
||||
export type WorkingDirectorySummary = { working_directory_id: string,
|
||||
/**
|
||||
* Optional human-facing label, never a routing key.
|
||||
*/
|
||||
display_name?: string | null, source: WorkingDirectorySource, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
|
||||
export type WorkerWorkspaceSummary = { visibility: string, identity: string, workspace_id?: string | null, };
|
||||
|
||||
export type WorkerImplementationSummary = { kind: string, display_hint: string, };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_stop: boolean, can_spawn_followup: boolean, };
|
||||
|
||||
export type RuntimeWorkerWorkdirAttachmentSummary = { alias: string, working_directory: RuntimeWorkingDirectorySummary, };
|
||||
|
||||
export type WorkerLaunchWorkerSummary = { runtime_id: string, worker_id: string, host_id: string, display_name: string, label: string, profile: string | null, singleton_key: string | null, tags: Array<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>, };
|
||||
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; };
|
||||
|
||||
@@ -6,15 +6,20 @@ import type {
|
||||
CreateWorkspaceWorkerTicketAssignmentRequest,
|
||||
Diagnostic,
|
||||
DiagnosticSeverity,
|
||||
RepositoryApiError,
|
||||
RuntimeWorkerWorkdirAttachmentSummary,
|
||||
RuntimeWorkingDirectoryCleanupTarget,
|
||||
RuntimeWorkingDirectorySummary,
|
||||
RuntimeWorkerWorkdirAttachmentSummary,
|
||||
WorkerCapabilitySummary,
|
||||
WorkerImplementationSummary,
|
||||
WorkerLaunchOptionsResponse,
|
||||
WorkerLaunchProfileCandidate,
|
||||
WorkerLaunchRuntimeOption,
|
||||
WorkerLaunchWorkerSummary,
|
||||
WorkerListResponse,
|
||||
WorkerStateSnapshot,
|
||||
WorkerSummary,
|
||||
WorkerWorkdirAttachmentSummary,
|
||||
WorkerWorkspaceSummary,
|
||||
WorkingDirectoryRepositoryOption,
|
||||
} from "$lib/generated/worker-launch-api";
|
||||
@@ -45,8 +50,14 @@ function exact(
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_STRING_LENGTH = 65_536;
|
||||
const MAX_COLLECTION_LENGTH = 4_096;
|
||||
|
||||
function string(value: unknown, label: string): string {
|
||||
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
||||
if (value.length > MAX_STRING_LENGTH) {
|
||||
throw new Error(`${label} exceeds its length limit`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -56,8 +67,8 @@ function boolean(value: unknown, label: string): boolean {
|
||||
}
|
||||
|
||||
function number(value: unknown, label: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new Error(`${label} must be a finite number`);
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
||||
throw new Error(`${label} must be a safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -72,6 +83,9 @@ function array<T>(
|
||||
parse: (item: unknown, label: string) => T,
|
||||
): T[] {
|
||||
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
||||
if (value.length > MAX_COLLECTION_LENGTH) {
|
||||
throw new Error(`${label} exceeds its item limit`);
|
||||
}
|
||||
return value.map((item, index) => parse(item, `${label}[${index}]`));
|
||||
}
|
||||
|
||||
@@ -165,6 +179,18 @@ function repositoryOption(
|
||||
};
|
||||
}
|
||||
|
||||
export function parseWorkerApiError(value: unknown): RepositoryApiError {
|
||||
const item = record(value, "Worker API error");
|
||||
exact(item, ["error", "message", "diagnostics"], "Worker API error");
|
||||
return {
|
||||
error: string(item.error, "error"),
|
||||
message: string(item.message, "message"),
|
||||
diagnostics: item.diagnostics === undefined
|
||||
? []
|
||||
: array(item.diagnostics, "diagnostics", diagnostic),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseWorkerLaunchOptionsResponse(
|
||||
value: unknown,
|
||||
): WorkerLaunchOptionsResponse {
|
||||
@@ -237,6 +263,181 @@ function capabilitySummary(
|
||||
};
|
||||
}
|
||||
|
||||
function workerStateSnapshot(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): WorkerStateSnapshot {
|
||||
const snapshot = record(value, label);
|
||||
exact(snapshot, ["last_command_id", "state"], label);
|
||||
const state = record(snapshot.state, `${label}.state`);
|
||||
const kind = string(state.kind, `${label}.state.kind`);
|
||||
if (kind === "idle") {
|
||||
exact(state, ["kind"], `${label}.state`);
|
||||
return {
|
||||
last_command_id: unsignedInteger(
|
||||
snapshot.last_command_id,
|
||||
`${label}.last_command_id`,
|
||||
),
|
||||
state: { kind: "idle" },
|
||||
};
|
||||
}
|
||||
if (kind !== "busy") throw new Error(`${label}.state.kind is invalid`);
|
||||
exact(state, ["kind", "state"], `${label}.state`);
|
||||
const busy = record(state.state, `${label}.state.state`);
|
||||
exact(busy, ["kind", "state"], `${label}.state.state`);
|
||||
const busyKind = string(busy.kind, `${label}.state.state.kind`);
|
||||
const busyState = string(busy.state, `${label}.state.state.state`);
|
||||
if (
|
||||
(busyKind === "run" &&
|
||||
["running", "pausing", "paused", "cancelling"].includes(busyState)) ||
|
||||
(busyKind === "maintenance" && busyState === "compacting")
|
||||
) {
|
||||
return {
|
||||
last_command_id: unsignedInteger(
|
||||
snapshot.last_command_id,
|
||||
`${label}.last_command_id`,
|
||||
),
|
||||
state: {
|
||||
kind: "busy",
|
||||
state: busyKind === "run"
|
||||
? {
|
||||
kind: "run",
|
||||
state: busyState as "running" | "pausing" | "paused" | "cancelling",
|
||||
}
|
||||
: { kind: "maintenance", state: "compacting" },
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`${label}.state.state is invalid`);
|
||||
}
|
||||
|
||||
function workerWorkdirAttachment(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): WorkerWorkdirAttachmentSummary {
|
||||
const item = record(value, label);
|
||||
exact(item, ["alias", "working_directory"], label);
|
||||
return {
|
||||
alias: string(item.alias, `${label}.alias`),
|
||||
working_directory: parseWorkingDirectorySummary(item.working_directory),
|
||||
};
|
||||
}
|
||||
|
||||
export type ParsedWorkerSummary =
|
||||
& Omit<
|
||||
WorkerSummary,
|
||||
"display_name" | "tags" | "diagnostics"
|
||||
>
|
||||
& {
|
||||
display_name: string;
|
||||
tags: string[];
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type ParsedWorkerListResponse = Omit<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(
|
||||
value: unknown,
|
||||
label: string,
|
||||
|
||||
@@ -9,10 +9,14 @@ import type {
|
||||
BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse,
|
||||
BrowserWorkerWorkingDirectorySelection
|
||||
as SharedBrowserWorkerWorkingDirectorySelection,
|
||||
Diagnostic as SharedDiagnostic,
|
||||
WorkerCapabilitySummary as SharedWorkerCapabilitySummary,
|
||||
WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse,
|
||||
WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate,
|
||||
WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption,
|
||||
WorkerLaunchWorkerSummary as WorkerSummary,
|
||||
WorkerLaunchWorkerSummary,
|
||||
WorkerSummary as SharedWorkerSummary,
|
||||
WorkerWorkdirAttachmentSummary as SharedWorkerWorkdirAttachmentSummary,
|
||||
WorkingDirectoryRepositoryOption as SharedWorkingDirectoryRepositoryOption,
|
||||
} from "$lib/generated/worker-launch-api";
|
||||
import type {
|
||||
@@ -53,11 +57,7 @@ export type {
|
||||
};
|
||||
export type WorkspaceResponse = SharedWorkspaceResponse;
|
||||
|
||||
export type Diagnostic = {
|
||||
code: string;
|
||||
severity: string;
|
||||
message: string;
|
||||
};
|
||||
export type Diagnostic = SharedDiagnostic;
|
||||
|
||||
export type Runtime = {
|
||||
runtime_id: string;
|
||||
@@ -78,35 +78,17 @@ export type Runtime = {
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkerCapabilities = {
|
||||
can_stop: boolean;
|
||||
can_spawn_followup: boolean;
|
||||
};
|
||||
export type WorkerCapabilities = SharedWorkerCapabilitySummary;
|
||||
|
||||
export type WorkerWorkdirAttachment = {
|
||||
alias: string;
|
||||
working_directory: WorkingDirectorySummary;
|
||||
};
|
||||
export type WorkerWorkdirAttachment = SharedWorkerWorkdirAttachmentSummary;
|
||||
|
||||
export type Worker = {
|
||||
runtime_id: string;
|
||||
worker_id: string;
|
||||
resource_key: string;
|
||||
host_id: string;
|
||||
export type Worker = Omit<
|
||||
SharedWorkerSummary,
|
||||
"display_name" | "tags" | "worker_state" | "diagnostics"
|
||||
> & {
|
||||
display_name: string;
|
||||
label: string;
|
||||
profile?: string | null;
|
||||
singleton_key?: string | null;
|
||||
tags: string[];
|
||||
workspace: { visibility: string; identity: string };
|
||||
state: string;
|
||||
worker_state?: WorkerStateSnapshot | null;
|
||||
pinned?: boolean;
|
||||
retention_state?: string;
|
||||
last_seen_at?: string | null;
|
||||
implementation: { kind: string; display_hint: string };
|
||||
capabilities: WorkerCapabilities;
|
||||
workdir_attachments?: WorkerWorkdirAttachment[];
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
@@ -121,7 +103,7 @@ export type WorkerRestoreState =
|
||||
|
||||
export type WorkerRestoreResult = {
|
||||
state: WorkerRestoreState;
|
||||
worker?: WorkerSummary | null;
|
||||
worker?: WorkerLaunchWorkerSummary | null;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseWorkerListResponse } from "$lib/workspace/api/workers";
|
||||
import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers";
|
||||
import type {
|
||||
ListResponse,
|
||||
RuntimeCleanupPlanResponse,
|
||||
Worker,
|
||||
} from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const workers = await loadJson<ListResponse<Worker>>(
|
||||
const workers = await loadJson(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/workers"),
|
||||
undefined,
|
||||
parseWorkerListResponse,
|
||||
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
|
||||
);
|
||||
const runtimeIds = Array.from(
|
||||
new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []),
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseWorkerSummary } from "$lib/workspace/api/workers";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const reference = resourceKey(params.workerRef);
|
||||
const result = await loadJson<Worker>(
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/workers/${encodeURIComponent(reference)}`,
|
||||
),
|
||||
undefined,
|
||||
parseWorkerSummary,
|
||||
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
|
||||
);
|
||||
if (result.data?.resource_key) {
|
||||
const canonical = canonicalResourceReference(
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseWorkerSummary } from "$lib/workspace/api/workers";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const reference = resourceKey(params.workerRef);
|
||||
const result = await loadJson<Worker>(
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/workers/${encodeURIComponent(reference)}`,
|
||||
),
|
||||
undefined,
|
||||
parseWorkerSummary,
|
||||
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
|
||||
);
|
||||
if (result.data?.resource_key) {
|
||||
const canonical = canonicalResourceReference(
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
import { workspaceApiPath } from '$lib/workspace/api/http';
|
||||
import { readBoundedJson, workspaceApiPath } from '$lib/workspace/api/http';
|
||||
import {
|
||||
parseWorkingDirectoryCreateResponse,
|
||||
validateWorkingDirectoryCreateRequest,
|
||||
} from '$lib/workspace/api/workdirs';
|
||||
import {
|
||||
parseBrowserCreateWorkerResponse,
|
||||
parseWorkerApiError,
|
||||
parseWorkerLaunchOptionsResponse,
|
||||
} from '$lib/workspace/api/workers';
|
||||
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||
@@ -29,12 +30,6 @@
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
type ErrorPayload = {
|
||||
error?: { message?: string; code?: string } | string;
|
||||
message?: string;
|
||||
diagnostics?: Diagnostic[];
|
||||
};
|
||||
|
||||
function workdirOptionLabel(directory: WorkingDirectorySummary): string {
|
||||
const repositoryKey = directory.source.kind === 'repository'
|
||||
? directory.source.repository_key
|
||||
@@ -171,7 +166,9 @@
|
||||
if (!response.ok) {
|
||||
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;
|
||||
const form = defaultWorkerLaunchForm(payload, {
|
||||
runtime_id: runtimeId,
|
||||
@@ -313,7 +310,9 @@
|
||||
submitError = await responseDisplayError(response, 'worker create failed');
|
||||
return;
|
||||
}
|
||||
const payload = parseBrowserCreateWorkerResponse(await response.json());
|
||||
const payload = parseBrowserCreateWorkerResponse(
|
||||
await readBoundedJson(response, 8 * 1024 * 1024),
|
||||
);
|
||||
await goto(payload.console_href);
|
||||
} catch (err) {
|
||||
submitError = exceptionDisplayError(err, 'worker create failed');
|
||||
@@ -324,18 +323,13 @@
|
||||
|
||||
async function responseDisplayError(response: Response, fallback: string): Promise<DisplayError> {
|
||||
try {
|
||||
const payload = (await response.json()) as ErrorPayload;
|
||||
const diagnostics = Array.isArray(payload.diagnostics) ? payload.diagnostics : [];
|
||||
if (typeof payload.error === 'object' && payload.error?.message) {
|
||||
return {
|
||||
message: `${payload.error.code ?? 'request_failed'}: ${payload.error.message}`,
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
if (payload.message) {
|
||||
const code = typeof payload.error === 'string' ? payload.error : 'request_failed';
|
||||
return { message: `${code}: ${payload.message}`, diagnostics };
|
||||
}
|
||||
const payload = parseWorkerApiError(
|
||||
await readBoundedJson(response, 1024 * 1024),
|
||||
);
|
||||
return {
|
||||
message: `${payload.error}: ${payload.message}`,
|
||||
diagnostics: payload.diagnostics ?? [],
|
||||
};
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user