refactor: share Worker launch REST DTOs

This commit is contained in:
2026-09-03 14:23:33 +09:00
parent 9fb1b90856
commit 1f32c693df
18 changed files with 1437 additions and 313 deletions
Generated
+1
View File
@@ -6706,6 +6706,7 @@ dependencies = [
name = "workspace-api" name = "workspace-api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"protocol",
"serde", "serde",
"serde_json", "serde_json",
"ts-rs", "ts-rs",
+23 -45
View File
@@ -1,6 +1,6 @@
use reqwest::Method; use reqwest::Method;
use serde::Serialize;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use ticket::{ use ticket::{
MarkdownText, NewOrchestrationPlanRecord, NewTicket, NewTicketEvent, NewTicketRelation, MarkdownText, NewOrchestrationPlanRecord, NewTicket, NewTicketEvent, NewTicketRelation,
OrchestrationPlanKind, OrchestrationPlanRecord, Ticket, TicketBackend, TicketDependencyCheck, OrchestrationPlanKind, OrchestrationPlanRecord, Ticket, TicketBackend, TicketDependencyCheck,
@@ -9,39 +9,17 @@ use ticket::{
TicketRelationKind, TicketRelationView, TicketStateChange, TicketStateSelector, TicketSummary, TicketRelationKind, TicketRelationView, TicketStateChange, TicketStateSelector, TicketSummary,
}; };
use workspace_api::{ use workspace_api::{
ListResponse, ObjectiveCreateRequest, ObjectiveDetail, ObjectiveEditRequest, BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse,
ObjectiveLinkTicketRequest, ObjectiveStateRequest, ObjectiveSummary, CreateWorkspaceWorkerRequest, ListResponse, ObjectiveCreateRequest, ObjectiveDetail,
ObjectiveEditRequest, ObjectiveLinkTicketRequest, ObjectiveStateRequest, ObjectiveSummary,
TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH,
WorkerLaunchOptionsResponse,
}; };
use crate::{BackendApiClient, BackendWorkspaceClientError}; use crate::{BackendApiClient, BackendWorkspaceClientError};
const DEFAULT_PRODUCT_LIST_LIMIT: usize = 1_000; const DEFAULT_PRODUCT_LIST_LIMIT: usize = 1_000;
#[derive(Debug, Deserialize)]
struct BackendWorkerLaunchOptions {
runtimes: Vec<BackendWorkerLaunchRuntime>,
}
#[derive(Debug, Deserialize)]
struct BackendWorkerLaunchRuntime {
runtime_id: String,
worker_creation_available: bool,
working_directory_required: bool,
}
#[derive(Debug, Deserialize)]
struct BackendCreateWorkerResponse {
runtime_id: String,
worker_id: String,
}
#[derive(Debug, Deserialize)]
struct BackendWorkspaceOrchestratorResponse {
disposition: String,
worker: Option<BackendCreateWorkerResponse>,
}
/// Workspace-scoped Backend client for Ticket and Objective product state. /// Workspace-scoped Backend client for Ticket and Objective product state.
/// ///
/// Construction requires both the selected Backend URL and Workspace identity. /// Construction requires both the selected Backend URL and Workspace identity.
@@ -267,7 +245,7 @@ impl BackendWorkspaceProductClient {
&self, &self,
ticket_id: &str, ticket_id: &str,
) -> Result<String, BackendWorkspaceClientError> { ) -> Result<String, BackendWorkspaceClientError> {
let options: BackendWorkerLaunchOptions = self.get_json("/workers/launch-options")?; let options: WorkerLaunchOptionsResponse = self.get_json("/workers/launch-options")?;
let runtime = options let runtime = options
.runtimes .runtimes
.iter() .iter()
@@ -278,19 +256,19 @@ impl BackendWorkspaceProductClient {
.to_string(), .to_string(),
) )
})?; })?;
let response: BackendCreateWorkerResponse = self.send_json( let request = CreateWorkspaceWorkerRequest {
Method::POST, runtime_id: runtime.runtime_id.clone(),
"/workers", display_name: format!("intake-{ticket_id}"),
Some(&serde_json::json!({ profile: Some("builtin:intake".to_string()),
"runtime_id": runtime.runtime_id, ticket_assignment: None,
"display_name": format!("intake-{ticket_id}"), initial_submit: vec![protocol::Segment::Text {
"profile": "builtin:intake", content: format!("Please handle intake for Ticket {ticket_id}."),
"initial_submit": [{ }],
"kind": "text", working_directory: None,
"content": format!("Please handle intake for Ticket {ticket_id}.") control_operation_id: None,
}] };
})), let response: BrowserCreateWorkerResponse =
)?; self.send_json(Method::POST, "/workers", Some(&request))?;
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
@@ -298,7 +276,7 @@ impl BackendWorkspaceProductClient {
} }
pub fn start_workspace_orchestrator(&self) -> Result<String, BackendWorkspaceClientError> { pub fn start_workspace_orchestrator(&self) -> Result<String, BackendWorkspaceClientError> {
let response: BackendWorkspaceOrchestratorResponse = let response: BrowserWorkspaceOrchestratorResponse =
self.send_json::<(), _>(Method::POST, "/orchestrator", None)?; self.send_json::<(), _>(Method::POST, "/orchestrator", None)?;
let worker = response.worker.ok_or_else(|| { let worker = response.worker.ok_or_else(|| {
BackendWorkspaceClientError::InvalidTarget( BackendWorkspaceClientError::InvalidTarget(
@@ -792,11 +770,11 @@ mod tests {
let (base_url, requests, handle) = response_sequence_server(vec![ let (base_url, requests, handle) = response_sequence_server(vec![
( (
"200 OK", "200 OK",
r#"{"runtimes":[{"runtime_id":"embedded","worker_creation_available":true,"working_directory_required":false}]}"#, r#"{"workspace_id":"workspace-a","runtimes":[{"runtime_id":"embedded","display_name":"Embedded","built_in":true,"worker_creation_available":true,"working_directory_required":false,"status":"connected","diagnostics":[]}],"default_profile":null,"profiles":[],"repositories":[],"working_directories":[],"diagnostics":[]}"#,
), ),
( (
"200 OK", "200 OK",
r#"{"runtime_id":"embedded","worker_id":"worker-1"}"#, r#"{"workspace_id":"workspace-a","runtime_id":"embedded","worker_id":"worker-1","console_href":"/w/workspace-a/workers/worker-1","worker":{"runtime_id":"embedded","worker_id":"worker-1","host_id":"embedded","display_name":"Intake","label":"worker-1","profile":"builtin:intake","singleton_key":null,"tags":[],"workspace":{"visibility":"workspace","identity":"workspace-a","workspace_id":"workspace-a"},"state":"idle","last_seen_at":null,"pinned":false,"retention_state":"active","implementation":{"kind":"runtime","display_hint":"Runtime Worker"},"capabilities":{"can_stop":true,"can_spawn_followup":false},"diagnostics":[]},"diagnostics":[]}"#,
), ),
]); ]);
let client = BackendWorkspaceProductClient::new_with_access_token( let client = BackendWorkspaceProductClient::new_with_access_token(
@@ -824,7 +802,7 @@ mod tests {
#[test] #[test]
fn workspace_orchestrator_launch_uses_scoped_backend_route() { fn workspace_orchestrator_launch_uses_scoped_backend_route() {
let body = r#"{"disposition":"created","worker":{"runtime_id":"embedded","worker_id":"worker-2"}}"#; let body = r#"{"workspace_id":"workspace-a","online":true,"disposition":"created","worker":{"runtime_id":"embedded","worker_id":"worker-2","host_id":"embedded","display_name":"Orchestrator","label":"worker-2","profile":"builtin:orchestrator","singleton_key":"workspace-orchestrator","tags":[],"workspace":{"visibility":"workspace","identity":"workspace-a","workspace_id":"workspace-a"},"state":"idle","last_seen_at":null,"pinned":true,"retention_state":"active","implementation":{"kind":"runtime","display_hint":"Runtime Worker"},"capabilities":{"can_stop":true,"can_spawn_followup":false},"diagnostics":[]},"diagnostics":[]}"#;
let (base_url, request, handle) = one_response_server("200 OK", body); let (base_url, request, handle) = one_response_server("200 OK", body);
let client = BackendWorkspaceProductClient::new_with_access_token( let client = BackendWorkspaceProductClient::new_with_access_token(
base_url, base_url,
+6 -1
View File
@@ -7,9 +7,10 @@ publish = false
[features] [features]
default = [] default = []
typescript = ["dep:ts-rs"] typescript = ["dep:ts-rs", "protocol/typescript"]
[dependencies] [dependencies]
protocol.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
ts-rs = { version = "12.0.1", optional = true } ts-rs = { version = "12.0.1", optional = true }
@@ -24,6 +25,10 @@ serde_json.workspace = true
name = "generate_workdir_api_types" name = "generate_workdir_api_types"
required-features = ["typescript"] required-features = ["typescript"]
[[example]]
name = "generate_worker_launch_api_types"
required-features = ["typescript"]
[[example]] [[example]]
name = "generate_companion_api_types" name = "generate_companion_api_types"
required-features = ["typescript"] required-features = ["typescript"]
@@ -0,0 +1,3 @@
fn main() {
print!("{}", workspace_api::worker_launch_api_typescript());
}
+320
View File
@@ -579,6 +579,8 @@ pub struct WorkingDirectoryOccupancy {
/// retains the Backend-generated Repository id and is never a Workspace public /// retains the Backend-generated Repository id and is never a Workspace public
/// projection. /// projection.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RuntimeWorkingDirectoryCleanupTarget { pub struct RuntimeWorkingDirectoryCleanupTarget {
pub kind: String, pub kind: String,
@@ -590,6 +592,8 @@ pub struct RuntimeWorkingDirectoryCleanupTarget {
/// surfaces must project this through [`WorkingDirectorySummary`] so the UUID is /// surfaces must project this through [`WorkingDirectorySummary`] so the UUID is
/// replaced with `repository_key`. /// replaced with `repository_key`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RuntimeWorkingDirectorySummary { pub struct RuntimeWorkingDirectorySummary {
pub working_directory_id: String, pub working_directory_id: String,
@@ -607,6 +611,7 @@ pub struct RuntimeWorkingDirectorySummary {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub current_tree: Option<String>, pub current_tree: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(type = "number | null"))]
pub observed_at_epoch_seconds: Option<u64>, pub observed_at_epoch_seconds: Option<u64>,
pub materializer_kind: WorkingDirectoryMaterializerKind, pub materializer_kind: WorkingDirectoryMaterializerKind,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -908,6 +913,7 @@ pub struct RuntimeConnectionTestResponse {
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkerWorkspaceSummary { pub struct WorkerWorkspaceSummary {
pub visibility: String, pub visibility: String,
pub identity: String, pub identity: String,
@@ -916,12 +922,14 @@ pub struct WorkerWorkspaceSummary {
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkerImplementationSummary { pub struct WorkerImplementationSummary {
pub kind: String, pub kind: String,
pub display_hint: String, pub display_hint: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkerCapabilitySummary { pub struct WorkerCapabilitySummary {
pub can_stop: bool, pub can_stop: bool,
pub can_spawn_followup: bool, pub can_spawn_followup: bool,
@@ -1095,6 +1103,7 @@ pub struct WorkspaceWorkerDiscoveryPage {
/// do not carry one. The Workspace Server must resolve it from Workspace /// do not carry one. The Workspace Server must resolve it from Workspace
/// authority before constructing this response. /// authority before constructing this response.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkerSummary { pub struct WorkerSummary {
pub runtime_id: String, pub runtime_id: String,
pub worker_id: String, pub worker_id: String,
@@ -1122,6 +1131,142 @@ pub struct WorkerSummary {
pub diagnostics: Vec<Diagnostic>, pub diagnostics: Vec<Diagnostic>,
} }
/// Runtime-owned Worker summary embedded in Worker launch responses.
///
/// This preserves the existing launch wire shape. Workspace-owned Worker list
/// and detail responses use [`WorkerSummary`] instead.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkerLaunchWorkerSummary {
pub runtime_id: String,
pub worker_id: String,
pub host_id: String,
pub display_name: String,
pub label: String,
pub profile: Option<String>,
pub singleton_key: Option<String>,
pub tags: Vec<String>,
pub workspace: WorkerWorkspaceSummary,
pub state: String,
pub last_seen_at: Option<String>,
pub pinned: bool,
pub retention_state: String,
pub implementation: WorkerImplementationSummary,
pub capabilities: WorkerCapabilitySummary,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub working_directory: Option<RuntimeWorkingDirectorySummary>,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkerLaunchOptionsResponse {
pub workspace_id: String,
pub runtimes: Vec<WorkerLaunchRuntimeOption>,
pub default_profile: Option<String>,
pub profiles: Vec<WorkerLaunchProfileCandidate>,
pub repositories: Vec<WorkingDirectoryRepositoryOption>,
pub working_directories: Vec<WorkingDirectorySummary>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkerLaunchRuntimeOption {
pub runtime_id: String,
pub display_name: String,
pub built_in: bool,
pub worker_creation_available: bool,
pub working_directory_required: bool,
pub status: String,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkerLaunchProfileCandidate {
pub id: String,
pub label: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkingDirectoryRepositoryOption {
pub repository_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub default_selector: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct BrowserWorkerWorkingDirectorySelection {
pub working_directory_id: String,
#[serde(default)]
pub relative_cwd: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CreateWorkspaceWorkerTicketAssignmentRequest {
pub ticket_id: String,
pub operation_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CreateWorkspaceWorkerRequest {
pub runtime_id: String,
pub display_name: String,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub ticket_assignment: Option<CreateWorkspaceWorkerTicketAssignmentRequest>,
#[serde(default)]
pub initial_submit: Vec<protocol::Segment>,
#[serde(default)]
pub working_directory: Option<BrowserWorkerWorkingDirectorySelection>,
/// Backend idempotency key used only for authenticated Worker-owned spawn/control.
#[serde(default)]
pub control_operation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct BrowserCreateWorkerResponse {
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub console_href: String,
pub worker: WorkerLaunchWorkerSummary,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct BrowserWorkspaceOrchestratorResponse {
pub workspace_id: String,
pub online: bool,
pub disposition: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub worker: Option<WorkerLaunchWorkerSummary>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkerOperationState { pub enum WorkerOperationState {
@@ -1390,6 +1535,74 @@ pub fn workdir_api_typescript() -> String {
) )
} }
#[cfg(feature = "typescript")]
pub fn worker_launch_api_typescript() -> String {
use ts_rs::TS;
let config = ts_rs::Config::default();
let declarations = [
DiagnosticSeverity::decl(&config),
Diagnostic::decl(&config),
WorkingDirectoryMaterializerKind::decl(&config),
WorkingDirectoryStatusKind::decl(&config),
WorkingDirectoryCleanupTarget::decl(&config),
RuntimeWorkingDirectoryCleanupTarget::decl(&config),
RuntimeWorkingDirectorySummary::decl(&config),
WorkingDirectoryOccupancy::decl(&config),
WorkingDirectorySummary::decl(&config),
WorkerWorkspaceSummary::decl(&config),
WorkerImplementationSummary::decl(&config),
WorkerCapabilitySummary::decl(&config),
WorkerLaunchWorkerSummary::decl(&config),
WorkerLaunchRuntimeOption::decl(&config),
WorkerLaunchProfileCandidate::decl(&config),
WorkingDirectoryRepositoryOption::decl(&config),
WorkerLaunchOptionsResponse::decl(&config),
BrowserWorkerWorkingDirectorySelection::decl(&config),
CreateWorkspaceWorkerTicketAssignmentRequest::decl(&config),
CreateWorkspaceWorkerRequest::decl(&config),
BrowserCreateWorkerResponse::decl(&config),
BrowserWorkspaceOrchestratorResponse::decl(&config),
];
format!(
"// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts\n\nimport type {{ Segment }} from \"./protocol\";\n\n{}\n",
declarations
.into_iter()
.map(|declaration| format!("export {declaration}"))
.collect::<Vec<_>>()
.join("\n\n")
)
}
#[cfg(all(test, feature = "typescript"))]
mod worker_launch_typescript_tests {
#[test]
fn generated_worker_launch_api_contract_is_current() {
let expected = super::worker_launch_api_typescript();
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../web/workspace/src/lib/generated/worker-launch-api.ts");
let actual = std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
assert_eq!(
normalize(&actual),
normalize(&expected),
"regenerate Worker launch API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts` and format the generated file",
);
}
fn normalize(value: &str) -> String {
value
.chars()
.filter_map(|character| match character {
character if character.is_whitespace() => None,
',' => Some(';'),
character => Some(character),
})
.collect::<String>()
.replace("=|", "=")
}
}
#[cfg(all(test, feature = "typescript"))] #[cfg(all(test, feature = "typescript"))]
mod workdir_typescript_tests { mod workdir_typescript_tests {
#[test] #[test]
@@ -1423,6 +1636,113 @@ mod workdir_typescript_tests {
mod tests { mod tests {
use super::*; use super::*;
fn worker_launch_summary() -> WorkerLaunchWorkerSummary {
WorkerLaunchWorkerSummary {
runtime_id: "runtime-a".to_string(),
worker_id: "worker-a".to_string(),
host_id: "host-a".to_string(),
display_name: "Worker A".to_string(),
label: "worker-a".to_string(),
profile: None,
singleton_key: None,
tags: Vec::new(),
workspace: WorkerWorkspaceSummary {
visibility: "workspace".to_string(),
identity: "workspace-a".to_string(),
workspace_id: Some("workspace-a".to_string()),
},
state: "idle".to_string(),
last_seen_at: None,
pinned: false,
retention_state: "active".to_string(),
implementation: WorkerImplementationSummary {
kind: "runtime".to_string(),
display_hint: "Runtime Worker".to_string(),
},
capabilities: WorkerCapabilitySummary {
can_stop: true,
can_spawn_followup: false,
},
working_directory: None,
diagnostics: Vec::new(),
}
}
#[test]
fn worker_launch_optional_omission_and_request_shape_are_stable() {
assert_eq!(
serde_json::to_value(WorkingDirectoryRepositoryOption {
repository_key: "main".to_string(),
default_selector: None,
})
.unwrap(),
serde_json::json!({ "repository_key": "main" })
);
let orchestrator = serde_json::to_value(BrowserWorkspaceOrchestratorResponse {
workspace_id: "workspace-a".to_string(),
online: false,
disposition: "unavailable".to_string(),
worker: None,
diagnostics: Vec::new(),
})
.unwrap();
assert_eq!(
orchestrator,
serde_json::json!({
"workspace_id": "workspace-a",
"online": false,
"disposition": "unavailable",
"diagnostics": [],
})
);
let worker = serde_json::to_value(worker_launch_summary()).unwrap();
assert!(
!worker
.as_object()
.unwrap()
.contains_key("working_directory")
);
assert_eq!(worker["profile"], serde_json::Value::Null);
assert_eq!(worker["singleton_key"], serde_json::Value::Null);
assert_eq!(worker["last_seen_at"], serde_json::Value::Null);
let request = serde_json::to_value(CreateWorkspaceWorkerRequest {
runtime_id: "runtime-a".to_string(),
display_name: "Worker A".to_string(),
profile: None,
ticket_assignment: None,
initial_submit: Vec::new(),
working_directory: None,
control_operation_id: None,
})
.unwrap();
assert_eq!(
request,
serde_json::json!({
"runtime_id": "runtime-a",
"display_name": "Worker A",
"profile": null,
"ticket_assignment": null,
"initial_submit": [],
"working_directory": null,
"control_operation_id": null,
})
);
}
#[test]
fn worker_launch_request_rejects_unknown_fields() {
let error = serde_json::from_value::<CreateWorkspaceWorkerRequest>(serde_json::json!({
"runtime_id": "runtime-a",
"display_name": "Worker A",
"unexpected": true,
}))
.unwrap_err();
assert!(error.to_string().contains("unknown field"));
}
#[test] #[test]
fn repository_key_validation_is_canonical_and_bounded() { fn repository_key_validation_is_canonical_and_bounded() {
let max = "a".repeat(64); let max = "a".repeat(64);
+140 -170
View File
@@ -58,25 +58,28 @@ use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjec
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest}; use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend}; use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
use workspace_api::{ use workspace_api::{
CreateRemoteRuntimeRequest, CreateRepositorySshCredentialRequest, BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse, CreateRemoteRuntimeRequest,
CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryResponse, CreateRepositorySshCredentialRequest, CreateWorkspaceRepositoryRequest,
DeleteRepositorySshCredentialRequest, DeleteRepositorySshHostTrustRequest, CreateWorkspaceRepositoryResponse, CreateWorkspaceWorkerRequest,
ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest, CreateWorkspaceWorkerTicketAssignmentRequest, DeleteRepositorySshCredentialRequest,
ObjectiveStateRequest, ProfileSettingsResponse, PutRepositorySshHostTrustRequest, DeleteRepositorySshHostTrustRequest, ObjectiveCreateRequest, ObjectiveEditRequest,
RepositoryAccessProjection, RepositoryDetailResponse, RepositoryListResponse, ObjectiveLinkTicketRequest, ObjectiveStateRequest, ProfileSettingsResponse,
RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust, PutRepositorySshHostTrustRequest, RepositoryAccessProjection, RepositoryDetailResponse,
RepositoryListResponse, RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust,
RotateRepositorySshCredentialRequest, RuntimeConnectionTestResponse, RuntimeManagementSummary, RotateRepositorySshCredentialRequest, RuntimeConnectionTestResponse, RuntimeManagementSummary,
TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH,
UpdateWorkspaceMetadataRequest, UpdateWorkspaceMetadataRequest, WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary,
WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest, WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest,
WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse, WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse,
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse, WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse, WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse,
WorkingDirectoryRemovalDisposition, WorkingDirectoryRemovalRequest, WorkingDirectoryRemovalDisposition, WorkingDirectoryRemovalRequest,
WorkingDirectoryRemovalResponse, WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkingDirectoryRemovalResponse, WorkingDirectoryRepositoryOption,
WorkspaceExtensionPointState, WorkspaceExtensionPoints, WorkspaceMetadataMutationResponse, WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkspaceExtensionPointState,
WorkspaceMetadataSettingsResponse, WorkspacePermissionSummary, WorkspaceRepositoryRecord, WorkspaceExtensionPoints, WorkspaceMetadataMutationResponse, WorkspaceMetadataSettingsResponse,
WorkspaceResponse, WorkspaceRuntimeResource, WorkspaceSummary, WorkspaceWorkerDiscoveryItem, WorkspacePermissionSummary, WorkspaceRepositoryRecord, WorkspaceResponse,
WorkspaceRuntimeResource, WorkspaceSummary, WorkspaceWorkerDiscoveryItem,
WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject, WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject,
}; };
@@ -3097,98 +3100,6 @@ pub struct WorkerRetentionResponse {
pub retention_state: String, pub retention_state: String,
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkerLaunchOptionsResponse {
pub workspace_id: String,
pub runtimes: Vec<WorkerLaunchRuntimeOption>,
pub default_profile: Option<String>,
pub profiles: Vec<WorkerLaunchProfileCandidate>,
pub repositories: Vec<WorkingDirectoryRepositoryOption>,
pub working_directories: Vec<WorkingDirectorySummary>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkerLaunchRuntimeOption {
pub runtime_id: String,
pub display_name: String,
pub built_in: bool,
pub worker_creation_available: bool,
pub working_directory_required: bool,
pub status: String,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerLaunchProfileCandidate {
pub id: String,
pub label: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkingDirectoryRepositoryOption {
pub repository_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_selector: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BrowserWorkerWorkingDirectorySelection {
pub working_directory_id: String,
#[serde(default)]
pub relative_cwd: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BrowserWorkspaceOrchestratorResponse {
pub workspace_id: String,
pub online: bool,
pub disposition: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker: Option<WorkerSummary>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CreateWorkspaceWorkerTicketAssignmentRequest {
pub ticket_id: String,
pub operation_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CreateWorkspaceWorkerRequest {
pub runtime_id: String,
pub display_name: String,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub ticket_assignment: Option<CreateWorkspaceWorkerTicketAssignmentRequest>,
#[serde(default)]
pub initial_submit: Vec<Segment>,
#[serde(default)]
pub working_directory: Option<BrowserWorkerWorkingDirectorySelection>,
/// Backend idempotency key used only for authenticated Worker-owned spawn/control.
#[serde(default)]
pub control_operation_id: Option<String>,
/// Trusted resolution populated only by the authenticated worker-control handler.
#[serde(skip, default)]
pub resolved_control_operation: Option<WorkerControlOperation>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BrowserCreateWorkerResponse {
pub workspace_id: String,
#[serde(flatten)]
pub worker_ref: RuntimeWorkerRef,
pub console_href: String,
pub worker: WorkerSummary,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct LogQuery { struct LogQuery {
limit: Option<usize>, limit: Option<usize>,
@@ -7758,9 +7669,9 @@ fn start_memory_staging_consolidation(
resolved_config_bundle, resolved_config_bundle,
resolved_worker_observation_enabled: false, resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(), resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_workspace_api: None, resolved_workspace_api: None,
resolved_memory_settings: None, resolved_memory_settings: None,
resolved_control_operation: None,
}, },
)?; )?;
if result.state != WorkerOperationState::Accepted { if result.state != WorkerOperationState::Accepted {
@@ -8711,18 +8622,20 @@ async fn spawn_known_worker(
.map(|byte| format!("{byte:02x}")) .map(|byte| format!("{byte:02x}"))
.collect::<String>() .collect::<String>()
); );
request.resolved_control_operation = Some(WorkerControlOperation { let resolved_control_operation = Some(WorkerControlOperation {
operation_id: scoped_worker_control_operation_id(&controller, &operation_id), operation_id: scoped_worker_control_operation_id(&controller, &operation_id),
input_fingerprint, input_fingerprint,
}); });
let response = create_workspace_worker(State(api.clone()), headers, Json(request)).await?; let response =
create_workspace_worker_inner(api.clone(), headers, request, resolved_control_operation)
.await?;
if let Err(error) = api if let Err(error) = api
.store .store
.create_worker_control_grant(&WorkerControlGrantRecord { .create_worker_control_grant(&WorkerControlGrantRecord {
workspace_id: path.workspace_id.clone(), workspace_id: path.workspace_id.clone(),
grant_id: new_id("wcg"), grant_id: new_id("wcg"),
controller, controller,
subject: response.0.worker_ref.clone(), subject: RuntimeWorkerRef::new(&response.0.runtime_id, &response.0.worker_id),
relation: relation.to_string(), relation: relation.to_string(),
origin: "worker_spawn".to_string(), origin: "worker_spawn".to_string(),
permissions: vec![ permissions: vec![
@@ -9055,9 +8968,9 @@ async fn scoped_start_workspace_orchestrator(
resolved_config_bundle: None, resolved_config_bundle: None,
resolved_worker_observation_enabled: true, resolved_worker_observation_enabled: true,
resolved_worker_observation_grants: Vec::new(), resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_workspace_api: None, resolved_workspace_api: None,
resolved_memory_settings: None, resolved_memory_settings: None,
resolved_control_operation: None,
}, },
)?; )?;
if result.state != WorkerOperationState::Accepted || result.worker.is_none() { if result.state != WorkerOperationState::Accepted || result.worker.is_none() {
@@ -9085,6 +8998,42 @@ async fn scoped_start_workspace_orchestrator(
Ok(Json(workspace_orchestrator_response(&api, "created"))) Ok(Json(workspace_orchestrator_response(&api, "created")))
} }
fn worker_launch_worker_summary(worker: WorkerSummary) -> WorkerLaunchWorkerSummary {
WorkerLaunchWorkerSummary {
runtime_id: worker.worker.runtime_id,
worker_id: worker.worker.worker_id,
host_id: worker.host_id,
display_name: worker.display_name,
label: worker.label,
profile: worker.profile,
singleton_key: worker.singleton_key,
tags: worker.tags,
workspace: workspace_api::WorkerWorkspaceSummary {
visibility: worker.workspace.visibility,
identity: worker.workspace.identity,
workspace_id: worker.workspace.workspace_id,
},
state: worker.state,
last_seen_at: worker.last_seen_at,
pinned: worker.pinned,
retention_state: worker.retention_state,
implementation: workspace_api::WorkerImplementationSummary {
kind: worker.implementation.kind,
display_hint: worker.implementation.display_hint,
},
capabilities: workspace_api::WorkerCapabilitySummary {
can_stop: worker.capabilities.can_stop,
can_spawn_followup: worker.capabilities.can_spawn_followup,
},
working_directory: worker.working_directory,
diagnostics: worker
.diagnostics
.into_iter()
.map(workspace_api::Diagnostic::from)
.collect(),
}
}
fn workspace_orchestrator_response( fn workspace_orchestrator_response(
api: &WorkspaceApi, api: &WorkspaceApi,
disposition: &str, disposition: &str,
@@ -9095,13 +9044,20 @@ fn workspace_orchestrator_response(
.is_some_and(workspace_orchestrator_is_online); .is_some_and(workspace_orchestrator_is_online);
let diagnostics = worker let diagnostics = worker
.as_ref() .as_ref()
.map(|worker| worker.diagnostics.clone()) .map(|worker| {
worker
.diagnostics
.iter()
.cloned()
.map(workspace_api::Diagnostic::from)
.collect()
})
.unwrap_or_default(); .unwrap_or_default();
BrowserWorkspaceOrchestratorResponse { BrowserWorkspaceOrchestratorResponse {
workspace_id: api.config.workspace_id.clone(), workspace_id: api.config.workspace_id.clone(),
online, online,
disposition: disposition.to_string(), disposition: disposition.to_string(),
worker, worker: worker.map(worker_launch_worker_summary),
diagnostics, diagnostics,
} }
} }
@@ -12458,6 +12414,15 @@ async fn create_workspace_worker(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
headers: HeaderMap, headers: HeaderMap,
Json(request): Json<CreateWorkspaceWorkerRequest>, Json(request): Json<CreateWorkspaceWorkerRequest>,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
create_workspace_worker_inner(api, headers, request, None).await
}
async fn create_workspace_worker_inner(
api: WorkspaceApi,
headers: HeaderMap,
request: CreateWorkspaceWorkerRequest,
resolved_control_operation: Option<WorkerControlOperation>,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> { ) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
let CreateWorkspaceWorkerRequest { let CreateWorkspaceWorkerRequest {
runtime_id, runtime_id,
@@ -12467,7 +12432,6 @@ async fn create_workspace_worker(
initial_submit, initial_submit,
working_directory, working_directory,
control_operation_id: _, control_operation_id: _,
resolved_control_operation,
} = request; } = request;
let config_state = api let config_state = api
.config_store .config_store
@@ -12765,10 +12729,14 @@ fn browser_worker_response_from_summary(
); );
Ok(BrowserCreateWorkerResponse { Ok(BrowserCreateWorkerResponse {
workspace_id, workspace_id,
worker_ref: RuntimeWorkerRef::new(&runtime_id, &worker_id), runtime_id,
worker_id,
console_href, console_href,
worker, worker: worker_launch_worker_summary(worker),
diagnostics, diagnostics: diagnostics
.into_iter()
.map(workspace_api::Diagnostic::from)
.collect(),
}) })
} }
@@ -14810,7 +14778,11 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> ApiResult<WorkerLaunchO
worker_creation_available: runtime.worker_creation_available, worker_creation_available: runtime.worker_creation_available,
working_directory_required: !built_in, working_directory_required: !built_in,
status: runtime.status, status: runtime.status,
diagnostics: runtime.diagnostics, diagnostics: runtime
.diagnostics
.into_iter()
.map(workspace_api::Diagnostic::from)
.collect(),
} }
}) })
.collect(); .collect();
@@ -17210,9 +17182,9 @@ mod tests {
resolved_config_bundle: None, resolved_config_bundle: None,
resolved_worker_observation_enabled: false, resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(), resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_workspace_api: None, resolved_workspace_api: None,
resolved_memory_settings: None, resolved_memory_settings: None,
resolved_control_operation: None,
}; };
assert!( assert!(
api.validate_worker_spawn_repository_scope(&workdir_flow_launch) api.validate_worker_spawn_repository_scope(&workdir_flow_launch)
@@ -17456,9 +17428,9 @@ mod tests {
resolved_config_bundle: None, resolved_config_bundle: None,
resolved_worker_observation_enabled: false, resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(), resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_workspace_api: None, resolved_workspace_api: None,
resolved_memory_settings: None, resolved_memory_settings: None,
resolved_control_operation: None,
}; };
assert!( assert!(
@@ -17505,7 +17477,6 @@ mod tests {
}], }],
working_directory: None, working_directory: None,
control_operation_id: None, control_operation_id: None,
resolved_control_operation: None,
}), }),
) )
.await .await
@@ -17521,7 +17492,8 @@ mod tests {
.get_current_ticket_coder_assignment(&api.config.workspace_id, &ticket.id) .get_current_ticket_coder_assignment(&api.config.workspace_id, &ticket.id)
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(current.worker, response.worker_ref); let response_ref = RuntimeWorkerRef::new(&response.runtime_id, &response.worker_id);
assert_eq!(current.worker, response_ref);
let operation = api let operation = api
.store .store
.get_ticket_assignment_operation( .get_ticket_assignment_operation(
@@ -17531,7 +17503,7 @@ mod tests {
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(operation.assignment_id, Some(current.assignment_id)); assert_eq!(operation.assignment_id, Some(current.assignment_id));
assert_eq!(operation.worker, Some(response.worker_ref)); assert_eq!(operation.worker, Some(response_ref));
} }
#[tokio::test] #[tokio::test]
@@ -17561,7 +17533,6 @@ mod tests {
}], }],
working_directory: None, working_directory: None,
control_operation_id: None, control_operation_id: None,
resolved_control_operation: None,
}), }),
) )
.await; .await;
@@ -17595,7 +17566,6 @@ mod tests {
initial_submit: Vec::new(), initial_submit: Vec::new(),
working_directory: None, working_directory: None,
control_operation_id: None, control_operation_id: None,
resolved_control_operation: None,
}), }),
) )
.await .await
@@ -17603,11 +17573,11 @@ mod tests {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert( headers.insert(
"x-yoi-runtime-id", "x-yoi-runtime-id",
axum::http::HeaderValue::from_str(&created.worker_ref.runtime_id).unwrap(), axum::http::HeaderValue::from_str(&created.runtime_id).unwrap(),
); );
headers.insert( headers.insert(
"x-yoi-worker-id", "x-yoi-worker-id",
axum::http::HeaderValue::from_str(&created.worker_ref.worker_id).unwrap(), axum::http::HeaderValue::from_str(&created.worker_id).unwrap(),
); );
let error = let error =
authenticate_worker_mutation_source(&api, "other-workspace", &headers).unwrap_err(); authenticate_worker_mutation_source(&api, "other-workspace", &headers).unwrap_err();
@@ -17631,14 +17601,14 @@ mod tests {
initial_submit: Vec::new(), initial_submit: Vec::new(),
working_directory: None, working_directory: None,
control_operation_id: None, control_operation_id: None,
resolved_control_operation: None,
}), }),
) )
.await .await
.unwrap(); .unwrap();
let generic_ref = RuntimeWorkerRef::new(&generic.runtime_id, &generic.worker_id);
assert!(matches!( assert!(matches!(
require_online_workspace_orchestrator_source(&api, &generic.worker_ref), require_online_workspace_orchestrator_source(&api, &generic_ref),
Err(Error::TicketAssignmentConflict(_)) Err(Error::TicketAssignmentConflict(_))
)); ));
@@ -17650,10 +17620,11 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
let orchestrator = started.worker.unwrap().worker; let orchestrator = started.worker.unwrap();
let orchestrator = RuntimeWorkerRef::new(&orchestrator.runtime_id, &orchestrator.worker_id);
require_online_workspace_orchestrator_source(&api, &orchestrator).unwrap(); require_online_workspace_orchestrator_source(&api, &orchestrator).unwrap();
assert!(matches!( assert!(matches!(
require_online_workspace_orchestrator_source(&api, &generic.worker_ref), require_online_workspace_orchestrator_source(&api, &generic_ref),
Err(Error::TicketAssignmentConflict(_)) Err(Error::TicketAssignmentConflict(_))
)); ));
@@ -17722,8 +17693,8 @@ mod tests {
assert!(started.online); assert!(started.online);
let worker = started let worker = started
.worker .worker
.expect("production Workspace Orchestrator Worker") .expect("production Workspace Orchestrator Worker");
.worker; let worker = RuntimeWorkerRef::new(&worker.runtime_id, &worker.worker_id);
let stopped = api let stopped = api
.runtime .runtime
@@ -17823,12 +17794,12 @@ mod tests {
initial_submit: Vec::new(), initial_submit: Vec::new(),
working_directory: None, working_directory: None,
control_operation_id: None, control_operation_id: None,
resolved_control_operation: None,
}), }),
) )
.await .await
.unwrap(); .unwrap();
let controller = controller_worker.worker_ref; let controller =
RuntimeWorkerRef::new(&controller_worker.runtime_id, &controller_worker.worker_id);
assert_ne!( assert_ne!(
scoped_worker_control_operation_id(&controller, "same-operation"), scoped_worker_control_operation_id(&controller, "same-operation"),
scoped_worker_control_operation_id( scoped_worker_control_operation_id(
@@ -17854,7 +17825,6 @@ mod tests {
initial_submit: Vec::new(), initial_submit: Vec::new(),
working_directory: None, working_directory: None,
control_operation_id: Some("control-spawn-retry".to_string()), control_operation_id: Some("control-spawn-retry".to_string()),
resolved_control_operation: None,
}; };
let Json(first) = spawn_known_worker( let Json(first) = spawn_known_worker(
@@ -17878,7 +17848,8 @@ mod tests {
.await .await
.unwrap(); .unwrap();
assert_eq!(retried.worker_ref, first.worker_ref); assert_eq!(retried.runtime_id, first.runtime_id);
assert_eq!(retried.worker_id, first.worker_id);
let mut conflicting_request = request(); let mut conflicting_request = request();
conflicting_request.display_name = "Different controlled child".to_string(); conflicting_request.display_name = "Different controlled child".to_string();
let conflict = spawn_known_worker( let conflict = spawn_known_worker(
@@ -17908,7 +17879,10 @@ mod tests {
.list_active_worker_control_grants(&workspace_id, &controller, 10) .list_active_worker_control_grants(&workspace_id, &controller, 10)
.unwrap(); .unwrap();
assert_eq!(grants.len(), 1); assert_eq!(grants.len(), 1);
assert_eq!(grants[0].subject, first.worker_ref); assert_eq!(
grants[0].subject,
RuntimeWorkerRef::new(&first.runtime_id, &first.worker_id)
);
assert_eq!(grants[0].operation_id, "control-spawn-retry"); assert_eq!(grants[0].operation_id, "control-spawn-retry");
} }
@@ -17930,7 +17904,6 @@ mod tests {
initial_submit: Vec::new(), initial_submit: Vec::new(),
working_directory: None, working_directory: None,
control_operation_id: None, control_operation_id: None,
resolved_control_operation: None,
}), }),
) )
.await .await
@@ -17948,7 +17921,6 @@ mod tests {
initial_submit: Vec::new(), initial_submit: Vec::new(),
working_directory: None, working_directory: None,
control_operation_id: None, control_operation_id: None,
resolved_control_operation: None,
}), }),
) )
.await .await
@@ -17969,14 +17941,16 @@ mod tests {
dedicated.singleton_key.as_deref(), dedicated.singleton_key.as_deref(),
Some(crate::hosts::WORKSPACE_ORCHESTRATOR_SINGLETON_KEY) Some(crate::hosts::WORKSPACE_ORCHESTRATOR_SINGLETON_KEY)
); );
assert_ne!(dedicated.worker.worker_id, generic.worker_ref.worker_id); let dedicated_ref = RuntimeWorkerRef::new(&dedicated.runtime_id, &dedicated.worker_id);
let generic_ref = RuntimeWorkerRef::new(&generic.runtime_id, &generic.worker_id);
assert_ne!(dedicated.worker_id, generic.worker_id);
api.store api.store
.create_worker_control_grant(&WorkerControlGrantRecord { .create_worker_control_grant(&WorkerControlGrantRecord {
workspace_id: workspace_id.clone(), workspace_id: workspace_id.clone(),
grant_id: "orchestrator-controls-generic".to_string(), grant_id: "orchestrator-controls-generic".to_string(),
controller: dedicated.worker.clone(), controller: dedicated_ref.clone(),
subject: generic.worker_ref.clone(), subject: generic_ref.clone(),
relation: "spawned".to_string(), relation: "spawned".to_string(),
origin: "test".to_string(), origin: "test".to_string(),
permissions: vec!["observe".to_string()], permissions: vec!["observe".to_string()],
@@ -17989,11 +17963,11 @@ mod tests {
let mut observation_headers = HeaderMap::new(); let mut observation_headers = HeaderMap::new();
observation_headers.insert( observation_headers.insert(
"x-yoi-runtime-id", "x-yoi-runtime-id",
axum::http::HeaderValue::from_str(&dedicated.worker.runtime_id).unwrap(), axum::http::HeaderValue::from_str(&dedicated.runtime_id).unwrap(),
); );
observation_headers.insert( observation_headers.insert(
"x-yoi-worker-id", "x-yoi-worker-id",
axum::http::HeaderValue::from_str(&dedicated.worker.worker_id).unwrap(), axum::http::HeaderValue::from_str(&dedicated.worker_id).unwrap(),
); );
let Json(known) = list_known_workers( let Json(known) = list_known_workers(
State(api.clone()), State(api.clone()),
@@ -18005,7 +17979,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
assert_eq!(known.items.len(), 1); assert_eq!(known.items.len(), 1);
assert_eq!(known.items[0].subject, generic.worker_ref); assert_eq!(known.items[0].subject, generic_ref);
assert_eq!(known.items[0].permissions, ["observe"]); assert_eq!(known.items[0].permissions, ["observe"]);
let Json(sessions) = scoped_list_worker_observation_sessions( let Json(sessions) = scoped_list_worker_observation_sessions(
@@ -18024,8 +17998,8 @@ mod tests {
.iter() .iter()
.any(|session| { .any(|session| {
session["subject"]["kind"] == "runtime_worker" session["subject"]["kind"] == "runtime_worker"
&& session["subject"]["runtime_id"] == generic.worker_ref.runtime_id && session["subject"]["runtime_id"] == generic.runtime_id
&& session["subject"]["worker_id"] == generic.worker_ref.worker_id && session["subject"]["worker_id"] == generic.worker_id
}) })
); );
let Json(capture) = scoped_capture_worker_observation_session( let Json(capture) = scoped_capture_worker_observation_session(
@@ -18035,8 +18009,8 @@ mod tests {
}), }),
observation_headers.clone(), observation_headers.clone(),
Json(WorkerObservationSubjectRef::RuntimeWorker { Json(WorkerObservationSubjectRef::RuntimeWorker {
runtime_id: generic.worker_ref.runtime_id.clone(), runtime_id: generic.runtime_id.clone(),
worker_id: generic.worker_ref.worker_id.clone(), worker_id: generic.worker_id.clone(),
}), }),
) )
.await .await
@@ -18059,8 +18033,8 @@ mod tests {
}), }),
observation_headers.clone(), observation_headers.clone(),
Json(WorkerObservationSubjectRef::RuntimeWorker { Json(WorkerObservationSubjectRef::RuntimeWorker {
runtime_id: generic.worker_ref.runtime_id.clone(), runtime_id: generic.runtime_id.clone(),
worker_id: generic.worker_ref.worker_id.clone(), worker_id: generic.worker_id.clone(),
}), }),
) )
.await .await
@@ -18073,11 +18047,11 @@ mod tests {
let mut unauthorized_headers = HeaderMap::new(); let mut unauthorized_headers = HeaderMap::new();
unauthorized_headers.insert( unauthorized_headers.insert(
"x-yoi-runtime-id", "x-yoi-runtime-id",
axum::http::HeaderValue::from_str(&generic.worker_ref.runtime_id).unwrap(), axum::http::HeaderValue::from_str(&generic.runtime_id).unwrap(),
); );
unauthorized_headers.insert( unauthorized_headers.insert(
"x-yoi-worker-id", "x-yoi-worker-id",
axum::http::HeaderValue::from_str(&generic.worker_ref.worker_id).unwrap(), axum::http::HeaderValue::from_str(&generic.worker_id).unwrap(),
); );
let Json(unauthorized) = scoped_list_worker_observation_sessions( let Json(unauthorized) = scoped_list_worker_observation_sessions(
State(api.clone()), State(api.clone()),
@@ -18099,10 +18073,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
assert_eq!(existing.disposition, "existing"); assert_eq!(existing.disposition, "existing");
assert_eq!( assert_eq!(existing.worker.unwrap().worker_id, dedicated.worker_id);
existing.worker.unwrap().worker.worker_id,
dedicated.worker.worker_id
);
let Json(status) = scoped_workspace_orchestrator_status( let Json(status) = scoped_workspace_orchestrator_status(
State(api), State(api),
@@ -18110,10 +18081,7 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(status.worker.unwrap().worker_id, dedicated.worker_id);
status.worker.unwrap().worker.worker_id,
dedicated.worker.worker_id
);
} }
#[tokio::test] #[tokio::test]
@@ -19917,7 +19885,8 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
let orchestrator = started.worker.unwrap().worker; let orchestrator = started.worker.unwrap();
let orchestrator = RuntimeWorkerRef::new(&orchestrator.runtime_id, &orchestrator.worker_id);
execution.take_inputs(); execution.take_inputs();
let mut input = ticket::NewTicket::new("Bounded notification"); let mut input = ticket::NewTicket::new("Bounded notification");
@@ -21028,8 +20997,8 @@ mod tests {
.unwrap() .unwrap()
.0 .0
.worker .worker
.expect("Workspace Orchestrator should be available") .expect("Workspace Orchestrator should be available");
.worker; let orchestrator = RuntimeWorkerRef::new(&orchestrator.runtime_id, &orchestrator.worker_id);
let _ = execution.take_inputs(); let _ = execution.take_inputs();
let backend = browser_ticket_backend(&api).unwrap(); let backend = browser_ticket_backend(&api).unwrap();
let mut input = ticket::NewTicket::new("Queued notification"); let mut input = ticket::NewTicket::new("Queued notification");
@@ -21225,7 +21194,7 @@ mod tests {
Some(ticket_ref.id.as_str()) Some(ticket_ref.id.as_str())
); );
*api.orchestrator_attention_fingerprint.lock().unwrap() = None; *api.orchestrator_attention_fingerprint.lock().unwrap() = None;
let worker_id = started.worker.as_ref().unwrap().worker.worker_id.clone(); let worker_id = started.worker.as_ref().unwrap().worker_id.clone();
maybe_dispatch_orchestrator_turn_end( maybe_dispatch_orchestrator_turn_end(
&api, &api,
&worker_id, &worker_id,
@@ -21356,9 +21325,9 @@ mod tests {
resolved_config_bundle: None, resolved_config_bundle: None,
resolved_worker_observation_enabled: false, resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(), resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_workspace_api: None, resolved_workspace_api: None,
resolved_memory_settings: None, resolved_memory_settings: None,
resolved_control_operation: None,
}; };
let Json(first) = scoped_create_runtime_worker( let Json(first) = scoped_create_runtime_worker(
State(api.clone()), State(api.clone()),
@@ -21521,7 +21490,6 @@ mod tests {
ticket_id: second_ticket.id.clone(), ticket_id: second_ticket.id.clone(),
operation_id: "pending-spawn-operation".to_string(), operation_id: "pending-spawn-operation".to_string(),
}), }),
resolved_control_operation: None,
..request ..request
}; };
pending_request.resolved_workspace_api = pending_request.resolved_workspace_api =
@@ -21639,9 +21607,9 @@ mod tests {
resolved_config_bundle: None, resolved_config_bundle: None,
resolved_worker_observation_enabled: false, resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(), resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_workspace_api: None, resolved_workspace_api: None,
resolved_memory_settings: None, resolved_memory_settings: None,
resolved_control_operation: None,
}; };
let Json(created) = scoped_create_runtime_worker( let Json(created) = scoped_create_runtime_worker(
State(api.clone()), State(api.clone()),
@@ -22349,7 +22317,8 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
let source = orchestrator.worker.unwrap().worker; let source = orchestrator.worker.unwrap();
let source = RuntimeWorkerRef::new(&source.runtime_id, &source.worker_id);
let verified_source = || crate::worker_source::VerifiedWorkerMutationSource { let verified_source = || crate::worker_source::VerifiedWorkerMutationSource {
runtime_id: source.runtime_id.clone(), runtime_id: source.runtime_id.clone(),
worker_id: source.worker_id.clone(), worker_id: source.worker_id.clone(),
@@ -22425,7 +22394,8 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
let source = orchestrator.worker.unwrap().worker; let source = orchestrator.worker.unwrap();
let source = RuntimeWorkerRef::new(&source.runtime_id, &source.worker_id);
let spawned = api let spawned = api
.spawn_workspace_worker( .spawn_workspace_worker(
@@ -26947,9 +26917,9 @@ mod tests {
resolved_config_bundle: Some(runtime_test_bundle()), resolved_config_bundle: Some(runtime_test_bundle()),
resolved_worker_observation_enabled: false, resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(), resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_workspace_api: None, resolved_workspace_api: None,
resolved_memory_settings: None, resolved_memory_settings: None,
resolved_control_operation: None,
}; };
let spawned = api let spawned = api
.spawn_workspace_worker(EMBEDDED_WORKER_RUNTIME_ID, spawn_request) .spawn_workspace_worker(EMBEDDED_WORKER_RUNTIME_ID, spawn_request)
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev", "dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts", "test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts",
"build": "deno run -A npm:vite@7.2.7 build", "build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview" "preview": "deno run -A npm:vite@7.2.7 preview"
}, },
@@ -0,0 +1,185 @@
// Generated from workspace-api. Do not edit by hand.
// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts
import type { Segment } from "./protocol";
export type DiagnosticSeverity = "info" | "warning" | "error";
export type Diagnostic = {
code: string;
severity: DiagnosticSeverity;
message: string;
};
export type WorkingDirectoryMaterializerKind =
| "runtime_git_cache"
| "local_git_worktree";
export type WorkingDirectoryStatusKind =
| "active"
| "cleanup_pending"
| "corrupted"
| "not_found"
| "unknown";
export type WorkingDirectoryCleanupTarget = {
kind: string;
working_directory_id: string;
repository_key: string;
};
export type RuntimeWorkingDirectoryCleanupTarget = {
kind: string;
working_directory_id: string;
repository_id: string;
};
export type RuntimeWorkingDirectorySummary = {
working_directory_id: string;
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;
primary_worker_id?: string | null;
occupied_by?: WorkingDirectoryOccupancy | null;
};
export type WorkingDirectoryOccupancy = {
runtime_id: string;
worker_id: string;
display_name: string;
linked_at: string;
};
export type WorkingDirectorySummary = {
working_directory_id: string;
repository_key: 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?: WorkingDirectoryCleanupTarget | null;
status: WorkingDirectoryStatusKind;
cleanliness?: string | null;
primary_worker_id?: 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 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;
working_directory?: RuntimeWorkingDirectorySummary | null;
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 = {
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>;
working_directory: BrowserWorkerWorkingDirectorySelection | null;
/**
* 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>;
};
@@ -55,7 +55,7 @@ export function parseWorkingDirectoryListResponse(
); );
return { return {
workspace_id: stringField(record, "workspace_id"), workspace_id: stringField(record, "workspace_id"),
items: arrayField(record, "items").map(parseSummary), items: arrayField(record, "items").map(parseWorkingDirectorySummary),
diagnostics: arrayField(record, "diagnostics").map(parseDiagnostic), diagnostics: arrayField(record, "diagnostics").map(parseDiagnostic),
}; };
} }
@@ -101,12 +101,14 @@ function parseDetailLike(
return { return {
workspace_id: stringField(record, "workspace_id"), workspace_id: stringField(record, "workspace_id"),
runtime_id: stringField(record, "runtime_id"), runtime_id: stringField(record, "runtime_id"),
item: parseSummary(record.item), item: parseWorkingDirectorySummary(record.item),
diagnostics: arrayField(record, "diagnostics").map(parseDiagnostic), diagnostics: arrayField(record, "diagnostics").map(parseDiagnostic),
}; };
} }
function parseSummary(value: unknown): WorkingDirectorySummary { export function parseWorkingDirectorySummary(
value: unknown,
): WorkingDirectorySummary {
const record = exactRecord(value, SUMMARY_KEYS, "Workdir summary"); const record = exactRecord(value, SUMMARY_KEYS, "Workdir summary");
const summary: WorkingDirectorySummary = { const summary: WorkingDirectorySummary = {
working_directory_id: stringField(record, "working_directory_id"), working_directory_id: stringField(record, "working_directory_id"),
@@ -0,0 +1,162 @@
declare const Deno: {
test(name: string, fn: () => Promise<void> | void): void;
};
function assertEquals(actual: unknown, expected: unknown): void {
const actualJson = JSON.stringify(actual);
const expectedJson = JSON.stringify(expected);
if (actualJson !== expectedJson) {
throw new Error(`Expected ${expectedJson}, received ${actualJson}`);
}
}
function assertThrows(
operation: () => unknown,
errorClass: typeof Error,
message: string,
): void {
try {
operation();
} catch (error) {
if (!(error instanceof errorClass) || !error.message.includes(message)) {
throw error;
}
return;
}
throw new Error(`Expected operation to throw ${errorClass.name}: ${message}`);
}
import {
parseBrowserCreateWorkerResponse,
parseBrowserWorkspaceOrchestratorResponse,
parseCreateWorkspaceWorkerRequest,
parseWorkerLaunchOptionsResponse,
} from "./workers.ts";
const worker = {
runtime_id: "runtime-a",
worker_id: "worker-a",
host_id: "host-a",
display_name: "Worker A",
label: "worker-a",
profile: "builtin:coder",
singleton_key: null,
tags: [],
workspace: {
visibility: "workspace",
identity: "workspace-a",
workspace_id: "workspace-a",
},
state: "idle",
last_seen_at: null,
pinned: false,
retention_state: "active",
implementation: {
kind: "runtime",
display_hint: "Runtime Worker",
},
capabilities: {
can_stop: true,
can_spawn_followup: false,
},
diagnostics: [],
};
Deno.test("Worker launch options parser accepts the generated wire shape", () => {
const parsed = parseWorkerLaunchOptionsResponse({
workspace_id: "workspace-a",
runtimes: [{
runtime_id: "runtime-a",
display_name: "Runtime A",
built_in: false,
worker_creation_available: true,
working_directory_required: true,
status: "connected",
diagnostics: [],
}],
default_profile: null,
profiles: [{ id: "builtin:coder", label: "Coder", description: "Code" }],
repositories: [{ repository_key: "main" }],
working_directories: [],
diagnostics: [],
});
assertEquals(parsed.runtimes[0].runtime_id, "runtime-a");
assertEquals(parsed.repositories[0].default_selector, undefined);
});
Deno.test("Worker launch response parsers reject missing and unknown fields", () => {
assertThrows(
() =>
parseWorkerLaunchOptionsResponse({
workspace_id: "workspace-a",
runtimes: [],
profiles: [],
repositories: [],
working_directories: [],
diagnostics: [],
}),
Error,
"default_profile",
);
assertThrows(
() =>
parseBrowserCreateWorkerResponse({
workspace_id: "workspace-a",
runtime_id: "runtime-a",
worker_id: "worker-a",
console_href: "/workers/worker-a",
worker,
diagnostics: [],
unexpected: true,
}),
Error,
"unknown field unexpected",
);
assertThrows(
() =>
parseBrowserWorkspaceOrchestratorResponse({
workspace_id: "workspace-a",
online: false,
disposition: "missing",
diagnostics: [],
extra: false,
}),
Error,
"unknown field extra",
);
});
Deno.test("Worker create request parser requires the complete shared request", () => {
const request = {
runtime_id: "runtime-a",
display_name: "Worker A",
profile: "builtin:coder",
ticket_assignment: null,
initial_submit: [{ kind: "text", content: "Implement T-565." }],
working_directory: {
working_directory_id: "workdir-a",
relative_cwd: null,
},
control_operation_id: null,
};
assertEquals(parseCreateWorkspaceWorkerRequest(request), request);
assertThrows(
() =>
parseCreateWorkspaceWorkerRequest({
...request,
operation_id: "legacy-literal",
}),
Error,
"unknown field operation_id",
);
const { initial_submit: _initialSubmit, ...missingInitialSubmit } = request;
assertThrows(
() => parseCreateWorkspaceWorkerRequest(missingInitialSubmit),
Error,
"initial_submit",
);
});
@@ -0,0 +1,535 @@
import type {
BrowserCreateWorkerResponse,
BrowserWorkerWorkingDirectorySelection,
BrowserWorkspaceOrchestratorResponse,
CreateWorkspaceWorkerRequest,
CreateWorkspaceWorkerTicketAssignmentRequest,
Diagnostic,
DiagnosticSeverity,
RuntimeWorkingDirectoryCleanupTarget,
RuntimeWorkingDirectorySummary,
WorkerCapabilitySummary,
WorkerImplementationSummary,
WorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption,
WorkerLaunchWorkerSummary,
WorkerWorkspaceSummary,
WorkingDirectoryRepositoryOption,
} from "$lib/generated/worker-launch-api";
import type { Segment } from "$lib/generated/protocol";
import { parseWorkingDirectorySummary } from "$lib/workspace/api/workdirs";
const DIAGNOSTIC_SEVERITIES = new Set<DiagnosticSeverity>([
"info",
"warning",
"error",
]);
function record(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error(`${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exact(
value: Record<string, unknown>,
allowed: readonly string[],
label: string,
): void {
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
if (unexpected.length > 0) {
throw new Error(`${label} contains unknown field ${unexpected[0]}`);
}
}
function string(value: unknown, label: string): string {
if (typeof value !== "string") throw new Error(`${label} must be a string`);
return value;
}
function boolean(value: unknown, label: string): boolean {
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
return value;
}
function number(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`${label} must be a finite number`);
}
return value;
}
function nullableString(value: unknown, label: string): string | null {
return value === null ? null : string(value, label);
}
function array<T>(
value: unknown,
label: string,
parse: (item: unknown, label: string) => T,
): T[] {
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
return value.map((item, index) => parse(item, `${label}[${index}]`));
}
function optional<T>(
value: unknown,
label: string,
parse: (item: unknown, label: string) => T,
): T | null | undefined {
return value === undefined
? undefined
: value === null
? null
: parse(value, label);
}
function diagnostic(value: unknown, label: string): Diagnostic {
const item = record(value, label);
exact(item, ["code", "severity", "message"], label);
const severity = string(item.severity, `${label}.severity`);
if (!DIAGNOSTIC_SEVERITIES.has(severity as DiagnosticSeverity)) {
throw new Error(`${label}.severity is invalid`);
}
return {
code: string(item.code, `${label}.code`),
severity: severity as DiagnosticSeverity,
message: string(item.message, `${label}.message`),
};
}
function runtimeOption(
value: unknown,
label: string,
): WorkerLaunchRuntimeOption {
const item = record(value, label);
exact(
item,
[
"runtime_id",
"display_name",
"built_in",
"worker_creation_available",
"working_directory_required",
"status",
"diagnostics",
],
label,
);
return {
runtime_id: string(item.runtime_id, `${label}.runtime_id`),
display_name: string(item.display_name, `${label}.display_name`),
built_in: boolean(item.built_in, `${label}.built_in`),
worker_creation_available: boolean(
item.worker_creation_available,
`${label}.worker_creation_available`,
),
working_directory_required: boolean(
item.working_directory_required,
`${label}.working_directory_required`,
),
status: string(item.status, `${label}.status`),
diagnostics: array(item.diagnostics, `${label}.diagnostics`, diagnostic),
};
}
function profileCandidate(
value: unknown,
label: string,
): WorkerLaunchProfileCandidate {
const item = record(value, label);
exact(item, ["id", "label", "description"], label);
return {
id: string(item.id, `${label}.id`),
label: string(item.label, `${label}.label`),
description: string(item.description, `${label}.description`),
};
}
function repositoryOption(
value: unknown,
label: string,
): WorkingDirectoryRepositoryOption {
const item = record(value, label);
exact(item, ["repository_key", "default_selector"], label);
return {
repository_key: string(item.repository_key, `${label}.repository_key`),
default_selector: optional(
item.default_selector,
`${label}.default_selector`,
string,
),
};
}
export function parseWorkerLaunchOptionsResponse(
value: unknown,
): WorkerLaunchOptionsResponse {
const item = record(value, "Worker launch options response");
exact(
item,
[
"workspace_id",
"runtimes",
"default_profile",
"profiles",
"repositories",
"working_directories",
"diagnostics",
],
"Worker launch options response",
);
return {
workspace_id: string(item.workspace_id, "workspace_id"),
runtimes: array(item.runtimes, "runtimes", runtimeOption),
default_profile: nullableString(item.default_profile, "default_profile"),
profiles: array(item.profiles, "profiles", profileCandidate),
repositories: array(item.repositories, "repositories", repositoryOption),
working_directories: array(
item.working_directories,
"working_directories",
parseWorkingDirectorySummary,
),
diagnostics: array(item.diagnostics, "diagnostics", diagnostic),
};
}
function workspaceSummary(
value: unknown,
label: string,
): WorkerWorkspaceSummary {
const item = record(value, label);
exact(item, ["visibility", "identity", "workspace_id"], label);
return {
visibility: string(item.visibility, `${label}.visibility`),
identity: string(item.identity, `${label}.identity`),
workspace_id: optional(item.workspace_id, `${label}.workspace_id`, string),
};
}
function implementationSummary(
value: unknown,
label: string,
): WorkerImplementationSummary {
const item = record(value, label);
exact(item, ["kind", "display_hint"], label);
return {
kind: string(item.kind, `${label}.kind`),
display_hint: string(item.display_hint, `${label}.display_hint`),
};
}
function capabilitySummary(
value: unknown,
label: string,
): WorkerCapabilitySummary {
const item = record(value, label);
exact(item, ["can_stop", "can_spawn_followup"], label);
return {
can_stop: boolean(item.can_stop, `${label}.can_stop`),
can_spawn_followup: boolean(
item.can_spawn_followup,
`${label}.can_spawn_followup`,
),
};
}
function runtimeCleanupTarget(
value: unknown,
label: string,
): RuntimeWorkingDirectoryCleanupTarget {
const item = record(value, label);
exact(item, ["kind", "working_directory_id", "repository_id"], label);
return {
kind: string(item.kind, `${label}.kind`),
working_directory_id: string(
item.working_directory_id,
`${label}.working_directory_id`,
),
repository_id: string(item.repository_id, `${label}.repository_id`),
};
}
function runtimeWorkingDirectory(
value: unknown,
label: string,
): RuntimeWorkingDirectorySummary {
const item = record(value, label);
exact(
item,
[
"working_directory_id",
"repository_id",
"creation_selector",
"creation_ref",
"creation_tree",
"current_selector",
"current_ref",
"current_tree",
"observed_at_epoch_seconds",
"materializer_kind",
"cleanup_target",
"status",
"cleanliness",
"primary_worker_id",
"occupied_by",
],
label,
);
const materializerKind = string(
item.materializer_kind,
`${label}.materializer_kind`,
);
if (
materializerKind !== "runtime_git_cache" &&
materializerKind !== "local_git_worktree"
) {
throw new Error(`${label}.materializer_kind is invalid`);
}
const status = string(item.status, `${label}.status`);
if (
!["active", "cleanup_pending", "corrupted", "not_found", "unknown"]
.includes(status)
) {
throw new Error(`${label}.status is invalid`);
}
const occupied = optional(
item.occupied_by,
`${label}.occupied_by`,
(value, occupiedLabel) => {
const occupancy = record(value, occupiedLabel);
exact(
occupancy,
["runtime_id", "worker_id", "display_name", "linked_at"],
occupiedLabel,
);
return {
runtime_id: string(occupancy.runtime_id, `${occupiedLabel}.runtime_id`),
worker_id: string(occupancy.worker_id, `${occupiedLabel}.worker_id`),
display_name: string(
occupancy.display_name,
`${occupiedLabel}.display_name`,
),
linked_at: string(occupancy.linked_at, `${occupiedLabel}.linked_at`),
};
},
);
return {
working_directory_id: string(
item.working_directory_id,
`${label}.working_directory_id`,
),
repository_id: string(item.repository_id, `${label}.repository_id`),
creation_selector: optional(
item.creation_selector,
`${label}.creation_selector`,
string,
),
creation_ref: optional(item.creation_ref, `${label}.creation_ref`, string),
creation_tree: optional(
item.creation_tree,
`${label}.creation_tree`,
string,
),
current_selector: optional(
item.current_selector,
`${label}.current_selector`,
string,
),
current_ref: optional(item.current_ref, `${label}.current_ref`, string),
current_tree: optional(item.current_tree, `${label}.current_tree`, string),
observed_at_epoch_seconds: optional(
item.observed_at_epoch_seconds,
`${label}.observed_at_epoch_seconds`,
number,
),
materializer_kind: materializerKind,
cleanup_target: optional(
item.cleanup_target,
`${label}.cleanup_target`,
runtimeCleanupTarget,
),
status: status as RuntimeWorkingDirectorySummary["status"],
cleanliness: optional(item.cleanliness, `${label}.cleanliness`, string),
primary_worker_id: optional(
item.primary_worker_id,
`${label}.primary_worker_id`,
string,
),
occupied_by: occupied,
};
}
function workerSummary(
value: unknown,
label: string,
): WorkerLaunchWorkerSummary {
const item = record(value, label);
exact(
item,
[
"runtime_id",
"worker_id",
"host_id",
"display_name",
"label",
"profile",
"singleton_key",
"tags",
"workspace",
"state",
"last_seen_at",
"pinned",
"retention_state",
"implementation",
"capabilities",
"working_directory",
"diagnostics",
],
label,
);
return {
runtime_id: string(item.runtime_id, `${label}.runtime_id`),
worker_id: string(item.worker_id, `${label}.worker_id`),
host_id: string(item.host_id, `${label}.host_id`),
display_name: string(item.display_name, `${label}.display_name`),
label: string(item.label, `${label}.label`),
profile: nullableString(item.profile, `${label}.profile`),
singleton_key: nullableString(item.singleton_key, `${label}.singleton_key`),
tags: array(item.tags, `${label}.tags`, string),
workspace: workspaceSummary(item.workspace, `${label}.workspace`),
state: string(item.state, `${label}.state`),
last_seen_at: nullableString(item.last_seen_at, `${label}.last_seen_at`),
pinned: boolean(item.pinned, `${label}.pinned`),
retention_state: string(item.retention_state, `${label}.retention_state`),
implementation: implementationSummary(
item.implementation,
`${label}.implementation`,
),
capabilities: capabilitySummary(item.capabilities, `${label}.capabilities`),
working_directory: optional(
item.working_directory,
`${label}.working_directory`,
runtimeWorkingDirectory,
),
diagnostics: array(item.diagnostics, `${label}.diagnostics`, diagnostic),
};
}
export function parseBrowserCreateWorkerResponse(
value: unknown,
): BrowserCreateWorkerResponse {
const item = record(value, "Worker create response");
exact(
item,
[
"workspace_id",
"runtime_id",
"worker_id",
"console_href",
"worker",
"diagnostics",
],
"Worker create response",
);
return {
workspace_id: string(item.workspace_id, "workspace_id"),
runtime_id: string(item.runtime_id, "runtime_id"),
worker_id: string(item.worker_id, "worker_id"),
console_href: string(item.console_href, "console_href"),
worker: workerSummary(item.worker, "worker"),
diagnostics: array(item.diagnostics, "diagnostics", diagnostic),
};
}
export function parseBrowserWorkspaceOrchestratorResponse(
value: unknown,
): BrowserWorkspaceOrchestratorResponse {
const item = record(value, "Workspace Orchestrator response");
exact(
item,
["workspace_id", "online", "disposition", "worker", "diagnostics"],
"Workspace Orchestrator response",
);
return {
workspace_id: string(item.workspace_id, "workspace_id"),
online: boolean(item.online, "online"),
disposition: string(item.disposition, "disposition"),
worker: optional(item.worker, "worker", workerSummary),
diagnostics: array(item.diagnostics, "diagnostics", diagnostic),
};
}
function workingDirectorySelection(
value: unknown,
label: string,
): BrowserWorkerWorkingDirectorySelection {
const item = record(value, label);
exact(item, ["working_directory_id", "relative_cwd"], label);
return {
working_directory_id: string(
item.working_directory_id,
`${label}.working_directory_id`,
),
relative_cwd: nullableString(item.relative_cwd, `${label}.relative_cwd`),
};
}
function ticketAssignment(
value: unknown,
label: string,
): CreateWorkspaceWorkerTicketAssignmentRequest {
const item = record(value, label);
exact(item, ["ticket_id", "operation_id"], label);
return {
ticket_id: string(item.ticket_id, `${label}.ticket_id`),
operation_id: string(item.operation_id, `${label}.operation_id`),
};
}
function segment(value: unknown, label: string): Segment {
const item = record(value, label);
const kind = string(item.kind, `${label}.kind`);
if (kind === "text") {
exact(item, ["kind", "content"], label);
return { kind, content: string(item.content, `${label}.content`) };
}
throw new Error(`${label}.kind is not supported by Worker creation`);
}
export function parseCreateWorkspaceWorkerRequest(
value: unknown,
): CreateWorkspaceWorkerRequest {
const item = record(value, "Worker create request");
exact(
item,
[
"runtime_id",
"display_name",
"profile",
"ticket_assignment",
"initial_submit",
"working_directory",
"control_operation_id",
],
"Worker create request",
);
return {
runtime_id: string(item.runtime_id, "runtime_id"),
display_name: string(item.display_name, "display_name"),
profile: nullableString(item.profile, "profile"),
ticket_assignment: item.ticket_assignment === null
? null
: ticketAssignment(item.ticket_assignment, "ticket_assignment"),
initial_submit: array(item.initial_submit, "initial_submit", segment),
working_directory: item.working_directory === null
? null
: workingDirectorySelection(item.working_directory, "working_directory"),
control_operation_id: nullableString(
item.control_operation_id,
"control_operation_id",
),
};
}
@@ -1,3 +1,12 @@
import type {
BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse,
BrowserWorkerWorkingDirectorySelection
as SharedBrowserWorkerWorkingDirectorySelection,
WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption,
WorkingDirectoryRepositoryOption as SharedWorkingDirectoryRepositoryOption,
} from "$lib/generated/worker-launch-api";
import type { import type {
WorkingDirectoryCreateRequest, WorkingDirectoryCreateRequest,
WorkingDirectoryCreateResponse, WorkingDirectoryCreateResponse,
@@ -101,26 +110,10 @@ export type Worker = {
export type WorkerOperationState = "accepted" | "unsupported" | "rejected"; export type WorkerOperationState = "accepted" | "unsupported" | "rejected";
export type WorkerLaunchRuntimeOption = { export type WorkerLaunchRuntimeOption = SharedWorkerLaunchRuntimeOption;
runtime_id: string; export type WorkerLaunchProfileCandidate = SharedWorkerLaunchProfileCandidate;
display_name: string; export type WorkingDirectoryRepositoryOption =
built_in: boolean; SharedWorkingDirectoryRepositoryOption;
worker_creation_available: boolean;
working_directory_required: boolean;
status: string;
diagnostics: Diagnostic[];
};
export type WorkerLaunchProfileCandidate = {
id: string;
label: string;
description: string;
};
export type WorkingDirectoryRepositoryOption = {
repository_key: string;
default_selector?: string | null;
};
export type CleanupTargetKind = export type CleanupTargetKind =
| "worker_delete" | "worker_delete"
@@ -185,29 +178,10 @@ export type RuntimeCleanupExecutionResponse = {
diagnostics: Diagnostic[]; diagnostics: Diagnostic[];
}; };
export type BrowserWorkerWorkingDirectorySelection = { export type BrowserWorkerWorkingDirectorySelection =
working_directory_id: string; SharedBrowserWorkerWorkingDirectorySelection;
relative_cwd?: string | null; export type WorkerLaunchOptionsResponse = SharedWorkerLaunchOptionsResponse;
}; export type BrowserCreateWorkerResponse = SharedBrowserCreateWorkerResponse;
export type WorkerLaunchOptionsResponse = {
workspace_id: string;
runtimes: WorkerLaunchRuntimeOption[];
default_profile?: string | null;
profiles: WorkerLaunchProfileCandidate[];
repositories: WorkingDirectoryRepositoryOption[];
working_directories: WorkingDirectorySummary[];
diagnostics: Diagnostic[];
};
export type BrowserCreateWorkerResponse = {
workspace_id: string;
runtime_id: string;
worker_id: string;
console_href: string;
worker: Worker;
diagnostics: Diagnostic[];
};
export type WorkerInputResult = { export type WorkerInputResult = {
state: WorkerOperationState; state: WorkerOperationState;
@@ -196,11 +196,13 @@ Deno.test("buildCreateWorkspaceWorkerRequest sends working_directory id and rela
runtime_id: "embedded", runtime_id: "embedded",
display_name: "Worker", display_name: "Worker",
profile: "builtin:coder", profile: "builtin:coder",
ticket_assignment: null,
initial_submit: [{ kind: "text", content: "go" }], initial_submit: [{ kind: "text", content: "go" }],
working_directory: { working_directory: {
working_directory_id: "wd-1-repo", working_directory_id: "wd-1-repo",
relative_cwd: "crates/yoi", relative_cwd: "crates/yoi",
}, },
control_operation_id: null,
}); });
}); });
@@ -219,7 +221,7 @@ Deno.test("buildCreateWorkspaceWorkerRequest sends no initial segments for an em
assertEquals(request.initial_submit, []); assertEquals(request.initial_submit, []);
}); });
Deno.test("buildCreateWorkspaceWorkerRequest omits working_directory for embedded no-workdir launches", () => { Deno.test("buildCreateWorkspaceWorkerRequest emits null for embedded no-workdir launches", () => {
const request = buildCreateWorkspaceWorkerRequest({ const request = buildCreateWorkspaceWorkerRequest({
runtime_id: "embedded", runtime_id: "embedded",
display_name: "Worker", display_name: "Worker",
@@ -235,6 +237,9 @@ Deno.test("buildCreateWorkspaceWorkerRequest omits working_directory for embedde
runtime_id: "embedded", runtime_id: "embedded",
display_name: "Worker", display_name: "Worker",
profile: "builtin:companion", profile: "builtin:companion",
ticket_assignment: null,
initial_submit: [{ kind: "text", content: "chat" }], initial_submit: [{ kind: "text", content: "chat" }],
working_directory: null,
control_operation_id: null,
}); });
}); });
@@ -1,9 +1,7 @@
import type { Segment } from "$lib/generated/protocol"; import type { CreateWorkspaceWorkerRequest } from "$lib/generated/worker-launch-api";
import { parseCreateWorkspaceWorkerRequest } from "$lib/workspace/api/workers";
import type { import type { WorkerLaunchOptionsResponse } from "./types";
BrowserWorkerWorkingDirectorySelection,
WorkerLaunchOptionsResponse,
} from "./types";
export type WorkerLaunchFormState = { export type WorkerLaunchFormState = {
runtime_id: string; runtime_id: string;
@@ -16,14 +14,6 @@ export type WorkerLaunchFormState = {
relative_cwd: string; relative_cwd: string;
}; };
export type CreateWorkspaceWorkerRequest = {
runtime_id: string;
display_name: string;
profile: string;
initial_submit: Segment[];
working_directory?: BrowserWorkerWorkingDirectorySelection;
};
export function defaultWorkerLaunchForm( export function defaultWorkerLaunchForm(
options: WorkerLaunchOptionsResponse | null, options: WorkerLaunchOptionsResponse | null,
current: WorkerLaunchFormState, current: WorkerLaunchFormState,
@@ -86,7 +76,8 @@ export function defaultWorkerLaunchForm(
) )
? current.working_directory_id ? current.working_directory_id
: preferredWorkingDirectory?.working_directory_id || "", : preferredWorkingDirectory?.working_directory_id || "",
working_directory_repository_key: current.working_directory_repository_key || working_directory_repository_key:
current.working_directory_repository_key ||
preferredRepository?.repository_key || "", preferredRepository?.repository_key || "",
working_directory_selector: current.working_directory_selector || working_directory_selector: current.working_directory_selector ||
preferredRepository?.default_selector || "HEAD", preferredRepository?.default_selector || "HEAD",
@@ -97,22 +88,21 @@ export function defaultWorkerLaunchForm(
export function buildCreateWorkspaceWorkerRequest( export function buildCreateWorkspaceWorkerRequest(
form: WorkerLaunchFormState, form: WorkerLaunchFormState,
): CreateWorkspaceWorkerRequest { ): CreateWorkspaceWorkerRequest {
const request: CreateWorkspaceWorkerRequest = { const initialMessage = form.initial_text.trim();
runtime_id: form.runtime_id, return parseCreateWorkspaceWorkerRequest({
display_name: form.display_name, runtime_id: form.runtime_id.trim(),
profile: form.profile, display_name: form.display_name.trim(),
initial_submit: form.initial_text.trim() profile: form.profile.trim() || null,
ticket_assignment: null,
initial_submit: initialMessage
? [{ kind: "text", content: form.initial_text }] ? [{ kind: "text", content: form.initial_text }]
: [], : [],
}; working_directory: form.working_directory_id
if (form.working_directory_id) { ? {
request.working_directory = { working_directory_id: form.working_directory_id,
working_directory_id: form.working_directory_id, relative_cwd: form.relative_cwd.trim() || null,
}; }
const relativeCwd = form.relative_cwd.trim(); : null,
if (relativeCwd) { control_operation_id: null,
request.working_directory.relative_cwd = relativeCwd; });
}
}
return request;
} }
@@ -1,3 +1,4 @@
import type { BrowserWorkspaceOrchestratorResponse } from "$lib/generated/worker-launch-api";
import type { TicketDetail, TicketSummary } from "$lib/generated/ticket-api"; import type { TicketDetail, TicketSummary } from "$lib/generated/ticket-api";
export const TICKET_STATES = [ export const TICKET_STATES = [
@@ -12,22 +13,7 @@ export const TICKET_STATES = [
export type TicketState = (typeof TICKET_STATES)[number]; export type TicketState = (typeof TICKET_STATES)[number];
export type TicketWorkerRole = "coder" | "reviewer"; export type TicketWorkerRole = "coder" | "reviewer";
export type WorkspaceOrchestratorStatus = { export type WorkspaceOrchestratorStatus = BrowserWorkspaceOrchestratorResponse;
workspace_id: string;
online: boolean;
disposition: string;
worker?: {
runtime_id: string;
worker_id: string;
state: string;
display_name: string;
} | null;
diagnostics: Array<{
code: string;
severity: string;
message: string;
}>;
};
const LANE_DEFINITIONS = [ const LANE_DEFINITIONS = [
{ {
@@ -2,6 +2,7 @@
import { untrack } from "svelte"; import { untrack } from "svelte";
import type { ApiResult } from "$lib/workspace/api/http"; import type { ApiResult } from "$lib/workspace/api/http";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseBrowserWorkspaceOrchestratorResponse } from "$lib/workspace/api/workers";
import type { import type {
QueryPage, QueryPage,
TicketListResponse, TicketListResponse,
@@ -99,6 +100,7 @@
fetch, fetch,
workspaceApiPath(data.workspaceId, "/orchestrator"), workspaceApiPath(data.workspaceId, "/orchestrator"),
{ method: "POST" }, { method: "POST" },
parseBrowserWorkspaceOrchestratorResponse,
); );
orchestratorStarting = false; orchestratorStarting = false;
} }
@@ -1,3 +1,4 @@
import { parseBrowserWorkspaceOrchestratorResponse } from "$lib/workspace/api/workers";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import type { TicketListResponse } from "$lib/generated/ticket-api"; import type { TicketListResponse } from "$lib/generated/ticket-api";
import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel"; import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel";
@@ -42,6 +43,8 @@ export const load: PageLoad = async ({ fetch, params }) => {
loadJson<WorkspaceOrchestratorStatus>( loadJson<WorkspaceOrchestratorStatus>(
fetch, fetch,
workspaceApiPath(workspaceId, "/orchestrator"), workspaceApiPath(workspaceId, "/orchestrator"),
undefined,
parseBrowserWorkspaceOrchestratorResponse,
), ),
]); ]);
@@ -6,10 +6,13 @@
parseWorkingDirectoryCreateResponse, parseWorkingDirectoryCreateResponse,
validateWorkingDirectoryCreateRequest, validateWorkingDirectoryCreateRequest,
} from '$lib/workspace/api/workdirs'; } from '$lib/workspace/api/workdirs';
import {
parseBrowserCreateWorkerResponse,
parseWorkerLaunchOptionsResponse,
} from '$lib/workspace/api/workers';
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision'; import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
import { buildCreateWorkspaceWorkerRequest, defaultWorkerLaunchForm } from '$lib/workspace/sidebar/worker-launch'; import { buildCreateWorkspaceWorkerRequest, defaultWorkerLaunchForm } from '$lib/workspace/sidebar/worker-launch';
import type { import type {
BrowserCreateWorkerResponse,
Diagnostic, Diagnostic,
WorkerLaunchOptionsResponse, WorkerLaunchOptionsResponse,
WorkingDirectorySummary, WorkingDirectorySummary,
@@ -115,7 +118,7 @@
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 = (await response.json()) as WorkerLaunchOptionsResponse; const payload = parseWorkerLaunchOptionsResponse(await response.json());
options = payload; options = payload;
const form = defaultWorkerLaunchForm(payload, { const form = defaultWorkerLaunchForm(payload, {
runtime_id: runtimeId, runtime_id: runtimeId,
@@ -229,7 +232,7 @@
submitError = await responseDisplayError(response, 'worker create failed'); submitError = await responseDisplayError(response, 'worker create failed');
return; return;
} }
const payload = (await response.json()) as BrowserCreateWorkerResponse; const payload = parseBrowserCreateWorkerResponse(await response.json());
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');