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