From c6cb0386835678e99c8043884ca3b9f5b0598f56 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 18 Sep 2026 06:21:46 +0900 Subject: [PATCH] refactor(api): rename workspace-api authority to server-api Absorb the Server HTTP DTO catalog and generators into server-api, remove the obsolete workspace-api package/path, preserve the separate runtime transport authority through explicit boundary DTO conversions, and rename the aggregate frontend projection as a bounded legacy artifact. --- Cargo.lock | 26 +- Cargo.toml | 3 - crates/client/Cargo.toml | 1 - crates/client/src/backend_auth.rs | 6 +- crates/client/src/backend_runtime.rs | 10 +- crates/client/src/backend_workspace.rs | 4 +- crates/client/src/lib.rs | 12 +- crates/client/src/workspace_product.rs | 18 +- crates/runtime-api/Cargo.toml | 1 - crates/runtime-api/src/lib.rs | 79 +- crates/server-api/Cargo.toml | 47 +- .../examples/generate_auth_api_types.rs | 6 + .../examples/generate_companion_api_types.rs | 6 + .../examples/generate_legacy_typescript.rs | 6 + .../examples/generate_memory_api_types.rs | 6 + .../generate_repository_access_types.rs | 6 + .../examples/generate_skill_api_types.rs | 6 + .../examples/generate_workdir_api_types.rs | 6 + .../generate_worker_launch_api_types.rs | 6 + crates/server-api/src/lib.rs | 4979 ++++++++++++++++- crates/workdir/Cargo.toml | 1 - crates/workdir/src/workspace.rs | 174 +- crates/worker-runtime/Cargo.toml | 2 +- crates/worker-runtime/src/catalog.rs | 4 +- crates/worker-runtime/src/http_server.rs | 2 +- .../src/http_server/runtime_management_api.rs | 15 +- crates/worker-runtime/src/main.rs | 2 +- crates/worker-runtime/src/runtime.rs | 16 +- crates/worker-runtime/src/worker_backend.rs | 4 +- crates/worker-runtime/src/worker_source.rs | 2 +- .../worker-runtime/src/working_directory.rs | 90 +- crates/worker-runtime/src/workspace_issuer.rs | 2 +- crates/worker/Cargo.toml | 2 +- .../src/feature/builtin/manage_workdir.rs | 2 +- .../builtin/workspace_worker_discovery.rs | 2 +- crates/worker/src/skill.rs | 4 +- crates/worker/src/worker.rs | 2 +- crates/workspace-api/Cargo.toml | 51 - .../examples/generate_auth_api_types.rs | 3 - .../examples/generate_companion_api_types.rs | 3 - .../examples/generate_memory_api_types.rs | 3 - .../generate_repository_access_types.rs | 3 - .../examples/generate_skill_api_types.rs | 3 - .../examples/generate_typescript.rs | 3 - .../examples/generate_workdir_api_types.rs | 3 - .../generate_worker_launch_api_types.rs | 3 - crates/workspace-api/src/lib.rs | 4872 ---------------- crates/workspace-server/Cargo.toml | 3 +- crates/workspace-server/src/auth.rs | 2 +- crates/workspace-server/src/companion.rs | 4 +- crates/workspace-server/src/hosts.rs | 67 +- crates/workspace-server/src/lib.rs | 4 +- crates/workspace-server/src/memory_staging.rs | 2 +- .../workspace-server/src/profile_settings.rs | 8 +- crates/workspace-server/src/records.rs | 2 +- crates/workspace-server/src/repositories.rs | 13 +- .../workspace-server/src/repository_access.rs | 20 +- .../workspace-server/src/repository_source.rs | 2 +- crates/workspace-server/src/server.rs | 460 +- crates/workspace-server/src/skills.rs | 4 +- crates/workspace-server/src/store.rs | 28 +- .../src/workdir_create_operations.rs | 6 +- .../workspace-server/src/workdir_removal.rs | 2 +- .../workspace-server/src/workspace_catalog.rs | 8 +- .../src/workspace_deletion.rs | 6 +- crates/yoi/Cargo.toml | 2 +- crates/yoi/src/objective_cli.rs | 2 +- crates/yoi/src/workspace_bootstrap.rs | 33 +- web/workspace/src/lib/generated/README.md | 19 + web/workspace/src/lib/generated/auth-api.ts | 101 +- .../src/lib/generated/companion-api.ts | 56 +- .../src/lib/generated/legacy-server-api.ts | 161 + web/workspace/src/lib/generated/memory-api.ts | 100 +- .../lib/generated/repository-access-api.ts | 93 +- web/workspace/src/lib/generated/skill-api.ts | 65 +- .../src/lib/generated/workdir-api.ts | 75 +- .../src/lib/generated/worker-launch-api.ts | 183 +- .../src/lib/generated/workspace-api.ts | 552 -- .../lib/workspace/api/runtime-connection.ts | 2 +- .../lib/workspace/api/runtime-management.ts | 2 +- .../src/lib/workspace/api/workspace-model.ts | 4 +- .../workspace/repositories/ssh-connection.ts | 2 +- .../src/lib/workspace/settings/model.ts | 4 +- .../src/lib/workspace/settings/profile-api.ts | 2 +- .../settings/workspace-deletion-api.ts | 2 +- .../repositories/[repositoryKey]/+page.svelte | 2 +- .../w/[workspaceId]/settings/+page.svelte | 2 +- .../settings/profiles/+page.svelte | 2 +- .../settings/repositories/+page.svelte | 2 +- .../settings/runtimes/+page.svelte | 2 +- .../runtimes/[runtimeId]/+page.svelte | 2 +- .../test/repository-access/ui.test.ts | 2 +- .../test/repository-ssh-connection-ui.test.ts | 2 +- 93 files changed, 6062 insertions(+), 6562 deletions(-) create mode 100644 crates/server-api/examples/generate_auth_api_types.rs create mode 100644 crates/server-api/examples/generate_companion_api_types.rs create mode 100644 crates/server-api/examples/generate_legacy_typescript.rs create mode 100644 crates/server-api/examples/generate_memory_api_types.rs create mode 100644 crates/server-api/examples/generate_repository_access_types.rs create mode 100644 crates/server-api/examples/generate_skill_api_types.rs create mode 100644 crates/server-api/examples/generate_workdir_api_types.rs create mode 100644 crates/server-api/examples/generate_worker_launch_api_types.rs delete mode 100644 crates/workspace-api/Cargo.toml delete mode 100644 crates/workspace-api/examples/generate_auth_api_types.rs delete mode 100644 crates/workspace-api/examples/generate_companion_api_types.rs delete mode 100644 crates/workspace-api/examples/generate_memory_api_types.rs delete mode 100644 crates/workspace-api/examples/generate_repository_access_types.rs delete mode 100644 crates/workspace-api/examples/generate_skill_api_types.rs delete mode 100644 crates/workspace-api/examples/generate_typescript.rs delete mode 100644 crates/workspace-api/examples/generate_workdir_api_types.rs delete mode 100644 crates/workspace-api/examples/generate_worker_launch_api_types.rs delete mode 100644 crates/workspace-api/src/lib.rs create mode 100644 web/workspace/src/lib/generated/README.md create mode 100644 web/workspace/src/lib/generated/legacy-server-api.ts delete mode 100644 web/workspace/src/lib/generated/workspace-api.ts diff --git a/Cargo.lock b/Cargo.lock index 21139f8db..afe3e5eef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -660,7 +660,6 @@ dependencies = [ "tokio", "tokio-tungstenite 0.29.0", "uuid", - "workspace-api", ] [[package]] @@ -3719,7 +3718,6 @@ dependencies = [ "serde_json", "tokio", "workdir", - "workspace-api", ] [[package]] @@ -4136,11 +4134,14 @@ name = "server-api" version = "0.1.0" dependencies = [ "api-macros", + "protocol", "runtime-api", "serde", "serde_json", "tokio", - "workspace-api", + "ts-rs", + "webauthn-rs-proto", + "workdir", ] [[package]] @@ -6090,7 +6091,6 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", - "workspace-api", ] [[package]] @@ -6123,6 +6123,7 @@ dependencies = [ "serde", "serde_json", "serial_test", + "server-api", "session-metrics", "session-store", "sha2 0.11.0", @@ -6137,7 +6138,6 @@ dependencies = [ "tungstenite 0.28.0", "uuid", "workdir", - "workspace-api", ] [[package]] @@ -6160,6 +6160,7 @@ dependencies = [ "serde", "serde_json", "serial_test", + "server-api", "session-store", "sha2 0.11.0", "tar", @@ -6175,21 +6176,9 @@ dependencies = [ "uuid", "workdir", "worker", - "workspace-api", "zeroize", ] -[[package]] -name = "workspace-api" -version = "0.1.0" -dependencies = [ - "protocol", - "serde", - "serde_json", - "ts-rs", - "webauthn-rs-proto", -] - [[package]] name = "writeable" version = "0.6.3" @@ -6252,6 +6241,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "server-api", "session-analytics", "session-store", "tempfile", @@ -6261,7 +6251,6 @@ dependencies = [ "tui", "uuid", "worker", - "workspace-api", ] [[package]] @@ -6327,7 +6316,6 @@ dependencies = [ "workdir", "worker", "worker-runtime", - "workspace-api", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index bd739b6be..9c284b62e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ members = [ "crates/ticket", "crates/merge-request", "crates/project-record", - "crates/workspace-api", "crates/workspace-server", "tests/e2e", ] @@ -68,7 +67,6 @@ default-members = [ "crates/ticket", "crates/merge-request", "crates/project-record", - "crates/workspace-api", "crates/workspace-server", ] @@ -97,7 +95,6 @@ ticket = { path = "crates/ticket" } project-record = { path = "crates/project-record" } worker = { path = "crates/worker" } worker-runtime = { path = "crates/worker-runtime" } -workspace-api = { path = "crates/workspace-api" } yoi-plugin-pdk = { path = "crates/plugin-pdk" } yoi = { path = "crates/yoi" } protocol = { path = "crates/protocol" } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 1e7411bab..81f695412 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -19,7 +19,6 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync", "time"] } tokio-tungstenite = { workspace = true } uuid = { workspace = true } -workspace-api.workspace = true [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/client/src/backend_auth.rs b/crates/client/src/backend_auth.rs index 1237c4733..9d0f51ba0 100644 --- a/crates/client/src/backend_auth.rs +++ b/crates/client/src/backend_auth.rs @@ -3,8 +3,8 @@ use serde::Deserialize; use std::fmt; use std::time::Duration; -use workspace_api::{DeviceLoginPollRequest, DeviceLoginPollStatus, DeviceLoginStartRequest}; -pub use workspace_api::{DeviceLoginPollResponse, DeviceLoginStartResponse}; +use server_api::{DeviceLoginPollRequest, DeviceLoginPollStatus, DeviceLoginStartRequest}; +pub use server_api::{DeviceLoginPollResponse, DeviceLoginStartResponse}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct BackendAuthTarget { @@ -153,7 +153,7 @@ async fn parse_json_response Deserialize<'de>>( #[cfg(test)] mod tests { use super::*; - use workspace_api::DeviceAccessTokenType; + use server_api::DeviceAccessTokenType; fn poll_response(status: DeviceLoginPollStatus) -> DeviceLoginPollResponse { DeviceLoginPollResponse { diff --git a/crates/client/src/backend_runtime.rs b/crates/client/src/backend_runtime.rs index 3d61f8966..77c352827 100644 --- a/crates/client/src/backend_runtime.rs +++ b/crates/client/src/backend_runtime.rs @@ -2,11 +2,7 @@ use crate::transport::websocket::{Socket as WebSocket, SocketError as WebSocketE use crate::{BackendApiClient, BackendApiClientError, Client}; use reqwest::Method as HttpMethod; use serde::Deserialize; -use std::fmt; -use tokio_tungstenite::tungstenite::client::IntoClientRequest; -use tokio_tungstenite::tungstenite::http::HeaderValue; -use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION; -pub use workspace_api::{ +pub use server_api::{ BrowserCreateWorkerResponse as BackendCreateWorkerResponse, CreateWorkspaceWorkerRequest as BackendCreateWorkerRequest, Diagnostic as BackendDiagnostic, DiagnosticSeverity as BackendDiagnosticSeverity, ListResponse as BackendRuntimeListResponse, @@ -27,6 +23,10 @@ pub use workspace_api::{ WorkingDirectoryListResponse as BackendWorkingDirectoryListResponse, WorkingDirectorySummary as BackendWorkingDirectorySummary, }; +use std::fmt; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tokio_tungstenite::tungstenite::http::HeaderValue; +use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION; #[derive(Debug, Clone, PartialEq)] pub struct BackendRuntimeTarget { diff --git a/crates/client/src/backend_workspace.rs b/crates/client/src/backend_workspace.rs index 60edbc7a1..2673b0aba 100644 --- a/crates/client/src/backend_workspace.rs +++ b/crates/client/src/backend_workspace.rs @@ -1,11 +1,11 @@ use crate::{BackendApiClient, BackendApiClientError}; use reqwest::Method; -use std::fmt; -use workspace_api::{ +use server_api::{ InitialRepositoryIntent, RepositoryListResponse, RepositorySummary, WorkspaceCatalogListResponse, WorkspaceCreateRequest, WorkspaceCreateResponse, WorkspaceSummary, }; +use std::fmt; const DEFAULT_WORKSPACE_LIMIT: usize = 200; diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 64d2b25f1..554f88c0d 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -43,16 +43,16 @@ pub use client::{Client, ClientError}; pub use runtime_api::{ RetainedSessionIdentity, WorkerSessionAvailability, WorkerSessionUnavailableReason, }; +pub use server_api::{ + CompanionCancelRequest, CompanionLifecycleState, CompanionMessageDisposition, + CompanionMessageRequest, CompanionMessageResponse, CompanionStatusResponse, + CompanionTranscriptItem, CompanionTranscriptProjection, CompanionTranscriptRole, + CompanionTransportSummary, ObjectiveDetail, ObjectiveSummary, +}; pub use server_api::{ServerApi, ServerApiClient, WorkspaceWorkerSessionResponse}; pub use target::{ BackendTarget, BackendWorkerLaunch, Dashboard, ResolvedTarget, StandaloneTarget, StandaloneWorkerListIntent, StandaloneWorkerResumeIntent, Target, TargetError, TargetKind, WorkerConnection, WorkerConnectionSelector, WorkerList, WorkerListRequest, WorkerSpawn, }; -pub use workspace_api::{ - CompanionCancelRequest, CompanionLifecycleState, CompanionMessageDisposition, - CompanionMessageRequest, CompanionMessageResponse, CompanionStatusResponse, - CompanionTranscriptItem, CompanionTranscriptProjection, CompanionTranscriptRole, - CompanionTransportSummary, ObjectiveDetail, ObjectiveSummary, -}; pub use workspace_product::BackendWorkspaceProductClient; diff --git a/crates/client/src/workspace_product.rs b/crates/client/src/workspace_product.rs index 030115b2c..4ba758f17 100644 --- a/crates/client/src/workspace_product.rs +++ b/crates/client/src/workspace_product.rs @@ -1,14 +1,7 @@ use reqwest::Method; use serde::Serialize; use serde::de::DeserializeOwned; -use ticket::{ - MarkdownText, NewOrchestrationPlanRecord, NewTicket, NewTicketEvent, NewTicketRelation, - OrchestrationPlanKind, OrchestrationPlanRecord, Ticket, TicketBackend, TicketDependencyCheck, - TicketDoctorReport, TicketError, TicketIdOrSlug, TicketIntakeSummary, TicketItemEdit, - TicketListQuery, TicketListState, TicketMarkReady, TicketRef, TicketRelation, - TicketRelationKind, TicketRelationView, TicketStateChange, TicketStateSelector, TicketSummary, -}; -use workspace_api::{ +use server_api::{ BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse, CreateWorkspaceWorkerRequest, ListResponse, MemoryDocumentResponse, MemoryStagingListResponse, ObjectiveCreateRequest, ObjectiveDetail, ObjectiveEditRequest, ObjectiveLinkTicketRequest, @@ -17,6 +10,13 @@ use workspace_api::{ TICKET_RELATIONS_QUERY_PATH, WorkerLaunchOptionsResponse, WorkspaceRuntimeDetail, WorkspaceRuntimeResource, }; +use ticket::{ + MarkdownText, NewOrchestrationPlanRecord, NewTicket, NewTicketEvent, NewTicketRelation, + OrchestrationPlanKind, OrchestrationPlanRecord, Ticket, TicketBackend, TicketDependencyCheck, + TicketDoctorReport, TicketError, TicketIdOrSlug, TicketIntakeSummary, TicketItemEdit, + TicketListQuery, TicketListState, TicketMarkReady, TicketRef, TicketRelation, + TicketRelationKind, TicketRelationView, TicketStateChange, TicketStateSelector, TicketSummary, +}; use crate::{BackendApiClient, BackendWorkspaceClientError}; @@ -758,7 +758,7 @@ mod tests { .as_ref() .unwrap() .kind, - workspace_api::MemoryEvidenceOriginKind::WorkerInput + server_api::MemoryEvidenceOriginKind::WorkerInput ); assert!( request diff --git a/crates/runtime-api/Cargo.toml b/crates/runtime-api/Cargo.toml index d2fe97960..4f250aad5 100644 --- a/crates/runtime-api/Cargo.toml +++ b/crates/runtime-api/Cargo.toml @@ -10,7 +10,6 @@ api-macros = { workspace = true, features = ["reqwest", "axum"] } manifest.workspace = true protocol.workspace = true serde = { workspace = true, features = ["derive"] } -workspace-api.workspace = true workdir.workspace = true [dev-dependencies] diff --git a/crates/runtime-api/src/lib.rs b/crates/runtime-api/src/lib.rs index a02e2488b..c6ab08b3a 100644 --- a/crates/runtime-api/src/lib.rs +++ b/crates/runtime-api/src/lib.rs @@ -13,10 +13,85 @@ pub use api_macros::{ApiContract, HttpMethod}; use protocol::{CompletionEntry, CompletionKind, Segment, WorkerId, WorkerStateSnapshot}; use serde::{Deserialize, Serialize}; use workdir::workspace::{MaterializerKind, RuntimeWorkerRef, RuntimeWorkingDirectorySummary}; -use workspace_api::{RepositoryAccessMode, RepositorySource}; pub const RUNTIME_API_VERSION: &str = "v1"; pub const RUNTIME_API_BASE_PATH: &str = "/v1"; +/// Runtime transport classification for a materialized Repository source. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RepositorySourceKind { + LocalPath, + File, + Ssh, + Https, + Invalid, +} + +impl RepositorySourceKind { + pub const fn is_remote(self) -> bool { + matches!(self, Self::Ssh | Self::Https) + } + + pub const fn as_str(self) -> &'static str { + match self { + Self::LocalPath => "local_path", + Self::File => "file", + Self::Ssh => "ssh", + Self::Https => "https", + Self::Invalid => "invalid", + } + } + + pub fn parse(value: &str) -> Option { + Some(match value { + "local_path" => Self::LocalPath, + "file" => Self::File, + "ssh" => Self::Ssh, + "https" => Self::Https, + "http" | "invalid" => Self::Invalid, + _ => return None, + }) + } +} + +impl<'de> Deserialize<'de> for RepositorySourceKind { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Self::parse(&value).ok_or_else(|| { + serde::de::Error::unknown_variant( + &value, + &["local_path", "file", "ssh", "https", "invalid"], + ) + }) + } +} + +/// Runtime transport identity for one Repository source. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct RepositorySource { + pub kind: RepositorySourceKind, + pub uri: String, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RepositoryAccessMode { + ReadOnly, + ReadWrite, +} + +/// Runtime transport outcome for an explicit Worker restore operation. +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum WorkerRestoreState { + Accepted, + Rejected, + RolledBack, + ReconciliationRequired, +} #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct RuntimeApiError { @@ -364,7 +439,7 @@ pub struct WorkerResponse { /// including preflight rejection and uncertain post-side-effect reconciliation. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct WorkerRestoreResponse { - pub state: workspace_api::WorkerRestoreState, + pub state: WorkerRestoreState, #[serde(default, skip_serializing_if = "Option::is_none")] pub worker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/crates/server-api/Cargo.toml b/crates/server-api/Cargo.toml index 609be216b..6a842a2f6 100644 --- a/crates/server-api/Cargo.toml +++ b/crates/server-api/Cargo.toml @@ -1,14 +1,55 @@ [package] name = "server-api" version = "0.1.0" -edition = "2024" +edition.workspace = true +license.workspace = true +publish = false + +[features] +default = [] +typescript = ["dep:ts-rs", "protocol/typescript"] [dependencies] api-macros = { workspace = true, features = ["reqwest", "axum"] } +protocol.workspace = true runtime-api.workspace = true serde = { workspace = true, features = ["derive"] } -workspace-api.workspace = true +ts-rs = { version = "12.0.1", optional = true } +webauthn-rs-proto = "0.5.5" +workdir.workspace = true [dev-dependencies] serde_json.workspace = true -tokio = { workspace = true } +tokio.workspace = true + +[[example]] +name = "generate_legacy_typescript" +required-features = ["typescript"] + +[[example]] +name = "generate_workdir_api_types" +required-features = ["typescript"] + +[[example]] +name = "generate_worker_launch_api_types" +required-features = ["typescript"] + +[[example]] +name = "generate_companion_api_types" +required-features = ["typescript"] + +[[example]] +name = "generate_memory_api_types" +required-features = ["typescript"] + +[[example]] +name = "generate_skill_api_types" +required-features = ["typescript"] + +[[example]] +name = "generate_auth_api_types" +required-features = ["typescript"] + +[[example]] +name = "generate_repository_access_types" +required-features = ["typescript"] diff --git a/crates/server-api/examples/generate_auth_api_types.rs b/crates/server-api/examples/generate_auth_api_types.rs new file mode 100644 index 000000000..776008b36 --- /dev/null +++ b/crates/server-api/examples/generate_auth_api_types.rs @@ -0,0 +1,6 @@ +fn main() { + print!( + "{}", + server_api::normalized_typescript(server_api::auth_api_typescript()) + ); +} diff --git a/crates/server-api/examples/generate_companion_api_types.rs b/crates/server-api/examples/generate_companion_api_types.rs new file mode 100644 index 000000000..fd402438a --- /dev/null +++ b/crates/server-api/examples/generate_companion_api_types.rs @@ -0,0 +1,6 @@ +fn main() { + print!( + "{}", + server_api::normalized_typescript(server_api::companion_api_typescript()) + ); +} diff --git a/crates/server-api/examples/generate_legacy_typescript.rs b/crates/server-api/examples/generate_legacy_typescript.rs new file mode 100644 index 000000000..4e2919bce --- /dev/null +++ b/crates/server-api/examples/generate_legacy_typescript.rs @@ -0,0 +1,6 @@ +fn main() { + print!( + "{}", + server_api::normalized_typescript(server_api::legacy_catalog_typescript()) + ); +} diff --git a/crates/server-api/examples/generate_memory_api_types.rs b/crates/server-api/examples/generate_memory_api_types.rs new file mode 100644 index 000000000..b0f841d8e --- /dev/null +++ b/crates/server-api/examples/generate_memory_api_types.rs @@ -0,0 +1,6 @@ +fn main() { + print!( + "{}", + server_api::normalized_typescript(server_api::memory_api_typescript()) + ); +} diff --git a/crates/server-api/examples/generate_repository_access_types.rs b/crates/server-api/examples/generate_repository_access_types.rs new file mode 100644 index 000000000..2049e707b --- /dev/null +++ b/crates/server-api/examples/generate_repository_access_types.rs @@ -0,0 +1,6 @@ +fn main() { + print!( + "{}", + server_api::normalized_typescript(server_api::repository_access_api_typescript()) + ); +} diff --git a/crates/server-api/examples/generate_skill_api_types.rs b/crates/server-api/examples/generate_skill_api_types.rs new file mode 100644 index 000000000..b610b1bda --- /dev/null +++ b/crates/server-api/examples/generate_skill_api_types.rs @@ -0,0 +1,6 @@ +fn main() { + print!( + "{}", + server_api::normalized_typescript(server_api::skill_api_typescript()) + ); +} diff --git a/crates/server-api/examples/generate_workdir_api_types.rs b/crates/server-api/examples/generate_workdir_api_types.rs new file mode 100644 index 000000000..13ed5f08b --- /dev/null +++ b/crates/server-api/examples/generate_workdir_api_types.rs @@ -0,0 +1,6 @@ +fn main() { + print!( + "{}", + server_api::normalized_typescript(server_api::workdir_api_typescript()) + ); +} diff --git a/crates/server-api/examples/generate_worker_launch_api_types.rs b/crates/server-api/examples/generate_worker_launch_api_types.rs new file mode 100644 index 000000000..5e326b368 --- /dev/null +++ b/crates/server-api/examples/generate_worker_launch_api_types.rs @@ -0,0 +1,6 @@ +fn main() { + print!( + "{}", + server_api::normalized_typescript(server_api::worker_launch_api_typescript()) + ); +} diff --git a/crates/server-api/src/lib.rs b/crates/server-api/src/lib.rs index 4e9bc6026..6bca6ee32 100644 --- a/crates/server-api/src/lib.rs +++ b/crates/server-api/src/lib.rs @@ -1,11 +1,18 @@ -//! Generated Workspace Server REST contract for public clients. +//! Shared Workspace HTTP resource contracts. +//! +//! This crate owns transport DTOs exposed by the Workspace Server and consumed +//! by Rust clients. Runtime-internal projections remain in their owning crates; +//! callers must explicitly construct these Server-authoritative resources. use api_macros::api; pub use api_macros::axum as server_support; pub use api_macros::reqwest as client_support; pub use api_macros::{ApiContract, HttpMethod}; use serde::{Deserialize, Serialize}; -use workspace_api::WorkspaceWorkerSubject; +use webauthn_rs_proto::{ + CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential, + RequestChallengeResponse, +}; pub type ServerApiError = runtime_api::RuntimeApiError; pub type ServerApiClientError = client_support::ClientError; @@ -32,10 +39,3609 @@ pub trait ServerApi { ) -> Result; } +/// Public browser-authentication configuration. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct AuthPublicConfig { + pub rp_id: String, + pub origin: String, + pub public_base_url: String, + pub cookie_name: String, +} + +/// Authentication method that established the current request actor. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum ActorAuthMethod { + BrowserSession, + ApiToken, +} + +/// Public user identity returned by authentication operations. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct AuthenticatedUser { + pub user_id: String, + pub account_id: String, + pub handle: String, + pub display_name: String, +} + +/// Authenticated actor returned by `GET /api/auth/whoami`. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RequestActor { + pub user_id: String, + pub account_id: String, + pub handle: String, + pub display_name: String, + pub auth_method: ActorAuthMethod, +} + +impl RequestActor { + pub fn user(&self) -> AuthenticatedUser { + AuthenticatedUser { + user_id: self.user_id.clone(), + account_id: self.account_id.clone(), + handle: self.handle.clone(), + display_name: self.display_name.clone(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WhoamiResponse { + pub actor: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct AuthBootstrapUserRequest { + pub handle: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub display_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct AuthUserResponse { + pub user: AuthenticatedUser, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct PasskeyRegistrationOptionsRequest { + pub handle: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub display_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub browser_origin: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct PasskeyRegistrationOptionsResponse { + pub challenge_id: String, + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] + pub public_key: CreationChallengeResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct PasskeyRegistrationCompleteRequest { + pub challenge_id: String, + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] + pub credential: RegisterPublicKeyCredential, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct PasskeyLoginOptionsRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub handle: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub browser_origin: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct PasskeyLoginOptionsResponse { + pub challenge_id: String, + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] + pub public_key: RequestChallengeResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct PasskeyLoginCompleteRequest { + pub challenge_id: String, + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] + pub credential: PublicKeyCredential, +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct DeviceLoginStartRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub client_name: Option, +} + +const DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS: u64 = 24 * 60 * 60; +const DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS: u64 = 60; + +#[derive(Clone, Serialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct DeviceLoginStartResponse { + pub device_code: String, + pub user_code: String, + pub verification_uri: String, + pub verification_uri_complete: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub expires_in: u64, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub interval: u64, +} + +impl<'de> Deserialize<'de> for DeviceLoginStartResponse { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct Wire { + device_code: String, + user_code: String, + verification_uri: String, + verification_uri_complete: String, + expires_in: u64, + interval: u64, + } + + let wire = Wire::deserialize(deserializer)?; + if wire.expires_in == 0 || wire.expires_in > DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS { + return Err(serde::de::Error::custom(format!( + "expires_in must be between 1 and {DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS}" + ))); + } + if wire.interval == 0 || wire.interval > DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS { + return Err(serde::de::Error::custom(format!( + "interval must be between 1 and {DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS}" + ))); + } + Ok(Self { + device_code: wire.device_code, + user_code: wire.user_code, + verification_uri: wire.verification_uri, + verification_uri_complete: wire.verification_uri_complete, + expires_in: wire.expires_in, + interval: wire.interval, + }) + } +} + +impl std::fmt::Debug for DeviceLoginStartResponse { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("DeviceLoginStartResponse") + .field("device_code", &"[redacted]") + .field("user_code", &self.user_code) + .field("verification_uri", &self.verification_uri) + .field("verification_uri_complete", &self.verification_uri_complete) + .field("expires_in", &self.expires_in) + .field("interval", &self.interval) + .finish() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct DeviceLoginApproveRequest { + pub user_code: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum DeviceLoginApprovalStatus { + Approved, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct DeviceLoginApproveResponse { + pub status: DeviceLoginApprovalStatus, + pub user: AuthenticatedUser, +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct DeviceLoginPollRequest { + pub device_code: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub enum DeviceAccessTokenType { + Bearer, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum DeviceLoginPollStatus { + Pending, + Approved, + Expired, + Denied, + Consumed, +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct DeviceLoginPollResponse { + pub status: DeviceLoginPollStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub access_token: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub token_type: Option, +} + +impl std::fmt::Debug for DeviceLoginPollResponse { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("DeviceLoginPollResponse") + .field("status", &self.status) + .field( + "access_token", + &self.access_token.as_ref().map(|_| "[redacted]"), + ) + .field("token_type", &self.token_type) + .finish() + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum LogoutStatus { + LoggedOut, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct LogoutResponse { + pub status: LogoutStatus, +} + +pub const REPOSITORY_KEY_MIN_LEN: usize = 1; +pub const REPOSITORY_KEY_MAX_LEN: usize = 64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RepositoryKeyError { + Length, + Character, + LeadingHyphen, + TrailingHyphen, +} + +impl std::fmt::Display for RepositoryKeyError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Length => "must contain between 1 and 64 ASCII bytes", + Self::Character => "must contain only lowercase ASCII letters, digits, and hyphens", + Self::LeadingHyphen => "must not start with a hyphen", + Self::TrailingHyphen => "must not end with a hyphen", + }) + } +} + +impl std::error::Error for RepositoryKeyError {} + +/// Validate one immutable Workspace-scoped Repository key. +/// +/// Keys are deliberately not normalized: callers must submit the exact canonical +/// lowercase ASCII spelling so idempotency and route identity cannot alias. +pub fn validate_repository_key(value: &str) -> Result<(), RepositoryKeyError> { + let bytes = value.as_bytes(); + if !(REPOSITORY_KEY_MIN_LEN..=REPOSITORY_KEY_MAX_LEN).contains(&bytes.len()) { + return Err(RepositoryKeyError::Length); + } + if bytes[0] == b'-' { + return Err(RepositoryKeyError::LeadingHyphen); + } + if bytes[bytes.len() - 1] == b'-' { + return Err(RepositoryKeyError::TrailingHyphen); + } + if !bytes + .iter() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-') + { + return Err(RepositoryKeyError::Character); + } + Ok(()) +} + +/// Provider-neutral classification of an authoritative Repository source. +/// +/// Local paths remain distinct from network Git transports so callers cannot +/// accidentally treat an unmaterialized remote as a server-local filesystem path. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RepositorySourceKind { + LocalPath, + File, + Ssh, + Https, + /// A legacy value that could not be classified during migration. It remains + /// inspectable but every provider operation must fail closed. Historical + /// `http` wire values decode into this non-executable classification. + Invalid, +} + +impl RepositorySourceKind { + pub const fn is_remote(self) -> bool { + matches!(self, Self::Ssh | Self::Https) + } + + pub const fn as_str(self) -> &'static str { + match self { + Self::LocalPath => "local_path", + Self::File => "file", + Self::Ssh => "ssh", + Self::Https => "https", + Self::Invalid => "invalid", + } + } + + pub fn parse(value: &str) -> Option { + Some(match value { + "local_path" => Self::LocalPath, + "file" => Self::File, + "ssh" => Self::Ssh, + "https" => Self::Https, + "http" | "invalid" => Self::Invalid, + _ => return None, + }) + } +} + +impl<'de> Deserialize<'de> for RepositorySourceKind { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Self::parse(&value).ok_or_else(|| { + serde::de::Error::unknown_variant( + &value, + &["local_path", "file", "ssh", "https", "invalid"], + ) + }) + } +} + +/// Stable Repository source identity stored by Workspace authority. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct RepositorySource { + pub kind: RepositorySourceKind, + /// Canonical source representation. This is an absolute local path for + /// `local_path`, and a normalized URI/remote specification otherwise. + pub uri: String, +} + +/// Browser/user intent for registering one Repository in a Workspace. +/// +/// The Server parses and canonicalizes `source`; callers cannot assert a +/// transport classification or supply credential material through this DTO. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct CreateWorkspaceRepositoryRequest { + pub repository_key: String, + pub source: String, + #[serde(default)] + pub default_ref: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct CreateWorkspaceRepositoryResponse { + pub workspace_id: String, + pub repository_key: String, + pub replayed: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RepositoryObservedStatus { + Unverified, + Ready, + Invalid, +} + +impl RepositoryObservedStatus { + pub const fn as_str(self) -> &'static str { + match self { + Self::Unverified => "unverified", + Self::Ready => "ready", + Self::Invalid => "invalid", + } + } + + pub fn parse(value: &str) -> Option { + Some(match value { + "unverified" => Self::Unverified, + "ready" => Self::Ready, + "invalid" => Self::Invalid, + _ => return None, + }) + } +} + +/// Public Workspace catalog item returned by `GET /api/workspaces`. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceSummary { + pub workspace_id: String, + pub owner_account_id: String, + pub display_name: String, + pub state: String, + pub created_at: String, + pub updated_at: String, +} + +/// Public response returned by `GET /api/workspaces`. +/// +/// The transparent newtype keeps the established top-level JSON array while making the +/// complete list response a named cross-crate and generated-TypeScript authority. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkspaceCatalogListResponse(pub Vec); + +/// Public Repository record embedded in Workspace creation responses. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceRepositoryRecord { + pub workspace_id: String, + pub repository_key: String, + pub kind: String, + pub provider: Option, + pub source: RepositorySource, + pub default_ref: Option, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub source_revision: u64, + pub source_fingerprint: String, + pub observed_status: RepositoryObservedStatus, + pub observed_at: Option, + pub created_at: String, + pub updated_at: String, +} + +/// Initial Repository registration intent for Workspace creation. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct InitialRepositoryIntent { + pub repository_key: String, + pub uri: String, + #[serde(default)] + pub default_ref: Option, +} + +/// Request for atomically creating a Workspace and its initial Repository. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorkspaceCreateRequest { + pub operation_key: String, + pub display_name: String, + pub repository: InitialRepositoryIntent, +} + +/// Response returned after atomically creating a Workspace and its first Repository. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceCreateResponse { + pub workspace: WorkspaceSummary, + pub repository: WorkspaceRepositoryRecord, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub config_revision: u64, + pub request_fingerprint: String, + pub replayed: bool, +} + +/// Browser authentication configuration exposed by the scoped Workspace summary. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub enum WorkspaceAuthConfig { + Passkey { + rp_id: String, + origin: String, + public_base_url: String, + cookie_name: String, + }, +} + +/// Backend-authoritative permissions for the current Workspace actor. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspacePermissionSummary { + pub manage_repositories: bool, + pub manage_secrets: bool, + pub manage_runtimes: bool, + pub delete_workspace: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceExtensionPointState { + pub status: String, + pub note: String, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceExtensionPoints { + pub store: String, + pub event_stream: WorkspaceExtensionPointState, + pub host_worker_bridge: WorkspaceExtensionPointState, + pub companion_console: WorkspaceExtensionPointState, +} + +/// Scoped Workspace metadata and current-actor permission projection. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceResponse { + pub workspace_id: String, + pub display_name: String, + pub record_authority: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub schema_version: i64, + pub auth: WorkspaceAuthConfig, + pub permissions: WorkspacePermissionSummary, + pub extension_points: WorkspaceExtensionPoints, +} + +/// Workspace display metadata exposed from the Server DB settings authority. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceMetadataSettingsResponse { + pub workspace_id: String, + pub display_name: String, + pub created_at: String, + pub revision: String, + pub source: String, + pub diagnostics: Vec, +} + +/// Compare-and-swap update for Workspace identity display metadata. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct UpdateWorkspaceMetadataRequest { + pub display_name: String, + pub revision: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceMetadataMutationResponse { + pub workspace: WorkspaceMetadataSettingsResponse, + pub diagnostics: Vec, +} + +/// Lifecycle state for a Workspace-scoped Ed25519 signing identity. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceSigningIdentityState { + PendingProvisioning, + Active, +} + +/// Public metadata for a Workspace signing identity. Private material and its +/// storage reference are deliberately not part of this wire authority. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceSigningIdentityPublic { + pub workspace_id: String, + pub key_id: String, + pub algorithm: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub public_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub public_key_fingerprint: Option, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub revision: u64, + pub state: WorkspaceSigningIdentityState, + pub created_at: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub provisioned_at: Option, +} + +/// Copyable public trust bundle consumed by future Runtime enrollment work. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspacePublicIdentityBundle { + pub workspace_id: String, + pub backend_url: String, + pub key_id: String, + pub algorithm: String, + pub public_key: String, + pub public_key_fingerprint: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub revision: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceSigningIdentityResponse { + pub identity: WorkspaceSigningIdentityPublic, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub public_bundle: Option, +} + +pub const WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES: usize = 128; +pub const WORKSPACE_DELETION_MAX_REVISION_BYTES: usize = 128; +pub const WORKSPACE_DELETION_MAX_CONFIRMATION_BYTES: usize = 256; +pub const WORKSPACE_DELETION_MAX_BLOCKERS: usize = 1024; +pub const WORKSPACE_DELETION_MAX_CHILD_OPERATION_IDS: usize = 4096; +pub const WORKSPACE_DELETION_MAX_RESOURCE_VALUE_BYTES: usize = 128; +pub const WORKSPACE_DELETION_MAX_BLOCKER_MESSAGE_BYTES: usize = 512; + +fn deserialize_workspace_deletion_operation_id<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + if value.len() > WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES { + return Err(serde::de::Error::custom( + "Workspace deletion operation_id is too long", + )); + } + Ok(value) +} + +fn deserialize_workspace_deletion_revision<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + if value.len() > WORKSPACE_DELETION_MAX_REVISION_BYTES { + return Err(serde::de::Error::custom( + "Workspace deletion revision is too long", + )); + } + Ok(value) +} + +fn deserialize_workspace_deletion_confirmation<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + if value.len() > WORKSPACE_DELETION_MAX_CONFIRMATION_BYTES { + return Err(serde::de::Error::custom( + "Workspace deletion confirmation is too long", + )); + } + Ok(value) +} + +fn deserialize_workspace_deletion_resource_value<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + if value + .as_ref() + .is_some_and(|value| value.len() > WORKSPACE_DELETION_MAX_RESOURCE_VALUE_BYTES) + { + return Err(serde::de::Error::custom( + "Workspace deletion resource value is too long", + )); + } + Ok(value) +} + +fn deserialize_workspace_deletion_blocker_message<'de, D>( + deserializer: D, +) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + if value.len() > WORKSPACE_DELETION_MAX_BLOCKER_MESSAGE_BYTES { + return Err(serde::de::Error::custom( + "Workspace deletion blocker message is too long", + )); + } + Ok(value) +} + +fn deserialize_workspace_deletion_blockers<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Vec::::deserialize(deserializer)?; + if value.len() > WORKSPACE_DELETION_MAX_BLOCKERS { + return Err(serde::de::Error::custom( + "too many Workspace deletion blockers", + )); + } + Ok(value) +} + +fn deserialize_workspace_deletion_child_operation_ids<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Vec::::deserialize(deserializer)?; + if value.len() > WORKSPACE_DELETION_MAX_CHILD_OPERATION_IDS { + return Err(serde::de::Error::custom( + "too many Workspace deletion child operations", + )); + } + if value + .iter() + .any(|operation_id| operation_id.len() > WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES) + { + return Err(serde::de::Error::custom( + "Workspace deletion child operation_id is too long", + )); + } + Ok(value) +} + +/// Lifecycle state for one durable Workspace deletion operation. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceDeletionState { + Queued, + Running, + Blocked, + Failed, + Succeeded, +} + +/// Stable category explaining why Workspace deletion cannot currently advance. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceDeletionBlockerKind { + LastAccessibleWorkspace, + RevisionConflict, + DirtyWorkdir, + WorkerRemovalBlocked, + WorkdirRemovalBlocked, + RetentionHold, + CleanupUnavailable, +} + +/// One bounded, user-actionable blocker returned by preflight or execution. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkspaceDeletionBlocker { + pub kind: WorkspaceDeletionBlockerKind, + pub resource_kind: Option, + pub resource_key: Option, + pub message: String, +} + +/// Workspace-owned resources summarized before destructive confirmation. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceDeletionResourceCounts { + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub workers: u64, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub workdirs: u64, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub repositories: u64, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub runtime_bindings: u64, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub secrets: u64, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub artifacts: u64, +} + +/// Owner-only impact preview for deleting one Workspace. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkspaceDeletionPreflightResponse { + pub workspace_id: String, + pub display_name: String, + /// Opaque persisted Workspace metadata revision used as a CAS fence. + pub expected_revision: String, + pub can_delete: bool, + pub resources: WorkspaceDeletionResourceCounts, + pub blockers: Vec, +} + +/// Idempotent request to start or resume Workspace deletion. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkspaceDeletionRequest { + pub operation_id: String, + pub expected_revision: String, + pub confirmation: String, +} + +/// Durable deletion operation projection used by request responses and polling. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkspaceDeletionOperationResponse { + pub operation_id: String, + pub workspace_id: String, + pub display_name: String, + pub state: WorkspaceDeletionState, + pub resources: WorkspaceDeletionResourceCounts, + pub child_operation_ids: Vec, + pub blockers: Vec, + pub failure_category: Option, + pub created_at: String, + pub updated_at: String, + pub completed_at: Option, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct WorkspaceDeletionBlockerWire { + kind: WorkspaceDeletionBlockerKind, + #[serde(deserialize_with = "deserialize_workspace_deletion_resource_value")] + resource_kind: Option, + #[serde(deserialize_with = "deserialize_workspace_deletion_resource_value")] + resource_key: Option, + #[serde(deserialize_with = "deserialize_workspace_deletion_blocker_message")] + message: String, +} + +impl<'de> Deserialize<'de> for WorkspaceDeletionBlocker { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let wire = WorkspaceDeletionBlockerWire::deserialize(deserializer)?; + Ok(Self { + kind: wire.kind, + resource_kind: wire.resource_kind, + resource_key: wire.resource_key, + message: wire.message, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct WorkspaceDeletionPreflightResponseWire { + workspace_id: String, + display_name: String, + #[serde(deserialize_with = "deserialize_workspace_deletion_revision")] + expected_revision: String, + can_delete: bool, + resources: WorkspaceDeletionResourceCounts, + #[serde(deserialize_with = "deserialize_workspace_deletion_blockers")] + blockers: Vec, +} + +impl<'de> Deserialize<'de> for WorkspaceDeletionPreflightResponse { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let wire = WorkspaceDeletionPreflightResponseWire::deserialize(deserializer)?; + Ok(Self { + workspace_id: wire.workspace_id, + display_name: wire.display_name, + expected_revision: wire.expected_revision, + can_delete: wire.can_delete, + resources: wire.resources, + blockers: wire.blockers, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct WorkspaceDeletionRequestWire { + #[serde(deserialize_with = "deserialize_workspace_deletion_operation_id")] + operation_id: String, + #[serde(deserialize_with = "deserialize_workspace_deletion_revision")] + expected_revision: String, + #[serde(deserialize_with = "deserialize_workspace_deletion_confirmation")] + confirmation: String, +} + +impl<'de> Deserialize<'de> for WorkspaceDeletionRequest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let wire = WorkspaceDeletionRequestWire::deserialize(deserializer)?; + Ok(Self { + operation_id: wire.operation_id, + expected_revision: wire.expected_revision, + confirmation: wire.confirmation, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct WorkspaceDeletionOperationResponseWire { + #[serde(deserialize_with = "deserialize_workspace_deletion_operation_id")] + operation_id: String, + workspace_id: String, + display_name: String, + state: WorkspaceDeletionState, + resources: WorkspaceDeletionResourceCounts, + #[serde(deserialize_with = "deserialize_workspace_deletion_child_operation_ids")] + child_operation_ids: Vec, + #[serde(deserialize_with = "deserialize_workspace_deletion_blockers")] + blockers: Vec, + failure_category: Option, + created_at: String, + updated_at: String, + completed_at: Option, +} + +impl<'de> Deserialize<'de> for WorkspaceDeletionOperationResponse { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let wire = WorkspaceDeletionOperationResponseWire::deserialize(deserializer)?; + Ok(Self { + operation_id: wire.operation_id, + workspace_id: wire.workspace_id, + display_name: wire.display_name, + state: wire.state, + resources: wire.resources, + child_operation_ids: wire.child_operation_ids, + blockers: wire.blockers, + failure_category: wire.failure_category, + created_at: wire.created_at, + updated_at: wire.updated_at, + completed_at: wire.completed_at, + }) + } +} + +/// Read-only Profile catalog projected from one active Workspace config revision. +#[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)] +pub struct ProfileSettingsResponse { + pub workspace_id: String, + pub registry_revision: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] + pub config_revision: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tree_digest: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub projection_digest: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_profile: Option, + pub profiles: Vec, + pub sources: Vec, + pub diagnostics: Vec, +} + +#[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)] +pub struct WorkspaceProfileSummary { + pub profile_id: String, + pub selector: String, + pub label: String, + pub source_kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_source_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub editable: bool, + pub is_default: bool, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceProfileSourceSummary { + pub profile_source_id: String, + pub display_path: String, + pub kind: String, + pub content_type: String, + pub content_digest: String, + pub provenance: WorkspaceProfileSourceProvenance, + pub editable: bool, + pub revision: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub size_bytes: u64, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceProfileSourceProvenance { + ProjectProfileSourceTree, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositoryDiagnostic { + pub severity: String, + pub code: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct GitRemoteSummary { + pub name: String, + pub fetch_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct GitRepositorySummary { + pub status: String, + pub head: Option, + pub branch: Option, + pub dirty: bool, + pub remotes: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositorySummary { + pub repository_key: String, + pub kind: String, + pub provider: String, + pub source: RepositorySource, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub source_revision: u64, + pub source_fingerprint: String, + pub observed_status: RepositoryObservedStatus, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub observed_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub default_selector: Option, + pub record_authority: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub git: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub diagnostics: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct GitCommitSummary { + pub hash: String, + pub short_hash: String, + pub summary: String, + pub author_name: String, + pub author_email: String, + pub author_date: String, + pub parents: Vec, + pub refs: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositoryListResponse { + pub workspace_id: String, + pub items: Vec, + pub source: String, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositoryDetailResponse { + pub workspace_id: String, + pub item: RepositorySummary, + pub source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositorySshConnectionProbeRequest { + pub runtime_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositorySshHostKeyCandidate { + pub algorithm: String, + pub host_key: String, + pub fingerprint: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RepositorySshConnectionTrustState { + Untrusted, + Verified, + Changed, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositorySshConnectionProbeResponse { + pub workspace_id: String, + pub repository_key: String, + pub runtime_id: String, + pub hostname: String, + pub port: u16, + pub trust_state: RepositorySshConnectionTrustState, + pub host_trust_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + pub expected_host_trust_revision: Option, + pub candidates: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct ConfirmRepositorySshHostTrustRequest { + pub operation_id: String, + pub runtime_id: String, + pub host_key: String, + #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + pub expected_host_trust_revision: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositoryLogResponse { + pub workspace_id: String, + pub repository_key: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub default_selector: Option, + pub limit: usize, + pub items: Vec, + pub diagnostics: Vec, +} + +pub const TICKET_RELATIONS_QUERY_PATH: &str = "/tickets/relations/search"; +pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search"; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum DiagnosticSeverity { + Info, + Warning, + Error, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct Diagnostic { + pub code: String, + pub severity: DiagnosticSeverity, + pub message: String, +} + +/// Public Workdir materializer classification. +/// +/// The value identifies stable materialization provenance without exposing a +/// provider path, Runtime handle, or session identity. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectoryMaterializerKind { + #[default] + RuntimeGitClone, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectoryStatusKind { + Active, + CleanupPending, + Corrupted, + NotFound, + Unknown, +} + +impl WorkingDirectoryStatusKind { + pub const fn as_str(&self) -> &'static str { + match self { + Self::Active => "active", + Self::CleanupPending => "cleanup_pending", + Self::Corrupted => "corrupted", + Self::NotFound => "not_found", + Self::Unknown => "unknown", + } + } +} + +impl std::fmt::Display for WorkingDirectoryStatusKind { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectoryCleanupTarget { + pub kind: String, + pub working_directory_id: String, + pub repository_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectoryRemovalRequest { + pub reason: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectoryRemovalDisposition { + Removed, + Retained, + AttentionRequired, +} + +#[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)] +pub struct WorkingDirectoryRemovalResponse { + pub working_directory_id: String, + pub disposition: WorkingDirectoryRemovalDisposition, + pub retryable: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub failure_category: Option, +} + +/// Durable Workspace occupancy projection for one Workdir. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectoryOccupancy { + pub runtime_id: String, + pub worker_id: String, + pub display_name: String, + pub linked_at: String, +} + +/// Runtime-internal Workdir cleanup authority. This transport intentionally +/// retains the Backend-generated Repository id and is never a Workspace public +/// projection. +#[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)] +pub struct RuntimeWorkingDirectoryCleanupTarget { + pub kind: String, + pub working_directory_id: String, + pub repository_id: String, +} + +/// Runtime-internal Workdir inventory transport. Workspace REST and model-facing +/// surfaces must project this through [`WorkingDirectorySummary`] so the UUID is +/// replaced with `repository_key`. +#[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)] +pub struct RuntimeWorkingDirectorySummary { + pub working_directory_id: String, + pub repository_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_tree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_tree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + pub observed_at_epoch_seconds: Option, + pub materializer_kind: WorkingDirectoryMaterializerKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cleanup_target: Option, + pub status: WorkingDirectoryStatusKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cleanliness: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_worker_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub occupied_by: Option, +} + +/// Public, provider-neutral Workdir inventory projection. +#[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)] +pub struct WorkingDirectorySummary { + pub working_directory_id: String, + pub repository_key: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_tree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_tree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] + pub observed_at_epoch_seconds: Option, + pub materializer_kind: WorkingDirectoryMaterializerKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cleanup_target: Option, + pub status: WorkingDirectoryStatusKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cleanliness: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_worker_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub occupied_by: Option, +} + +impl WorkingDirectorySummary { + /// Workspace-managed inventory rows carry explicit cleanup authority. + pub fn is_workspace_managed(&self) -> bool { + self.cleanup_target.is_some() + } +} +impl From + for WorkingDirectoryMaterializerKind +{ + fn from(value: workdir::workspace::WorkingDirectoryMaterializerKind) -> Self { + match value { + workdir::workspace::WorkingDirectoryMaterializerKind::RuntimeGitClone => { + Self::RuntimeGitClone + } + } + } +} + +impl From for WorkingDirectoryStatusKind { + fn from(value: workdir::workspace::WorkingDirectoryStatusKind) -> Self { + match value { + workdir::workspace::WorkingDirectoryStatusKind::Active => Self::Active, + workdir::workspace::WorkingDirectoryStatusKind::CleanupPending => Self::CleanupPending, + workdir::workspace::WorkingDirectoryStatusKind::Corrupted => Self::Corrupted, + workdir::workspace::WorkingDirectoryStatusKind::NotFound => Self::NotFound, + workdir::workspace::WorkingDirectoryStatusKind::Unknown => Self::Unknown, + } + } +} + +impl From for WorkingDirectoryCleanupTarget { + fn from(value: workdir::workspace::WorkingDirectoryCleanupTarget) -> Self { + Self { + kind: value.kind, + working_directory_id: value.working_directory_id, + repository_key: value.repository_key, + } + } +} + +impl From for WorkingDirectoryOccupancy { + fn from(value: workdir::workspace::WorkingDirectoryOccupancy) -> Self { + Self { + runtime_id: value.runtime_id, + worker_id: value.worker_id, + display_name: value.display_name, + linked_at: value.linked_at, + } + } +} + +impl From + for RuntimeWorkingDirectoryCleanupTarget +{ + fn from(value: workdir::workspace::RuntimeWorkingDirectoryCleanupTarget) -> Self { + Self { + kind: value.kind, + working_directory_id: value.working_directory_id, + repository_id: value.repository_id, + } + } +} + +impl From for RuntimeWorkingDirectorySummary { + fn from(value: workdir::workspace::RuntimeWorkingDirectorySummary) -> Self { + Self { + working_directory_id: value.working_directory_id, + repository_id: value.repository_id, + creation_selector: value.creation_selector, + creation_ref: value.creation_ref, + creation_tree: value.creation_tree, + current_selector: value.current_selector, + current_ref: value.current_ref, + current_tree: value.current_tree, + observed_at_epoch_seconds: value.observed_at_epoch_seconds, + materializer_kind: value.materializer_kind.into(), + cleanup_target: value.cleanup_target.map(Into::into), + status: value.status.into(), + cleanliness: value.cleanliness, + primary_worker_id: value.primary_worker_id, + occupied_by: value.occupied_by.map(Into::into), + } + } +} + +impl From for WorkingDirectorySummary { + fn from(value: workdir::workspace::WorkingDirectorySummary) -> Self { + Self { + working_directory_id: value.working_directory_id, + repository_key: value.repository_key, + creation_selector: value.creation_selector, + creation_ref: value.creation_ref, + creation_tree: value.creation_tree, + current_selector: value.current_selector, + current_ref: value.current_ref, + current_tree: value.current_tree, + observed_at_epoch_seconds: value.observed_at_epoch_seconds, + materializer_kind: value.materializer_kind.into(), + cleanup_target: value.cleanup_target.map(Into::into), + status: value.status.into(), + cleanliness: value.cleanliness, + primary_worker_id: value.primary_worker_id, + occupied_by: value.occupied_by.map(Into::into), + } + } +} + +/// Browser/Rust-client Workdir materialization request. +/// +/// `runtime_id = None` requests Workspace default Runtime resolution and +/// `operation_id = Some(_)` fences exact replay. All four fields deliberately +/// preserve the Server's existing optionality. +#[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)] +pub struct WorkingDirectoryCreateRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime_id: Option, + pub repository_key: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub operation_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectoryListResponse { + pub workspace_id: String, + pub items: Vec, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectoryDetailResponse { + pub workspace_id: String, + pub runtime_id: String, + pub item: WorkingDirectorySummary, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectoryCreateResponse { + pub workspace_id: String, + pub runtime_id: String, + pub item: WorkingDirectorySummary, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ListResponse { + pub workspace_id: String, + pub limit: usize, + pub items: Vec, + pub source: String, + #[serde(default)] + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct QueryPage { + pub limit: usize, + pub returned: usize, + pub has_more: bool, + pub next_cursor: Option, + pub sort: String, + pub source_limit: Option, + pub source_truncated: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveEventDetail { + pub event_ref: String, + pub kind: String, + pub body: Option, + pub created_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveLinkedTicketSummary { + pub id: String, + pub resource_key: String, + pub title: String, + pub state: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveResourceSummary { + pub path: String, + pub media_type: Option, + pub bytes: usize, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveSummary { + pub id: String, + pub resource_key: String, + pub title: String, + pub state: String, + pub created_at: Option, + pub updated_at: Option, + pub summary: String, + pub linked_tickets: Vec, + pub record_source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveDetail { + pub id: String, + pub resource_key: String, + pub title: String, + pub state: String, + pub revision: String, + pub created_at: Option, + pub updated_at: Option, + pub linked_tickets: Vec, + pub linked_ticket_summaries: Vec, + pub resources: Vec, + pub body: String, + pub body_truncated: bool, + pub events: Vec, + pub event_page: QueryPage, + pub record_source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveCreateRequest { + pub title: String, + #[serde(default)] + pub body_md: String, + #[serde(default = "default_objective_state")] + pub state: String, + #[serde(default)] + pub linked_tickets: Vec, +} + +fn default_objective_state() -> String { + "active".to_string() +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct ObjectiveEditRequest { + pub title: Option, + pub old_string: Option, + pub new_string: Option, + #[serde(default)] + pub replace_all: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveStateRequest { + pub state: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveLinkTicketRequest { + pub ticket_id: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeSourceKind { + EmbeddedWorkerRuntime, + RemoteHttp, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeSourceStatus { + Active, + Reserved, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeIdentityAuthority { + RuntimeRegistryProjection, + ServerRuntimeConfiguration, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeSourceSummary { + pub kind: RuntimeSourceKind, + pub status: RuntimeSourceStatus, + pub identity_authority: RuntimeIdentityAuthority, + pub note: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct RuntimeSummary { + pub runtime_id: String, + pub label: String, + pub kind: String, + pub status: String, + pub source: RuntimeSourceSummary, + #[serde(default)] + pub host_ids: Vec, + pub worker_creation_available: bool, + pub os: String, + pub arch: String, + #[serde(default)] + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceRuntimeBindingState { + Configured, + Verified, + Revoked, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeConnectionDisplayState { + Configured, + Verified, + Unavailable, + Revoked, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeVerificationOutcome { + Verified, + ChallengeIssued, + VerificationFailed, + ConnectivityFailed, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeVerificationEvidenceSummary { + pub verified_at: Option, + pub last_checked_at: String, + pub last_outcome: RuntimeVerificationOutcome, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub binding_revision: u64, + pub workspace_key_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub workspace_identity_revision: u64, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub workspace_trust_generation: u64, + pub runtime_public_key_fingerprint: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub runtime_identity_revision: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceRuntimeBindingSummary { + pub state: WorkspaceRuntimeBindingState, + pub connection_state: RuntimeConnectionDisplayState, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub revision: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_key_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + pub workspace_key_generation: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub verification: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeManagementSummary { + pub built_in: bool, + pub config_managed: bool, + pub removable: bool, + pub endpoint_configured: bool, + pub token_ref_configured: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub binding: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkspaceRuntimeResource { + #[serde(flatten)] + pub runtime: RuntimeSummary, + pub management: RuntimeManagementSummary, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeTrustKeyStatus { + Unconfigured, + Active, + Revoked, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeTrustKeyState { + pub status: RuntimeTrustKeyStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fingerprint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + pub revision: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub created_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub updated_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub revoked_at: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeTrustAuditAction { + Created, + Replaced, + Reactivated, + Revoked, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeTrustAuditEntry { + pub action: RuntimeTrustAuditAction, + pub actor_account_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub old_fingerprint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub new_fingerprint: Option, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub revision: u64, + pub at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceRuntimeDetail { + pub workspace_id: String, + pub runtime: WorkspaceRuntimeResource, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub endpoint: Option, + pub trust_key: RuntimeTrustKeyState, + #[serde(default)] + pub recent_audit: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeTrustKeyRevealResponse { + pub public_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RevokeRuntimeTrustKeyRequest { + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub expected_revision: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RemoveRuntimeRequest { + pub operation_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub expected_binding_revision: u64, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeRemovalOperationState { + Pending, + CleanupPending, + Succeeded, + Failed, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeRemovalOperationResponse { + pub operation_id: String, + pub workspace_id: String, + pub runtime_id: String, + pub state: RuntimeRemovalOperationState, + pub binding_removed: bool, + pub runtime_registration_removed: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub failure_category: Option, + pub created_at: String, + pub updated_at: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub completed_at: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeTrustConflictKind { + StaleRevision, + FingerprintInUse, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeTrustConflictResponse { + pub error: RuntimeTrustConflictKind, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub current_revision: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_fingerprint: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimePublicIdentityBundle { + pub identity_id: String, + pub public_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CreateRemoteRuntimeRequest { + pub public_bundle: RuntimePublicIdentityBundle, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, + pub endpoint: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + pub expected_revision: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct UpdateRemoteRuntimeRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, + pub endpoint: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeConnectionTestStatus { + Compatible, + Failed, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum RuntimeConnectionTestFailureKind { + Authentication, + Authorization, + NetworkUnreachable, + Timeout, + TlsOrTransport, + MalformedResponse, + ProtocolVersionMismatch, + RuntimeIdentityMismatch, + Configuration, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeConnectionTestResponse { + pub workspace_id: String, + pub runtime_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub binding_revision: u64, + pub connection_state: RuntimeConnectionDisplayState, + pub verification: Option, + pub checked_at: String, + pub status: RuntimeConnectionTestStatus, + pub failure_kind: Option, + pub expected_protocol_version: u32, + pub actual_protocol_version: Option, + #[serde(default)] + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkerWorkspaceSummary { + pub visibility: String, + pub identity: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkerImplementationSummary { + pub kind: String, + pub display_hint: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkerCapabilitySummary { + pub can_stop: bool, + pub can_spawn_followup: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum WorkspaceWorkerSubject { + RuntimeWorker { + runtime_id: String, + worker_id: String, + }, +} + +/// Bounded, model-safe projection used by privileged Workspace Worker discovery. +/// Runtime placement appears only in the typed subject required by Worker +/// control operations; provider and launch internals are intentionally omitted. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkspaceWorkerDiscoveryItem { + pub subject: WorkspaceWorkerSubject, + pub resource_key: String, + pub display_name: String, + pub profile: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +/// Public lifecycle projection for the Workspace Companion endpoint. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum CompanionLifecycleState { + Idle, + Running, + Stopped, +} + +/// Public outcome of a Companion message submission. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum CompanionMessageDisposition { + Accepted, + Rejected, +} + +/// Public, bounded transport metadata for Companion status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CompanionTransportSummary { + pub mode: String, + pub available: bool, +} + +/// Public Workspace Companion status response. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CompanionStatusResponse { + pub state: CompanionLifecycleState, + pub worker: Option, + pub transport: CompanionTransportSummary, + #[serde(default)] + pub diagnostics: Vec, +} + +/// Public Workspace Companion message request. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CompanionMessageRequest { + pub content: String, +} + +/// Public Workspace Companion cancellation request. +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CompanionCancelRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +/// Public Workspace Companion message response. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CompanionMessageResponse { + pub state: CompanionMessageDisposition, + pub message: String, +} + +/// User-visible role accepted in the public Companion transcript. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum CompanionTranscriptRole { + User, + Assistant, +} + +/// One allowlisted, user-visible Companion transcript item. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CompanionTranscriptItem { + pub sequence: usize, + pub role: CompanionTranscriptRole, + pub content: String, + pub created_at: String, +} + +/// Bounded public Companion transcript projection. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CompanionTranscriptProjection { + pub state: CompanionLifecycleState, + pub start: usize, + pub limit: usize, + pub total: usize, + pub next: Option, + pub items: Vec, +} + +#[cfg(feature = "typescript")] +pub fn companion_api_typescript() -> String { + use ts_rs::TS; + + let config = ts_rs::Config::default(); + let declarations = [ + DiagnosticSeverity::decl(&config), + Diagnostic::decl(&config), + WorkspaceWorkerSubject::decl(&config), + WorkspaceWorkerDiscoveryItem::decl(&config), + CompanionLifecycleState::decl(&config), + CompanionMessageDisposition::decl(&config), + CompanionTransportSummary::decl(&config), + CompanionStatusResponse::decl(&config), + CompanionMessageRequest::decl(&config), + CompanionCancelRequest::decl(&config), + CompanionMessageResponse::decl(&config), + CompanionTranscriptRole::decl(&config), + CompanionTranscriptItem::decl(&config), + CompanionTranscriptProjection::decl(&config), + ]; + + format!( + "// Generated by `cargo run -p server-api --features typescript --example generate_companion_api_types`.\n// Do not edit manually.\n\n{}\n", + declarations + .into_iter() + .map(|declaration| format!("export {declaration}")) + .collect::>() + .join("\n\n") + ) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct WorkspaceWorkerDiscoveryPage { + pub workers: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, +} + +/// Workspace-authoritative Worker projection. +/// +/// `resource_key` is required here even though Runtime-internal Worker summaries +/// do not carry one. The Workspace Server must resolve it from Workspace +/// authority before constructing this response. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkerSummary { + pub runtime_id: String, + pub worker_id: String, + pub resource_key: String, + pub host_id: String, + #[serde(default)] + pub display_name: String, + pub label: String, + pub profile: Option, + pub singleton_key: Option, + #[serde(default)] + pub tags: Vec, + pub workspace: WorkerWorkspaceSummary, + /// Runtime catalog lifecycle compatibility state. Live foreground state, when + /// available, is carried separately in `worker_state`. + pub state: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub worker_state: Option, + pub last_seen_at: Option, + #[serde(default)] + pub pinned: bool, + #[serde(default)] + pub retention_state: String, + pub implementation: WorkerImplementationSummary, + pub capabilities: WorkerCapabilitySummary, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directory: Option, + #[serde(default)] + pub diagnostics: Vec, +} + +/// Runtime-owned Worker summary embedded in Worker launch responses. +/// +/// This preserves the existing launch wire shape. Workspace-owned Worker list +/// and detail responses use [`WorkerSummary`] instead. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[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, + pub singleton_key: Option, + pub tags: Vec, + pub workspace: WorkerWorkspaceSummary, + pub state: String, + pub last_seen_at: Option, + 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, + #[serde(default)] + pub diagnostics: Vec, +} + +#[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, + pub default_profile: Option, + pub profiles: Vec, + pub repositories: Vec, + pub working_directories: Vec, + pub diagnostics: Vec, +} + +#[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, +} + +#[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, +} + +#[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, +} + +#[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, + #[serde(default)] + pub ticket_assignment: Option, + #[serde(default)] + pub initial_submit: Vec, + #[serde(default)] + pub working_directory: Option, + /// Backend idempotency key used only for authenticated Worker-owned spawn/control. + #[serde(default)] + pub control_operation_id: Option, +} + +#[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, +} + +#[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, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum WorkerOperationState { + Accepted, + Unsupported, + Rejected, +} + +/// Cross-layer outcome of an explicit Worker restore operation. +/// +/// `Rejected` is reserved for read-only preflight failures. Once live restore +/// work starts, failure is either a confirmed `RolledBack` operation or a +/// `ReconciliationRequired` result whose commit state must be reread/retried. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkerRestoreState { + Accepted, + Rejected, + RolledBack, + ReconciliationRequired, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkerRestoreResult { + pub state: WorkerRestoreState, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub worker: Option, + #[serde(default)] + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkerRestoreResponse { + pub workspace_id: String, + pub runtime_id: String, + pub worker_id: String, + pub result: WorkerRestoreResult, +} + +/// Public Workspace Memory document projection. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryDocumentResponse { + pub body_md: String, + pub created_at: String, + pub updated_at: String, + pub bytes: usize, + pub record_source: String, +} + +/// Candidate kinds exposed by the Memory staging resource. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum MemoryCandidateKind { + Preference, + WorkingAssumption, + Constraint, + Decision, + OpenQuestion, + Lesson, +} + +/// Typed, bounded provenance classification for public Memory evidence anchors. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum MemoryEvidenceOriginKind { + HumanInput, + WorkerInput, + FlowInstruction, + BackendInstruction, + ModelOutput, + ToolOutput, + DerivedSummary, + LegacyUnknown, +} + +/// Bounded origin metadata copied from one typed Memory evidence anchor. +/// +/// This is provenance only. It carries no message body, prompt, reasoning, +/// secret, tool output, or authorization authority. +#[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)] +pub struct MemoryEvidenceOrigin { + pub kind: MemoryEvidenceOriginKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub account_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worker_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flow_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flow_definition_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] + pub flow_definition_revision: Option, +} + +/// Record-level source range for one Memory staging candidate. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemorySourceRef { + pub segment_id: String, + #[cfg_attr(feature = "typescript", ts(type = "[number, number]"))] + pub range: [u64; 2], +} + +/// Bounded evidence snippet included in one Memory staging record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryStagingEvidence { + pub id: String, + pub kind: String, + #[cfg_attr(feature = "typescript", ts(type = "[number, number] | null"))] + pub entry_range: Option<[u64; 2]>, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "typescript", + ts(optional, type = "MemoryEvidenceOrigin | null") + )] + pub origin: Option, + pub excerpt: Option, + pub summary: Option, +} + +/// Bounded source anchor included in one Memory staging record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemorySourceEvidenceRef { + pub session_id: Option, + pub segment_id: Option, + #[cfg_attr(feature = "typescript", ts(type = "[number, number] | null"))] + pub entry_range: Option<[u64; 2]>, + pub evidence_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "typescript", + ts(optional, type = "MemoryEvidenceOrigin | null") + )] + pub origin: Option, + pub evidence_kind: Option, + pub label: Option, + pub summary: Option, +} + +/// Public projection of one valid Memory staging record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryStagingRecord { + pub schema_version: u32, + pub id: String, + pub extract_run_id: String, + pub source: MemorySourceRef, + pub kind: MemoryCandidateKind, + pub claim: String, + pub why_useful: String, + pub staleness: Option, + pub evidence: Vec, + pub source_refs: Vec, +} + +/// Public list entry for one valid Memory staging record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryStagingEntry { + pub id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub byte_len: u64, + pub record: MemoryStagingRecord, +} + +/// Public response returned by the Workspace Memory staging list resource. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryStagingListResponse { + pub limit: usize, + pub returned_count: usize, + pub total_valid_count: usize, + pub invalid_count: usize, + pub truncated: bool, + pub order: String, + pub record_authority: String, + pub items: Vec, + pub diagnostics: Vec, +} + +#[cfg(feature = "typescript")] +pub fn memory_api_typescript() -> String { + use ts_rs::TS; + + let config = ts_rs::Config::default(); + let declarations = [ + DiagnosticSeverity::decl(&config), + Diagnostic::decl(&config), + MemoryDocumentResponse::decl(&config), + MemoryCandidateKind::decl(&config), + MemoryEvidenceOriginKind::decl(&config), + MemoryEvidenceOrigin::decl(&config), + MemorySourceRef::decl(&config), + MemoryStagingEvidence::decl(&config), + MemorySourceEvidenceRef::decl(&config), + MemoryStagingRecord::decl(&config), + MemoryStagingEntry::decl(&config), + MemoryStagingListResponse::decl(&config), + ]; + + format!( + "// Generated from server-api. Do not edit by hand.\n// Regenerate: cargo run -q -p server-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts\n\n{}\n", + declarations + .into_iter() + .map(|declaration| format!("export {declaration}")) + .collect::>() + .join("\n\n") + ) +} + +/// Workspace-owned Memory settings returned by the shared Server API. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorkspaceMemorySettings { + pub workspace_id: String, + pub settings_revision: u64, + pub language: String, +} + +/// Compare-and-swap update for Workspace-owned Memory settings. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct UpdateWorkspaceMemorySettingsRequest { + pub expected_revision: u64, + pub language: String, +} + +/// Public metadata for one Workspace-scoped Repository SSH credential. +/// +/// Secret references and secret material are deliberately not part of this DTO. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositorySshCredential { + pub credential_id: String, + pub workspace_id: String, + pub name: String, + pub public_key_algorithm: String, + pub public_key_fingerprint: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub current_revision: u64, + pub status: String, + pub created_at: String, + pub rotated_at: Option, + #[serde(default)] + pub referenced_repositories: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct CreateRepositorySshCredentialRequest { + pub operation_id: String, + pub credential_id: String, + pub name: String, + pub private_key: String, + #[serde(default)] + pub passphrase: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct GenerateRepositorySshCredentialRequest { + pub operation_id: String, + pub credential_id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositorySshPublicKey { + pub credential_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub current_revision: u64, + pub public_key_algorithm: String, + pub public_key_fingerprint: String, + pub public_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RotateRepositorySshCredentialRequest { + pub operation_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub expected_revision: u64, + pub private_key: String, + #[serde(default)] + pub passphrase: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct DeleteRepositorySshCredentialRequest { + pub operation_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub expected_revision: u64, +} + +/// Public metadata for an explicitly pinned SSH host key. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositorySshHostTrust { + pub host_trust_id: String, + pub workspace_id: String, + pub hostname: String, + pub port: u16, + pub key_algorithm: String, + pub host_key: String, + pub fingerprint: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub current_revision: u64, + pub created_at: String, + pub updated_at: String, + #[serde(default)] + pub referenced_repositories: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct PutRepositorySshHostTrustRequest { + pub operation_id: String, + pub host_trust_id: String, + pub hostname: String, + pub port: u16, + pub host_key: String, + #[serde(default)] + #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + pub expected_revision: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct DeleteRepositorySshHostTrustRequest { + pub operation_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub expected_revision: u64, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] +#[serde(rename_all = "snake_case")] +pub enum RepositoryAccessMode { + ReadOnly, + ReadWrite, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositorySshAccessBinding { + pub repository_key: String, + pub credential_id: String, + pub host_trust_id: String, + pub access: RepositoryAccessMode, +} + +/// Secret-free active Repository access projection consumed by later Runtime work. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RepositoryAccessProjection { + pub workspace_id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub config_revision: u64, + pub projection_digest: String, + pub bindings: Vec, +} + +pub const SKILL_CATALOG_AUTHORITY: &str = "workspace-config-skills-v1"; +pub const SKILL_API_MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; +pub const SKILL_API_MAX_CATALOG_ENTRIES: usize = 500; +pub const SKILL_API_MAX_OVERRIDES: usize = 64; +pub const SKILL_API_MAX_DIAGNOSTICS: usize = 100; +pub const SKILL_API_MAX_RESOURCES: usize = 500; +pub const SKILL_API_MAX_ALLOWED_TOOLS: usize = 100; +pub const SKILL_API_MAX_NAME_BYTES: usize = 128; +pub const SKILL_API_MAX_LABEL_BYTES: usize = 4_096; +pub const SKILL_API_MAX_BODY_BYTES: usize = 1_048_576; +pub const SKILL_API_MAX_PATH_BYTES: usize = 1_024; +pub const SKILL_API_MAX_DIGEST_BYTES: usize = 128; +pub const SKILL_API_MAX_RESPONSE_BYTES: usize = 2_097_152; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] +#[serde(rename_all = "snake_case")] +pub enum SkillDiagnosticSeverity { + Error, + Warning, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct SkillDiagnostic { + pub severity: SkillDiagnosticSeverity, + pub code: String, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub source: Option, +} + +impl SkillDiagnostic { + pub fn error( + code: impl Into, + message: impl Into, + source: Option, + ) -> Self { + Self { + severity: SkillDiagnosticSeverity::Error, + code: code.into(), + message: message.into(), + source, + } + } + + pub fn warning( + code: impl Into, + message: impl Into, + source: Option, + ) -> Self { + Self { + severity: SkillDiagnosticSeverity::Warning, + code: code.into(), + message: message.into(), + source, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] +#[serde(rename_all = "snake_case")] +pub enum SkillSourceKind { + Builtin, + Workspace, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct SkillProvenance { + pub kind: SkillSourceKind, + pub id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub virtual_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional, type = "number"))] + pub revision: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub source_digest: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub tree_digest: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] +#[serde(rename_all = "snake_case")] +pub enum SkillActivationStatus { + Active, + Inactive, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] +#[serde(rename_all = "snake_case")] +pub enum SkillProjectionStatus { + Valid, + Invalid, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct SkillProjectionIdentity { + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub config_revision: u64, + pub tree_digest: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct SkillResourceRef { + pub kind: String, + pub name: String, + pub supported: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub diagnostic: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct SkillCatalogEntry { + pub name: String, + pub description: String, + pub activation_status: SkillActivationStatus, + pub projection_status: SkillProjectionStatus, + pub provenance: SkillProvenance, + pub overrides: Vec, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct SkillCatalogResponse { + pub authority: String, + pub projection: SkillProjectionIdentity, + pub entries: Vec, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct SkillDetailResponse { + pub authority: String, + pub projection: SkillProjectionIdentity, + pub name: String, + pub description: String, + pub provenance: SkillProvenance, + pub overrides: Vec, + pub diagnostics: Vec, + pub activation_status: SkillActivationStatus, + pub projection_status: SkillProjectionStatus, + pub body: String, + pub allowed_tools: Vec, + pub allowed_tools_status: String, + pub resources: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SkillApiValidationError { + CollectionTooLarge, + StringTooLarge, + InvalidProjectionIdentity, + InvalidProvenance, + InvalidVirtualPath, + StaleProjection, +} + +impl std::fmt::Display for SkillApiValidationError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = match self { + Self::CollectionTooLarge => "Skill API collection exceeds its limit", + Self::StringTooLarge => "Skill API string exceeds its limit", + Self::InvalidProjectionIdentity => "Skill API projection identity is invalid", + Self::InvalidProvenance => "Skill API provenance is invalid", + Self::InvalidVirtualPath => "Skill API virtual path is invalid", + Self::StaleProjection => "Workspace Skill projection is stale", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for SkillApiValidationError {} + +impl SkillProjectionIdentity { + fn validate(&self) -> Result<(), SkillApiValidationError> { + validate_safe_integer(self.config_revision)?; + validate_nonempty_string(&self.tree_digest, SKILL_API_MAX_DIGEST_BYTES) + .map_err(|_| SkillApiValidationError::InvalidProjectionIdentity) + } +} + +impl SkillProvenance { + fn validate( + &self, + projection: &SkillProjectionIdentity, + ) -> Result<(), SkillApiValidationError> { + validate_nonempty_string(&self.id, SKILL_API_MAX_LABEL_BYTES)?; + validate_optional_string(self.virtual_path.as_deref(), SKILL_API_MAX_PATH_BYTES)?; + validate_optional_string(self.source_digest.as_deref(), SKILL_API_MAX_DIGEST_BYTES)?; + validate_optional_string(self.tree_digest.as_deref(), SKILL_API_MAX_DIGEST_BYTES)?; + if let Some(revision) = self.revision { + validate_safe_integer(revision)?; + } + + let expected_prefix = match self.kind { + SkillSourceKind::Builtin => "builtin:", + SkillSourceKind::Workspace => "workspace:", + }; + if !self.id.starts_with(expected_prefix) + || self + .virtual_path + .as_deref() + .is_none_or(|path| !is_virtual_path(path)) + || self.source_digest.is_none() + { + return Err(SkillApiValidationError::InvalidProvenance); + } + + match self.kind { + SkillSourceKind::Builtin => { + if self.revision.is_some() || self.tree_digest.is_some() { + return Err(SkillApiValidationError::InvalidProvenance); + } + } + SkillSourceKind::Workspace => { + let Some(revision) = self.revision else { + return Err(SkillApiValidationError::InvalidProvenance); + }; + let Some(tree_digest) = self.tree_digest.as_deref() else { + return Err(SkillApiValidationError::InvalidProvenance); + }; + if revision != projection.config_revision || tree_digest != projection.tree_digest { + return Err(SkillApiValidationError::StaleProjection); + } + } + } + Ok(()) + } +} + +impl SkillCatalogEntry { + fn validate( + &self, + projection: &SkillProjectionIdentity, + ) -> Result<(), SkillApiValidationError> { + validate_nonempty_string(&self.name, SKILL_API_MAX_NAME_BYTES)?; + validate_string(&self.description, SKILL_API_MAX_LABEL_BYTES)?; + validate_collection(&self.overrides, SKILL_API_MAX_OVERRIDES)?; + validate_diagnostics(&self.diagnostics)?; + self.provenance.validate(projection)?; + for provenance in &self.overrides { + provenance.validate(projection)?; + } + Ok(()) + } +} + +impl SkillCatalogResponse { + pub fn validate(&self) -> Result<(), SkillApiValidationError> { + validate_nonempty_string(&self.authority, SKILL_API_MAX_LABEL_BYTES)?; + if self.authority != SKILL_CATALOG_AUTHORITY { + return Err(SkillApiValidationError::InvalidProjectionIdentity); + } + self.projection.validate()?; + validate_collection(&self.entries, SKILL_API_MAX_CATALOG_ENTRIES)?; + validate_diagnostics(&self.diagnostics)?; + for entry in &self.entries { + entry.validate(&self.projection)?; + } + Ok(()) + } +} + +impl SkillDetailResponse { + pub fn validate(&self) -> Result<(), SkillApiValidationError> { + validate_nonempty_string(&self.authority, SKILL_API_MAX_LABEL_BYTES)?; + if self.authority != SKILL_CATALOG_AUTHORITY { + return Err(SkillApiValidationError::InvalidProjectionIdentity); + } + self.projection.validate()?; + validate_nonempty_string(&self.name, SKILL_API_MAX_NAME_BYTES)?; + validate_string(&self.description, SKILL_API_MAX_LABEL_BYTES)?; + validate_string(&self.body, SKILL_API_MAX_BODY_BYTES)?; + validate_strings( + &self.allowed_tools, + SKILL_API_MAX_ALLOWED_TOOLS, + SKILL_API_MAX_LABEL_BYTES, + )?; + validate_nonempty_string(&self.allowed_tools_status, SKILL_API_MAX_LABEL_BYTES)?; + validate_resources(&self.resources)?; + validate_collection(&self.overrides, SKILL_API_MAX_OVERRIDES)?; + validate_diagnostics(&self.diagnostics)?; + self.provenance.validate(&self.projection)?; + for provenance in &self.overrides { + provenance.validate(&self.projection)?; + } + Ok(()) + } +} + +fn validate_safe_integer(value: u64) -> Result<(), SkillApiValidationError> { + if value <= SKILL_API_MAX_SAFE_INTEGER { + Ok(()) + } else { + Err(SkillApiValidationError::InvalidProjectionIdentity) + } +} + +fn validate_collection(values: &[T], limit: usize) -> Result<(), SkillApiValidationError> { + if values.len() <= limit { + Ok(()) + } else { + Err(SkillApiValidationError::CollectionTooLarge) + } +} + +fn validate_string(value: &str, limit: usize) -> Result<(), SkillApiValidationError> { + if value.len() <= limit { + Ok(()) + } else { + Err(SkillApiValidationError::StringTooLarge) + } +} + +fn validate_nonempty_string(value: &str, limit: usize) -> Result<(), SkillApiValidationError> { + validate_string(value, limit)?; + if value.is_empty() { + Err(SkillApiValidationError::StringTooLarge) + } else { + Ok(()) + } +} + +fn validate_optional_string( + value: Option<&str>, + limit: usize, +) -> Result<(), SkillApiValidationError> { + if let Some(value) = value { + validate_nonempty_string(value, limit)?; + } + Ok(()) +} + +fn validate_strings( + values: &[String], + collection_limit: usize, + string_limit: usize, +) -> Result<(), SkillApiValidationError> { + validate_collection(values, collection_limit)?; + for value in values { + validate_nonempty_string(value, string_limit)?; + } + Ok(()) +} + +fn validate_resources(resources: &[SkillResourceRef]) -> Result<(), SkillApiValidationError> { + validate_collection(resources, SKILL_API_MAX_RESOURCES)?; + for resource in resources { + validate_nonempty_string(&resource.kind, SKILL_API_MAX_LABEL_BYTES)?; + validate_nonempty_string(&resource.name, SKILL_API_MAX_PATH_BYTES)?; + if !is_virtual_path(&resource.name) { + return Err(SkillApiValidationError::InvalidVirtualPath); + } + validate_optional_string(resource.diagnostic.as_deref(), SKILL_API_MAX_LABEL_BYTES)?; + } + Ok(()) +} + +fn is_virtual_path(value: &str) -> bool { + !value.starts_with('/') + && !value.contains('\\') + && value + .split('/') + .all(|component| !component.is_empty() && component != "." && component != "..") +} + +fn validate_diagnostics(diagnostics: &[SkillDiagnostic]) -> Result<(), SkillApiValidationError> { + validate_collection(diagnostics, SKILL_API_MAX_DIAGNOSTICS)?; + for diagnostic in diagnostics { + validate_nonempty_string(&diagnostic.code, SKILL_API_MAX_LABEL_BYTES)?; + validate_nonempty_string(&diagnostic.message, SKILL_API_MAX_LABEL_BYTES)?; + validate_optional_string(diagnostic.source.as_deref(), SKILL_API_MAX_PATH_BYTES)?; + } + Ok(()) +} + +#[cfg(feature = "typescript")] +pub fn normalized_typescript(generated: String) -> String { + let mut normalized = generated + .lines() + .map(str::trim_end) + .collect::>() + .join("\n"); + normalized.push('\n'); + normalized +} + +#[cfg(feature = "typescript")] +pub fn legacy_catalog_typescript() -> String { + use ts_rs::TS; + + let config = ts_rs::Config::default(); + let declarations = [ + WorkspaceSummary::decl(&config), + WorkspaceCatalogListResponse::decl(&config), + WorkspaceRepositoryRecord::decl(&config), + WorkspaceCreateResponse::decl(&config), + WorkspaceAuthConfig::decl(&config), + WorkspacePermissionSummary::decl(&config), + WorkspaceDeletionState::decl(&config), + WorkspaceDeletionBlockerKind::decl(&config), + WorkspaceDeletionBlocker::decl(&config), + WorkspaceDeletionResourceCounts::decl(&config), + WorkspaceDeletionPreflightResponse::decl(&config), + WorkspaceDeletionRequest::decl(&config), + WorkspaceDeletionOperationResponse::decl(&config), + DiagnosticSeverity::decl(&config), + Diagnostic::decl(&config), + WorkspaceExtensionPointState::decl(&config), + WorkspaceExtensionPoints::decl(&config), + WorkspaceResponse::decl(&config), + WorkspaceMetadataSettingsResponse::decl(&config), + UpdateWorkspaceMetadataRequest::decl(&config), + WorkspaceMetadataMutationResponse::decl(&config), + WorkspaceSigningIdentityState::decl(&config), + WorkspaceSigningIdentityPublic::decl(&config), + WorkspacePublicIdentityBundle::decl(&config), + WorkspaceSigningIdentityResponse::decl(&config), + ProfileSettingsResponse::decl(&config), + WorkspaceProfileSummary::decl(&config), + WorkspaceProfileSourceSummary::decl(&config), + WorkspaceProfileSourceProvenance::decl(&config), + RepositorySourceKind::decl(&config), + RepositorySource::decl(&config), + RepositoryObservedStatus::decl(&config), + RepositoryDiagnostic::decl(&config), + GitRemoteSummary::decl(&config), + GitRepositorySummary::decl(&config), + RepositorySummary::decl(&config), + GitCommitSummary::decl(&config), + RepositoryListResponse::decl(&config), + RepositoryDetailResponse::decl(&config), + RepositorySshConnectionProbeRequest::decl(&config), + RepositorySshHostKeyCandidate::decl(&config), + RepositorySshConnectionTrustState::decl(&config), + RepositorySshConnectionProbeResponse::decl(&config), + ConfirmRepositorySshHostTrustRequest::decl(&config), + RepositoryLogResponse::decl(&config), + RuntimeSourceKind::decl(&config), + RuntimeSourceStatus::decl(&config), + RuntimeIdentityAuthority::decl(&config), + RuntimeSourceSummary::decl(&config), + RuntimeSummary::decl(&config), + WorkspaceRuntimeBindingState::decl(&config), + RuntimeConnectionDisplayState::decl(&config), + RuntimeVerificationOutcome::decl(&config), + RuntimeVerificationEvidenceSummary::decl(&config), + WorkspaceRuntimeBindingSummary::decl(&config), + RuntimeManagementSummary::decl(&config), + WorkspaceRuntimeResource::decl(&config), + RuntimeTrustKeyStatus::decl(&config), + RuntimeTrustKeyState::decl(&config), + RuntimeTrustAuditAction::decl(&config), + RuntimeTrustAuditEntry::decl(&config), + WorkspaceRuntimeDetail::decl(&config), + RuntimeTrustKeyRevealResponse::decl(&config), + RevokeRuntimeTrustKeyRequest::decl(&config), + RemoveRuntimeRequest::decl(&config), + RuntimeRemovalOperationState::decl(&config), + RuntimeRemovalOperationResponse::decl(&config), + RuntimeTrustConflictKind::decl(&config), + RuntimeTrustConflictResponse::decl(&config), + RuntimePublicIdentityBundle::decl(&config), + CreateRemoteRuntimeRequest::decl(&config), + UpdateRemoteRuntimeRequest::decl(&config), + RuntimeConnectionTestStatus::decl(&config), + RuntimeConnectionTestFailureKind::decl(&config), + RuntimeConnectionTestResponse::decl(&config), + ] + .map(|declaration| format!("export {declaration}")); + + format!( + "// This file is generated by `cargo run -p server-api --features typescript --example generate_legacy_typescript | deno fmt -`.\n// Do not edit this file directly.\n\n{}\n", + declarations.join("\n\n") + ) +} + +#[cfg(feature = "typescript")] +pub fn repository_access_api_typescript() -> String { + use ts_rs::TS; + + let config = ts_rs::Config::default(); + let declarations = [ + RepositorySshCredential::decl(&config), + CreateRepositorySshCredentialRequest::decl(&config), + GenerateRepositorySshCredentialRequest::decl(&config), + RepositorySshPublicKey::decl(&config), + RotateRepositorySshCredentialRequest::decl(&config), + DeleteRepositorySshCredentialRequest::decl(&config), + RepositorySshHostTrust::decl(&config), + PutRepositorySshHostTrustRequest::decl(&config), + DeleteRepositorySshHostTrustRequest::decl(&config), + RepositoryAccessMode::decl(&config), + RepositorySshAccessBinding::decl(&config), + RepositoryAccessProjection::decl(&config), + ]; + format!( + "// Generated from server-api. Do not edit by hand.\n// Regenerate: cargo run -q -p server-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts\n\n{}\n", + declarations + .into_iter() + .map(|declaration| format!("export {declaration}")) + .collect::>() + .join("\n\n") + ) +} + +#[cfg(feature = "typescript")] +pub fn skill_api_typescript() -> String { + use ts_rs::TS; + + let config = ts_rs::Config::default(); + let declarations = [ + SkillDiagnosticSeverity::decl(&config), + SkillDiagnostic::decl(&config), + SkillSourceKind::decl(&config), + SkillProvenance::decl(&config), + SkillActivationStatus::decl(&config), + SkillProjectionStatus::decl(&config), + SkillProjectionIdentity::decl(&config), + SkillResourceRef::decl(&config), + SkillCatalogEntry::decl(&config), + SkillCatalogResponse::decl(&config), + SkillDetailResponse::decl(&config), + ]; + let limits = format!( + "export const SKILL_API_AUTHORITY = \"{SKILL_CATALOG_AUTHORITY}\" as const;\n\nexport const SKILL_API_LIMITS = {{\n maxSafeInteger: {SKILL_API_MAX_SAFE_INTEGER},\n maxCatalogEntries: {SKILL_API_MAX_CATALOG_ENTRIES},\n maxOverrides: {SKILL_API_MAX_OVERRIDES},\n maxDiagnostics: {SKILL_API_MAX_DIAGNOSTICS},\n maxResources: {SKILL_API_MAX_RESOURCES},\n maxAllowedTools: {SKILL_API_MAX_ALLOWED_TOOLS},\n maxNameBytes: {SKILL_API_MAX_NAME_BYTES},\n maxLabelBytes: {SKILL_API_MAX_LABEL_BYTES},\n maxBodyBytes: {SKILL_API_MAX_BODY_BYTES},\n maxPathBytes: {SKILL_API_MAX_PATH_BYTES},\n maxDigestBytes: {SKILL_API_MAX_DIGEST_BYTES},\n maxResponseBytes: {SKILL_API_MAX_RESPONSE_BYTES},\n}} as const;" + ); + format!( + "// Generated from server-api. Do not edit by hand.\n// Regenerate: cargo run -q -p server-api --features typescript --example generate_skill_api_types > web/workspace/src/lib/generated/skill-api.ts\n\n{limits}\n\n{}\n", + declarations + .into_iter() + .map(|declaration| format!("export {declaration}")) + .collect::>() + .join("\n\n") + ) +} + +#[cfg(feature = "typescript")] +pub fn auth_api_typescript() -> String { + use ts_rs::TS; + + let config = ts_rs::Config::default(); + let declarations = [ + AuthPublicConfig::decl(&config), + ActorAuthMethod::decl(&config), + AuthenticatedUser::decl(&config), + RequestActor::decl(&config), + WhoamiResponse::decl(&config), + AuthBootstrapUserRequest::decl(&config), + AuthUserResponse::decl(&config), + PasskeyRegistrationOptionsRequest::decl(&config), + PasskeyRegistrationOptionsResponse::decl(&config), + PasskeyRegistrationCompleteRequest::decl(&config), + PasskeyLoginOptionsRequest::decl(&config), + PasskeyLoginOptionsResponse::decl(&config), + PasskeyLoginCompleteRequest::decl(&config), + DeviceLoginStartRequest::decl(&config), + DeviceLoginStartResponse::decl(&config), + DeviceLoginApproveRequest::decl(&config), + DeviceLoginApprovalStatus::decl(&config), + DeviceLoginApproveResponse::decl(&config), + DeviceLoginPollRequest::decl(&config), + DeviceAccessTokenType::decl(&config), + DeviceLoginPollStatus::decl(&config), + DeviceLoginPollResponse::decl(&config), + LogoutStatus::decl(&config), + LogoutResponse::decl(&config), + ]; + format!( + "// Generated from server-api. Do not edit by hand.\n// Regenerate: cargo run -q -p server-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts\n\n{}\n", + declarations + .into_iter() + .map(|declaration| format!("export {declaration}")) + .collect::>() + .join("\n\n") + ) +} + +#[cfg(feature = "typescript")] +pub fn workdir_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), + WorkingDirectoryOccupancy::decl(&config), + WorkingDirectorySummary::decl(&config), + WorkingDirectoryCreateRequest::decl(&config), + WorkingDirectoryListResponse::decl(&config), + WorkingDirectoryDetailResponse::decl(&config), + WorkingDirectoryCreateResponse::decl(&config), + ]; + format!( + "// Generated from server-api. Do not edit by hand.\n// Regenerate: cargo run -q -p server-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts\n\n{}\n", + declarations + .into_iter() + .map(|declaration| format!("export {declaration}")) + .collect::>() + .join("\n\n") + ) +} + +#[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 server-api. Do not edit by hand.\n// Regenerate: cargo run -q -p server-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::>() + .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 server-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::() + .replace("=|", "=") + } +} + +#[cfg(all(test, feature = "typescript"))] +mod memory_typescript_tests { + #[test] + fn generated_memory_api_contract_is_current() { + let expected = super::memory_api_typescript(); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/generated/memory-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 Memory API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-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::() + .replace("=|", "=") + .replace(";}", "}") + } +} + +#[cfg(all(test, feature = "typescript"))] +mod skill_typescript_tests { + #[test] + fn generated_skill_api_contract_is_current() { + let expected = super::skill_api_typescript(); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/generated/skill-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 Skill API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_skill_api_types > web/workspace/src/lib/generated/skill-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::() + .replace("=|", "=") + .replace(";}", "}") + } +} + +#[cfg(all(test, feature = "typescript"))] +mod workdir_typescript_tests { + #[test] + fn generated_workdir_api_contract_is_current() { + let expected = super::workdir_api_typescript(); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/generated/workdir-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 Workdir API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-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::() + .replace("=|", "=") + } +} + #[cfg(test)] mod tests { use super::*; - #[test] fn worker_session_contract_and_flattened_availability_are_stable() { let operations = ServerApiMetadata::OPERATIONS; @@ -57,4 +3663,1371 @@ mod tests { assert_eq!(value["availability"], "live_protocol"); assert_eq!(value["subject"]["kind"], "runtime_worker"); } + + #[test] + fn worker_restore_state_round_trips_and_rejects_unknown_variants() { + for (state, wire) in [ + (WorkerRestoreState::Accepted, "accepted"), + (WorkerRestoreState::Rejected, "rejected"), + (WorkerRestoreState::RolledBack, "rolled_back"), + ( + WorkerRestoreState::ReconciliationRequired, + "reconciliation_required", + ), + ] { + assert_eq!( + serde_json::to_value(state).unwrap(), + serde_json::json!(wire) + ); + assert_eq!( + serde_json::from_value::(serde_json::json!(wire)).unwrap(), + state + ); + } + assert!( + serde_json::from_value::(serde_json::json!("unknown")).is_err() + ); + } + + #[test] + fn worker_restore_result_rejects_unknown_fields() { + let result = serde_json::from_value::(serde_json::json!({ + "state": "rejected", + "worker": null, + "diagnostics": [], + "unexpected": true + })); + assert!(result.is_err()); + } + + #[test] + fn historical_http_repository_source_kind_decodes_as_invalid_evidence() { + let source: RepositorySource = serde_json::from_value(serde_json::json!({ + "kind": "http", + "uri": "http://git.example.test/team/project.git", + "revision": 1, + })) + .unwrap(); + + assert_eq!(source.kind, RepositorySourceKind::Invalid); + assert_eq!( + serde_json::to_value(source).unwrap()["kind"], + serde_json::json!("invalid") + ); + assert_eq!( + RepositorySourceKind::parse("http"), + Some(RepositorySourceKind::Invalid) + ); + assert!(!RepositorySourceKind::Invalid.is_remote()); + } + + fn skill_projection() -> SkillProjectionIdentity { + SkillProjectionIdentity { + config_revision: 42, + tree_digest: "tree-digest".to_string(), + } + } + + fn builtin_skill_provenance() -> SkillProvenance { + SkillProvenance { + kind: SkillSourceKind::Builtin, + id: "builtin:errors".to_string(), + virtual_path: Some("skills/errors/SKILL.md".to_string()), + revision: None, + source_digest: Some("builtin-source-digest".to_string()), + tree_digest: None, + } + } + + fn workspace_skill_provenance() -> SkillProvenance { + SkillProvenance { + kind: SkillSourceKind::Workspace, + id: "workspace:skills/release/SKILL.md".to_string(), + virtual_path: Some("skills/release/SKILL.md".to_string()), + revision: Some(42), + source_digest: Some("workspace-source-digest".to_string()), + tree_digest: Some("tree-digest".to_string()), + } + } + + #[test] + fn skill_catalog_round_trips_builtin_workspace_and_invalid_projection_entries() { + let response = SkillCatalogResponse { + authority: "workspace-config-skills-v1".to_string(), + projection: skill_projection(), + entries: vec![ + SkillCatalogEntry { + name: "errors".to_string(), + description: "Builtin guidance".to_string(), + activation_status: SkillActivationStatus::Active, + projection_status: SkillProjectionStatus::Valid, + provenance: builtin_skill_provenance(), + overrides: vec![], + diagnostics: vec![], + }, + SkillCatalogEntry { + name: "release".to_string(), + description: "Workspace guidance".to_string(), + activation_status: SkillActivationStatus::Inactive, + projection_status: SkillProjectionStatus::Invalid, + provenance: workspace_skill_provenance(), + overrides: vec![builtin_skill_provenance()], + diagnostics: vec![SkillDiagnostic { + severity: SkillDiagnosticSeverity::Error, + code: "invalid_projection".to_string(), + message: "invalid projected Skill".to_string(), + source: Some("skills/release/SKILL.md".to_string()), + }], + }, + ], + diagnostics: vec![], + }; + + response.validate().expect("fixture should be valid"); + let json = serde_json::to_string(&response).expect("serialize Skill catalog"); + let decoded: SkillCatalogResponse = + serde_json::from_str(&json).expect("deserialize Skill catalog"); + assert_eq!(decoded, response); + assert!(!json.contains("\"revision\":null")); + assert!(!json.contains("\"tree_digest\":null")); + } + + #[test] + fn skill_detail_round_trips_shared_response() { + let response = SkillDetailResponse { + authority: "workspace-config-skills-v1".to_string(), + projection: skill_projection(), + name: "release".to_string(), + description: "Workspace guidance".to_string(), + body: "# Release\n".to_string(), + allowed_tools: vec!["Bash".to_string()], + allowed_tools_status: "experimental_hint_only".to_string(), + resources: vec![], + activation_status: SkillActivationStatus::Active, + projection_status: SkillProjectionStatus::Valid, + provenance: workspace_skill_provenance(), + overrides: vec![], + diagnostics: vec![], + }; + + response.validate().expect("fixture should be valid"); + let decoded: SkillDetailResponse = serde_json::from_value( + serde_json::to_value(&response).expect("serialize Skill detail"), + ) + .expect("deserialize Skill detail"); + assert_eq!(decoded, response); + } + + #[test] + fn skill_projection_validation_detects_stale_workspace_revision() { + let mut provenance = workspace_skill_provenance(); + provenance.revision = Some(41); + let response = SkillCatalogResponse { + authority: "workspace-config-skills-v1".to_string(), + projection: skill_projection(), + entries: vec![SkillCatalogEntry { + name: "release".to_string(), + description: String::new(), + activation_status: SkillActivationStatus::Active, + projection_status: SkillProjectionStatus::Valid, + provenance, + overrides: vec![], + diagnostics: vec![], + }], + diagnostics: vec![], + }; + + assert_eq!( + response.validate(), + Err(SkillApiValidationError::StaleProjection) + ); + } + + #[test] + fn skill_dto_rejects_unknown_fields_and_unknown_provenance_kind() { + let unknown_field = serde_json::json!({ + "authority": "workspace-config-skills-v1", + "projection": {"config_revision": 42, "tree_digest": "tree-digest"}, + "entries": [], + "diagnostics": [], + "body": "must not be accepted" + }); + assert!(serde_json::from_value::(unknown_field).is_err()); + + let mut provenance = + serde_json::to_value(workspace_skill_provenance()).expect("serialize provenance"); + provenance["kind"] = serde_json::Value::String("newer_source_kind".to_string()); + assert!(serde_json::from_value::(provenance).is_err()); + } + + #[test] + fn memory_evidence_origins_round_trip_as_typed_provenance() { + let kinds = [ + MemoryEvidenceOriginKind::HumanInput, + MemoryEvidenceOriginKind::WorkerInput, + MemoryEvidenceOriginKind::FlowInstruction, + MemoryEvidenceOriginKind::BackendInstruction, + MemoryEvidenceOriginKind::ModelOutput, + MemoryEvidenceOriginKind::ToolOutput, + MemoryEvidenceOriginKind::DerivedSummary, + MemoryEvidenceOriginKind::LegacyUnknown, + ]; + for kind in kinds { + let origin = MemoryEvidenceOrigin { + kind, + account_id: Some("account-1".to_string()), + workspace_id: Some("workspace-1".to_string()), + runtime_id: Some("runtime-1".to_string()), + worker_id: Some("worker-1".to_string()), + flow_selector: Some("builtin:coder-review".to_string()), + flow_definition_id: Some("flow-1".to_string()), + flow_definition_revision: Some(7), + }; + let encoded = serde_json::to_value(&origin).unwrap(); + let decoded: MemoryEvidenceOrigin = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, origin); + } + } + + #[test] + fn memory_evidence_origin_rejects_unknown_kind_and_fields() { + assert!( + serde_json::from_value::( + serde_json::json!({"kind": "future_origin"}) + ) + .is_err() + ); + assert!( + serde_json::from_value::(serde_json::json!({ + "kind": "human_input", + "future_field": "not current schema" + })) + .is_err() + ); + } + + 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 remote_runtime_metadata_update_cannot_carry_public_key_authority() { + let request = UpdateRemoteRuntimeRequest { + display_name: Some("Runtime A".to_string()), + endpoint: "https://runtime.example.test".to_string(), + }; + assert_eq!( + serde_json::to_value(&request).unwrap(), + serde_json::json!({ + "display_name": "Runtime A", + "endpoint": "https://runtime.example.test", + }) + ); + assert!( + serde_json::from_value::(serde_json::json!({ + "display_name": "Runtime A", + "endpoint": "https://runtime.example.test", + "public_bundle": { + "identity_id": "runtime-a", + "public_key": "yoi-ed25519-pub:v1:not-accepted", + }, + })) + .is_err(), + "metadata updates must reject public key fields" + ); + } + + #[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::(serde_json::json!({ + "runtime_id": "runtime-a", + "display_name": "Worker A", + "unexpected": true, + })) + .unwrap_err(); + assert!(error.to_string().contains("unknown field")); + } + + #[test] + fn repository_key_validation_is_canonical_and_bounded() { + let max = "a".repeat(64); + for valid in ["a", "main", "repo-42", max.as_str()] { + assert_eq!(validate_repository_key(valid), Ok(()), "{valid}"); + } + let too_long = "a".repeat(65); + for invalid in [ + "", + "-main", + "main-", + "Main", + "main_repo", + "main.repo", + "日本語", + too_long.as_str(), + ] { + assert!(validate_repository_key(invalid).is_err(), "{invalid}"); + } + } + + #[test] + fn workspace_deletion_wire_contract_is_closed_and_typed() { + let preflight = WorkspaceDeletionPreflightResponse { + workspace_id: "workspace-test".to_string(), + display_name: "Test".to_string(), + expected_revision: "revision-7".to_string(), + can_delete: true, + resources: WorkspaceDeletionResourceCounts { + workers: 2, + workdirs: 1, + repositories: 1, + runtime_bindings: 1, + secrets: 0, + artifacts: 3, + }, + blockers: Vec::new(), + }; + let value = serde_json::to_value(&preflight).unwrap(); + assert_eq!( + serde_json::from_value::(value.clone()).unwrap(), + preflight + ); + let mut stale = value.as_object().unwrap().clone(); + stale.insert("revision".to_string(), serde_json::json!(7)); + assert!( + serde_json::from_value::(stale.into()).is_err() + ); + + assert!( + serde_json::from_value::(serde_json::json!({ + "operation_id": "delete-test", + "expected_revision": "revision-7", + "confirmation": "Test", + "workspace_id": "caller-controlled" + })) + .is_err() + ); + assert!( + serde_json::from_value::(serde_json::json!({ + "operation_id": "x".repeat(WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES + 1), + "expected_revision": "revision-7", + "confirmation": "Test" + })) + .is_err() + ); + assert!( + serde_json::from_value::(serde_json::json!({ + "operation_id": "delete-test", + "workspace_id": "workspace-test", + "display_name": "Test", + "state": "blocked", + "resources": { + "workers": 0, + "workdirs": 0, + "repositories": 0, + "runtime_bindings": 0, + "secrets": 0, + "artifacts": 0 + }, + "child_operation_ids": [], + "blockers": (0..=WORKSPACE_DELETION_MAX_BLOCKERS).map(|_| serde_json::json!({ + "kind": "cleanup_unavailable", + "resource_kind": null, + "resource_key": null, + "message": "blocked" + })).collect::>(), + "failure_category": null, + "created_at": "1", + "updated_at": "1", + "completed_at": null + })) + .is_err() + ); + } + + #[test] + fn workspace_create_request_has_one_closed_shared_wire_shape() { + let request = WorkspaceCreateRequest { + operation_key: "workspace-create-1".to_string(), + display_name: "Workspace".to_string(), + repository: InitialRepositoryIntent { + repository_key: "main".to_string(), + uri: "/srv/repositories/main".to_string(), + default_ref: Some("develop".to_string()), + }, + }; + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["operation_key"], "workspace-create-1"); + assert_eq!(json["repository"]["uri"], "/srv/repositories/main"); + assert!(json.get("operation_id").is_none()); + assert!(json["repository"].get("source").is_none()); + assert!( + serde_json::from_value::(serde_json::json!({ + "operation_id": "workspace-create-1", + "display_name": "Workspace", + "repository": { + "repository_key": "main", + "source": "/srv/repositories/main" + } + })) + .is_err() + ); + } + + #[test] + fn workspace_and_repository_response_shapes_round_trip() { + let workspace = serde_json::json!({ + "workspace_id": "workspace-test", + "display_name": "Test", + "record_authority": "workspace-control-plane", + "schema_version": 46, + "auth": {"Passkey": { + "rp_id": "example.test", + "origin": "https://example.test", + "public_base_url": "https://example.test", + "cookie_name": "yoi_session" + }}, + "permissions": { + "manage_repositories": true, + "manage_secrets": true, + "manage_runtimes": true, + "delete_workspace": true + }, + "extension_points": { + "store": "sqlite", + "event_stream": {"status": "available", "note": "ready", "diagnostics": []}, + "host_worker_bridge": {"status": "available", "note": "ready", "diagnostics": []}, + "companion_console": {"status": "available", "note": "ready", "diagnostics": []} + } + }); + let parsed: WorkspaceResponse = serde_json::from_value(workspace.clone()).unwrap(); + assert_eq!(serde_json::to_value(parsed).unwrap(), workspace); + + let catalog = serde_json::json!([{ + "workspace_id": "workspace-test", + "owner_account_id": "user-test", + "display_name": "Test", + "state": "active", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z" + }]); + let parsed: WorkspaceCatalogListResponse = serde_json::from_value(catalog.clone()).unwrap(); + assert_eq!(serde_json::to_value(parsed).unwrap(), catalog); + + let repositories = serde_json::json!({ + "workspace_id": "workspace-test", + "items": [{ + "repository_key": "main", + "kind": "git", + "provider": "git", + "source": {"kind": "local_path", "uri": "/srv/project"}, + "source_revision": 1, + "source_fingerprint": "sha256:test", + "observed_status": "ready", + "record_authority": "workspace-control-plane" + }], + "source": "workspace-control-plane", + "diagnostics": [] + }); + let parsed: RepositoryListResponse = serde_json::from_value(repositories.clone()).unwrap(); + assert_eq!(serde_json::to_value(parsed).unwrap(), repositories); + } + + #[test] + fn repository_response_rejects_stale_field_aliases() { + let stale = serde_json::json!({ + "workspace_id": "workspace-test", + "items": [{ + "repository_key": "main", + "display_name": "main", + "kind": "git", + "provider": "git", + "source": {"kind": "local_path", "uri": "/srv/project"}, + "source_revision": 1, + "source_fingerprint": "sha256:test", + "observed_status": "ready", + "record_authority": "workspace-control-plane" + }], + "source": "workspace-control-plane", + "diagnostics": [] + }); + + assert!(serde_json::from_value::(stale).is_err()); + } + + #[test] + fn runtime_detail_and_trust_mutations_are_closed_and_typed() { + let detail = serde_json::json!({ + "workspace_id": "workspace-test", + "runtime": { + "runtime_id": "runtime-test", + "label": "Runtime Test", + "kind": "remote_http", + "status": "active", + "source": { + "kind": "remote_http", + "status": "active", + "identity_authority": "runtime_registry_projection", + "note": "active" + }, + "host_ids": [], + "worker_creation_available": true, + "os": "linux", + "arch": "x86_64", + "diagnostics": [], + "management": { + "built_in": false, + "config_managed": true, + "removable": true, + "endpoint_configured": true, + "token_ref_configured": false + } + }, + "endpoint": "https://runtime.example", + "trust_key": { + "status": "active", + "fingerprint": "SHA256:test", + "revision": 2, + "created_at": "2026-09-01T12:00:00Z", + "updated_at": "2026-09-01T13:00:00Z" + }, + "recent_audit": [{ + "action": "replaced", + "actor_account_id": "account-owner", + "old_fingerprint": "SHA256:old", + "new_fingerprint": "SHA256:test", + "revision": 2, + "at": "2026-09-01T13:00:00Z" + }] + }); + let parsed: WorkspaceRuntimeDetail = serde_json::from_value(detail.clone()).unwrap(); + assert_eq!(serde_json::to_value(parsed).unwrap(), detail); + + let mut unknown = detail; + unknown["trust_key"]["private_key"] = serde_json::json!("forbidden"); + assert!(serde_json::from_value::(unknown).is_err()); + assert!( + serde_json::from_value::(serde_json::json!({ + "public_key": "yoi-ed25519-pub:v1:key", + "private_key": "forbidden" + })) + .is_err() + ); + assert!( + serde_json::from_value::(serde_json::json!({ + "expected_revision": 1, + "delete_runtime": true + })) + .is_err() + ); + } + + #[test] + fn runtime_connection_test_response_is_closed_and_typed() { + let compatible = serde_json::json!({ + "workspace_id": "workspace-test", + "runtime_id": "runtime-test", + "binding_revision": 3, + "connection_state": "verified", + "verification": null, + "checked_at": "2026-09-01T12:00:00Z", + "status": "compatible", + "failure_kind": null, + "expected_protocol_version": 1, + "actual_protocol_version": 1, + "diagnostics": [] + }); + let parsed: RuntimeConnectionTestResponse = + serde_json::from_value(compatible.clone()).unwrap(); + assert_eq!(serde_json::to_value(parsed).unwrap(), compatible); + + let mut unknown = compatible; + unknown["capabilities"] = serde_json::json!(["shell"]); + assert!(serde_json::from_value::(unknown).is_err()); + } + + #[cfg(feature = "typescript")] + #[test] + fn generated_catalog_typescript_keeps_public_wrappers_and_nullability() { + let output = legacy_catalog_typescript(); + assert!( + output.contains("export type WorkspaceCatalogListResponse = Array") + ); + assert!(output.contains("export type WorkspaceResponse =")); + assert!(output.contains("permissions: WorkspacePermissionSummary")); + assert!(output.contains("export type RepositoryListResponse =")); + assert!(output.contains("items: Array")); + assert!(output.contains("observed_at?: string | null")); + assert!(output.contains("export type WorkspaceMetadataSettingsResponse =")); + assert!(output.contains("export type WorkspaceMetadataMutationResponse =")); + assert!(output.contains("export type ProfileSettingsResponse =")); + assert!(output.contains("config_revision?: number | null")); + assert!(output.contains("provenance: WorkspaceProfileSourceProvenance")); + assert!(output.contains( + "export type WorkspaceProfileSourceProvenance = \"project_profile_source_tree\"" + )); + assert!(output.contains("export type RuntimeConnectionTestResponse =")); + assert!(output.contains("status: RuntimeConnectionTestStatus")); + assert!(output.contains("failure_kind: RuntimeConnectionTestFailureKind | null")); + assert!(!output.contains("repository_key: string, display_name")); + } + + #[test] + fn worker_resource_key_is_required() { + let payload = serde_json::json!({ + "runtime_id": "arcadia", + "worker_id": "worker-1", + "host_id": "host", + "display_name": "Coder", + "label": "Coder", + "workspace": { + "visibility": "workspace", + "identity": "workspace-test" + }, + "state": "idle", + "implementation": {"kind": "worker", "display_hint": "Coder"}, + "capabilities": {"can_stop": true, "can_spawn_followup": false} + }); + + assert!(serde_json::from_value::(payload).is_err()); + } + + fn round_trip(value: T) + where + T: std::fmt::Debug + PartialEq + Serialize + for<'de> Deserialize<'de>, + { + let encoded = serde_json::to_vec(&value).expect("fixture should serialize"); + let decoded: T = serde_json::from_slice(&encoded).expect("fixture should deserialize"); + assert_eq!(decoded, value); + } + + #[test] + fn workspace_metadata_and_profile_projection_fixtures_round_trip() { + let diagnostic = Diagnostic { + code: "profile_projection_warning".to_string(), + severity: DiagnosticSeverity::Warning, + message: "projected from the active config revision".to_string(), + }; + let metadata = WorkspaceMetadataSettingsResponse { + workspace_id: "workspace-test".to_string(), + display_name: "Test".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + revision: "sha256:metadata".to_string(), + source: "workspace-config".to_string(), + diagnostics: vec![diagnostic.clone()], + }; + round_trip(metadata.clone()); + round_trip(UpdateWorkspaceMetadataRequest { + display_name: "Renamed".to_string(), + revision: metadata.revision.clone(), + }); + round_trip(WorkspaceMetadataMutationResponse { + workspace: metadata, + diagnostics: vec![], + }); + + round_trip(ProfileSettingsResponse { + workspace_id: "workspace-test".to_string(), + registry_revision: "config-source:7:sha256:tree:sha256:projection".to_string(), + config_revision: Some(7), + tree_digest: Some("sha256:tree".to_string()), + projection_digest: Some("sha256:projection".to_string()), + default_profile: Some("workspace:coder".to_string()), + profiles: vec![WorkspaceProfileSummary { + profile_id: "workspace:coder".to_string(), + selector: "workspace:coder".to_string(), + label: "Coder".to_string(), + source_kind: "project".to_string(), + profile_source_id: Some("profile-source-1".to_string()), + description: None, + editable: true, + is_default: true, + diagnostics: vec![diagnostic.clone()], + }], + sources: vec![WorkspaceProfileSourceSummary { + profile_source_id: "profile-source-1".to_string(), + display_path: "profiles/coder.dcdl".to_string(), + kind: "profile".to_string(), + content_type: "text/x-decodal".to_string(), + content_digest: "sha256:source".to_string(), + provenance: WorkspaceProfileSourceProvenance::ProjectProfileSourceTree, + editable: false, + revision: "config-source:7".to_string(), + size_bytes: 128, + diagnostics: vec![], + }], + diagnostics: vec![diagnostic], + }); + + let absent_optional_fields = serde_json::json!({ + "workspace_id": "workspace-test", + "registry_revision": "builtin", + "profiles": [], + "sources": [], + "diagnostics": [] + }); + let decoded: ProfileSettingsResponse = + serde_json::from_value(absent_optional_fields.clone()).unwrap(); + assert_eq!(decoded.config_revision, None); + assert_eq!(decoded.tree_digest, None); + assert_eq!(decoded.projection_digest, None); + assert_eq!(decoded.default_profile, None); + assert_eq!( + serde_json::to_value(decoded).unwrap(), + absent_optional_fields + ); + } + + #[test] + fn workspace_signing_identity_wire_contract_omits_private_and_pending_fields() { + let response = WorkspaceSigningIdentityResponse { + identity: WorkspaceSigningIdentityPublic { + workspace_id: "workspace-test".to_string(), + key_id: "WK-test".to_string(), + algorithm: "ed25519".to_string(), + public_key: None, + public_key_fingerprint: None, + revision: 1, + state: WorkspaceSigningIdentityState::PendingProvisioning, + created_at: "2026-01-01T00:00:00Z".to_string(), + provisioned_at: None, + }, + public_bundle: None, + }; + let encoded = serde_json::to_value(&response).unwrap(); + assert_eq!( + encoded, + serde_json::json!({ + "identity": { + "workspace_id": "workspace-test", + "key_id": "WK-test", + "algorithm": "ed25519", + "revision": 1, + "state": "pending_provisioning", + "created_at": "2026-01-01T00:00:00Z" + } + }) + ); + assert!( + serde_json::from_value::(serde_json::json!({ + "identity": encoded["identity"].clone(), + "private_material_ref": "must-not-cross-the-wire" + })) + .is_err() + ); + } + + fn companion_worker() -> WorkspaceWorkerDiscoveryItem { + WorkspaceWorkerDiscoveryItem { + subject: WorkspaceWorkerSubject::RuntimeWorker { + runtime_id: "arcadia".to_string(), + worker_id: "worker-7".to_string(), + }, + resource_key: "W-7".to_string(), + display_name: "Companion".to_string(), + profile: Some("builtin:companion".to_string()), + status: Some("idle".to_string()), + } + } + + #[test] + fn companion_status_fixtures_round_trip() { + for state in [ + CompanionLifecycleState::Idle, + CompanionLifecycleState::Running, + CompanionLifecycleState::Stopped, + ] { + round_trip(CompanionStatusResponse { + state, + worker: Some(companion_worker()), + transport: CompanionTransportSummary { + mode: "worker_runtime".to_string(), + available: state != CompanionLifecycleState::Stopped, + }, + diagnostics: Vec::new(), + }); + } + } + + #[test] + fn companion_message_fixtures_round_trip() { + for state in [ + CompanionMessageDisposition::Accepted, + CompanionMessageDisposition::Rejected, + ] { + round_trip(CompanionMessageResponse { + state, + message: if state == CompanionMessageDisposition::Accepted { + "accepted" + } else { + "rejected" + } + .to_string(), + }); + } + } + + #[test] + fn auth_server_fixtures_round_trip_through_shared_dtos() { + let whoami = serde_json::json!({ + "actor": { + "user_id": "user-1", + "account_id": "account-1", + "handle": "hare", + "display_name": "Hare", + "auth_method": "browser_session" + } + }); + let decoded = serde_json::from_value::(whoami.clone()) + .expect("server whoami fixture should match shared DTO"); + assert_eq!(serde_json::to_value(decoded).unwrap(), whoami); + + let auth_config = serde_json::json!({ + "rp_id": "yoi.example", + "origin": "https://yoi.example", + "public_base_url": "https://yoi.example", + "cookie_name": "yoi_workspace_session" + }); + let decoded = serde_json::from_value::(auth_config.clone()) + .expect("server auth-config fixture should match shared DTO"); + assert_eq!(serde_json::to_value(decoded).unwrap(), auth_config); + + let auth_user = serde_json::json!({ + "user": { + "user_id": "user-1", + "account_id": "account-1", + "handle": "hare", + "display_name": "Hare" + } + }); + let decoded = serde_json::from_value::(auth_user.clone()) + .expect("server auth-user fixture should match shared DTO"); + assert_eq!(serde_json::to_value(decoded).unwrap(), auth_user); + + let registration_options = serde_json::json!({ + "challenge_id": "challenge-1", + "public_key": { + "publicKey": { + "challenge": "AQID", + "rp": {"id": "localhost", "name": "Yoi"}, + "user": {"id": "BAUG", "name": "hare", "displayName": "Hare"}, + "pubKeyCredParams": [{"type": "public-key", "alg": -7}] + } + } + }); + let decoded = serde_json::from_value::( + registration_options.clone(), + ) + .expect("server registration options fixture should match shared DTO"); + assert_eq!(serde_json::to_value(decoded).unwrap(), registration_options); + + let registration_complete = serde_json::json!({ + "challenge_id": "challenge-1", + "credential": { + "id": "AQID", + "rawId": "AQID", + "response": { + "attestationObject": "AQID", + "clientDataJSON": "AQID", + "transports": ["internal"] + }, + "type": "public-key", + "clientExtensionResults": {}, + "authenticatorAttachment": "platform" + } + }); + let decoded = + serde_json::from_value::(registration_complete) + .expect("server registration-complete fixture should match shared DTO"); + let encoded = serde_json::to_value(decoded).unwrap(); + serde_json::from_value::(encoded) + .expect("registration-complete DTO should round-trip"); + + let login_options = serde_json::json!({ + "challenge_id": "challenge-2", + "public_key": { + "publicKey": { + "challenge": "AQID", + "rpId": "localhost", + "allowCredentials": [], + "userVerification": "preferred" + } + } + }); + let decoded = serde_json::from_value::(login_options.clone()) + .expect("server login-options fixture should match shared DTO"); + assert_eq!(serde_json::to_value(decoded).unwrap(), login_options); + + let login_complete = serde_json::json!({ + "challenge_id": "challenge-2", + "credential": { + "id": "AQID", + "rawId": "AQID", + "response": { + "authenticatorData": "AQID", + "clientDataJSON": "AQID", + "signature": "AQID", + "userHandle": null + }, + "type": "public-key", + "clientExtensionResults": {}, + "authenticatorAttachment": "platform" + } + }); + let decoded = serde_json::from_value::(login_complete) + .expect("server login-complete fixture should match shared DTO"); + let encoded = serde_json::to_value(decoded).unwrap(); + serde_json::from_value::(encoded) + .expect("login-complete DTO should round-trip"); + + let device_start = serde_json::json!({ + "device_code": "device-secret", + "user_code": "ABCD-EFGH", + "verification_uri": "https://yoi.example/login/device", + "verification_uri_complete": "https://yoi.example/login/device?user_code=ABCD-EFGH", + "expires_in": 600, + "interval": 2 + }); + let decoded = serde_json::from_value::(device_start.clone()) + .expect("server device-login fixture should match shared DTO"); + assert_eq!(serde_json::to_value(decoded).unwrap(), device_start); + + let approved_user = AuthenticatedUser { + user_id: "user-1".to_string(), + account_id: "account-1".to_string(), + handle: "hare".to_string(), + display_name: "Hare".to_string(), + }; + round_trip(DeviceLoginApproveResponse { + status: DeviceLoginApprovalStatus::Approved, + user: approved_user, + }); + for status in [ + DeviceLoginPollStatus::Pending, + DeviceLoginPollStatus::Expired, + DeviceLoginPollStatus::Denied, + DeviceLoginPollStatus::Consumed, + ] { + round_trip(DeviceLoginPollResponse { + status, + access_token: None, + token_type: None, + }); + } + round_trip(DeviceLoginPollResponse { + status: DeviceLoginPollStatus::Approved, + access_token: Some("access-secret".to_string()), + token_type: Some(DeviceAccessTokenType::Bearer), + }); + round_trip(LogoutResponse { + status: LogoutStatus::LoggedOut, + }); + } + + #[test] + fn auth_dtos_reject_unknown_status_and_fields() { + let unknown_status = serde_json::json!({"status": "future_status"}); + assert!( + serde_json::from_value::(unknown_status).is_err(), + "unknown device-login statuses must fail closed" + ); + + let unsafe_expiry = serde_json::json!({ + "device_code": "device-secret", + "user_code": "ABCD-EFGH", + "verification_uri": "https://yoi.example/login/device", + "verification_uri_complete": "https://yoi.example/login/device?user_code=ABCD-EFGH", + "expires_in": 0, + "interval": 2 + }); + assert!(serde_json::from_value::(unsafe_expiry).is_err()); + + let malformed_credential = serde_json::json!({ + "challenge_id": "challenge-1", + "credential": { + "id": "AQID", + "rawId": "AQID", + "type": "public-key", + "response": {"clientDataJSON": 42} + } + }); + assert!( + serde_json::from_value::(malformed_credential).is_err(), + "malformed passkey credential payloads must fail closed" + ); + + let unexpected_field = serde_json::json!({ + "actor": null, + "access_token": "must-not-be-accepted" + }); + assert!(serde_json::from_value::(unexpected_field).is_err()); + } + + #[test] + fn device_login_debug_output_redacts_secret_material() { + let start = DeviceLoginStartResponse { + device_code: "device-secret".to_string(), + user_code: "ABCD-EFGH".to_string(), + verification_uri: "https://yoi.example/login/device".to_string(), + verification_uri_complete: "https://yoi.example/login/device?user_code=ABCD-EFGH" + .to_string(), + expires_in: 600, + interval: 2, + }; + let start_debug = format!("{start:?}"); + assert!(!start_debug.contains("device-secret")); + assert!(start_debug.contains("[redacted]")); + + let poll = DeviceLoginPollResponse { + status: DeviceLoginPollStatus::Approved, + access_token: Some("access-secret".to_string()), + token_type: Some(DeviceAccessTokenType::Bearer), + }; + let poll_debug = format!("{poll:?}"); + assert!(!poll_debug.contains("access-secret")); + assert!(poll_debug.contains("[redacted]")); + } + + #[cfg(feature = "typescript")] + #[test] + fn generated_auth_api_contract_is_current() { + let expected = auth_api_typescript(); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/generated/auth-api.ts"); + let actual = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + assert_eq!( + normalize_typescript(&actual), + normalize_typescript(&expected), + "regenerate auth API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts` and format the generated file", + ); + } + + #[test] + fn companion_transcript_fixture_round_trips() { + round_trip(CompanionTranscriptProjection { + state: CompanionLifecycleState::Idle, + start: 0, + limit: 2, + total: 2, + next: None, + items: vec![ + CompanionTranscriptItem { + sequence: 1, + role: CompanionTranscriptRole::User, + content: "hello".to_string(), + created_at: "2026-08-31T00:00:00Z".to_string(), + }, + CompanionTranscriptItem { + sequence: 2, + role: CompanionTranscriptRole::Assistant, + content: "hi".to_string(), + created_at: "2026-08-31T00:00:01Z".to_string(), + }, + ], + }); + } + + #[test] + fn companion_transcript_rejects_system_and_private_fields() { + let public_item = CompanionTranscriptItem { + sequence: 1, + role: CompanionTranscriptRole::Assistant, + content: "visible".to_string(), + created_at: "2026-08-31T00:00:00Z".to_string(), + }; + let public_fields = serde_json::to_value(public_item) + .expect("public transcript item should serialize") + .as_object() + .expect("public transcript item should be an object") + .keys() + .cloned() + .collect::>(); + assert_eq!( + public_fields, + ["content", "created_at", "role", "sequence"] + .into_iter() + .map(str::to_string) + .collect() + ); + + let system_item = serde_json::json!({ + "sequence": 1, + "role": "system", + "content": "raw system prompt", + "created_at": "2026-08-31T00:00:00Z" + }); + assert!(serde_json::from_value::(system_item).is_err()); + + let private_item = serde_json::json!({ + "sequence": 1, + "role": "assistant", + "content": "visible", + "created_at": "2026-08-31T00:00:00Z", + "reasoning": "hidden", + "credential": "secret", + "provider_session_id": "session-private" + }); + assert!(serde_json::from_value::(private_item).is_err()); + } + + #[cfg(feature = "typescript")] + #[test] + fn generated_companion_api_contract_is_current() { + let expected = companion_api_typescript(); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/generated/companion-api.ts"); + let actual = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + assert_eq!( + normalize_typescript(&actual), + normalize_typescript(&expected), + "regenerate Companion API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_companion_api_types > web/workspace/src/lib/generated/companion-api.ts` and format the generated file", + ); + } + + #[cfg(feature = "typescript")] + fn normalize_typescript(value: &str) -> String { + value + .chars() + .filter_map(|character| match character { + character if character.is_whitespace() => None, + ',' => Some(';'), + character => Some(character), + }) + .collect::() + .replace(";}", "}") + .replace("=|", "=") + } + + #[test] + fn workdir_create_request_preserves_optional_operation_fields() { + let payload = serde_json::json!({"repository_key": "main"}); + let request = serde_json::from_value::(payload) + .expect("optional create fields may be absent"); + + assert_eq!(request.runtime_id, None); + assert_eq!(request.selector, None); + assert_eq!(request.operation_id, None); + + let serialized = serde_json::to_value(request).expect("serialize create request"); + assert_eq!(serialized, serde_json::json!({"repository_key": "main"})); + } + + #[test] + fn workdir_create_request_rejects_stale_or_incomplete_json() { + let stale = serde_json::json!({ + "repository_key": "main", + "selector": "develop", + "path": "/tmp/workdir" + }); + assert!(serde_json::from_value::(stale).is_err()); + + let incomplete = serde_json::json!({ + "runtime_id": "arcadia", + "operation_id": "operation-1" + }); + assert!(serde_json::from_value::(incomplete).is_err()); + } + + #[test] + fn workdir_summary_omits_absent_optional_fields_on_the_wire() { + let value = serde_json::to_value(WorkingDirectorySummary { + working_directory_id: "workdir-1".into(), + repository_key: "main".into(), + creation_selector: None, + creation_ref: None, + creation_tree: None, + current_selector: None, + current_ref: None, + current_tree: None, + observed_at_epoch_seconds: None, + materializer_kind: WorkingDirectoryMaterializerKind::RuntimeGitClone, + cleanup_target: None, + status: WorkingDirectoryStatusKind::Active, + cleanliness: None, + primary_worker_id: None, + occupied_by: None, + }) + .expect("serialize Workdir summary"); + let object = value.as_object().expect("Workdir summary object"); + + for key in [ + "creation_selector", + "creation_ref", + "creation_tree", + "current_selector", + "current_ref", + "current_tree", + "observed_at_epoch_seconds", + "cleanup_target", + "cleanliness", + "primary_worker_id", + "occupied_by", + ] { + assert!( + !object.contains_key(key), + "absent field {key} must be omitted" + ); + } + } + + #[test] + fn workdir_response_rejects_stale_occupancy_shape() { + let stale = serde_json::json!({ + "workspace_id": "workspace-test", + "items": [{ + "working_directory_id": "workdir-1", + "repository_key": "main", + "materializer_kind": "runtime_git_clone", + "status": "active", + "occupied_by": { + "runtime_worker_id": "worker-1", + "display_name": "Coder", + "linked_at": "2026-01-01T00:00:00Z" + } + }], + "diagnostics": [] + }); + + assert!(serde_json::from_value::(stale).is_err()); + } +} + +#[cfg(all(test, feature = "typescript"))] +mod typescript_tests { + #[test] + fn generated_repository_access_contract_is_current() { + let expected = super::repository_access_api_typescript(); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/generated/repository-access-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 Repository Access TypeScript types with `cargo run -q -p server-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts` and format the generated file", + ); + } + + #[test] + fn generated_repository_access_responses_remain_secret_free() { + use ts_rs::TS; + + let config = ts_rs::Config::default(); + for declaration in [ + super::RepositorySshCredential::decl(&config), + super::RepositorySshHostTrust::decl(&config), + super::RepositoryAccessProjection::decl(&config), + ] { + for forbidden in ["private_key", "passphrase", "secret_ref"] { + assert!( + !declaration.contains(forbidden), + "Repository Access response declaration must not expose `{forbidden}`" + ); + } + } + } + + fn normalize(value: &str) -> String { + value + .chars() + .filter_map(|character| match character { + character if character.is_whitespace() => None, + ',' => Some(';'), + character => Some(character), + }) + .collect() + } } diff --git a/crates/workdir/Cargo.toml b/crates/workdir/Cargo.toml index 33f6c72b5..22c70a495 100644 --- a/crates/workdir/Cargo.toml +++ b/crates/workdir/Cargo.toml @@ -19,7 +19,6 @@ sha2.workspace = true tempfile.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["process", "rt", "sync", "time"] } -workspace-api = { workspace = true } [dev-dependencies] serde_json.workspace = true diff --git a/crates/workdir/src/workspace.rs b/crates/workdir/src/workspace.rs index fc696f991..76990f398 100644 --- a/crates/workdir/src/workspace.rs +++ b/crates/workdir/src/workspace.rs @@ -1,17 +1,139 @@ -//! Shared Workspace-facing Workdir inventory transport contracts. +//! Shared Workdir inventory domain contracts. //! -//! These projections describe Workspace inventory and durable occupancy. They -//! intentionally do not expose provider/session handles, host paths, Runtime -//! URLs, or credentials. Runtime-local Workdir operation contracts live in -//! [`crate::http`]. +//! These types are a neutral lower-level authority shared by Server and Runtime +//! transport projections. They intentionally do not expose provider/session +//! handles, host paths, Runtime URLs, or credentials. use serde::{Deserialize, Serialize}; -pub use workspace_api::{ - RuntimeWorkingDirectoryCleanupTarget, RuntimeWorkingDirectorySummary, - WorkingDirectoryCleanupTarget, WorkingDirectoryMaterializerKind as MaterializerKind, - WorkingDirectoryOccupancy, WorkingDirectoryStatusKind, WorkingDirectorySummary, -}; +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectoryMaterializerKind { + #[default] + RuntimeGitClone, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectoryStatusKind { + Active, + CleanupPending, + Corrupted, + NotFound, + Unknown, +} + +impl WorkingDirectoryStatusKind { + pub const fn as_str(&self) -> &'static str { + match self { + Self::Active => "active", + Self::CleanupPending => "cleanup_pending", + Self::Corrupted => "corrupted", + Self::NotFound => "not_found", + Self::Unknown => "unknown", + } + } +} + +impl std::fmt::Display for WorkingDirectoryStatusKind { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectoryCleanupTarget { + pub kind: String, + pub working_directory_id: String, + pub repository_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectoryOccupancy { + pub runtime_id: String, + pub worker_id: String, + pub display_name: String, + pub linked_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct RuntimeWorkingDirectoryCleanupTarget { + pub kind: String, + pub working_directory_id: String, + pub repository_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct RuntimeWorkingDirectorySummary { + pub working_directory_id: String, + pub repository_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_tree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_tree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub observed_at_epoch_seconds: Option, + pub materializer_kind: WorkingDirectoryMaterializerKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cleanup_target: Option, + pub status: WorkingDirectoryStatusKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cleanliness: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_worker_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub occupied_by: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorkingDirectorySummary { + pub working_directory_id: String, + pub repository_key: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_tree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_tree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub observed_at_epoch_seconds: Option, + pub materializer_kind: WorkingDirectoryMaterializerKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cleanup_target: Option, + pub status: WorkingDirectoryStatusKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cleanliness: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_worker_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub occupied_by: Option, +} + +impl WorkingDirectorySummary { + pub fn is_workspace_managed(&self) -> bool { + self.cleanup_target.is_some() + } +} +pub use WorkingDirectoryMaterializerKind as MaterializerKind; /// Stable Workspace identity for a Worker hosted by a Runtime. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] @@ -89,15 +211,29 @@ mod tests { } #[test] - fn workspace_workdir_projection_reexports_workspace_api_authority() { - assert_eq!( - std::any::TypeId::of::(), - std::any::TypeId::of::() - ); - assert_eq!( - std::any::TypeId::of::(), - std::any::TypeId::of::() - ); + fn working_directory_summary_preserves_server_projection_wire_shape() { + let summary = WorkingDirectorySummary { + working_directory_id: "wd-1".to_string(), + repository_key: "repo-1".to_string(), + creation_selector: None, + creation_ref: None, + creation_tree: None, + current_selector: None, + current_ref: None, + current_tree: None, + observed_at_epoch_seconds: None, + materializer_kind: WorkingDirectoryMaterializerKind::RuntimeGitClone, + cleanup_target: None, + status: WorkingDirectoryStatusKind::Active, + cleanliness: None, + primary_worker_id: None, + occupied_by: None, + }; + let value = serde_json::to_value(summary).unwrap(); + assert_eq!(value["working_directory_id"], "wd-1"); + assert_eq!(value["materializer_kind"], "runtime_git_clone"); + assert_eq!(value["status"], "active"); + assert!(value.get("cleanup_target").is_none()); } } diff --git a/crates/worker-runtime/Cargo.toml b/crates/worker-runtime/Cargo.toml index 68eb0cd97..e07dde7ef 100644 --- a/crates/worker-runtime/Cargo.toml +++ b/crates/worker-runtime/Cargo.toml @@ -50,7 +50,7 @@ uuid = { workspace = true, features = ["v7"] } zeroize.workspace = true tower = { workspace = true, features = ["util"], optional = true } worker.workspace = true -workspace-api = { path = "../workspace-api" } +server-api = { path = "../server-api" } workdir.workspace = true [dev-dependencies] diff --git a/crates/worker-runtime/src/catalog.rs b/crates/worker-runtime/src/catalog.rs index f9cfa9b5d..ff99a2d9a 100644 --- a/crates/worker-runtime/src/catalog.rs +++ b/crates/worker-runtime/src/catalog.rs @@ -74,7 +74,7 @@ impl std::ops::Deref for RepositorySelector { pub struct WorkingDirectoryRepository { pub id: String, pub provider: String, - pub source: workspace_api::RepositorySource, + pub source: server_api::RepositorySource, pub source_revision: u64, pub source_fingerprint: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -131,7 +131,7 @@ pub struct RepositorySshMaterializationAccess { pub credential_candidates: Vec, pub host_trust_id: String, pub host_trust_revision: u64, - pub access: workspace_api::RepositoryAccessMode, + pub access: server_api::RepositoryAccessMode, pub expires_at_epoch_seconds: u64, pub repository_id: String, pub repository_source_fingerprint: String, diff --git a/crates/worker-runtime/src/http_server.rs b/crates/worker-runtime/src/http_server.rs index 9f531ae60..404071938 100644 --- a/crates/worker-runtime/src/http_server.rs +++ b/crates/worker-runtime/src/http_server.rs @@ -2584,9 +2584,9 @@ mod tests { use axum::body::to_bytes; use axum::http::Method; use manifest::{Scope, SharedScope}; + use runtime_api::WorkerRestoreState; use sha2::Digest as _; use tower::ServiceExt; - use workspace_api::WorkerRestoreState; #[derive(Clone)] struct TestRuntimeAuthorizer(String); diff --git a/crates/worker-runtime/src/http_server/runtime_management_api.rs b/crates/worker-runtime/src/http_server/runtime_management_api.rs index c650f1be5..8d29fe188 100644 --- a/crates/worker-runtime/src/http_server/runtime_management_api.rs +++ b/crates/worker-runtime/src/http_server/runtime_management_api.rs @@ -377,7 +377,20 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi { .map_err(RuntimeHttpRestError::runtime) .map_err(api_error)?; Ok(runtime_api::WorkerRestoreResponse { - state: result.state, + state: match result.state { + server_api::WorkerRestoreState::Accepted => { + runtime_api::WorkerRestoreState::Accepted + } + server_api::WorkerRestoreState::Rejected => { + runtime_api::WorkerRestoreState::Rejected + } + server_api::WorkerRestoreState::RolledBack => { + runtime_api::WorkerRestoreState::RolledBack + } + server_api::WorkerRestoreState::ReconciliationRequired => { + runtime_api::WorkerRestoreState::ReconciliationRequired + } + }, worker: result.worker.map(response).transpose()?, reason_code: result.reason_code, message: result.message, diff --git a/crates/worker-runtime/src/main.rs b/crates/worker-runtime/src/main.rs index adec4f1bb..835efde8b 100644 --- a/crates/worker-runtime/src/main.rs +++ b/crates/worker-runtime/src/main.rs @@ -1355,8 +1355,8 @@ mod tests { #[test] fn workspace_issuer_trust_cli_persists_across_reload_and_writes_private_mode() { + use server_api::WorkspacePublicIdentityBundle; use sha2::{Digest as _, Sha256}; - use workspace_api::WorkspacePublicIdentityBundle; let temp = tempfile::tempdir().unwrap(); let identity = RuntimeIdentityMaterial::generate("WK-1").unwrap(); diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index 6236dac05..d9b614552 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -46,6 +46,7 @@ use protocol::subscription::{ SubscriptionWorkerState, }; use protocol::{Event, Method}; +use server_api::WorkerRestoreState; use std::collections::BTreeMap; #[cfg(feature = "ws-server")] use std::collections::VecDeque; @@ -55,7 +56,6 @@ use std::sync::{Arc, Mutex, MutexGuard, Weak}; use tokio::sync::broadcast; use tokio::sync::mpsc; use uuid::Uuid; -use workspace_api::WorkerRestoreState; /// Workspace-scoped Runtime authorization context supplied by a trusted backend. #[derive(Clone, Debug, Eq, PartialEq)] @@ -4835,8 +4835,8 @@ mod tests { repository: WorkingDirectoryRepository { id: "repository-1".to_string(), provider: "git".to_string(), - source: workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + source: server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, uri: "ssh://git@example.test/repo.git".to_string(), }, source_revision: 1, @@ -4859,7 +4859,7 @@ mod tests { }], host_trust_id: "host-trust-1".to_string(), host_trust_revision: 1, - access: workspace_api::RepositoryAccessMode::ReadOnly, + access: server_api::RepositoryAccessMode::ReadOnly, expires_at_epoch_seconds: u64::MAX, repository_id: "repository-1".to_string(), repository_source_fingerprint: "sha256:source".to_string(), @@ -4977,7 +4977,7 @@ mod tests { ], host_trust_id: "host-trust-1".to_string(), host_trust_revision: 1, - access: workspace_api::RepositoryAccessMode::ReadOnly, + access: server_api::RepositoryAccessMode::ReadOnly, expires_at_epoch_seconds: u64::MAX, repository_id: "repository-1".to_string(), repository_source_fingerprint: "sha256:source".to_string(), @@ -5066,8 +5066,8 @@ mod tests { repository: WorkingDirectoryRepository { id: "repository-1".to_string(), provider: "git".to_string(), - source: workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + source: server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, uri: "ssh://git@example.test/repo.git".to_string(), }, source_revision: 1, @@ -5097,7 +5097,7 @@ mod tests { ], host_trust_id: "host-trust-1".to_string(), host_trust_revision: 1, - access: workspace_api::RepositoryAccessMode::ReadOnly, + access: server_api::RepositoryAccessMode::ReadOnly, expires_at_epoch_seconds: u64::MAX, repository_id: "repository-1".to_string(), repository_source_fingerprint: "sha256:source".to_string(), diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index 7715196b7..b25f27749 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -3583,8 +3583,8 @@ mod tests { repository: WorkingDirectoryRepository { id: "repo-main".to_string(), provider: "git".to_string(), - source: workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + source: server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: repo.display().to_string(), }, source_revision: 1, diff --git a/crates/worker-runtime/src/worker_source.rs b/crates/worker-runtime/src/worker_source.rs index f57e3a9e4..f42aba2a3 100644 --- a/crates/worker-runtime/src/worker_source.rs +++ b/crates/worker-runtime/src/worker_source.rs @@ -455,7 +455,7 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient { fn list_workspace_workers( &self, request: worker::WorkspaceWorkerDiscoveryRequest, - ) -> Result { + ) -> Result { let mut path = format!( "/api/w/{}/worker-discovery/workers?limit={}", self.workspace_id, request.limit diff --git a/crates/worker-runtime/src/working_directory.rs b/crates/worker-runtime/src/working_directory.rs index 8d6aac3b9..79cb50190 100644 --- a/crates/worker-runtime/src/working_directory.rs +++ b/crates/worker-runtime/src/working_directory.rs @@ -842,8 +842,8 @@ impl RuntimeGitMaterializer { binding.command_environment.insert( "YOI_REPOSITORY_ACCESS".to_string(), match access.access { - workspace_api::RepositoryAccessMode::ReadOnly => "read_only", - workspace_api::RepositoryAccessMode::ReadWrite => "read_write", + server_api::RepositoryAccessMode::ReadOnly => "read_only", + server_api::RepositoryAccessMode::ReadWrite => "read_write", } .to_string(), ); @@ -884,7 +884,7 @@ impl RuntimeGitMaterializer { Self::validate_plain_http_source(request)?; if matches!( request.repository.source.kind, - workspace_api::RepositorySourceKind::Https | workspace_api::RepositorySourceKind::Ssh + server_api::RepositorySourceKind::Https | server_api::RepositorySourceKind::Ssh ) { validate_remote_source_uri(request)?; let materialization = request.materialization.as_ref().ok_or_else(|| { @@ -906,10 +906,10 @@ impl RuntimeGitMaterializer { } } match request.repository.source.kind { - workspace_api::RepositorySourceKind::LocalPath - | workspace_api::RepositorySourceKind::File - | workspace_api::RepositorySourceKind::Https => {} - workspace_api::RepositorySourceKind::Ssh => { + server_api::RepositorySourceKind::LocalPath + | server_api::RepositorySourceKind::File + | server_api::RepositorySourceKind::Https => {} + server_api::RepositorySourceKind::Ssh => { let ssh = request .materialization .as_ref() @@ -922,7 +922,7 @@ impl RuntimeGitMaterializer { })?; validate_ssh_materialization_access(ssh)?; } - workspace_api::RepositorySourceKind::Invalid => { + server_api::RepositorySourceKind::Invalid => { return Err(WorkingDirectoryDiagnostic::new( "working_directory_repository_source_invalid", "configured Repository source is invalid and cannot be materialized", @@ -939,7 +939,7 @@ impl RuntimeGitMaterializer { ) -> Result { let mut request = request.clone(); if request.repository.provider != "git" - || request.repository.source.kind != workspace_api::RepositorySourceKind::Ssh + || request.repository.source.kind != server_api::RepositorySourceKind::Ssh { return Ok(request); } @@ -1911,7 +1911,7 @@ struct RepositorySshCommandPolicy { username: Option, port: Option, repository_path: String, - access: workspace_api::RepositoryAccessMode, + access: server_api::RepositoryAccessMode, } impl RepositorySshCommandPolicy { @@ -2053,9 +2053,7 @@ fn repository_command_matches(command: &str, policy: &RepositorySshCommandPolicy } else { return false; }; - if operation == "receive-pack" - && policy.access != workspace_api::RepositoryAccessMode::ReadWrite - { + if operation == "receive-pack" && policy.access != server_api::RepositoryAccessMode::ReadWrite { return false; } let argument = argument @@ -2218,7 +2216,7 @@ impl RepositoryCommandAccess { runtime_root: &Path, request: &WorkingDirectoryRequest, ) -> Result, WorkingDirectoryDiagnostic> { - if request.repository.source.kind != workspace_api::RepositorySourceKind::Ssh { + if request.repository.source.kind != server_api::RepositorySourceKind::Ssh { return Ok(None); } let Some(ssh) = request @@ -2403,7 +2401,7 @@ fn validate_ssh_materialization_access( fn validate_remote_source_uri( request: &WorkingDirectoryRequest, ) -> Result<(), WorkingDirectoryDiagnostic> { - if request.repository.source.kind == workspace_api::RepositorySourceKind::Ssh { + if request.repository.source.kind == server_api::RepositorySourceKind::Ssh { let source = parse_repository_ssh_source(&request.repository.source.uri).ok_or_else(|| { WorkingDirectoryDiagnostic::new( @@ -2441,7 +2439,7 @@ fn validate_remote_source_uri( "remote Repository source URI is invalid", ) })?; - if request.repository.source.kind != workspace_api::RepositorySourceKind::Https { + if request.repository.source.kind != server_api::RepositorySourceKind::Https { return Ok(()); } if url.scheme() != "https" @@ -2512,7 +2510,7 @@ fn repository_git_command( let mut command = isolated_git_command(); let file_policy = if matches!( request.repository.source.kind, - workspace_api::RepositorySourceKind::LocalPath | workspace_api::RepositorySourceKind::File + server_api::RepositorySourceKind::LocalPath | server_api::RepositorySourceKind::File ) { "always" } else { @@ -2589,7 +2587,7 @@ fn test_repository_git_invocation_count() -> usize { fn run_repository_git_stdout( mut command: Command, - source_kind: workspace_api::RepositorySourceKind, + source_kind: server_api::RepositorySourceKind, ) -> Result { #[cfg(test)] TEST_REPOSITORY_GIT_INVOCATIONS.with(|count| count.set(count.get().saturating_add(1))); @@ -2837,10 +2835,10 @@ fn resolve_cloned_commit( fn apply_repository_access_policy( repository_root: &Path, - access: workspace_api::RepositoryAccessMode, + access: server_api::RepositoryAccessMode, ) -> Result<(), WorkingDirectoryDiagnostic> { match access { - workspace_api::RepositoryAccessMode::ReadOnly => { + server_api::RepositoryAccessMode::ReadOnly => { let mut disable_push = isolated_git_command(); disable_push.arg("-C").arg(repository_root).args([ "remote", @@ -2851,7 +2849,7 @@ fn apply_repository_access_policy( ]); run_repository_git(disable_push, "working_directory_repository_policy_failed") } - workspace_api::RepositoryAccessMode::ReadWrite => { + server_api::RepositoryAccessMode::ReadWrite => { if git_stdout( repository_root, ["config", "--get-all", "remote.origin.pushurl"], @@ -3122,7 +3120,7 @@ mod tests { }], host_trust_id: "trust-1".to_string(), host_trust_revision: 4, - access: workspace_api::RepositoryAccessMode::ReadOnly, + access: server_api::RepositoryAccessMode::ReadOnly, expires_at_epoch_seconds: u64::MAX, repository_id: "repo-main".to_string(), repository_source_fingerprint: "sha256:test".to_string(), @@ -3310,8 +3308,8 @@ mod tests { repository: WorkingDirectoryRepository { id: "repo-main".to_string(), provider: "git".to_string(), - source: workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + source: server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: repo.display().to_string(), }, source_revision: 1, @@ -3594,7 +3592,7 @@ mod tests { assert!(!runtime_root.path().join(".repository-cache").exists()); let mut file_request = request(repo.path()); - file_request.repository.source.kind = workspace_api::RepositorySourceKind::File; + file_request.repository.source.kind = server_api::RepositorySourceKind::File; file_request.repository.source.uri = format!("file://{}", repo.path().display()); file_request.repository.source_revision = 2; file_request.repository.source_fingerprint = "sha256:file-source".to_string(); @@ -3723,7 +3721,7 @@ mod tests { ], host_trust_id: "trust-1".to_string(), host_trust_revision: 1, - access: workspace_api::RepositoryAccessMode::ReadWrite, + access: server_api::RepositoryAccessMode::ReadWrite, expires_at_epoch_seconds: u64::MAX, repository_id: "repo-main".to_string(), repository_source_fingerprint: "sha256:test".to_string(), @@ -3735,8 +3733,8 @@ mod tests { }), }); let mut ssh_operation = request.clone(); - ssh_operation.repository.source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + ssh_operation.repository.source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, uri: "ssh://git@example.test/repo.git".to_string(), }; let command_access = RepositoryCommandAccess::prepare(runtime_root.path(), &ssh_operation) @@ -3810,8 +3808,8 @@ mod tests { request.backend_workdir_id = Some(working_directory_id.clone()); request.materialization.as_mut().unwrap().ssh = None; let mut authorized_ssh_request = request.clone(); - authorized_ssh_request.repository.source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + authorized_ssh_request.repository.source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, uri: "ssh://git@example.test/repo.git".to_string(), }; assert!( @@ -3910,7 +3908,7 @@ mod tests { let mut rotated = initial_materialization.clone(); rotated.operation_id = "operation-agent-rotated".to_string(); rotated.ssh.as_mut().unwrap().credential_candidates[0].credential_revision = 2; - rotated.ssh.as_mut().unwrap().access = workspace_api::RepositoryAccessMode::ReadOnly; + rotated.ssh.as_mut().unwrap().access = server_api::RepositoryAccessMode::ReadOnly; materializer .authorize_repository_access(&WorkingDirectoryRepositoryAccessRequest { working_directory_id: id.clone(), @@ -4028,7 +4026,7 @@ mod tests { let mut read_write = rotated; read_write.operation_id = "operation-agent-read-write".to_string(); read_write.ssh.as_mut().unwrap().credential_candidates[0].credential_revision = 3; - read_write.ssh.as_mut().unwrap().access = workspace_api::RepositoryAccessMode::ReadWrite; + read_write.ssh.as_mut().unwrap().access = server_api::RepositoryAccessMode::ReadWrite; let read_write_command_policy = RepositorySshCommandPolicy::from_access(read_write.ssh.as_ref().unwrap()).unwrap(); materializer @@ -4099,7 +4097,7 @@ mod tests { }], host_trust_id: "trust-1".to_string(), host_trust_revision: 1, - access: workspace_api::RepositoryAccessMode::ReadOnly, + access: server_api::RepositoryAccessMode::ReadOnly, expires_at_epoch_seconds: u64::MAX, repository_id: "repo-main".to_string(), repository_source_fingerprint: "sha256:test".to_string(), @@ -4251,8 +4249,8 @@ mod tests { assert_eq!(policy.repository_path, "team/repo.git"); let mut request = request(Path::new(".")); - request.repository.source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + request.repository.source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, uri: uri.to_string(), }; request.materialization = Some(crate::catalog::RepositoryMaterializationContext { @@ -4294,8 +4292,8 @@ mod tests { }; let mut https = request(repo.path()); - https.repository.source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Https, + https.repository.source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Https, uri: "https://token@example.test/repo.git".to_string(), }; https.materialization = Some(context(None)); @@ -4330,15 +4328,15 @@ mod tests { ); for (offset, kind) in [ - workspace_api::RepositorySourceKind::LocalPath, - workspace_api::RepositorySourceKind::File, - workspace_api::RepositorySourceKind::Https, + server_api::RepositorySourceKind::LocalPath, + server_api::RepositorySourceKind::File, + server_api::RepositorySourceKind::Https, ] .into_iter() .enumerate() { let mut mismatched_http = request(repo.path()); - mismatched_http.repository.source = workspace_api::RepositorySource { + mismatched_http.repository.source = server_api::RepositorySource { kind, uri: "http://example.test/repo.git".to_string(), }; @@ -4358,8 +4356,8 @@ mod tests { } let mut ssh = request(repo.path()); - ssh.repository.source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + ssh.repository.source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, uri: "ssh://git@example.test/repo.git".to_string(), }; ssh.materialization = Some(context(Some( @@ -4371,7 +4369,7 @@ mod tests { }], host_trust_id: "trust-1".to_string(), host_trust_revision: 1, - access: workspace_api::RepositoryAccessMode::ReadOnly, + access: server_api::RepositoryAccessMode::ReadOnly, expires_at_epoch_seconds: u64::MAX, repository_id: "repo-main".to_string(), repository_source_fingerprint: "sha256:test".to_string(), @@ -4396,8 +4394,8 @@ mod tests { let runtime_root = tempfile::tempdir().unwrap(); let materializer = RuntimeGitMaterializer::new(runtime_root.path()); let mut remote = request(Path::new(".")); - remote.repository.source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + remote.repository.source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, uri: "ssh://git@example.invalid/repo.git".to_string(), }; remote.materialization = Some(crate::catalog::RepositoryMaterializationContext { diff --git a/crates/worker-runtime/src/workspace_issuer.rs b/crates/worker-runtime/src/workspace_issuer.rs index dd4a05c19..5e5bdaaec 100644 --- a/crates/worker-runtime/src/workspace_issuer.rs +++ b/crates/worker-runtime/src/workspace_issuer.rs @@ -8,8 +8,8 @@ use base64::Engine as _; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use ring::signature::Ed25519KeyPair; use serde::{Deserialize, Serialize}; +use server_api::WorkspacePublicIdentityBundle; use sha2::{Digest, Sha256}; -use workspace_api::WorkspacePublicIdentityBundle; use crate::auth::RuntimeAuthError; diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 64d1be223..dacab4226 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -28,7 +28,7 @@ toml = { workspace = true } tracing = { workspace = true } tools = { workspace = true } workdir = { workspace = true } -workspace-api = { workspace = true } +server-api = { workspace = true } minijinja = "2.19.0" chrono = "0.4" config-source = { path = "../config-source" } diff --git a/crates/worker/src/feature/builtin/manage_workdir.rs b/crates/worker/src/feature/builtin/manage_workdir.rs index 33a93de8d..371718d9d 100644 --- a/crates/worker/src/feature/builtin/manage_workdir.rs +++ b/crates/worker/src/feature/builtin/manage_workdir.rs @@ -23,7 +23,7 @@ use workdir::{ WorkdirSessionCapabilities, WorkdirSessionHandle, WriteRequest, WriteResult, }; -use workspace_api::{ +use server_api::{ WorkingDirectoryCreateRequest as WorkdirCreateRequest, WorkingDirectoryCreateResponse as WorkdirCreateResponse, WorkingDirectoryListResponse as WorkdirListResponse, diff --git a/crates/worker/src/feature/builtin/workspace_worker_discovery.rs b/crates/worker/src/feature/builtin/workspace_worker_discovery.rs index 4d8ea2685..5fcfc0e51 100644 --- a/crates/worker/src/feature/builtin/workspace_worker_discovery.rs +++ b/crates/worker/src/feature/builtin/workspace_worker_discovery.rs @@ -168,7 +168,7 @@ fn input_schema() -> serde_json::Value { mod tests { use std::sync::Mutex; - use workspace_api::{ + use server_api::{ WorkspaceWorkerDiscoveryItem, WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject, }; diff --git a/crates/worker/src/skill.rs b/crates/worker/src/skill.rs index ecb47ced3..9eca47898 100644 --- a/crates/worker/src/skill.rs +++ b/crates/worker/src/skill.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::worker::WorkspaceClient; -pub use workspace_api::{ +pub use server_api::{ SkillActivationStatus, SkillCatalogEntry, SkillCatalogResponse, SkillDetailResponse, SkillDiagnostic, SkillDiagnosticSeverity, SkillProjectionIdentity, SkillProjectionStatus, SkillProvenance, SkillResourceRef, SkillSourceKind, @@ -29,7 +29,7 @@ pub enum SkillClientError { #[error("Skill API response JSON is invalid: {0}")] Json(#[from] serde_json::Error), #[error("Skill API response violates the shared contract: {0}")] - InvalidResponse(#[from] workspace_api::SkillApiValidationError), + InvalidResponse(#[from] server_api::SkillApiValidationError), #[error("Skill API returned HTTP {status}: {body}")] Http { status: reqwest::StatusCode, diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index af46ab494..216826f0b 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -559,7 +559,7 @@ pub trait WorkspaceClient: std::fmt::Debug + Send + Sync { fn list_workspace_workers( &self, _request: WorkspaceWorkerDiscoveryRequest, - ) -> Result { + ) -> Result { Err(WorkspaceClientError::Unavailable( "Workspace Worker discovery authority is unavailable".to_string(), )) diff --git a/crates/workspace-api/Cargo.toml b/crates/workspace-api/Cargo.toml deleted file mode 100644 index 468c65472..000000000 --- a/crates/workspace-api/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "workspace-api" -version = "0.1.0" -edition.workspace = true -license.workspace = true -publish = false - -[features] -default = [] -typescript = ["dep:ts-rs", "protocol/typescript"] - -[dependencies] -protocol.workspace = true -serde = { workspace = true, features = ["derive"] } -ts-rs = { version = "12.0.1", optional = true } -webauthn-rs-proto = "0.5.5" - -[[example]] -name = "generate_typescript" -required-features = ["typescript"] - -[dev-dependencies] -serde_json.workspace = true - -[[example]] -name = "generate_workdir_api_types" -required-features = ["typescript"] - -[[example]] -name = "generate_worker_launch_api_types" -required-features = ["typescript"] - -[[example]] -name = "generate_companion_api_types" -required-features = ["typescript"] - -[[example]] -name = "generate_memory_api_types" -required-features = ["typescript"] - -[[example]] -name = "generate_skill_api_types" -required-features = ["typescript"] - -[[example]] -name = "generate_auth_api_types" -required-features = ["typescript"] - -[[example]] -name = "generate_repository_access_types" -required-features = ["typescript"] diff --git a/crates/workspace-api/examples/generate_auth_api_types.rs b/crates/workspace-api/examples/generate_auth_api_types.rs deleted file mode 100644 index b8506b217..000000000 --- a/crates/workspace-api/examples/generate_auth_api_types.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - print!("{}", workspace_api::auth_api_typescript()); -} diff --git a/crates/workspace-api/examples/generate_companion_api_types.rs b/crates/workspace-api/examples/generate_companion_api_types.rs deleted file mode 100644 index 8c27410d1..000000000 --- a/crates/workspace-api/examples/generate_companion_api_types.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - print!("{}", workspace_api::companion_api_typescript()); -} diff --git a/crates/workspace-api/examples/generate_memory_api_types.rs b/crates/workspace-api/examples/generate_memory_api_types.rs deleted file mode 100644 index 87a9de912..000000000 --- a/crates/workspace-api/examples/generate_memory_api_types.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - print!("{}", workspace_api::memory_api_typescript()); -} diff --git a/crates/workspace-api/examples/generate_repository_access_types.rs b/crates/workspace-api/examples/generate_repository_access_types.rs deleted file mode 100644 index 48cc26657..000000000 --- a/crates/workspace-api/examples/generate_repository_access_types.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - print!("{}", workspace_api::repository_access_api_typescript()); -} diff --git a/crates/workspace-api/examples/generate_skill_api_types.rs b/crates/workspace-api/examples/generate_skill_api_types.rs deleted file mode 100644 index 4fa47ded2..000000000 --- a/crates/workspace-api/examples/generate_skill_api_types.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - print!("{}", workspace_api::skill_api_typescript()); -} diff --git a/crates/workspace-api/examples/generate_typescript.rs b/crates/workspace-api/examples/generate_typescript.rs deleted file mode 100644 index 9b4e949d2..000000000 --- a/crates/workspace-api/examples/generate_typescript.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - print!("{}", workspace_api::catalog_typescript()); -} diff --git a/crates/workspace-api/examples/generate_workdir_api_types.rs b/crates/workspace-api/examples/generate_workdir_api_types.rs deleted file mode 100644 index 53371add2..000000000 --- a/crates/workspace-api/examples/generate_workdir_api_types.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - print!("{}", workspace_api::workdir_api_typescript()); -} diff --git a/crates/workspace-api/examples/generate_worker_launch_api_types.rs b/crates/workspace-api/examples/generate_worker_launch_api_types.rs deleted file mode 100644 index ffcf6248a..000000000 --- a/crates/workspace-api/examples/generate_worker_launch_api_types.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - print!("{}", workspace_api::worker_launch_api_typescript()); -} diff --git a/crates/workspace-api/src/lib.rs b/crates/workspace-api/src/lib.rs deleted file mode 100644 index c3452b24f..000000000 --- a/crates/workspace-api/src/lib.rs +++ /dev/null @@ -1,4872 +0,0 @@ -//! Shared Workspace HTTP resource contracts. -//! -//! This crate owns transport DTOs exposed by the Workspace Server and consumed -//! by Rust clients. Runtime-internal projections remain in their owning crates; -//! callers must explicitly construct these Workspace-authoritative resources. - -use serde::{Deserialize, Serialize}; -use webauthn_rs_proto::{ - CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential, - RequestChallengeResponse, -}; - -/// Public browser-authentication configuration. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct AuthPublicConfig { - pub rp_id: String, - pub origin: String, - pub public_base_url: String, - pub cookie_name: String, -} - -/// Authentication method that established the current request actor. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum ActorAuthMethod { - BrowserSession, - ApiToken, -} - -/// Public user identity returned by authentication operations. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct AuthenticatedUser { - pub user_id: String, - pub account_id: String, - pub handle: String, - pub display_name: String, -} - -/// Authenticated actor returned by `GET /api/auth/whoami`. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RequestActor { - pub user_id: String, - pub account_id: String, - pub handle: String, - pub display_name: String, - pub auth_method: ActorAuthMethod, -} - -impl RequestActor { - pub fn user(&self) -> AuthenticatedUser { - AuthenticatedUser { - user_id: self.user_id.clone(), - account_id: self.account_id.clone(), - handle: self.handle.clone(), - display_name: self.display_name.clone(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WhoamiResponse { - pub actor: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct AuthBootstrapUserRequest { - pub handle: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub display_name: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct AuthUserResponse { - pub user: AuthenticatedUser, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct PasskeyRegistrationOptionsRequest { - pub handle: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub display_name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub browser_origin: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct PasskeyRegistrationOptionsResponse { - pub challenge_id: String, - #[cfg_attr(feature = "typescript", ts(type = "unknown"))] - pub public_key: CreationChallengeResponse, -} - -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct PasskeyRegistrationCompleteRequest { - pub challenge_id: String, - #[cfg_attr(feature = "typescript", ts(type = "unknown"))] - pub credential: RegisterPublicKeyCredential, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct PasskeyLoginOptionsRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub handle: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub browser_origin: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct PasskeyLoginOptionsResponse { - pub challenge_id: String, - #[cfg_attr(feature = "typescript", ts(type = "unknown"))] - pub public_key: RequestChallengeResponse, -} - -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct PasskeyLoginCompleteRequest { - pub challenge_id: String, - #[cfg_attr(feature = "typescript", ts(type = "unknown"))] - pub credential: PublicKeyCredential, -} - -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct DeviceLoginStartRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub client_name: Option, -} - -const DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS: u64 = 24 * 60 * 60; -const DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS: u64 = 60; - -#[derive(Clone, Serialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct DeviceLoginStartResponse { - pub device_code: String, - pub user_code: String, - pub verification_uri: String, - pub verification_uri_complete: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub expires_in: u64, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub interval: u64, -} - -impl<'de> Deserialize<'de> for DeviceLoginStartResponse { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct Wire { - device_code: String, - user_code: String, - verification_uri: String, - verification_uri_complete: String, - expires_in: u64, - interval: u64, - } - - let wire = Wire::deserialize(deserializer)?; - if wire.expires_in == 0 || wire.expires_in > DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS { - return Err(serde::de::Error::custom(format!( - "expires_in must be between 1 and {DEVICE_LOGIN_EXPIRES_IN_MAX_SECONDS}" - ))); - } - if wire.interval == 0 || wire.interval > DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS { - return Err(serde::de::Error::custom(format!( - "interval must be between 1 and {DEVICE_LOGIN_POLL_INTERVAL_MAX_SECONDS}" - ))); - } - Ok(Self { - device_code: wire.device_code, - user_code: wire.user_code, - verification_uri: wire.verification_uri, - verification_uri_complete: wire.verification_uri_complete, - expires_in: wire.expires_in, - interval: wire.interval, - }) - } -} - -impl std::fmt::Debug for DeviceLoginStartResponse { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("DeviceLoginStartResponse") - .field("device_code", &"[redacted]") - .field("user_code", &self.user_code) - .field("verification_uri", &self.verification_uri) - .field("verification_uri_complete", &self.verification_uri_complete) - .field("expires_in", &self.expires_in) - .field("interval", &self.interval) - .finish() - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct DeviceLoginApproveRequest { - pub user_code: String, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum DeviceLoginApprovalStatus { - Approved, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct DeviceLoginApproveResponse { - pub status: DeviceLoginApprovalStatus, - pub user: AuthenticatedUser, -} - -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct DeviceLoginPollRequest { - pub device_code: String, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub enum DeviceAccessTokenType { - Bearer, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum DeviceLoginPollStatus { - Pending, - Approved, - Expired, - Denied, - Consumed, -} - -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct DeviceLoginPollResponse { - pub status: DeviceLoginPollStatus, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub access_token: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub token_type: Option, -} - -impl std::fmt::Debug for DeviceLoginPollResponse { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("DeviceLoginPollResponse") - .field("status", &self.status) - .field( - "access_token", - &self.access_token.as_ref().map(|_| "[redacted]"), - ) - .field("token_type", &self.token_type) - .finish() - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum LogoutStatus { - LoggedOut, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct LogoutResponse { - pub status: LogoutStatus, -} - -pub const REPOSITORY_KEY_MIN_LEN: usize = 1; -pub const REPOSITORY_KEY_MAX_LEN: usize = 64; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RepositoryKeyError { - Length, - Character, - LeadingHyphen, - TrailingHyphen, -} - -impl std::fmt::Display for RepositoryKeyError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(match self { - Self::Length => "must contain between 1 and 64 ASCII bytes", - Self::Character => "must contain only lowercase ASCII letters, digits, and hyphens", - Self::LeadingHyphen => "must not start with a hyphen", - Self::TrailingHyphen => "must not end with a hyphen", - }) - } -} - -impl std::error::Error for RepositoryKeyError {} - -/// Validate one immutable Workspace-scoped Repository key. -/// -/// Keys are deliberately not normalized: callers must submit the exact canonical -/// lowercase ASCII spelling so idempotency and route identity cannot alias. -pub fn validate_repository_key(value: &str) -> Result<(), RepositoryKeyError> { - let bytes = value.as_bytes(); - if !(REPOSITORY_KEY_MIN_LEN..=REPOSITORY_KEY_MAX_LEN).contains(&bytes.len()) { - return Err(RepositoryKeyError::Length); - } - if bytes[0] == b'-' { - return Err(RepositoryKeyError::LeadingHyphen); - } - if bytes[bytes.len() - 1] == b'-' { - return Err(RepositoryKeyError::TrailingHyphen); - } - if !bytes - .iter() - .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-') - { - return Err(RepositoryKeyError::Character); - } - Ok(()) -} - -/// Provider-neutral classification of an authoritative Repository source. -/// -/// Local paths remain distinct from network Git transports so callers cannot -/// accidentally treat an unmaterialized remote as a server-local filesystem path. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RepositorySourceKind { - LocalPath, - File, - Ssh, - Https, - /// A legacy value that could not be classified during migration. It remains - /// inspectable but every provider operation must fail closed. Historical - /// `http` wire values decode into this non-executable classification. - Invalid, -} - -impl RepositorySourceKind { - pub const fn is_remote(self) -> bool { - matches!(self, Self::Ssh | Self::Https) - } - - pub const fn as_str(self) -> &'static str { - match self { - Self::LocalPath => "local_path", - Self::File => "file", - Self::Ssh => "ssh", - Self::Https => "https", - Self::Invalid => "invalid", - } - } - - pub fn parse(value: &str) -> Option { - Some(match value { - "local_path" => Self::LocalPath, - "file" => Self::File, - "ssh" => Self::Ssh, - "https" => Self::Https, - "http" | "invalid" => Self::Invalid, - _ => return None, - }) - } -} - -impl<'de> Deserialize<'de> for RepositorySourceKind { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - Self::parse(&value).ok_or_else(|| { - serde::de::Error::unknown_variant( - &value, - &["local_path", "file", "ssh", "https", "invalid"], - ) - }) - } -} - -/// Stable Repository source identity stored by Workspace authority. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct RepositorySource { - pub kind: RepositorySourceKind, - /// Canonical source representation. This is an absolute local path for - /// `local_path`, and a normalized URI/remote specification otherwise. - pub uri: String, -} - -/// Browser/user intent for registering one Repository in a Workspace. -/// -/// The Server parses and canonicalizes `source`; callers cannot assert a -/// transport classification or supply credential material through this DTO. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct CreateWorkspaceRepositoryRequest { - pub repository_key: String, - pub source: String, - #[serde(default)] - pub default_ref: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CreateWorkspaceRepositoryResponse { - pub workspace_id: String, - pub repository_key: String, - pub replayed: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RepositoryObservedStatus { - Unverified, - Ready, - Invalid, -} - -impl RepositoryObservedStatus { - pub const fn as_str(self) -> &'static str { - match self { - Self::Unverified => "unverified", - Self::Ready => "ready", - Self::Invalid => "invalid", - } - } - - pub fn parse(value: &str) -> Option { - Some(match value { - "unverified" => Self::Unverified, - "ready" => Self::Ready, - "invalid" => Self::Invalid, - _ => return None, - }) - } -} - -/// Public Workspace catalog item returned by `GET /api/workspaces`. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceSummary { - pub workspace_id: String, - pub owner_account_id: String, - pub display_name: String, - pub state: String, - pub created_at: String, - pub updated_at: String, -} - -/// Public response returned by `GET /api/workspaces`. -/// -/// The transparent newtype keeps the established top-level JSON array while making the -/// complete list response a named cross-crate and generated-TypeScript authority. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkspaceCatalogListResponse(pub Vec); - -/// Public Repository record embedded in Workspace creation responses. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceRepositoryRecord { - pub workspace_id: String, - pub repository_key: String, - pub kind: String, - pub provider: Option, - pub source: RepositorySource, - pub default_ref: Option, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub source_revision: u64, - pub source_fingerprint: String, - pub observed_status: RepositoryObservedStatus, - pub observed_at: Option, - pub created_at: String, - pub updated_at: String, -} - -/// Initial Repository registration intent for Workspace creation. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct InitialRepositoryIntent { - pub repository_key: String, - pub uri: String, - #[serde(default)] - pub default_ref: Option, -} - -/// Request for atomically creating a Workspace and its initial Repository. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct WorkspaceCreateRequest { - pub operation_key: String, - pub display_name: String, - pub repository: InitialRepositoryIntent, -} - -/// Response returned after atomically creating a Workspace and its first Repository. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceCreateResponse { - pub workspace: WorkspaceSummary, - pub repository: WorkspaceRepositoryRecord, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub config_revision: u64, - pub request_fingerprint: String, - pub replayed: bool, -} - -/// Browser authentication configuration exposed by the scoped Workspace summary. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub enum WorkspaceAuthConfig { - Passkey { - rp_id: String, - origin: String, - public_base_url: String, - cookie_name: String, - }, -} - -/// Backend-authoritative permissions for the current Workspace actor. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspacePermissionSummary { - pub manage_repositories: bool, - pub manage_secrets: bool, - pub manage_runtimes: bool, - pub delete_workspace: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceExtensionPointState { - pub status: String, - pub note: String, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceExtensionPoints { - pub store: String, - pub event_stream: WorkspaceExtensionPointState, - pub host_worker_bridge: WorkspaceExtensionPointState, - pub companion_console: WorkspaceExtensionPointState, -} - -/// Scoped Workspace metadata and current-actor permission projection. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceResponse { - pub workspace_id: String, - pub display_name: String, - pub record_authority: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub schema_version: i64, - pub auth: WorkspaceAuthConfig, - pub permissions: WorkspacePermissionSummary, - pub extension_points: WorkspaceExtensionPoints, -} - -/// Workspace display metadata exposed from the Server DB settings authority. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceMetadataSettingsResponse { - pub workspace_id: String, - pub display_name: String, - pub created_at: String, - pub revision: String, - pub source: String, - pub diagnostics: Vec, -} - -/// Compare-and-swap update for Workspace identity display metadata. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct UpdateWorkspaceMetadataRequest { - pub display_name: String, - pub revision: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceMetadataMutationResponse { - pub workspace: WorkspaceMetadataSettingsResponse, - pub diagnostics: Vec, -} - -/// Lifecycle state for a Workspace-scoped Ed25519 signing identity. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkspaceSigningIdentityState { - PendingProvisioning, - Active, -} - -/// Public metadata for a Workspace signing identity. Private material and its -/// storage reference are deliberately not part of this wire authority. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceSigningIdentityPublic { - pub workspace_id: String, - pub key_id: String, - pub algorithm: String, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub public_key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub public_key_fingerprint: Option, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub revision: u64, - pub state: WorkspaceSigningIdentityState, - pub created_at: String, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub provisioned_at: Option, -} - -/// Copyable public trust bundle consumed by future Runtime enrollment work. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspacePublicIdentityBundle { - pub workspace_id: String, - pub backend_url: String, - pub key_id: String, - pub algorithm: String, - pub public_key: String, - pub public_key_fingerprint: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub revision: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceSigningIdentityResponse { - pub identity: WorkspaceSigningIdentityPublic, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub public_bundle: Option, -} - -pub const WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES: usize = 128; -pub const WORKSPACE_DELETION_MAX_REVISION_BYTES: usize = 128; -pub const WORKSPACE_DELETION_MAX_CONFIRMATION_BYTES: usize = 256; -pub const WORKSPACE_DELETION_MAX_BLOCKERS: usize = 1024; -pub const WORKSPACE_DELETION_MAX_CHILD_OPERATION_IDS: usize = 4096; -pub const WORKSPACE_DELETION_MAX_RESOURCE_VALUE_BYTES: usize = 128; -pub const WORKSPACE_DELETION_MAX_BLOCKER_MESSAGE_BYTES: usize = 512; - -fn deserialize_workspace_deletion_operation_id<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let value = String::deserialize(deserializer)?; - if value.len() > WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES { - return Err(serde::de::Error::custom( - "Workspace deletion operation_id is too long", - )); - } - Ok(value) -} - -fn deserialize_workspace_deletion_revision<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let value = String::deserialize(deserializer)?; - if value.len() > WORKSPACE_DELETION_MAX_REVISION_BYTES { - return Err(serde::de::Error::custom( - "Workspace deletion revision is too long", - )); - } - Ok(value) -} - -fn deserialize_workspace_deletion_confirmation<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let value = String::deserialize(deserializer)?; - if value.len() > WORKSPACE_DELETION_MAX_CONFIRMATION_BYTES { - return Err(serde::de::Error::custom( - "Workspace deletion confirmation is too long", - )); - } - Ok(value) -} - -fn deserialize_workspace_deletion_resource_value<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let value = Option::::deserialize(deserializer)?; - if value - .as_ref() - .is_some_and(|value| value.len() > WORKSPACE_DELETION_MAX_RESOURCE_VALUE_BYTES) - { - return Err(serde::de::Error::custom( - "Workspace deletion resource value is too long", - )); - } - Ok(value) -} - -fn deserialize_workspace_deletion_blocker_message<'de, D>( - deserializer: D, -) -> Result -where - D: serde::Deserializer<'de>, -{ - let value = String::deserialize(deserializer)?; - if value.len() > WORKSPACE_DELETION_MAX_BLOCKER_MESSAGE_BYTES { - return Err(serde::de::Error::custom( - "Workspace deletion blocker message is too long", - )); - } - Ok(value) -} - -fn deserialize_workspace_deletion_blockers<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let value = Vec::::deserialize(deserializer)?; - if value.len() > WORKSPACE_DELETION_MAX_BLOCKERS { - return Err(serde::de::Error::custom( - "too many Workspace deletion blockers", - )); - } - Ok(value) -} - -fn deserialize_workspace_deletion_child_operation_ids<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let value = Vec::::deserialize(deserializer)?; - if value.len() > WORKSPACE_DELETION_MAX_CHILD_OPERATION_IDS { - return Err(serde::de::Error::custom( - "too many Workspace deletion child operations", - )); - } - if value - .iter() - .any(|operation_id| operation_id.len() > WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES) - { - return Err(serde::de::Error::custom( - "Workspace deletion child operation_id is too long", - )); - } - Ok(value) -} - -/// Lifecycle state for one durable Workspace deletion operation. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkspaceDeletionState { - Queued, - Running, - Blocked, - Failed, - Succeeded, -} - -/// Stable category explaining why Workspace deletion cannot currently advance. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkspaceDeletionBlockerKind { - LastAccessibleWorkspace, - RevisionConflict, - DirtyWorkdir, - WorkerRemovalBlocked, - WorkdirRemovalBlocked, - RetentionHold, - CleanupUnavailable, -} - -/// One bounded, user-actionable blocker returned by preflight or execution. -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkspaceDeletionBlocker { - pub kind: WorkspaceDeletionBlockerKind, - pub resource_kind: Option, - pub resource_key: Option, - pub message: String, -} - -/// Workspace-owned resources summarized before destructive confirmation. -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceDeletionResourceCounts { - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub workers: u64, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub workdirs: u64, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub repositories: u64, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub runtime_bindings: u64, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub secrets: u64, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub artifacts: u64, -} - -/// Owner-only impact preview for deleting one Workspace. -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkspaceDeletionPreflightResponse { - pub workspace_id: String, - pub display_name: String, - /// Opaque persisted Workspace metadata revision used as a CAS fence. - pub expected_revision: String, - pub can_delete: bool, - pub resources: WorkspaceDeletionResourceCounts, - pub blockers: Vec, -} - -/// Idempotent request to start or resume Workspace deletion. -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkspaceDeletionRequest { - pub operation_id: String, - pub expected_revision: String, - pub confirmation: String, -} - -/// Durable deletion operation projection used by request responses and polling. -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkspaceDeletionOperationResponse { - pub operation_id: String, - pub workspace_id: String, - pub display_name: String, - pub state: WorkspaceDeletionState, - pub resources: WorkspaceDeletionResourceCounts, - pub child_operation_ids: Vec, - pub blockers: Vec, - pub failure_category: Option, - pub created_at: String, - pub updated_at: String, - pub completed_at: Option, -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct WorkspaceDeletionBlockerWire { - kind: WorkspaceDeletionBlockerKind, - #[serde(deserialize_with = "deserialize_workspace_deletion_resource_value")] - resource_kind: Option, - #[serde(deserialize_with = "deserialize_workspace_deletion_resource_value")] - resource_key: Option, - #[serde(deserialize_with = "deserialize_workspace_deletion_blocker_message")] - message: String, -} - -impl<'de> Deserialize<'de> for WorkspaceDeletionBlocker { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let wire = WorkspaceDeletionBlockerWire::deserialize(deserializer)?; - Ok(Self { - kind: wire.kind, - resource_kind: wire.resource_kind, - resource_key: wire.resource_key, - message: wire.message, - }) - } -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct WorkspaceDeletionPreflightResponseWire { - workspace_id: String, - display_name: String, - #[serde(deserialize_with = "deserialize_workspace_deletion_revision")] - expected_revision: String, - can_delete: bool, - resources: WorkspaceDeletionResourceCounts, - #[serde(deserialize_with = "deserialize_workspace_deletion_blockers")] - blockers: Vec, -} - -impl<'de> Deserialize<'de> for WorkspaceDeletionPreflightResponse { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let wire = WorkspaceDeletionPreflightResponseWire::deserialize(deserializer)?; - Ok(Self { - workspace_id: wire.workspace_id, - display_name: wire.display_name, - expected_revision: wire.expected_revision, - can_delete: wire.can_delete, - resources: wire.resources, - blockers: wire.blockers, - }) - } -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct WorkspaceDeletionRequestWire { - #[serde(deserialize_with = "deserialize_workspace_deletion_operation_id")] - operation_id: String, - #[serde(deserialize_with = "deserialize_workspace_deletion_revision")] - expected_revision: String, - #[serde(deserialize_with = "deserialize_workspace_deletion_confirmation")] - confirmation: String, -} - -impl<'de> Deserialize<'de> for WorkspaceDeletionRequest { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let wire = WorkspaceDeletionRequestWire::deserialize(deserializer)?; - Ok(Self { - operation_id: wire.operation_id, - expected_revision: wire.expected_revision, - confirmation: wire.confirmation, - }) - } -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct WorkspaceDeletionOperationResponseWire { - #[serde(deserialize_with = "deserialize_workspace_deletion_operation_id")] - operation_id: String, - workspace_id: String, - display_name: String, - state: WorkspaceDeletionState, - resources: WorkspaceDeletionResourceCounts, - #[serde(deserialize_with = "deserialize_workspace_deletion_child_operation_ids")] - child_operation_ids: Vec, - #[serde(deserialize_with = "deserialize_workspace_deletion_blockers")] - blockers: Vec, - failure_category: Option, - created_at: String, - updated_at: String, - completed_at: Option, -} - -impl<'de> Deserialize<'de> for WorkspaceDeletionOperationResponse { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let wire = WorkspaceDeletionOperationResponseWire::deserialize(deserializer)?; - Ok(Self { - operation_id: wire.operation_id, - workspace_id: wire.workspace_id, - display_name: wire.display_name, - state: wire.state, - resources: wire.resources, - child_operation_ids: wire.child_operation_ids, - blockers: wire.blockers, - failure_category: wire.failure_category, - created_at: wire.created_at, - updated_at: wire.updated_at, - completed_at: wire.completed_at, - }) - } -} - -/// Read-only Profile catalog projected from one active Workspace config revision. -#[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)] -pub struct ProfileSettingsResponse { - pub workspace_id: String, - pub registry_revision: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] - pub config_revision: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tree_digest: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub projection_digest: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub default_profile: Option, - pub profiles: Vec, - pub sources: Vec, - pub diagnostics: Vec, -} - -#[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)] -pub struct WorkspaceProfileSummary { - pub profile_id: String, - pub selector: String, - pub label: String, - pub source_kind: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub profile_source_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - pub editable: bool, - pub is_default: bool, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceProfileSourceSummary { - pub profile_source_id: String, - pub display_path: String, - pub kind: String, - pub content_type: String, - pub content_digest: String, - pub provenance: WorkspaceProfileSourceProvenance, - pub editable: bool, - pub revision: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub size_bytes: u64, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkspaceProfileSourceProvenance { - ProjectProfileSourceTree, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositoryDiagnostic { - pub severity: String, - pub code: String, - pub message: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct GitRemoteSummary { - pub name: String, - pub fetch_url: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct GitRepositorySummary { - pub status: String, - pub head: Option, - pub branch: Option, - pub dirty: bool, - pub remotes: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositorySummary { - pub repository_key: String, - pub kind: String, - pub provider: String, - pub source: RepositorySource, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub source_revision: u64, - pub source_fingerprint: String, - pub observed_status: RepositoryObservedStatus, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub observed_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub default_selector: Option, - pub record_authority: String, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub git: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub diagnostics: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct GitCommitSummary { - pub hash: String, - pub short_hash: String, - pub summary: String, - pub author_name: String, - pub author_email: String, - pub author_date: String, - pub parents: Vec, - pub refs: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositoryListResponse { - pub workspace_id: String, - pub items: Vec, - pub source: String, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositoryDetailResponse { - pub workspace_id: String, - pub item: RepositorySummary, - pub source: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositorySshConnectionProbeRequest { - pub runtime_id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositorySshHostKeyCandidate { - pub algorithm: String, - pub host_key: String, - pub fingerprint: String, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RepositorySshConnectionTrustState { - Untrusted, - Verified, - Changed, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositorySshConnectionProbeResponse { - pub workspace_id: String, - pub repository_key: String, - pub runtime_id: String, - pub hostname: String, - pub port: u16, - pub trust_state: RepositorySshConnectionTrustState, - pub host_trust_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number | null"))] - pub expected_host_trust_revision: Option, - pub candidates: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct ConfirmRepositorySshHostTrustRequest { - pub operation_id: String, - pub runtime_id: String, - pub host_key: String, - #[cfg_attr(feature = "typescript", ts(type = "number | null"))] - pub expected_host_trust_revision: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositoryLogResponse { - pub workspace_id: String, - pub repository_key: String, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub default_selector: Option, - pub limit: usize, - pub items: Vec, - pub diagnostics: Vec, -} - -pub const TICKET_RELATIONS_QUERY_PATH: &str = "/tickets/relations/search"; -pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search"; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum DiagnosticSeverity { - Info, - Warning, - Error, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct Diagnostic { - pub code: String, - pub severity: DiagnosticSeverity, - pub message: String, -} - -/// Public Workdir materializer classification. -/// -/// The value identifies stable materialization provenance without exposing a -/// provider path, Runtime handle, or session identity. -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectoryMaterializerKind { - #[default] - RuntimeGitClone, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectoryStatusKind { - Active, - CleanupPending, - Corrupted, - NotFound, - Unknown, -} - -impl WorkingDirectoryStatusKind { - pub const fn as_str(&self) -> &'static str { - match self { - Self::Active => "active", - Self::CleanupPending => "cleanup_pending", - Self::Corrupted => "corrupted", - Self::NotFound => "not_found", - Self::Unknown => "unknown", - } - } -} - -impl std::fmt::Display for WorkingDirectoryStatusKind { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(self.as_str()) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkingDirectoryCleanupTarget { - pub kind: String, - pub working_directory_id: String, - pub repository_key: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkingDirectoryRemovalRequest { - pub reason: String, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectoryRemovalDisposition { - Removed, - Retained, - AttentionRequired, -} - -#[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)] -pub struct WorkingDirectoryRemovalResponse { - pub working_directory_id: String, - pub disposition: WorkingDirectoryRemovalDisposition, - pub retryable: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure_category: Option, -} - -/// Durable Workspace occupancy projection for one Workdir. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkingDirectoryOccupancy { - pub runtime_id: String, - pub worker_id: String, - pub display_name: String, - pub linked_at: String, -} - -/// Runtime-internal Workdir cleanup authority. This transport intentionally -/// retains the Backend-generated Repository id and is never a Workspace public -/// projection. -#[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)] -pub struct RuntimeWorkingDirectoryCleanupTarget { - pub kind: String, - pub working_directory_id: String, - pub repository_id: String, -} - -/// Runtime-internal Workdir inventory transport. Workspace REST and model-facing -/// surfaces must project this through [`WorkingDirectorySummary`] so the UUID is -/// replaced with `repository_key`. -#[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)] -pub struct RuntimeWorkingDirectorySummary { - pub working_directory_id: String, - pub repository_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_selector: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_ref: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_tree: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_selector: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_ref: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_tree: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(type = "number | null"))] - pub observed_at_epoch_seconds: Option, - pub materializer_kind: WorkingDirectoryMaterializerKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cleanup_target: Option, - pub status: WorkingDirectoryStatusKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cleanliness: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub primary_worker_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub occupied_by: Option, -} - -/// Public, provider-neutral Workdir inventory projection. -#[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)] -pub struct WorkingDirectorySummary { - pub working_directory_id: String, - pub repository_key: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_selector: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_ref: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_tree: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_selector: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_ref: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_tree: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] - pub observed_at_epoch_seconds: Option, - pub materializer_kind: WorkingDirectoryMaterializerKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cleanup_target: Option, - pub status: WorkingDirectoryStatusKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cleanliness: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub primary_worker_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub occupied_by: Option, -} - -impl WorkingDirectorySummary { - /// Workspace-managed inventory rows carry explicit cleanup authority. - pub fn is_workspace_managed(&self) -> bool { - self.cleanup_target.is_some() - } -} - -/// Browser/Rust-client Workdir materialization request. -/// -/// `runtime_id = None` requests Workspace default Runtime resolution and -/// `operation_id = Some(_)` fences exact replay. All four fields deliberately -/// preserve the Server's existing optionality. -#[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)] -pub struct WorkingDirectoryCreateRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub runtime_id: Option, - pub repository_key: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub selector: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub operation_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkingDirectoryListResponse { - pub workspace_id: String, - pub items: Vec, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkingDirectoryDetailResponse { - pub workspace_id: String, - pub runtime_id: String, - pub item: WorkingDirectorySummary, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkingDirectoryCreateResponse { - pub workspace_id: String, - pub runtime_id: String, - pub item: WorkingDirectorySummary, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ListResponse { - pub workspace_id: String, - pub limit: usize, - pub items: Vec, - pub source: String, - #[serde(default)] - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct QueryPage { - pub limit: usize, - pub returned: usize, - pub has_more: bool, - pub next_cursor: Option, - pub sort: String, - pub source_limit: Option, - pub source_truncated: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ObjectiveEventDetail { - pub event_ref: String, - pub kind: String, - pub body: Option, - pub created_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ObjectiveLinkedTicketSummary { - pub id: String, - pub resource_key: String, - pub title: String, - pub state: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ObjectiveResourceSummary { - pub path: String, - pub media_type: Option, - pub bytes: usize, - pub updated_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ObjectiveSummary { - pub id: String, - pub resource_key: String, - pub title: String, - pub state: String, - pub created_at: Option, - pub updated_at: Option, - pub summary: String, - pub linked_tickets: Vec, - pub record_source: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ObjectiveDetail { - pub id: String, - pub resource_key: String, - pub title: String, - pub state: String, - pub revision: String, - pub created_at: Option, - pub updated_at: Option, - pub linked_tickets: Vec, - pub linked_ticket_summaries: Vec, - pub resources: Vec, - pub body: String, - pub body_truncated: bool, - pub events: Vec, - pub event_page: QueryPage, - pub record_source: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ObjectiveCreateRequest { - pub title: String, - #[serde(default)] - pub body_md: String, - #[serde(default = "default_objective_state")] - pub state: String, - #[serde(default)] - pub linked_tickets: Vec, -} - -fn default_objective_state() -> String { - "active".to_string() -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -pub struct ObjectiveEditRequest { - pub title: Option, - pub old_string: Option, - pub new_string: Option, - #[serde(default)] - pub replace_all: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ObjectiveStateRequest { - pub state: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ObjectiveLinkTicketRequest { - pub ticket_id: String, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeSourceKind { - EmbeddedWorkerRuntime, - RemoteHttp, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeSourceStatus { - Active, - Reserved, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeIdentityAuthority { - RuntimeRegistryProjection, - ServerRuntimeConfiguration, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeSourceSummary { - pub kind: RuntimeSourceKind, - pub status: RuntimeSourceStatus, - pub identity_authority: RuntimeIdentityAuthority, - pub note: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct RuntimeSummary { - pub runtime_id: String, - pub label: String, - pub kind: String, - pub status: String, - pub source: RuntimeSourceSummary, - #[serde(default)] - pub host_ids: Vec, - pub worker_creation_available: bool, - pub os: String, - pub arch: String, - #[serde(default)] - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkspaceRuntimeBindingState { - Configured, - Verified, - Revoked, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeConnectionDisplayState { - Configured, - Verified, - Unavailable, - Revoked, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeVerificationOutcome { - Verified, - ChallengeIssued, - VerificationFailed, - ConnectivityFailed, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeVerificationEvidenceSummary { - pub verified_at: Option, - pub last_checked_at: String, - pub last_outcome: RuntimeVerificationOutcome, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub binding_revision: u64, - pub workspace_key_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub workspace_identity_revision: u64, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub workspace_trust_generation: u64, - pub runtime_public_key_fingerprint: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub runtime_identity_revision: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceRuntimeBindingSummary { - pub state: WorkspaceRuntimeBindingState, - pub connection_state: RuntimeConnectionDisplayState, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub revision: u64, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub workspace_key_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(type = "number | null"))] - pub workspace_key_generation: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub verification: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeManagementSummary { - pub built_in: bool, - pub config_managed: bool, - pub removable: bool, - pub endpoint_configured: bool, - pub token_ref_configured: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub binding: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkspaceRuntimeResource { - #[serde(flatten)] - pub runtime: RuntimeSummary, - pub management: RuntimeManagementSummary, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeTrustKeyStatus { - Unconfigured, - Active, - Revoked, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeTrustKeyState { - pub status: RuntimeTrustKeyStatus, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fingerprint: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(type = "number | null"))] - pub revision: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub created_at: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub updated_at: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub revoked_at: Option, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeTrustAuditAction { - Created, - Replaced, - Reactivated, - Revoked, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeTrustAuditEntry { - pub action: RuntimeTrustAuditAction, - pub actor_account_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub old_fingerprint: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub new_fingerprint: Option, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub revision: u64, - pub at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkspaceRuntimeDetail { - pub workspace_id: String, - pub runtime: WorkspaceRuntimeResource, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, - pub trust_key: RuntimeTrustKeyState, - #[serde(default)] - pub recent_audit: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeTrustKeyRevealResponse { - pub public_key: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RevokeRuntimeTrustKeyRequest { - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub expected_revision: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RemoveRuntimeRequest { - pub operation_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub expected_binding_revision: u64, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeRemovalOperationState { - Pending, - CleanupPending, - Succeeded, - Failed, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeRemovalOperationResponse { - pub operation_id: String, - pub workspace_id: String, - pub runtime_id: String, - pub state: RuntimeRemovalOperationState, - pub binding_removed: bool, - pub runtime_registration_removed: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure_category: Option, - pub created_at: String, - pub updated_at: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub completed_at: Option, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeTrustConflictKind { - StaleRevision, - FingerprintInUse, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeTrustConflictResponse { - pub error: RuntimeTrustConflictKind, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub current_revision: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_fingerprint: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimePublicIdentityBundle { - pub identity_id: String, - pub public_key: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CreateRemoteRuntimeRequest { - pub public_bundle: RuntimePublicIdentityBundle, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub display_name: Option, - pub endpoint: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(type = "number | null"))] - pub expected_revision: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct UpdateRemoteRuntimeRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub display_name: Option, - pub endpoint: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeConnectionTestStatus { - Compatible, - Failed, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum RuntimeConnectionTestFailureKind { - Authentication, - Authorization, - NetworkUnreachable, - Timeout, - TlsOrTransport, - MalformedResponse, - ProtocolVersionMismatch, - RuntimeIdentityMismatch, - Configuration, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RuntimeConnectionTestResponse { - pub workspace_id: String, - pub runtime_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub binding_revision: u64, - pub connection_state: RuntimeConnectionDisplayState, - pub verification: Option, - pub checked_at: String, - pub status: RuntimeConnectionTestStatus, - pub failure_kind: Option, - pub expected_protocol_version: u32, - pub actual_protocol_version: Option, - #[serde(default)] - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkerWorkspaceSummary { - pub visibility: String, - pub identity: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub workspace_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkerImplementationSummary { - pub kind: String, - pub display_hint: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkerCapabilitySummary { - pub can_stop: bool, - pub can_spawn_followup: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum WorkspaceWorkerSubject { - RuntimeWorker { - runtime_id: String, - worker_id: String, - }, -} - -/// Bounded, model-safe projection used by privileged Workspace Worker discovery. -/// Runtime placement appears only in the typed subject required by Worker -/// control operations; provider and launch internals are intentionally omitted. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkspaceWorkerDiscoveryItem { - pub subject: WorkspaceWorkerSubject, - pub resource_key: String, - pub display_name: String, - pub profile: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, -} - -/// Public lifecycle projection for the Workspace Companion endpoint. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum CompanionLifecycleState { - Idle, - Running, - Stopped, -} - -/// Public outcome of a Companion message submission. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum CompanionMessageDisposition { - Accepted, - Rejected, -} - -/// Public, bounded transport metadata for Companion status. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CompanionTransportSummary { - pub mode: String, - pub available: bool, -} - -/// Public Workspace Companion status response. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CompanionStatusResponse { - pub state: CompanionLifecycleState, - pub worker: Option, - pub transport: CompanionTransportSummary, - #[serde(default)] - pub diagnostics: Vec, -} - -/// Public Workspace Companion message request. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CompanionMessageRequest { - pub content: String, -} - -/// Public Workspace Companion cancellation request. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CompanionCancelRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason: Option, -} - -/// Public Workspace Companion message response. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CompanionMessageResponse { - pub state: CompanionMessageDisposition, - pub message: String, -} - -/// User-visible role accepted in the public Companion transcript. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum CompanionTranscriptRole { - User, - Assistant, -} - -/// One allowlisted, user-visible Companion transcript item. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CompanionTranscriptItem { - pub sequence: usize, - pub role: CompanionTranscriptRole, - pub content: String, - pub created_at: String, -} - -/// Bounded public Companion transcript projection. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CompanionTranscriptProjection { - pub state: CompanionLifecycleState, - pub start: usize, - pub limit: usize, - pub total: usize, - pub next: Option, - pub items: Vec, -} - -#[cfg(feature = "typescript")] -pub fn companion_api_typescript() -> String { - use ts_rs::TS; - - let config = ts_rs::Config::default(); - let declarations = [ - DiagnosticSeverity::decl(&config), - Diagnostic::decl(&config), - WorkspaceWorkerSubject::decl(&config), - WorkspaceWorkerDiscoveryItem::decl(&config), - CompanionLifecycleState::decl(&config), - CompanionMessageDisposition::decl(&config), - CompanionTransportSummary::decl(&config), - CompanionStatusResponse::decl(&config), - CompanionMessageRequest::decl(&config), - CompanionCancelRequest::decl(&config), - CompanionMessageResponse::decl(&config), - CompanionTranscriptRole::decl(&config), - CompanionTranscriptItem::decl(&config), - CompanionTranscriptProjection::decl(&config), - ]; - - format!( - "// Generated by `cargo run -p workspace-api --features typescript --example generate_companion_api_types`.\n// Do not edit manually.\n\n{}\n", - declarations - .into_iter() - .map(|declaration| format!("export {declaration}")) - .collect::>() - .join("\n\n") - ) -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct WorkspaceWorkerDiscoveryPage { - pub workers: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub next_cursor: Option, -} - -/// Workspace-authoritative Worker projection. -/// -/// `resource_key` is required here even though Runtime-internal Worker summaries -/// do not carry one. The Workspace Server must resolve it from Workspace -/// authority before constructing this response. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -pub struct WorkerSummary { - pub runtime_id: String, - pub worker_id: String, - pub resource_key: String, - pub host_id: String, - #[serde(default)] - pub display_name: String, - pub label: String, - pub profile: Option, - pub singleton_key: Option, - #[serde(default)] - pub tags: Vec, - pub workspace: WorkerWorkspaceSummary, - /// Runtime catalog lifecycle compatibility state. Live foreground state, when - /// available, is carried separately in `worker_state`. - pub state: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub worker_state: Option, - pub last_seen_at: Option, - #[serde(default)] - pub pinned: bool, - #[serde(default)] - pub retention_state: String, - pub implementation: WorkerImplementationSummary, - pub capabilities: WorkerCapabilitySummary, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, - #[serde(default)] - pub diagnostics: Vec, -} - -/// Runtime-owned Worker summary embedded in Worker launch responses. -/// -/// This preserves the existing launch wire shape. Workspace-owned Worker list -/// and detail responses use [`WorkerSummary`] instead. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[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, - pub singleton_key: Option, - pub tags: Vec, - pub workspace: WorkerWorkspaceSummary, - pub state: String, - pub last_seen_at: Option, - 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, - #[serde(default)] - pub diagnostics: Vec, -} - -#[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, - pub default_profile: Option, - pub profiles: Vec, - pub repositories: Vec, - pub working_directories: Vec, - pub diagnostics: Vec, -} - -#[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, -} - -#[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, -} - -#[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, -} - -#[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, - #[serde(default)] - pub ticket_assignment: Option, - #[serde(default)] - pub initial_submit: Vec, - #[serde(default)] - pub working_directory: Option, - /// Backend idempotency key used only for authenticated Worker-owned spawn/control. - #[serde(default)] - pub control_operation_id: Option, -} - -#[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, -} - -#[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, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum WorkerOperationState { - Accepted, - Unsupported, - Rejected, -} - -/// Cross-layer outcome of an explicit Worker restore operation. -/// -/// `Rejected` is reserved for read-only preflight failures. Once live restore -/// work starts, failure is either a confirmed `RolledBack` operation or a -/// `ReconciliationRequired` result whose commit state must be reread/retried. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum WorkerRestoreState { - Accepted, - Rejected, - RolledBack, - ReconciliationRequired, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkerRestoreResult { - pub state: WorkerRestoreState, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional = nullable))] - pub worker: Option, - #[serde(default)] - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct WorkerRestoreResponse { - pub workspace_id: String, - pub runtime_id: String, - pub worker_id: String, - pub result: WorkerRestoreResult, -} - -/// Public Workspace Memory document projection. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct MemoryDocumentResponse { - pub body_md: String, - pub created_at: String, - pub updated_at: String, - pub bytes: usize, - pub record_source: String, -} - -/// Candidate kinds exposed by the Memory staging resource. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum MemoryCandidateKind { - Preference, - WorkingAssumption, - Constraint, - Decision, - OpenQuestion, - Lesson, -} - -/// Typed, bounded provenance classification for public Memory evidence anchors. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(rename_all = "snake_case")] -pub enum MemoryEvidenceOriginKind { - HumanInput, - WorkerInput, - FlowInstruction, - BackendInstruction, - ModelOutput, - ToolOutput, - DerivedSummary, - LegacyUnknown, -} - -/// Bounded origin metadata copied from one typed Memory evidence anchor. -/// -/// This is provenance only. It carries no message body, prompt, reasoning, -/// secret, tool output, or authorization authority. -#[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)] -pub struct MemoryEvidenceOrigin { - pub kind: MemoryEvidenceOriginKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub account_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub workspace_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub runtime_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub worker_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub flow_selector: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub flow_definition_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] - pub flow_definition_revision: Option, -} - -/// Record-level source range for one Memory staging candidate. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct MemorySourceRef { - pub segment_id: String, - #[cfg_attr(feature = "typescript", ts(type = "[number, number]"))] - pub range: [u64; 2], -} - -/// Bounded evidence snippet included in one Memory staging record. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct MemoryStagingEvidence { - pub id: String, - pub kind: String, - #[cfg_attr(feature = "typescript", ts(type = "[number, number] | null"))] - pub entry_range: Option<[u64; 2]>, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr( - feature = "typescript", - ts(optional, type = "MemoryEvidenceOrigin | null") - )] - pub origin: Option, - pub excerpt: Option, - pub summary: Option, -} - -/// Bounded source anchor included in one Memory staging record. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct MemorySourceEvidenceRef { - pub session_id: Option, - pub segment_id: Option, - #[cfg_attr(feature = "typescript", ts(type = "[number, number] | null"))] - pub entry_range: Option<[u64; 2]>, - pub evidence_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr( - feature = "typescript", - ts(optional, type = "MemoryEvidenceOrigin | null") - )] - pub origin: Option, - pub evidence_kind: Option, - pub label: Option, - pub summary: Option, -} - -/// Public projection of one valid Memory staging record. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct MemoryStagingRecord { - pub schema_version: u32, - pub id: String, - pub extract_run_id: String, - pub source: MemorySourceRef, - pub kind: MemoryCandidateKind, - pub claim: String, - pub why_useful: String, - pub staleness: Option, - pub evidence: Vec, - pub source_refs: Vec, -} - -/// Public list entry for one valid Memory staging record. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct MemoryStagingEntry { - pub id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub byte_len: u64, - pub record: MemoryStagingRecord, -} - -/// Public response returned by the Workspace Memory staging list resource. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct MemoryStagingListResponse { - pub limit: usize, - pub returned_count: usize, - pub total_valid_count: usize, - pub invalid_count: usize, - pub truncated: bool, - pub order: String, - pub record_authority: String, - pub items: Vec, - pub diagnostics: Vec, -} - -#[cfg(feature = "typescript")] -pub fn memory_api_typescript() -> String { - use ts_rs::TS; - - let config = ts_rs::Config::default(); - let declarations = [ - DiagnosticSeverity::decl(&config), - Diagnostic::decl(&config), - MemoryDocumentResponse::decl(&config), - MemoryCandidateKind::decl(&config), - MemoryEvidenceOriginKind::decl(&config), - MemoryEvidenceOrigin::decl(&config), - MemorySourceRef::decl(&config), - MemoryStagingEvidence::decl(&config), - MemorySourceEvidenceRef::decl(&config), - MemoryStagingRecord::decl(&config), - MemoryStagingEntry::decl(&config), - MemoryStagingListResponse::decl(&config), - ]; - - format!( - "// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts\n\n{}\n", - declarations - .into_iter() - .map(|declaration| format!("export {declaration}")) - .collect::>() - .join("\n\n") - ) -} - -/// Workspace-owned Memory settings returned by the shared Server API. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct WorkspaceMemorySettings { - pub workspace_id: String, - pub settings_revision: u64, - pub language: String, -} - -/// Compare-and-swap update for Workspace-owned Memory settings. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct UpdateWorkspaceMemorySettingsRequest { - pub expected_revision: u64, - pub language: String, -} - -/// Public metadata for one Workspace-scoped Repository SSH credential. -/// -/// Secret references and secret material are deliberately not part of this DTO. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositorySshCredential { - pub credential_id: String, - pub workspace_id: String, - pub name: String, - pub public_key_algorithm: String, - pub public_key_fingerprint: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub current_revision: u64, - pub status: String, - pub created_at: String, - pub rotated_at: Option, - #[serde(default)] - pub referenced_repositories: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct CreateRepositorySshCredentialRequest { - pub operation_id: String, - pub credential_id: String, - pub name: String, - pub private_key: String, - #[serde(default)] - pub passphrase: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct GenerateRepositorySshCredentialRequest { - pub operation_id: String, - pub credential_id: String, - pub name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositorySshPublicKey { - pub credential_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub current_revision: u64, - pub public_key_algorithm: String, - pub public_key_fingerprint: String, - pub public_key: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RotateRepositorySshCredentialRequest { - pub operation_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub expected_revision: u64, - pub private_key: String, - #[serde(default)] - pub passphrase: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct DeleteRepositorySshCredentialRequest { - pub operation_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub expected_revision: u64, -} - -/// Public metadata for an explicitly pinned SSH host key. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositorySshHostTrust { - pub host_trust_id: String, - pub workspace_id: String, - pub hostname: String, - pub port: u16, - pub key_algorithm: String, - pub host_key: String, - pub fingerprint: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub current_revision: u64, - pub created_at: String, - pub updated_at: String, - #[serde(default)] - pub referenced_repositories: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct PutRepositorySshHostTrustRequest { - pub operation_id: String, - pub host_trust_id: String, - pub hostname: String, - pub port: u16, - pub host_key: String, - #[serde(default)] - #[cfg_attr(feature = "typescript", ts(type = "number | null"))] - pub expected_revision: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct DeleteRepositorySshHostTrustRequest { - pub operation_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub expected_revision: u64, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] -#[serde(rename_all = "snake_case")] -pub enum RepositoryAccessMode { - ReadOnly, - ReadWrite, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositorySshAccessBinding { - pub repository_key: String, - pub credential_id: String, - pub host_trust_id: String, - pub access: RepositoryAccessMode, -} - -/// Secret-free active Repository access projection consumed by later Runtime work. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct RepositoryAccessProjection { - pub workspace_id: String, - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub config_revision: u64, - pub projection_digest: String, - pub bindings: Vec, -} - -pub const SKILL_CATALOG_AUTHORITY: &str = "workspace-config-skills-v1"; -pub const SKILL_API_MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; -pub const SKILL_API_MAX_CATALOG_ENTRIES: usize = 500; -pub const SKILL_API_MAX_OVERRIDES: usize = 64; -pub const SKILL_API_MAX_DIAGNOSTICS: usize = 100; -pub const SKILL_API_MAX_RESOURCES: usize = 500; -pub const SKILL_API_MAX_ALLOWED_TOOLS: usize = 100; -pub const SKILL_API_MAX_NAME_BYTES: usize = 128; -pub const SKILL_API_MAX_LABEL_BYTES: usize = 4_096; -pub const SKILL_API_MAX_BODY_BYTES: usize = 1_048_576; -pub const SKILL_API_MAX_PATH_BYTES: usize = 1_024; -pub const SKILL_API_MAX_DIGEST_BYTES: usize = 128; -pub const SKILL_API_MAX_RESPONSE_BYTES: usize = 2_097_152; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] -#[serde(rename_all = "snake_case")] -pub enum SkillDiagnosticSeverity { - Error, - Warning, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct SkillDiagnostic { - pub severity: SkillDiagnosticSeverity, - pub code: String, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub source: Option, -} - -impl SkillDiagnostic { - pub fn error( - code: impl Into, - message: impl Into, - source: Option, - ) -> Self { - Self { - severity: SkillDiagnosticSeverity::Error, - code: code.into(), - message: message.into(), - source, - } - } - - pub fn warning( - code: impl Into, - message: impl Into, - source: Option, - ) -> Self { - Self { - severity: SkillDiagnosticSeverity::Warning, - code: code.into(), - message: message.into(), - source, - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] -#[serde(rename_all = "snake_case")] -pub enum SkillSourceKind { - Builtin, - Workspace, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct SkillProvenance { - pub kind: SkillSourceKind, - pub id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub virtual_path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional, type = "number"))] - pub revision: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub source_digest: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub tree_digest: Option, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] -#[serde(rename_all = "snake_case")] -pub enum SkillActivationStatus { - Active, - Inactive, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))] -#[serde(rename_all = "snake_case")] -pub enum SkillProjectionStatus { - Valid, - Invalid, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct SkillProjectionIdentity { - #[cfg_attr(feature = "typescript", ts(type = "number"))] - pub config_revision: u64, - pub tree_digest: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct SkillResourceRef { - pub kind: String, - pub name: String, - pub supported: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "typescript", ts(optional))] - pub diagnostic: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct SkillCatalogEntry { - pub name: String, - pub description: String, - pub activation_status: SkillActivationStatus, - pub projection_status: SkillProjectionStatus, - pub provenance: SkillProvenance, - pub overrides: Vec, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct SkillCatalogResponse { - pub authority: String, - pub projection: SkillProjectionIdentity, - pub entries: Vec, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] -#[serde(deny_unknown_fields)] -pub struct SkillDetailResponse { - pub authority: String, - pub projection: SkillProjectionIdentity, - pub name: String, - pub description: String, - pub provenance: SkillProvenance, - pub overrides: Vec, - pub diagnostics: Vec, - pub activation_status: SkillActivationStatus, - pub projection_status: SkillProjectionStatus, - pub body: String, - pub allowed_tools: Vec, - pub allowed_tools_status: String, - pub resources: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SkillApiValidationError { - CollectionTooLarge, - StringTooLarge, - InvalidProjectionIdentity, - InvalidProvenance, - InvalidVirtualPath, - StaleProjection, -} - -impl std::fmt::Display for SkillApiValidationError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let message = match self { - Self::CollectionTooLarge => "Skill API collection exceeds its limit", - Self::StringTooLarge => "Skill API string exceeds its limit", - Self::InvalidProjectionIdentity => "Skill API projection identity is invalid", - Self::InvalidProvenance => "Skill API provenance is invalid", - Self::InvalidVirtualPath => "Skill API virtual path is invalid", - Self::StaleProjection => "Workspace Skill projection is stale", - }; - formatter.write_str(message) - } -} - -impl std::error::Error for SkillApiValidationError {} - -impl SkillProjectionIdentity { - fn validate(&self) -> Result<(), SkillApiValidationError> { - validate_safe_integer(self.config_revision)?; - validate_nonempty_string(&self.tree_digest, SKILL_API_MAX_DIGEST_BYTES) - .map_err(|_| SkillApiValidationError::InvalidProjectionIdentity) - } -} - -impl SkillProvenance { - fn validate( - &self, - projection: &SkillProjectionIdentity, - ) -> Result<(), SkillApiValidationError> { - validate_nonempty_string(&self.id, SKILL_API_MAX_LABEL_BYTES)?; - validate_optional_string(self.virtual_path.as_deref(), SKILL_API_MAX_PATH_BYTES)?; - validate_optional_string(self.source_digest.as_deref(), SKILL_API_MAX_DIGEST_BYTES)?; - validate_optional_string(self.tree_digest.as_deref(), SKILL_API_MAX_DIGEST_BYTES)?; - if let Some(revision) = self.revision { - validate_safe_integer(revision)?; - } - - let expected_prefix = match self.kind { - SkillSourceKind::Builtin => "builtin:", - SkillSourceKind::Workspace => "workspace:", - }; - if !self.id.starts_with(expected_prefix) - || self - .virtual_path - .as_deref() - .is_none_or(|path| !is_virtual_path(path)) - || self.source_digest.is_none() - { - return Err(SkillApiValidationError::InvalidProvenance); - } - - match self.kind { - SkillSourceKind::Builtin => { - if self.revision.is_some() || self.tree_digest.is_some() { - return Err(SkillApiValidationError::InvalidProvenance); - } - } - SkillSourceKind::Workspace => { - let Some(revision) = self.revision else { - return Err(SkillApiValidationError::InvalidProvenance); - }; - let Some(tree_digest) = self.tree_digest.as_deref() else { - return Err(SkillApiValidationError::InvalidProvenance); - }; - if revision != projection.config_revision || tree_digest != projection.tree_digest { - return Err(SkillApiValidationError::StaleProjection); - } - } - } - Ok(()) - } -} - -impl SkillCatalogEntry { - fn validate( - &self, - projection: &SkillProjectionIdentity, - ) -> Result<(), SkillApiValidationError> { - validate_nonempty_string(&self.name, SKILL_API_MAX_NAME_BYTES)?; - validate_string(&self.description, SKILL_API_MAX_LABEL_BYTES)?; - validate_collection(&self.overrides, SKILL_API_MAX_OVERRIDES)?; - validate_diagnostics(&self.diagnostics)?; - self.provenance.validate(projection)?; - for provenance in &self.overrides { - provenance.validate(projection)?; - } - Ok(()) - } -} - -impl SkillCatalogResponse { - pub fn validate(&self) -> Result<(), SkillApiValidationError> { - validate_nonempty_string(&self.authority, SKILL_API_MAX_LABEL_BYTES)?; - if self.authority != SKILL_CATALOG_AUTHORITY { - return Err(SkillApiValidationError::InvalidProjectionIdentity); - } - self.projection.validate()?; - validate_collection(&self.entries, SKILL_API_MAX_CATALOG_ENTRIES)?; - validate_diagnostics(&self.diagnostics)?; - for entry in &self.entries { - entry.validate(&self.projection)?; - } - Ok(()) - } -} - -impl SkillDetailResponse { - pub fn validate(&self) -> Result<(), SkillApiValidationError> { - validate_nonempty_string(&self.authority, SKILL_API_MAX_LABEL_BYTES)?; - if self.authority != SKILL_CATALOG_AUTHORITY { - return Err(SkillApiValidationError::InvalidProjectionIdentity); - } - self.projection.validate()?; - validate_nonempty_string(&self.name, SKILL_API_MAX_NAME_BYTES)?; - validate_string(&self.description, SKILL_API_MAX_LABEL_BYTES)?; - validate_string(&self.body, SKILL_API_MAX_BODY_BYTES)?; - validate_strings( - &self.allowed_tools, - SKILL_API_MAX_ALLOWED_TOOLS, - SKILL_API_MAX_LABEL_BYTES, - )?; - validate_nonempty_string(&self.allowed_tools_status, SKILL_API_MAX_LABEL_BYTES)?; - validate_resources(&self.resources)?; - validate_collection(&self.overrides, SKILL_API_MAX_OVERRIDES)?; - validate_diagnostics(&self.diagnostics)?; - self.provenance.validate(&self.projection)?; - for provenance in &self.overrides { - provenance.validate(&self.projection)?; - } - Ok(()) - } -} - -fn validate_safe_integer(value: u64) -> Result<(), SkillApiValidationError> { - if value <= SKILL_API_MAX_SAFE_INTEGER { - Ok(()) - } else { - Err(SkillApiValidationError::InvalidProjectionIdentity) - } -} - -fn validate_collection(values: &[T], limit: usize) -> Result<(), SkillApiValidationError> { - if values.len() <= limit { - Ok(()) - } else { - Err(SkillApiValidationError::CollectionTooLarge) - } -} - -fn validate_string(value: &str, limit: usize) -> Result<(), SkillApiValidationError> { - if value.len() <= limit { - Ok(()) - } else { - Err(SkillApiValidationError::StringTooLarge) - } -} - -fn validate_nonempty_string(value: &str, limit: usize) -> Result<(), SkillApiValidationError> { - validate_string(value, limit)?; - if value.is_empty() { - Err(SkillApiValidationError::StringTooLarge) - } else { - Ok(()) - } -} - -fn validate_optional_string( - value: Option<&str>, - limit: usize, -) -> Result<(), SkillApiValidationError> { - if let Some(value) = value { - validate_nonempty_string(value, limit)?; - } - Ok(()) -} - -fn validate_strings( - values: &[String], - collection_limit: usize, - string_limit: usize, -) -> Result<(), SkillApiValidationError> { - validate_collection(values, collection_limit)?; - for value in values { - validate_nonempty_string(value, string_limit)?; - } - Ok(()) -} - -fn validate_resources(resources: &[SkillResourceRef]) -> Result<(), SkillApiValidationError> { - validate_collection(resources, SKILL_API_MAX_RESOURCES)?; - for resource in resources { - validate_nonempty_string(&resource.kind, SKILL_API_MAX_LABEL_BYTES)?; - validate_nonempty_string(&resource.name, SKILL_API_MAX_PATH_BYTES)?; - if !is_virtual_path(&resource.name) { - return Err(SkillApiValidationError::InvalidVirtualPath); - } - validate_optional_string(resource.diagnostic.as_deref(), SKILL_API_MAX_LABEL_BYTES)?; - } - Ok(()) -} - -fn is_virtual_path(value: &str) -> bool { - !value.starts_with('/') - && !value.contains('\\') - && value - .split('/') - .all(|component| !component.is_empty() && component != "." && component != "..") -} - -fn validate_diagnostics(diagnostics: &[SkillDiagnostic]) -> Result<(), SkillApiValidationError> { - validate_collection(diagnostics, SKILL_API_MAX_DIAGNOSTICS)?; - for diagnostic in diagnostics { - validate_nonempty_string(&diagnostic.code, SKILL_API_MAX_LABEL_BYTES)?; - validate_nonempty_string(&diagnostic.message, SKILL_API_MAX_LABEL_BYTES)?; - validate_optional_string(diagnostic.source.as_deref(), SKILL_API_MAX_PATH_BYTES)?; - } - Ok(()) -} - -#[cfg(feature = "typescript")] -pub fn catalog_typescript() -> String { - use ts_rs::TS; - - let config = ts_rs::Config::default(); - let declarations = [ - WorkspaceSummary::decl(&config), - WorkspaceCatalogListResponse::decl(&config), - WorkspaceRepositoryRecord::decl(&config), - WorkspaceCreateResponse::decl(&config), - WorkspaceAuthConfig::decl(&config), - WorkspacePermissionSummary::decl(&config), - WorkspaceDeletionState::decl(&config), - WorkspaceDeletionBlockerKind::decl(&config), - WorkspaceDeletionBlocker::decl(&config), - WorkspaceDeletionResourceCounts::decl(&config), - WorkspaceDeletionPreflightResponse::decl(&config), - WorkspaceDeletionRequest::decl(&config), - WorkspaceDeletionOperationResponse::decl(&config), - DiagnosticSeverity::decl(&config), - Diagnostic::decl(&config), - WorkspaceExtensionPointState::decl(&config), - WorkspaceExtensionPoints::decl(&config), - WorkspaceResponse::decl(&config), - WorkspaceMetadataSettingsResponse::decl(&config), - UpdateWorkspaceMetadataRequest::decl(&config), - WorkspaceMetadataMutationResponse::decl(&config), - WorkspaceSigningIdentityState::decl(&config), - WorkspaceSigningIdentityPublic::decl(&config), - WorkspacePublicIdentityBundle::decl(&config), - WorkspaceSigningIdentityResponse::decl(&config), - ProfileSettingsResponse::decl(&config), - WorkspaceProfileSummary::decl(&config), - WorkspaceProfileSourceSummary::decl(&config), - WorkspaceProfileSourceProvenance::decl(&config), - RepositorySourceKind::decl(&config), - RepositorySource::decl(&config), - RepositoryObservedStatus::decl(&config), - RepositoryDiagnostic::decl(&config), - GitRemoteSummary::decl(&config), - GitRepositorySummary::decl(&config), - RepositorySummary::decl(&config), - GitCommitSummary::decl(&config), - RepositoryListResponse::decl(&config), - RepositoryDetailResponse::decl(&config), - RepositorySshConnectionProbeRequest::decl(&config), - RepositorySshHostKeyCandidate::decl(&config), - RepositorySshConnectionTrustState::decl(&config), - RepositorySshConnectionProbeResponse::decl(&config), - ConfirmRepositorySshHostTrustRequest::decl(&config), - RepositoryLogResponse::decl(&config), - RuntimeSourceKind::decl(&config), - RuntimeSourceStatus::decl(&config), - RuntimeIdentityAuthority::decl(&config), - RuntimeSourceSummary::decl(&config), - RuntimeSummary::decl(&config), - WorkspaceRuntimeBindingState::decl(&config), - RuntimeConnectionDisplayState::decl(&config), - RuntimeVerificationOutcome::decl(&config), - RuntimeVerificationEvidenceSummary::decl(&config), - WorkspaceRuntimeBindingSummary::decl(&config), - RuntimeManagementSummary::decl(&config), - WorkspaceRuntimeResource::decl(&config), - RuntimeTrustKeyStatus::decl(&config), - RuntimeTrustKeyState::decl(&config), - RuntimeTrustAuditAction::decl(&config), - RuntimeTrustAuditEntry::decl(&config), - WorkspaceRuntimeDetail::decl(&config), - RuntimeTrustKeyRevealResponse::decl(&config), - RevokeRuntimeTrustKeyRequest::decl(&config), - RemoveRuntimeRequest::decl(&config), - RuntimeRemovalOperationState::decl(&config), - RuntimeRemovalOperationResponse::decl(&config), - RuntimeTrustConflictKind::decl(&config), - RuntimeTrustConflictResponse::decl(&config), - RuntimePublicIdentityBundle::decl(&config), - CreateRemoteRuntimeRequest::decl(&config), - UpdateRemoteRuntimeRequest::decl(&config), - RuntimeConnectionTestStatus::decl(&config), - RuntimeConnectionTestFailureKind::decl(&config), - RuntimeConnectionTestResponse::decl(&config), - ] - .map(|declaration| format!("export {declaration}")); - - format!( - "// This file is generated by `cargo run -p workspace-api --features typescript --example generate_typescript | deno fmt -`.\n// Do not edit this file directly.\n\n{}\n", - declarations.join("\n\n") - ) -} - -#[cfg(feature = "typescript")] -pub fn repository_access_api_typescript() -> String { - use ts_rs::TS; - - let config = ts_rs::Config::default(); - let declarations = [ - RepositorySshCredential::decl(&config), - CreateRepositorySshCredentialRequest::decl(&config), - GenerateRepositorySshCredentialRequest::decl(&config), - RepositorySshPublicKey::decl(&config), - RotateRepositorySshCredentialRequest::decl(&config), - DeleteRepositorySshCredentialRequest::decl(&config), - RepositorySshHostTrust::decl(&config), - PutRepositorySshHostTrustRequest::decl(&config), - DeleteRepositorySshHostTrustRequest::decl(&config), - RepositoryAccessMode::decl(&config), - RepositorySshAccessBinding::decl(&config), - RepositoryAccessProjection::decl(&config), - ]; - format!( - "// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts\n\n{}\n", - declarations - .into_iter() - .map(|declaration| format!("export {declaration}")) - .collect::>() - .join("\n\n") - ) -} - -#[cfg(feature = "typescript")] -pub fn skill_api_typescript() -> String { - use ts_rs::TS; - - let config = ts_rs::Config::default(); - let declarations = [ - SkillDiagnosticSeverity::decl(&config), - SkillDiagnostic::decl(&config), - SkillSourceKind::decl(&config), - SkillProvenance::decl(&config), - SkillActivationStatus::decl(&config), - SkillProjectionStatus::decl(&config), - SkillProjectionIdentity::decl(&config), - SkillResourceRef::decl(&config), - SkillCatalogEntry::decl(&config), - SkillCatalogResponse::decl(&config), - SkillDetailResponse::decl(&config), - ]; - let limits = format!( - "export const SKILL_API_AUTHORITY = \"{SKILL_CATALOG_AUTHORITY}\" as const;\n\nexport const SKILL_API_LIMITS = {{\n maxSafeInteger: {SKILL_API_MAX_SAFE_INTEGER},\n maxCatalogEntries: {SKILL_API_MAX_CATALOG_ENTRIES},\n maxOverrides: {SKILL_API_MAX_OVERRIDES},\n maxDiagnostics: {SKILL_API_MAX_DIAGNOSTICS},\n maxResources: {SKILL_API_MAX_RESOURCES},\n maxAllowedTools: {SKILL_API_MAX_ALLOWED_TOOLS},\n maxNameBytes: {SKILL_API_MAX_NAME_BYTES},\n maxLabelBytes: {SKILL_API_MAX_LABEL_BYTES},\n maxBodyBytes: {SKILL_API_MAX_BODY_BYTES},\n maxPathBytes: {SKILL_API_MAX_PATH_BYTES},\n maxDigestBytes: {SKILL_API_MAX_DIGEST_BYTES},\n maxResponseBytes: {SKILL_API_MAX_RESPONSE_BYTES},\n}} as const;" - ); - format!( - "// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_skill_api_types > web/workspace/src/lib/generated/skill-api.ts\n\n{limits}\n\n{}\n", - declarations - .into_iter() - .map(|declaration| format!("export {declaration}")) - .collect::>() - .join("\n\n") - ) -} - -#[cfg(feature = "typescript")] -pub fn auth_api_typescript() -> String { - use ts_rs::TS; - - let config = ts_rs::Config::default(); - let declarations = [ - AuthPublicConfig::decl(&config), - ActorAuthMethod::decl(&config), - AuthenticatedUser::decl(&config), - RequestActor::decl(&config), - WhoamiResponse::decl(&config), - AuthBootstrapUserRequest::decl(&config), - AuthUserResponse::decl(&config), - PasskeyRegistrationOptionsRequest::decl(&config), - PasskeyRegistrationOptionsResponse::decl(&config), - PasskeyRegistrationCompleteRequest::decl(&config), - PasskeyLoginOptionsRequest::decl(&config), - PasskeyLoginOptionsResponse::decl(&config), - PasskeyLoginCompleteRequest::decl(&config), - DeviceLoginStartRequest::decl(&config), - DeviceLoginStartResponse::decl(&config), - DeviceLoginApproveRequest::decl(&config), - DeviceLoginApprovalStatus::decl(&config), - DeviceLoginApproveResponse::decl(&config), - DeviceLoginPollRequest::decl(&config), - DeviceAccessTokenType::decl(&config), - DeviceLoginPollStatus::decl(&config), - DeviceLoginPollResponse::decl(&config), - LogoutStatus::decl(&config), - LogoutResponse::decl(&config), - ]; - format!( - "// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts\n\n{}\n", - declarations - .into_iter() - .map(|declaration| format!("export {declaration}")) - .collect::>() - .join("\n\n") - ) -} - -#[cfg(feature = "typescript")] -pub fn workdir_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), - WorkingDirectoryOccupancy::decl(&config), - WorkingDirectorySummary::decl(&config), - WorkingDirectoryCreateRequest::decl(&config), - WorkingDirectoryListResponse::decl(&config), - WorkingDirectoryDetailResponse::decl(&config), - WorkingDirectoryCreateResponse::decl(&config), - ]; - format!( - "// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts\n\n{}\n", - declarations - .into_iter() - .map(|declaration| format!("export {declaration}")) - .collect::>() - .join("\n\n") - ) -} - -#[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::>() - .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::() - .replace("=|", "=") - } -} - -#[cfg(all(test, feature = "typescript"))] -mod memory_typescript_tests { - #[test] - fn generated_memory_api_contract_is_current() { - let expected = super::memory_api_typescript(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../web/workspace/src/lib/generated/memory-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 Memory API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-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::() - .replace("=|", "=") - .replace(";}", "}") - } -} - -#[cfg(all(test, feature = "typescript"))] -mod skill_typescript_tests { - #[test] - fn generated_skill_api_contract_is_current() { - let expected = super::skill_api_typescript(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../web/workspace/src/lib/generated/skill-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 Skill API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_skill_api_types > web/workspace/src/lib/generated/skill-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::() - .replace("=|", "=") - .replace(";}", "}") - } -} - -#[cfg(all(test, feature = "typescript"))] -mod workdir_typescript_tests { - #[test] - fn generated_workdir_api_contract_is_current() { - let expected = super::workdir_api_typescript(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../web/workspace/src/lib/generated/workdir-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 Workdir API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-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::() - .replace("=|", "=") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn worker_restore_state_round_trips_and_rejects_unknown_variants() { - for (state, wire) in [ - (WorkerRestoreState::Accepted, "accepted"), - (WorkerRestoreState::Rejected, "rejected"), - (WorkerRestoreState::RolledBack, "rolled_back"), - ( - WorkerRestoreState::ReconciliationRequired, - "reconciliation_required", - ), - ] { - assert_eq!( - serde_json::to_value(state).unwrap(), - serde_json::json!(wire) - ); - assert_eq!( - serde_json::from_value::(serde_json::json!(wire)).unwrap(), - state - ); - } - assert!( - serde_json::from_value::(serde_json::json!("unknown")).is_err() - ); - } - - #[test] - fn worker_restore_result_rejects_unknown_fields() { - let result = serde_json::from_value::(serde_json::json!({ - "state": "rejected", - "worker": null, - "diagnostics": [], - "unexpected": true - })); - assert!(result.is_err()); - } - - #[test] - fn historical_http_repository_source_kind_decodes_as_invalid_evidence() { - let source: RepositorySource = serde_json::from_value(serde_json::json!({ - "kind": "http", - "uri": "http://git.example.test/team/project.git", - "revision": 1, - })) - .unwrap(); - - assert_eq!(source.kind, RepositorySourceKind::Invalid); - assert_eq!( - serde_json::to_value(source).unwrap()["kind"], - serde_json::json!("invalid") - ); - assert_eq!( - RepositorySourceKind::parse("http"), - Some(RepositorySourceKind::Invalid) - ); - assert!(!RepositorySourceKind::Invalid.is_remote()); - } - - fn skill_projection() -> SkillProjectionIdentity { - SkillProjectionIdentity { - config_revision: 42, - tree_digest: "tree-digest".to_string(), - } - } - - fn builtin_skill_provenance() -> SkillProvenance { - SkillProvenance { - kind: SkillSourceKind::Builtin, - id: "builtin:errors".to_string(), - virtual_path: Some("skills/errors/SKILL.md".to_string()), - revision: None, - source_digest: Some("builtin-source-digest".to_string()), - tree_digest: None, - } - } - - fn workspace_skill_provenance() -> SkillProvenance { - SkillProvenance { - kind: SkillSourceKind::Workspace, - id: "workspace:skills/release/SKILL.md".to_string(), - virtual_path: Some("skills/release/SKILL.md".to_string()), - revision: Some(42), - source_digest: Some("workspace-source-digest".to_string()), - tree_digest: Some("tree-digest".to_string()), - } - } - - #[test] - fn skill_catalog_round_trips_builtin_workspace_and_invalid_projection_entries() { - let response = SkillCatalogResponse { - authority: "workspace-config-skills-v1".to_string(), - projection: skill_projection(), - entries: vec![ - SkillCatalogEntry { - name: "errors".to_string(), - description: "Builtin guidance".to_string(), - activation_status: SkillActivationStatus::Active, - projection_status: SkillProjectionStatus::Valid, - provenance: builtin_skill_provenance(), - overrides: vec![], - diagnostics: vec![], - }, - SkillCatalogEntry { - name: "release".to_string(), - description: "Workspace guidance".to_string(), - activation_status: SkillActivationStatus::Inactive, - projection_status: SkillProjectionStatus::Invalid, - provenance: workspace_skill_provenance(), - overrides: vec![builtin_skill_provenance()], - diagnostics: vec![SkillDiagnostic { - severity: SkillDiagnosticSeverity::Error, - code: "invalid_projection".to_string(), - message: "invalid projected Skill".to_string(), - source: Some("skills/release/SKILL.md".to_string()), - }], - }, - ], - diagnostics: vec![], - }; - - response.validate().expect("fixture should be valid"); - let json = serde_json::to_string(&response).expect("serialize Skill catalog"); - let decoded: SkillCatalogResponse = - serde_json::from_str(&json).expect("deserialize Skill catalog"); - assert_eq!(decoded, response); - assert!(!json.contains("\"revision\":null")); - assert!(!json.contains("\"tree_digest\":null")); - } - - #[test] - fn skill_detail_round_trips_shared_response() { - let response = SkillDetailResponse { - authority: "workspace-config-skills-v1".to_string(), - projection: skill_projection(), - name: "release".to_string(), - description: "Workspace guidance".to_string(), - body: "# Release\n".to_string(), - allowed_tools: vec!["Bash".to_string()], - allowed_tools_status: "experimental_hint_only".to_string(), - resources: vec![], - activation_status: SkillActivationStatus::Active, - projection_status: SkillProjectionStatus::Valid, - provenance: workspace_skill_provenance(), - overrides: vec![], - diagnostics: vec![], - }; - - response.validate().expect("fixture should be valid"); - let decoded: SkillDetailResponse = serde_json::from_value( - serde_json::to_value(&response).expect("serialize Skill detail"), - ) - .expect("deserialize Skill detail"); - assert_eq!(decoded, response); - } - - #[test] - fn skill_projection_validation_detects_stale_workspace_revision() { - let mut provenance = workspace_skill_provenance(); - provenance.revision = Some(41); - let response = SkillCatalogResponse { - authority: "workspace-config-skills-v1".to_string(), - projection: skill_projection(), - entries: vec![SkillCatalogEntry { - name: "release".to_string(), - description: String::new(), - activation_status: SkillActivationStatus::Active, - projection_status: SkillProjectionStatus::Valid, - provenance, - overrides: vec![], - diagnostics: vec![], - }], - diagnostics: vec![], - }; - - assert_eq!( - response.validate(), - Err(SkillApiValidationError::StaleProjection) - ); - } - - #[test] - fn skill_dto_rejects_unknown_fields_and_unknown_provenance_kind() { - let unknown_field = serde_json::json!({ - "authority": "workspace-config-skills-v1", - "projection": {"config_revision": 42, "tree_digest": "tree-digest"}, - "entries": [], - "diagnostics": [], - "body": "must not be accepted" - }); - assert!(serde_json::from_value::(unknown_field).is_err()); - - let mut provenance = - serde_json::to_value(workspace_skill_provenance()).expect("serialize provenance"); - provenance["kind"] = serde_json::Value::String("newer_source_kind".to_string()); - assert!(serde_json::from_value::(provenance).is_err()); - } - - #[test] - fn memory_evidence_origins_round_trip_as_typed_provenance() { - let kinds = [ - MemoryEvidenceOriginKind::HumanInput, - MemoryEvidenceOriginKind::WorkerInput, - MemoryEvidenceOriginKind::FlowInstruction, - MemoryEvidenceOriginKind::BackendInstruction, - MemoryEvidenceOriginKind::ModelOutput, - MemoryEvidenceOriginKind::ToolOutput, - MemoryEvidenceOriginKind::DerivedSummary, - MemoryEvidenceOriginKind::LegacyUnknown, - ]; - for kind in kinds { - let origin = MemoryEvidenceOrigin { - kind, - account_id: Some("account-1".to_string()), - workspace_id: Some("workspace-1".to_string()), - runtime_id: Some("runtime-1".to_string()), - worker_id: Some("worker-1".to_string()), - flow_selector: Some("builtin:coder-review".to_string()), - flow_definition_id: Some("flow-1".to_string()), - flow_definition_revision: Some(7), - }; - let encoded = serde_json::to_value(&origin).unwrap(); - let decoded: MemoryEvidenceOrigin = serde_json::from_value(encoded).unwrap(); - assert_eq!(decoded, origin); - } - } - - #[test] - fn memory_evidence_origin_rejects_unknown_kind_and_fields() { - assert!( - serde_json::from_value::( - serde_json::json!({"kind": "future_origin"}) - ) - .is_err() - ); - assert!( - serde_json::from_value::(serde_json::json!({ - "kind": "human_input", - "future_field": "not current schema" - })) - .is_err() - ); - } - - 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 remote_runtime_metadata_update_cannot_carry_public_key_authority() { - let request = UpdateRemoteRuntimeRequest { - display_name: Some("Runtime A".to_string()), - endpoint: "https://runtime.example.test".to_string(), - }; - assert_eq!( - serde_json::to_value(&request).unwrap(), - serde_json::json!({ - "display_name": "Runtime A", - "endpoint": "https://runtime.example.test", - }) - ); - assert!( - serde_json::from_value::(serde_json::json!({ - "display_name": "Runtime A", - "endpoint": "https://runtime.example.test", - "public_bundle": { - "identity_id": "runtime-a", - "public_key": "yoi-ed25519-pub:v1:not-accepted", - }, - })) - .is_err(), - "metadata updates must reject public key fields" - ); - } - - #[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::(serde_json::json!({ - "runtime_id": "runtime-a", - "display_name": "Worker A", - "unexpected": true, - })) - .unwrap_err(); - assert!(error.to_string().contains("unknown field")); - } - - #[test] - fn repository_key_validation_is_canonical_and_bounded() { - let max = "a".repeat(64); - for valid in ["a", "main", "repo-42", max.as_str()] { - assert_eq!(validate_repository_key(valid), Ok(()), "{valid}"); - } - let too_long = "a".repeat(65); - for invalid in [ - "", - "-main", - "main-", - "Main", - "main_repo", - "main.repo", - "日本語", - too_long.as_str(), - ] { - assert!(validate_repository_key(invalid).is_err(), "{invalid}"); - } - } - - #[test] - fn workspace_deletion_wire_contract_is_closed_and_typed() { - let preflight = WorkspaceDeletionPreflightResponse { - workspace_id: "workspace-test".to_string(), - display_name: "Test".to_string(), - expected_revision: "revision-7".to_string(), - can_delete: true, - resources: WorkspaceDeletionResourceCounts { - workers: 2, - workdirs: 1, - repositories: 1, - runtime_bindings: 1, - secrets: 0, - artifacts: 3, - }, - blockers: Vec::new(), - }; - let value = serde_json::to_value(&preflight).unwrap(); - assert_eq!( - serde_json::from_value::(value.clone()).unwrap(), - preflight - ); - let mut stale = value.as_object().unwrap().clone(); - stale.insert("revision".to_string(), serde_json::json!(7)); - assert!( - serde_json::from_value::(stale.into()).is_err() - ); - - assert!( - serde_json::from_value::(serde_json::json!({ - "operation_id": "delete-test", - "expected_revision": "revision-7", - "confirmation": "Test", - "workspace_id": "caller-controlled" - })) - .is_err() - ); - assert!( - serde_json::from_value::(serde_json::json!({ - "operation_id": "x".repeat(WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES + 1), - "expected_revision": "revision-7", - "confirmation": "Test" - })) - .is_err() - ); - assert!( - serde_json::from_value::(serde_json::json!({ - "operation_id": "delete-test", - "workspace_id": "workspace-test", - "display_name": "Test", - "state": "blocked", - "resources": { - "workers": 0, - "workdirs": 0, - "repositories": 0, - "runtime_bindings": 0, - "secrets": 0, - "artifacts": 0 - }, - "child_operation_ids": [], - "blockers": (0..=WORKSPACE_DELETION_MAX_BLOCKERS).map(|_| serde_json::json!({ - "kind": "cleanup_unavailable", - "resource_kind": null, - "resource_key": null, - "message": "blocked" - })).collect::>(), - "failure_category": null, - "created_at": "1", - "updated_at": "1", - "completed_at": null - })) - .is_err() - ); - } - - #[test] - fn workspace_create_request_has_one_closed_shared_wire_shape() { - let request = WorkspaceCreateRequest { - operation_key: "workspace-create-1".to_string(), - display_name: "Workspace".to_string(), - repository: InitialRepositoryIntent { - repository_key: "main".to_string(), - uri: "/srv/repositories/main".to_string(), - default_ref: Some("develop".to_string()), - }, - }; - let json = serde_json::to_value(&request).unwrap(); - assert_eq!(json["operation_key"], "workspace-create-1"); - assert_eq!(json["repository"]["uri"], "/srv/repositories/main"); - assert!(json.get("operation_id").is_none()); - assert!(json["repository"].get("source").is_none()); - assert!( - serde_json::from_value::(serde_json::json!({ - "operation_id": "workspace-create-1", - "display_name": "Workspace", - "repository": { - "repository_key": "main", - "source": "/srv/repositories/main" - } - })) - .is_err() - ); - } - - #[test] - fn workspace_and_repository_response_shapes_round_trip() { - let workspace = serde_json::json!({ - "workspace_id": "workspace-test", - "display_name": "Test", - "record_authority": "workspace-control-plane", - "schema_version": 46, - "auth": {"Passkey": { - "rp_id": "example.test", - "origin": "https://example.test", - "public_base_url": "https://example.test", - "cookie_name": "yoi_session" - }}, - "permissions": { - "manage_repositories": true, - "manage_secrets": true, - "manage_runtimes": true, - "delete_workspace": true - }, - "extension_points": { - "store": "sqlite", - "event_stream": {"status": "available", "note": "ready", "diagnostics": []}, - "host_worker_bridge": {"status": "available", "note": "ready", "diagnostics": []}, - "companion_console": {"status": "available", "note": "ready", "diagnostics": []} - } - }); - let parsed: WorkspaceResponse = serde_json::from_value(workspace.clone()).unwrap(); - assert_eq!(serde_json::to_value(parsed).unwrap(), workspace); - - let catalog = serde_json::json!([{ - "workspace_id": "workspace-test", - "owner_account_id": "user-test", - "display_name": "Test", - "state": "active", - "created_at": "2026-01-01T00:00:00Z", - "updated_at": "2026-01-01T00:00:00Z" - }]); - let parsed: WorkspaceCatalogListResponse = serde_json::from_value(catalog.clone()).unwrap(); - assert_eq!(serde_json::to_value(parsed).unwrap(), catalog); - - let repositories = serde_json::json!({ - "workspace_id": "workspace-test", - "items": [{ - "repository_key": "main", - "kind": "git", - "provider": "git", - "source": {"kind": "local_path", "uri": "/srv/project"}, - "source_revision": 1, - "source_fingerprint": "sha256:test", - "observed_status": "ready", - "record_authority": "workspace-control-plane" - }], - "source": "workspace-control-plane", - "diagnostics": [] - }); - let parsed: RepositoryListResponse = serde_json::from_value(repositories.clone()).unwrap(); - assert_eq!(serde_json::to_value(parsed).unwrap(), repositories); - } - - #[test] - fn repository_response_rejects_stale_field_aliases() { - let stale = serde_json::json!({ - "workspace_id": "workspace-test", - "items": [{ - "repository_key": "main", - "display_name": "main", - "kind": "git", - "provider": "git", - "source": {"kind": "local_path", "uri": "/srv/project"}, - "source_revision": 1, - "source_fingerprint": "sha256:test", - "observed_status": "ready", - "record_authority": "workspace-control-plane" - }], - "source": "workspace-control-plane", - "diagnostics": [] - }); - - assert!(serde_json::from_value::(stale).is_err()); - } - - #[test] - fn runtime_detail_and_trust_mutations_are_closed_and_typed() { - let detail = serde_json::json!({ - "workspace_id": "workspace-test", - "runtime": { - "runtime_id": "runtime-test", - "label": "Runtime Test", - "kind": "remote_http", - "status": "active", - "source": { - "kind": "remote_http", - "status": "active", - "identity_authority": "runtime_registry_projection", - "note": "active" - }, - "host_ids": [], - "worker_creation_available": true, - "os": "linux", - "arch": "x86_64", - "diagnostics": [], - "management": { - "built_in": false, - "config_managed": true, - "removable": true, - "endpoint_configured": true, - "token_ref_configured": false - } - }, - "endpoint": "https://runtime.example", - "trust_key": { - "status": "active", - "fingerprint": "SHA256:test", - "revision": 2, - "created_at": "2026-09-01T12:00:00Z", - "updated_at": "2026-09-01T13:00:00Z" - }, - "recent_audit": [{ - "action": "replaced", - "actor_account_id": "account-owner", - "old_fingerprint": "SHA256:old", - "new_fingerprint": "SHA256:test", - "revision": 2, - "at": "2026-09-01T13:00:00Z" - }] - }); - let parsed: WorkspaceRuntimeDetail = serde_json::from_value(detail.clone()).unwrap(); - assert_eq!(serde_json::to_value(parsed).unwrap(), detail); - - let mut unknown = detail; - unknown["trust_key"]["private_key"] = serde_json::json!("forbidden"); - assert!(serde_json::from_value::(unknown).is_err()); - assert!( - serde_json::from_value::(serde_json::json!({ - "public_key": "yoi-ed25519-pub:v1:key", - "private_key": "forbidden" - })) - .is_err() - ); - assert!( - serde_json::from_value::(serde_json::json!({ - "expected_revision": 1, - "delete_runtime": true - })) - .is_err() - ); - } - - #[test] - fn runtime_connection_test_response_is_closed_and_typed() { - let compatible = serde_json::json!({ - "workspace_id": "workspace-test", - "runtime_id": "runtime-test", - "binding_revision": 3, - "connection_state": "verified", - "verification": null, - "checked_at": "2026-09-01T12:00:00Z", - "status": "compatible", - "failure_kind": null, - "expected_protocol_version": 1, - "actual_protocol_version": 1, - "diagnostics": [] - }); - let parsed: RuntimeConnectionTestResponse = - serde_json::from_value(compatible.clone()).unwrap(); - assert_eq!(serde_json::to_value(parsed).unwrap(), compatible); - - let mut unknown = compatible; - unknown["capabilities"] = serde_json::json!(["shell"]); - assert!(serde_json::from_value::(unknown).is_err()); - } - - #[cfg(feature = "typescript")] - #[test] - fn generated_catalog_typescript_keeps_public_wrappers_and_nullability() { - let output = catalog_typescript(); - assert!( - output.contains("export type WorkspaceCatalogListResponse = Array") - ); - assert!(output.contains("export type WorkspaceResponse =")); - assert!(output.contains("permissions: WorkspacePermissionSummary")); - assert!(output.contains("export type RepositoryListResponse =")); - assert!(output.contains("items: Array")); - assert!(output.contains("observed_at?: string | null")); - assert!(output.contains("export type WorkspaceMetadataSettingsResponse =")); - assert!(output.contains("export type WorkspaceMetadataMutationResponse =")); - assert!(output.contains("export type ProfileSettingsResponse =")); - assert!(output.contains("config_revision?: number | null")); - assert!(output.contains("provenance: WorkspaceProfileSourceProvenance")); - assert!(output.contains( - "export type WorkspaceProfileSourceProvenance = \"project_profile_source_tree\"" - )); - assert!(output.contains("export type RuntimeConnectionTestResponse =")); - assert!(output.contains("status: RuntimeConnectionTestStatus")); - assert!(output.contains("failure_kind: RuntimeConnectionTestFailureKind | null")); - assert!(!output.contains("repository_key: string, display_name")); - } - - #[test] - fn worker_resource_key_is_required() { - let payload = serde_json::json!({ - "runtime_id": "arcadia", - "worker_id": "worker-1", - "host_id": "host", - "display_name": "Coder", - "label": "Coder", - "workspace": { - "visibility": "workspace", - "identity": "workspace-test" - }, - "state": "idle", - "implementation": {"kind": "worker", "display_hint": "Coder"}, - "capabilities": {"can_stop": true, "can_spawn_followup": false} - }); - - assert!(serde_json::from_value::(payload).is_err()); - } - - fn round_trip(value: T) - where - T: std::fmt::Debug + PartialEq + Serialize + for<'de> Deserialize<'de>, - { - let encoded = serde_json::to_vec(&value).expect("fixture should serialize"); - let decoded: T = serde_json::from_slice(&encoded).expect("fixture should deserialize"); - assert_eq!(decoded, value); - } - - #[test] - fn workspace_metadata_and_profile_projection_fixtures_round_trip() { - let diagnostic = Diagnostic { - code: "profile_projection_warning".to_string(), - severity: DiagnosticSeverity::Warning, - message: "projected from the active config revision".to_string(), - }; - let metadata = WorkspaceMetadataSettingsResponse { - workspace_id: "workspace-test".to_string(), - display_name: "Test".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - revision: "sha256:metadata".to_string(), - source: "workspace-config".to_string(), - diagnostics: vec![diagnostic.clone()], - }; - round_trip(metadata.clone()); - round_trip(UpdateWorkspaceMetadataRequest { - display_name: "Renamed".to_string(), - revision: metadata.revision.clone(), - }); - round_trip(WorkspaceMetadataMutationResponse { - workspace: metadata, - diagnostics: vec![], - }); - - round_trip(ProfileSettingsResponse { - workspace_id: "workspace-test".to_string(), - registry_revision: "config-source:7:sha256:tree:sha256:projection".to_string(), - config_revision: Some(7), - tree_digest: Some("sha256:tree".to_string()), - projection_digest: Some("sha256:projection".to_string()), - default_profile: Some("workspace:coder".to_string()), - profiles: vec![WorkspaceProfileSummary { - profile_id: "workspace:coder".to_string(), - selector: "workspace:coder".to_string(), - label: "Coder".to_string(), - source_kind: "project".to_string(), - profile_source_id: Some("profile-source-1".to_string()), - description: None, - editable: true, - is_default: true, - diagnostics: vec![diagnostic.clone()], - }], - sources: vec![WorkspaceProfileSourceSummary { - profile_source_id: "profile-source-1".to_string(), - display_path: "profiles/coder.dcdl".to_string(), - kind: "profile".to_string(), - content_type: "text/x-decodal".to_string(), - content_digest: "sha256:source".to_string(), - provenance: WorkspaceProfileSourceProvenance::ProjectProfileSourceTree, - editable: false, - revision: "config-source:7".to_string(), - size_bytes: 128, - diagnostics: vec![], - }], - diagnostics: vec![diagnostic], - }); - - let absent_optional_fields = serde_json::json!({ - "workspace_id": "workspace-test", - "registry_revision": "builtin", - "profiles": [], - "sources": [], - "diagnostics": [] - }); - let decoded: ProfileSettingsResponse = - serde_json::from_value(absent_optional_fields.clone()).unwrap(); - assert_eq!(decoded.config_revision, None); - assert_eq!(decoded.tree_digest, None); - assert_eq!(decoded.projection_digest, None); - assert_eq!(decoded.default_profile, None); - assert_eq!( - serde_json::to_value(decoded).unwrap(), - absent_optional_fields - ); - } - - #[test] - fn workspace_signing_identity_wire_contract_omits_private_and_pending_fields() { - let response = WorkspaceSigningIdentityResponse { - identity: WorkspaceSigningIdentityPublic { - workspace_id: "workspace-test".to_string(), - key_id: "WK-test".to_string(), - algorithm: "ed25519".to_string(), - public_key: None, - public_key_fingerprint: None, - revision: 1, - state: WorkspaceSigningIdentityState::PendingProvisioning, - created_at: "2026-01-01T00:00:00Z".to_string(), - provisioned_at: None, - }, - public_bundle: None, - }; - let encoded = serde_json::to_value(&response).unwrap(); - assert_eq!( - encoded, - serde_json::json!({ - "identity": { - "workspace_id": "workspace-test", - "key_id": "WK-test", - "algorithm": "ed25519", - "revision": 1, - "state": "pending_provisioning", - "created_at": "2026-01-01T00:00:00Z" - } - }) - ); - assert!( - serde_json::from_value::(serde_json::json!({ - "identity": encoded["identity"].clone(), - "private_material_ref": "must-not-cross-the-wire" - })) - .is_err() - ); - } - - fn companion_worker() -> WorkspaceWorkerDiscoveryItem { - WorkspaceWorkerDiscoveryItem { - subject: WorkspaceWorkerSubject::RuntimeWorker { - runtime_id: "arcadia".to_string(), - worker_id: "worker-7".to_string(), - }, - resource_key: "W-7".to_string(), - display_name: "Companion".to_string(), - profile: Some("builtin:companion".to_string()), - status: Some("idle".to_string()), - } - } - - #[test] - fn companion_status_fixtures_round_trip() { - for state in [ - CompanionLifecycleState::Idle, - CompanionLifecycleState::Running, - CompanionLifecycleState::Stopped, - ] { - round_trip(CompanionStatusResponse { - state, - worker: Some(companion_worker()), - transport: CompanionTransportSummary { - mode: "worker_runtime".to_string(), - available: state != CompanionLifecycleState::Stopped, - }, - diagnostics: Vec::new(), - }); - } - } - - #[test] - fn companion_message_fixtures_round_trip() { - for state in [ - CompanionMessageDisposition::Accepted, - CompanionMessageDisposition::Rejected, - ] { - round_trip(CompanionMessageResponse { - state, - message: if state == CompanionMessageDisposition::Accepted { - "accepted" - } else { - "rejected" - } - .to_string(), - }); - } - } - - #[test] - fn auth_server_fixtures_round_trip_through_shared_dtos() { - let whoami = serde_json::json!({ - "actor": { - "user_id": "user-1", - "account_id": "account-1", - "handle": "hare", - "display_name": "Hare", - "auth_method": "browser_session" - } - }); - let decoded = serde_json::from_value::(whoami.clone()) - .expect("server whoami fixture should match shared DTO"); - assert_eq!(serde_json::to_value(decoded).unwrap(), whoami); - - let auth_config = serde_json::json!({ - "rp_id": "yoi.example", - "origin": "https://yoi.example", - "public_base_url": "https://yoi.example", - "cookie_name": "yoi_workspace_session" - }); - let decoded = serde_json::from_value::(auth_config.clone()) - .expect("server auth-config fixture should match shared DTO"); - assert_eq!(serde_json::to_value(decoded).unwrap(), auth_config); - - let auth_user = serde_json::json!({ - "user": { - "user_id": "user-1", - "account_id": "account-1", - "handle": "hare", - "display_name": "Hare" - } - }); - let decoded = serde_json::from_value::(auth_user.clone()) - .expect("server auth-user fixture should match shared DTO"); - assert_eq!(serde_json::to_value(decoded).unwrap(), auth_user); - - let registration_options = serde_json::json!({ - "challenge_id": "challenge-1", - "public_key": { - "publicKey": { - "challenge": "AQID", - "rp": {"id": "localhost", "name": "Yoi"}, - "user": {"id": "BAUG", "name": "hare", "displayName": "Hare"}, - "pubKeyCredParams": [{"type": "public-key", "alg": -7}] - } - } - }); - let decoded = serde_json::from_value::( - registration_options.clone(), - ) - .expect("server registration options fixture should match shared DTO"); - assert_eq!(serde_json::to_value(decoded).unwrap(), registration_options); - - let registration_complete = serde_json::json!({ - "challenge_id": "challenge-1", - "credential": { - "id": "AQID", - "rawId": "AQID", - "response": { - "attestationObject": "AQID", - "clientDataJSON": "AQID", - "transports": ["internal"] - }, - "type": "public-key", - "clientExtensionResults": {}, - "authenticatorAttachment": "platform" - } - }); - let decoded = - serde_json::from_value::(registration_complete) - .expect("server registration-complete fixture should match shared DTO"); - let encoded = serde_json::to_value(decoded).unwrap(); - serde_json::from_value::(encoded) - .expect("registration-complete DTO should round-trip"); - - let login_options = serde_json::json!({ - "challenge_id": "challenge-2", - "public_key": { - "publicKey": { - "challenge": "AQID", - "rpId": "localhost", - "allowCredentials": [], - "userVerification": "preferred" - } - } - }); - let decoded = serde_json::from_value::(login_options.clone()) - .expect("server login-options fixture should match shared DTO"); - assert_eq!(serde_json::to_value(decoded).unwrap(), login_options); - - let login_complete = serde_json::json!({ - "challenge_id": "challenge-2", - "credential": { - "id": "AQID", - "rawId": "AQID", - "response": { - "authenticatorData": "AQID", - "clientDataJSON": "AQID", - "signature": "AQID", - "userHandle": null - }, - "type": "public-key", - "clientExtensionResults": {}, - "authenticatorAttachment": "platform" - } - }); - let decoded = serde_json::from_value::(login_complete) - .expect("server login-complete fixture should match shared DTO"); - let encoded = serde_json::to_value(decoded).unwrap(); - serde_json::from_value::(encoded) - .expect("login-complete DTO should round-trip"); - - let device_start = serde_json::json!({ - "device_code": "device-secret", - "user_code": "ABCD-EFGH", - "verification_uri": "https://yoi.example/login/device", - "verification_uri_complete": "https://yoi.example/login/device?user_code=ABCD-EFGH", - "expires_in": 600, - "interval": 2 - }); - let decoded = serde_json::from_value::(device_start.clone()) - .expect("server device-login fixture should match shared DTO"); - assert_eq!(serde_json::to_value(decoded).unwrap(), device_start); - - let approved_user = AuthenticatedUser { - user_id: "user-1".to_string(), - account_id: "account-1".to_string(), - handle: "hare".to_string(), - display_name: "Hare".to_string(), - }; - round_trip(DeviceLoginApproveResponse { - status: DeviceLoginApprovalStatus::Approved, - user: approved_user, - }); - for status in [ - DeviceLoginPollStatus::Pending, - DeviceLoginPollStatus::Expired, - DeviceLoginPollStatus::Denied, - DeviceLoginPollStatus::Consumed, - ] { - round_trip(DeviceLoginPollResponse { - status, - access_token: None, - token_type: None, - }); - } - round_trip(DeviceLoginPollResponse { - status: DeviceLoginPollStatus::Approved, - access_token: Some("access-secret".to_string()), - token_type: Some(DeviceAccessTokenType::Bearer), - }); - round_trip(LogoutResponse { - status: LogoutStatus::LoggedOut, - }); - } - - #[test] - fn auth_dtos_reject_unknown_status_and_fields() { - let unknown_status = serde_json::json!({"status": "future_status"}); - assert!( - serde_json::from_value::(unknown_status).is_err(), - "unknown device-login statuses must fail closed" - ); - - let unsafe_expiry = serde_json::json!({ - "device_code": "device-secret", - "user_code": "ABCD-EFGH", - "verification_uri": "https://yoi.example/login/device", - "verification_uri_complete": "https://yoi.example/login/device?user_code=ABCD-EFGH", - "expires_in": 0, - "interval": 2 - }); - assert!(serde_json::from_value::(unsafe_expiry).is_err()); - - let malformed_credential = serde_json::json!({ - "challenge_id": "challenge-1", - "credential": { - "id": "AQID", - "rawId": "AQID", - "type": "public-key", - "response": {"clientDataJSON": 42} - } - }); - assert!( - serde_json::from_value::(malformed_credential).is_err(), - "malformed passkey credential payloads must fail closed" - ); - - let unexpected_field = serde_json::json!({ - "actor": null, - "access_token": "must-not-be-accepted" - }); - assert!(serde_json::from_value::(unexpected_field).is_err()); - } - - #[test] - fn device_login_debug_output_redacts_secret_material() { - let start = DeviceLoginStartResponse { - device_code: "device-secret".to_string(), - user_code: "ABCD-EFGH".to_string(), - verification_uri: "https://yoi.example/login/device".to_string(), - verification_uri_complete: "https://yoi.example/login/device?user_code=ABCD-EFGH" - .to_string(), - expires_in: 600, - interval: 2, - }; - let start_debug = format!("{start:?}"); - assert!(!start_debug.contains("device-secret")); - assert!(start_debug.contains("[redacted]")); - - let poll = DeviceLoginPollResponse { - status: DeviceLoginPollStatus::Approved, - access_token: Some("access-secret".to_string()), - token_type: Some(DeviceAccessTokenType::Bearer), - }; - let poll_debug = format!("{poll:?}"); - assert!(!poll_debug.contains("access-secret")); - assert!(poll_debug.contains("[redacted]")); - } - - #[cfg(feature = "typescript")] - #[test] - fn generated_auth_api_contract_is_current() { - let expected = auth_api_typescript(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../web/workspace/src/lib/generated/auth-api.ts"); - let actual = std::fs::read_to_string(&path) - .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); - assert_eq!( - normalize_typescript(&actual), - normalize_typescript(&expected), - "regenerate auth API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts` and format the generated file", - ); - } - - #[test] - fn companion_transcript_fixture_round_trips() { - round_trip(CompanionTranscriptProjection { - state: CompanionLifecycleState::Idle, - start: 0, - limit: 2, - total: 2, - next: None, - items: vec![ - CompanionTranscriptItem { - sequence: 1, - role: CompanionTranscriptRole::User, - content: "hello".to_string(), - created_at: "2026-08-31T00:00:00Z".to_string(), - }, - CompanionTranscriptItem { - sequence: 2, - role: CompanionTranscriptRole::Assistant, - content: "hi".to_string(), - created_at: "2026-08-31T00:00:01Z".to_string(), - }, - ], - }); - } - - #[test] - fn companion_transcript_rejects_system_and_private_fields() { - let public_item = CompanionTranscriptItem { - sequence: 1, - role: CompanionTranscriptRole::Assistant, - content: "visible".to_string(), - created_at: "2026-08-31T00:00:00Z".to_string(), - }; - let public_fields = serde_json::to_value(public_item) - .expect("public transcript item should serialize") - .as_object() - .expect("public transcript item should be an object") - .keys() - .cloned() - .collect::>(); - assert_eq!( - public_fields, - ["content", "created_at", "role", "sequence"] - .into_iter() - .map(str::to_string) - .collect() - ); - - let system_item = serde_json::json!({ - "sequence": 1, - "role": "system", - "content": "raw system prompt", - "created_at": "2026-08-31T00:00:00Z" - }); - assert!(serde_json::from_value::(system_item).is_err()); - - let private_item = serde_json::json!({ - "sequence": 1, - "role": "assistant", - "content": "visible", - "created_at": "2026-08-31T00:00:00Z", - "reasoning": "hidden", - "credential": "secret", - "provider_session_id": "session-private" - }); - assert!(serde_json::from_value::(private_item).is_err()); - } - - #[cfg(feature = "typescript")] - #[test] - fn generated_companion_api_contract_is_current() { - let expected = companion_api_typescript(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../web/workspace/src/lib/generated/companion-api.ts"); - let actual = std::fs::read_to_string(&path) - .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); - assert_eq!( - normalize_typescript(&actual), - normalize_typescript(&expected), - "regenerate Companion API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_companion_api_types > web/workspace/src/lib/generated/companion-api.ts` and format the generated file", - ); - } - - #[cfg(feature = "typescript")] - fn normalize_typescript(value: &str) -> String { - value - .chars() - .filter_map(|character| match character { - character if character.is_whitespace() => None, - ',' => Some(';'), - character => Some(character), - }) - .collect::() - .replace(";}", "}") - .replace("=|", "=") - } - - #[test] - fn workdir_create_request_preserves_optional_operation_fields() { - let payload = serde_json::json!({"repository_key": "main"}); - let request = serde_json::from_value::(payload) - .expect("optional create fields may be absent"); - - assert_eq!(request.runtime_id, None); - assert_eq!(request.selector, None); - assert_eq!(request.operation_id, None); - - let serialized = serde_json::to_value(request).expect("serialize create request"); - assert_eq!(serialized, serde_json::json!({"repository_key": "main"})); - } - - #[test] - fn workdir_create_request_rejects_stale_or_incomplete_json() { - let stale = serde_json::json!({ - "repository_key": "main", - "selector": "develop", - "path": "/tmp/workdir" - }); - assert!(serde_json::from_value::(stale).is_err()); - - let incomplete = serde_json::json!({ - "runtime_id": "arcadia", - "operation_id": "operation-1" - }); - assert!(serde_json::from_value::(incomplete).is_err()); - } - - #[test] - fn workdir_summary_omits_absent_optional_fields_on_the_wire() { - let value = serde_json::to_value(WorkingDirectorySummary { - working_directory_id: "workdir-1".into(), - repository_key: "main".into(), - creation_selector: None, - creation_ref: None, - creation_tree: None, - current_selector: None, - current_ref: None, - current_tree: None, - observed_at_epoch_seconds: None, - materializer_kind: WorkingDirectoryMaterializerKind::RuntimeGitClone, - cleanup_target: None, - status: WorkingDirectoryStatusKind::Active, - cleanliness: None, - primary_worker_id: None, - occupied_by: None, - }) - .expect("serialize Workdir summary"); - let object = value.as_object().expect("Workdir summary object"); - - for key in [ - "creation_selector", - "creation_ref", - "creation_tree", - "current_selector", - "current_ref", - "current_tree", - "observed_at_epoch_seconds", - "cleanup_target", - "cleanliness", - "primary_worker_id", - "occupied_by", - ] { - assert!( - !object.contains_key(key), - "absent field {key} must be omitted" - ); - } - } - - #[test] - fn workdir_response_rejects_stale_occupancy_shape() { - let stale = serde_json::json!({ - "workspace_id": "workspace-test", - "items": [{ - "working_directory_id": "workdir-1", - "repository_key": "main", - "materializer_kind": "runtime_git_clone", - "status": "active", - "occupied_by": { - "runtime_worker_id": "worker-1", - "display_name": "Coder", - "linked_at": "2026-01-01T00:00:00Z" - } - }], - "diagnostics": [] - }); - - assert!(serde_json::from_value::(stale).is_err()); - } -} - -#[cfg(all(test, feature = "typescript"))] -mod typescript_tests { - #[test] - fn generated_repository_access_contract_is_current() { - let expected = super::repository_access_api_typescript(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../web/workspace/src/lib/generated/repository-access-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 Repository Access TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts` and format the generated file", - ); - } - - #[test] - fn generated_repository_access_responses_remain_secret_free() { - use ts_rs::TS; - - let config = ts_rs::Config::default(); - for declaration in [ - super::RepositorySshCredential::decl(&config), - super::RepositorySshHostTrust::decl(&config), - super::RepositoryAccessProjection::decl(&config), - ] { - for forbidden in ["private_key", "passphrase", "secret_ref"] { - assert!( - !declaration.contains(forbidden), - "Repository Access response declaration must not expose `{forbidden}`" - ); - } - } - } - - fn normalize(value: &str) -> String { - value - .chars() - .filter_map(|character| match character { - character if character.is_whitespace() => None, - ',' => Some(';'), - character => Some(character), - }) - .collect() - } -} diff --git a/crates/workspace-server/Cargo.toml b/crates/workspace-server/Cargo.toml index 4560ddd15..39b41e9c4 100644 --- a/crates/workspace-server/Cargo.toml +++ b/crates/workspace-server/Cargo.toml @@ -12,7 +12,7 @@ path = "src/main.rs" [features] default = ["typescript"] -typescript = ["dep:ts-rs", "workspace-api/typescript"] +typescript = ["dep:ts-rs", "server-api/typescript"] [dependencies] async-trait.workspace = true @@ -42,7 +42,6 @@ tokio = { workspace = true, features = ["fs", "macros", "net", "rt-multi-thread" tower.workspace = true tokio-tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } worker.workspace = true -workspace-api.workspace = true workdir = { workspace = true, features = ["http-client"] } worker-runtime.workspace = true toml.workspace = true diff --git a/crates/workspace-server/src/auth.rs b/crates/workspace-server/src/auth.rs index a135f5e01..aa202f2de 100644 --- a/crates/workspace-server/src/auth.rs +++ b/crates/workspace-server/src/auth.rs @@ -1,8 +1,8 @@ use axum::http::{HeaderMap, header}; use chrono::{DateTime, Duration, Utc}; +pub use server_api::{ActorAuthMethod, AuthPublicConfig, AuthenticatedUser, RequestActor}; use sha2::{Digest, Sha256}; use uuid::Uuid; -pub use workspace_api::{ActorAuthMethod, AuthPublicConfig, AuthenticatedUser, RequestActor}; use crate::{Error, Result, store::ControlPlaneStore}; diff --git a/crates/workspace-server/src/companion.rs b/crates/workspace-server/src/companion.rs index 08271a74b..f58859d2d 100644 --- a/crates/workspace-server/src/companion.rs +++ b/crates/workspace-server/src/companion.rs @@ -1,9 +1,9 @@ -use workspace_api::{ +use server_api::{ CompanionLifecycleState, CompanionMessageDisposition, CompanionTransportSummary, Diagnostic, DiagnosticSeverity, }; -pub use workspace_api::{ +pub use server_api::{ CompanionCancelRequest, CompanionMessageRequest, CompanionMessageResponse, CompanionStatusResponse, CompanionTranscriptProjection, }; diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index cd278145f..fd9c19172 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -275,12 +275,12 @@ pub struct WorkerSummary { pub diagnostics: Vec, } -impl From for workspace_api::Diagnostic { +impl From for server_api::Diagnostic { fn from(diagnostic: RuntimeDiagnostic) -> Self { let severity = match diagnostic.severity { - DiagnosticSeverity::Info => workspace_api::DiagnosticSeverity::Info, - DiagnosticSeverity::Warning => workspace_api::DiagnosticSeverity::Warning, - DiagnosticSeverity::Error => workspace_api::DiagnosticSeverity::Error, + DiagnosticSeverity::Info => server_api::DiagnosticSeverity::Info, + DiagnosticSeverity::Warning => server_api::DiagnosticSeverity::Warning, + DiagnosticSeverity::Error => server_api::DiagnosticSeverity::Error, }; Self { code: diagnostic.code, @@ -290,21 +290,21 @@ impl From for workspace_api::Diagnostic { } } -impl From for workspace_api::RuntimeSourceSummary { +impl From for server_api::RuntimeSourceSummary { fn from(source: RuntimeSourceSummary) -> Self { let kind = match source.kind { RuntimeSourceKind::EmbeddedWorkerRuntime => { - workspace_api::RuntimeSourceKind::EmbeddedWorkerRuntime + server_api::RuntimeSourceKind::EmbeddedWorkerRuntime } - RuntimeSourceKind::RemoteHttp => workspace_api::RuntimeSourceKind::RemoteHttp, + RuntimeSourceKind::RemoteHttp => server_api::RuntimeSourceKind::RemoteHttp, }; let status = match source.status { - RuntimeSourceStatus::Active => workspace_api::RuntimeSourceStatus::Active, - RuntimeSourceStatus::Reserved => workspace_api::RuntimeSourceStatus::Reserved, + RuntimeSourceStatus::Active => server_api::RuntimeSourceStatus::Active, + RuntimeSourceStatus::Reserved => server_api::RuntimeSourceStatus::Reserved, }; let identity_authority = match source.identity_authority { RuntimeIdentityAuthority::RuntimeRegistryProjection => { - workspace_api::RuntimeIdentityAuthority::RuntimeRegistryProjection + server_api::RuntimeIdentityAuthority::RuntimeRegistryProjection } }; Self { @@ -316,7 +316,7 @@ impl From for workspace_api::RuntimeSourceSummary { } } -impl From for workspace_api::RuntimeSummary { +impl From for server_api::RuntimeSummary { fn from(runtime: RuntimeSummary) -> Self { Self { runtime_id: runtime.runtime_id, @@ -336,9 +336,9 @@ impl From for workspace_api::RuntimeSummary { pub(crate) fn workspace_worker_summary( summary: WorkerSummary, resource_key: String, - working_directory: Option, -) -> workspace_api::WorkerSummary { - workspace_api::WorkerSummary { + working_directory: Option, +) -> server_api::WorkerSummary { + server_api::WorkerSummary { runtime_id: summary.worker.runtime_id, worker_id: summary.worker.worker_id, resource_key, @@ -348,7 +348,7 @@ pub(crate) fn workspace_worker_summary( profile: summary.profile, singleton_key: summary.singleton_key, tags: summary.tags, - workspace: workspace_api::WorkerWorkspaceSummary { + workspace: server_api::WorkerWorkspaceSummary { visibility: summary.workspace.visibility, identity: summary.workspace.identity, workspace_id: summary.workspace.workspace_id, @@ -358,11 +358,11 @@ pub(crate) fn workspace_worker_summary( last_seen_at: summary.last_seen_at, pinned: summary.pinned, retention_state: summary.retention_state, - implementation: workspace_api::WorkerImplementationSummary { + implementation: server_api::WorkerImplementationSummary { kind: summary.implementation.kind, display_hint: summary.implementation.display_hint, }, - capabilities: workspace_api::WorkerCapabilitySummary { + capabilities: server_api::WorkerCapabilitySummary { can_stop: summary.capabilities.can_stop, can_spawn_followup: summary.capabilities.can_spawn_followup, }, @@ -374,7 +374,7 @@ pub(crate) fn workspace_worker_summary( #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct WorkerRestoreResult { - pub state: workspace_api::WorkerRestoreState, + pub state: server_api::WorkerRestoreState, #[serde(skip_serializing_if = "Option::is_none")] pub worker: Option, pub diagnostics: Vec, @@ -613,7 +613,7 @@ pub enum WorkerOperationState { Rejected, } -impl From for workspace_api::WorkerOperationState { +impl From for server_api::WorkerOperationState { fn from(state: WorkerOperationState) -> Self { match state { WorkerOperationState::Accepted => Self::Accepted, @@ -877,7 +877,7 @@ pub trait WorkspaceWorkerRuntime: Send + Sync { fn restore_worker(&self, worker_id: &str) -> WorkerRestoreResult { WorkerRestoreResult { - state: workspace_api::WorkerRestoreState::Rejected, + state: server_api::WorkerRestoreState::Rejected, worker: None, diagnostics: vec![diagnostic( "worker_restore_unsupported", @@ -2365,7 +2365,7 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime { fn restore_worker(&self, worker_id: &str) -> WorkerRestoreResult { let Some(worker_ref) = self.worker_ref(worker_id) else { return WorkerRestoreResult { - state: workspace_api::WorkerRestoreState::Rejected, + state: server_api::WorkerRestoreState::Rejected, worker: None, diagnostics: vec![diagnostic( "embedded_worker_id_invalid", @@ -2389,7 +2389,7 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime { } } Err(err) => WorkerRestoreResult { - state: workspace_api::WorkerRestoreState::ReconciliationRequired, + state: server_api::WorkerRestoreState::ReconciliationRequired, worker: None, diagnostics: vec![embedded_runtime_diagnostic(&err)], }, @@ -4318,7 +4318,20 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { _ => Vec::new(), }; WorkerRestoreResult { - state, + state: match state { + runtime_api::WorkerRestoreState::Accepted => { + server_api::WorkerRestoreState::Accepted + } + runtime_api::WorkerRestoreState::Rejected => { + server_api::WorkerRestoreState::Rejected + } + runtime_api::WorkerRestoreState::RolledBack => { + server_api::WorkerRestoreState::RolledBack + } + runtime_api::WorkerRestoreState::ReconciliationRequired => { + server_api::WorkerRestoreState::ReconciliationRequired + } + }, worker: worker.map(|worker| self.map_worker_detail(worker)), diagnostics, } @@ -4326,7 +4339,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { Err(diagnostic) => WorkerRestoreResult { // A transport/protocol failure cannot prove that the Runtime // rejected before side effects. Preserve uncertainty. - state: workspace_api::WorkerRestoreState::ReconciliationRequired, + state: server_api::WorkerRestoreState::ReconciliationRequired, worker: None, diagnostics: vec![diagnostic], }, @@ -5769,7 +5782,7 @@ mod tests { assert_eq!( result.state, - workspace_api::WorkerRestoreState::ReconciliationRequired + server_api::WorkerRestoreState::ReconciliationRequired ); assert!(result.worker.is_none()); assert_eq!(result.diagnostics.len(), 1); @@ -6206,8 +6219,8 @@ mod tests { repository: worker_runtime::catalog::WorkingDirectoryRepository { id: "repository-1".to_string(), provider: "git".to_string(), - source: workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + source: server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: "/provider/repository.git".to_string(), }, source_revision: 7, diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index 03c3e4963..ba71ec3d9 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -48,11 +48,11 @@ pub use server::{ AuthConfig, ServerConfig, WorkspaceApi, WorkspaceServerApi, build_router, build_workspace_server_router, serve, serve_workspace_catalog, }; -pub use store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord}; -pub use workspace_api::{ +pub use server_api::{ GitCommitSummary, GitRemoteSummary, GitRepositorySummary, RepositorySummary, WorkspaceCreateResponse, }; +pub use store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord}; pub use workspace_catalog::{ InitialRepositoryIntent, WorkspaceCatalogService, WorkspaceCreateRequest, WorkspaceCreateResult, }; diff --git a/crates/workspace-server/src/memory_staging.rs b/crates/workspace-server/src/memory_staging.rs index 5ab7e01a7..ccc62c1db 100644 --- a/crates/workspace-server/src/memory_staging.rs +++ b/crates/workspace-server/src/memory_staging.rs @@ -1,6 +1,6 @@ use memory::extract::{CandidateKind, StagingRecord}; use memory::schema::{EvidenceOrigin, EvidenceOriginKind, SourceEvidenceRef, SourceRef}; -use workspace_api::{ +use server_api::{ Diagnostic, DiagnosticSeverity, MemoryCandidateKind, MemoryEvidenceOrigin, MemoryEvidenceOriginKind, MemorySourceEvidenceRef, MemorySourceRef, MemoryStagingEntry, MemoryStagingEvidence, MemoryStagingListResponse, MemoryStagingRecord, diff --git a/crates/workspace-server/src/profile_settings.rs b/crates/workspace-server/src/profile_settings.rs index 0e37f4a9f..0cc848b80 100644 --- a/crates/workspace-server/src/profile_settings.rs +++ b/crates/workspace-server/src/profile_settings.rs @@ -4,16 +4,16 @@ use std::path::{Component, Path, PathBuf}; use config_source::{ConfigContentType, ConfigSchemaContribution, VirtualPath}; use manifest::{ProfileSource, builtin_profile_catalog_snapshot, resolve_profile_artifact_value}; use serde::Deserialize; +use server_api::{ + ProfileSettingsResponse, WorkspaceMetadataSettingsResponse, WorkspaceProfileSourceProvenance, + WorkspaceProfileSourceSummary, WorkspaceProfileSummary, +}; use sha2::{Digest, Sha256}; use worker::EffectivePromptCatalog; use worker_runtime::config_bundle::{ ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigProfileDescriptor, }; use worker_runtime::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveInput}; -use workspace_api::{ - ProfileSettingsResponse, WorkspaceMetadataSettingsResponse, WorkspaceProfileSourceProvenance, - WorkspaceProfileSourceSummary, WorkspaceProfileSummary, -}; use crate::config_source::{ WorkspaceConfigSchemaProvider, WorkspaceConfigState, evaluate_workspace_config_state, diff --git a/crates/workspace-server/src/records.rs b/crates/workspace-server/src/records.rs index 2cde25d89..8071de4f7 100644 --- a/crates/workspace-server/src/records.rs +++ b/crates/workspace-server/src/records.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -pub use workspace_api::{ +pub use server_api::{ ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkedTicketSummary, ObjectiveResourceSummary, ObjectiveSummary, QueryPage, }; diff --git a/crates/workspace-server/src/repositories.rs b/crates/workspace-server/src/repositories.rs index 4a4096042..69840f65e 100644 --- a/crates/workspace-server/src/repositories.rs +++ b/crates/workspace-server/src/repositories.rs @@ -4,7 +4,7 @@ use std::{ process::Command, }; -use workspace_api::{ +use server_api::{ Diagnostic, DiagnosticSeverity, GitCommitSummary, GitRemoteSummary, GitRepositorySummary, RepositoryDiagnostic, RepositoryObservedStatus, RepositorySource, RepositorySummary, }; @@ -629,7 +629,7 @@ mod tests { let projection = reader.list(); assert_eq!( projection.items[0].source.kind, - workspace_api::RepositorySourceKind::Invalid + server_api::RepositorySourceKind::Invalid ); let diagnostics = projection.items[0].diagnostics.as_ref().unwrap(); assert!(diagnostics.iter().any(|diagnostic| { @@ -642,7 +642,7 @@ mod tests { #[test] fn remote_source_is_visible_but_local_provider_operations_fail_closed() { let source = RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + kind: server_api::RepositorySourceKind::Ssh, uri: "git@example.test:org/repository.git".to_string(), }; let reader = RepositoryRegistryReader::new(vec![ConfiguredRepository { @@ -660,10 +660,7 @@ mod tests { let projection = reader.list(); let summary = &projection.items[0]; - assert_eq!( - summary.source.kind, - workspace_api::RepositorySourceKind::Ssh - ); + assert_eq!(summary.source.kind, server_api::RepositorySourceKind::Ssh); assert_eq!( summary.observed_status, RepositoryObservedStatus::Unverified @@ -766,7 +763,7 @@ mod tests { ); let source_descriptor = RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + kind: server_api::RepositorySourceKind::LocalPath, uri: path.display().to_string(), }; let reader = RepositoryRegistryReader::new(vec![ConfiguredRepository { diff --git a/crates/workspace-server/src/repository_access.rs b/crates/workspace-server/src/repository_access.rs index 1efc9ec50..226db2532 100644 --- a/crates/workspace-server/src/repository_access.rs +++ b/crates/workspace-server/src/repository_access.rs @@ -11,16 +11,16 @@ use ring::hmac; use ring::rand::{SecureRandom, SystemRandom}; use rusqlite::{OptionalExtension, TransactionBehavior, params}; use serde::Deserialize; -use sha2::{Digest, Sha256}; -use ssh_key::private::Ed25519Keypair; -use ssh_key::{Algorithm, HashAlg, LineEnding, PrivateKey, PublicKey}; -use workspace_api::{ +use server_api::{ CreateRepositorySshCredentialRequest, DeleteRepositorySshCredentialRequest, DeleteRepositorySshHostTrustRequest, GenerateRepositorySshCredentialRequest, PutRepositorySshHostTrustRequest, RepositoryAccessMode, RepositoryAccessProjection, RepositorySshAccessBinding, RepositorySshCredential, RepositorySshHostTrust, RepositorySshPublicKey, RotateRepositorySshCredentialRequest, }; +use sha2::{Digest, Sha256}; +use ssh_key::private::Ed25519Keypair; +use ssh_key::{Algorithm, HashAlg, LineEnding, PrivateKey, PublicKey}; use crate::config_source::{ EvaluatedConfigCandidate, WorkspaceConfigSchemaProvider, WorkspaceConfigState, @@ -138,7 +138,7 @@ pub fn project_repository_access_state( fn validate_repository_access_source( repository_key: &str, - source: &workspace_api::RepositorySource, + source: &server_api::RepositorySource, ) -> Result<()> { if crate::repository_source::is_plain_http_repository_source(source) { return Err(Error::InvalidInput(format!( @@ -205,7 +205,7 @@ fn project_repository_access_evaluation( } let mut bindings = Vec::with_capacity(config.repository_access.len()); for (repository_key, access) in config.repository_access { - workspace_api::validate_repository_key(&repository_key) + server_api::validate_repository_key(&repository_key) .map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?; validate_identifier("credential_id", &access.ssh.credential)?; validate_identifier("host_trust_id", &access.ssh.host_trust)?; @@ -213,7 +213,7 @@ fn project_repository_access_evaluation( .get_repository_by_key(workspace_id, &repository_key)? .ok_or_else(|| Error::InvalidInput(format!("unknown Repository `{repository_key}`")))?; validate_repository_access_source(&repository_key, &repository.source)?; - if repository.source.kind != workspace_api::RepositorySourceKind::Ssh { + if repository.source.kind != server_api::RepositorySourceKind::Ssh { return Err(Error::InvalidInput(format!( "Repository `{repository_key}` is not an ssh:// Repository" ))); @@ -1940,8 +1940,8 @@ mod tests { DEFAULT_SCHEMA_VERSION, SnapshotEnvironment, ToolchainContract, VirtualPath, WorkspaceConfigSchemaBundle, }; + use server_api::{RepositoryObservedStatus, RepositorySource, RepositorySourceKind}; use ssh_key::private::Ed25519Keypair; - use workspace_api::{RepositoryObservedStatus, RepositorySource, RepositorySourceKind}; fn test_private_key(seed: u8) -> (String, String) { let key = PrivateKey::from(Ed25519Keypair::from_seed(&[seed; 32])); @@ -1995,8 +1995,8 @@ mod tests { assert!(error.to_string().contains("unsupported plain HTTP")); assert!(error.to_string().contains("HTTPS or SSH")); - let mismatched = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Https, + let mismatched = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Https, uri: "http://git.example.test/team/project.git".to_string(), }; let error = validate_repository_access_source("remote", &mismatched).unwrap_err(); diff --git a/crates/workspace-server/src/repository_source.rs b/crates/workspace-server/src/repository_source.rs index 7d3bf4074..50690c4ae 100644 --- a/crates/workspace-server/src/repository_source.rs +++ b/crates/workspace-server/src/repository_source.rs @@ -1,8 +1,8 @@ use std::path::Path; +use server_api::{RepositorySource, RepositorySourceKind}; use sha2::{Digest, Sha256}; use url::Url; -use workspace_api::{RepositorySource, RepositorySourceKind}; use crate::{Error, Result}; diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index f7624f4f3..3877d94d2 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -26,48 +26,7 @@ use memory::backend::{ use protocol::Segment; use protocol::stream::{decode_method, encode_event}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use ticket::{ - MarkdownText, NewTicketEvent, TicketBackend, TicketBodyReplacement, TicketEventKind, - TicketIdOrSlug, TicketItemEdit, TicketStateChange, TicketTargetEdit, TicketWorkflowState, -}; -use ticket::{ - SqliteTicketBackend, TicketBackendOperation, TicketBackendOperationResult, - execute_ticket_backend_operation, -}; -use tokio::net::TcpListener; -use tokio::sync::Mutex as AsyncMutex; -use tokio_tungstenite::connect_async; -use tokio_tungstenite::tungstenite::Message as TungsteniteMessage; -use tokio_tungstenite::tungstenite::client::IntoClientRequest; -use tower::ServiceExt; -use url::Url; -use uuid::Uuid; -use webauthn_rs::prelude::{ - Passkey, PasskeyAuthentication, PasskeyRegistration, Webauthn, WebauthnBuilder, -}; -use workdir::http::{ - WorkdirSessionOperation, WorkdirSessionOperationResult, WorkdirTransportError, -}; -use workdir::workspace::{ - MaterializerKind, WorkingDirectoryCleanupTarget, WorkingDirectoryOccupancy, - WorkingDirectoryStatusKind, WorkingDirectorySummary, WorkspaceWorkdirSessionOperationRequest, -}; -use workdir::{CommandHandle, WorkdirSessionHandle}; -use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjectRef}; -use worker_runtime::http_server::{ - RUNTIME_HTTP_PROTOCOL_MAX_VERSION, RUNTIME_HTTP_PROTOCOL_MIN_VERSION, - RUNTIME_HTTP_PROTOCOL_VERSION, -}; -use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest}; -use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend}; -use worker_runtime::workspace_issuer::{ - WORKSPACE_VERIFICATION_ACK_PATH, WORKSPACE_VERIFICATION_CHALLENGE_PATH, - WORKSPACE_VERIFICATION_OPERATION, WorkspaceCapabilityClaims, - WorkspaceRuntimeVerificationAcknowledgement, WorkspaceRuntimeVerificationChallenge, - verify_runtime_verification_response, workspace_request_body_digest, -}; -use workspace_api::{ +use server_api::{ ActorAuthMethod, AuthBootstrapUserRequest, AuthPublicConfig, AuthUserResponse, AuthenticatedUser, BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse, ConfirmRepositorySshHostTrustRequest, CreateRemoteRuntimeRequest, @@ -114,6 +73,47 @@ use workspace_api::{ WorkspaceSigningIdentityState, WorkspaceSummary, WorkspaceWorkerDiscoveryItem, WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject, }; +use sha2::{Digest, Sha256}; +use ticket::{ + MarkdownText, NewTicketEvent, TicketBackend, TicketBodyReplacement, TicketEventKind, + TicketIdOrSlug, TicketItemEdit, TicketStateChange, TicketTargetEdit, TicketWorkflowState, +}; +use ticket::{ + SqliteTicketBackend, TicketBackendOperation, TicketBackendOperationResult, + execute_ticket_backend_operation, +}; +use tokio::net::TcpListener; +use tokio::sync::Mutex as AsyncMutex; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::tungstenite::Message as TungsteniteMessage; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tower::ServiceExt; +use url::Url; +use uuid::Uuid; +use webauthn_rs::prelude::{ + Passkey, PasskeyAuthentication, PasskeyRegistration, Webauthn, WebauthnBuilder, +}; +use workdir::http::{ + WorkdirSessionOperation, WorkdirSessionOperationResult, WorkdirTransportError, +}; +use workdir::workspace::{ + MaterializerKind, WorkingDirectoryCleanupTarget, WorkingDirectoryOccupancy, + WorkingDirectoryStatusKind, WorkingDirectorySummary, WorkspaceWorkdirSessionOperationRequest, +}; +use workdir::{CommandHandle, WorkdirSessionHandle}; +use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjectRef}; +use worker_runtime::http_server::{ + RUNTIME_HTTP_PROTOCOL_MAX_VERSION, RUNTIME_HTTP_PROTOCOL_MIN_VERSION, + RUNTIME_HTTP_PROTOCOL_VERSION, +}; +use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest}; +use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend}; +use worker_runtime::workspace_issuer::{ + WORKSPACE_VERIFICATION_ACK_PATH, WORKSPACE_VERIFICATION_CHALLENGE_PATH, + WORKSPACE_VERIFICATION_OPERATION, WorkspaceCapabilityClaims, + WorkspaceRuntimeVerificationAcknowledgement, WorkspaceRuntimeVerificationChallenge, + verify_runtime_verification_response, workspace_request_body_digest, +}; use crate::auth::{ SessionCookiePolicy, auth_error, is_expired, mint_secret, new_id, new_user_code, @@ -203,7 +203,7 @@ use worker_runtime::identity::{RuntimeWorkerRef, WorkerId}; const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime"; -pub use workspace_api::WorkspaceAuthConfig as AuthConfig; +pub use server_api::WorkspaceAuthConfig as AuthConfig; #[derive(Clone)] pub struct ServerConfig { @@ -360,15 +360,15 @@ impl ServerConfig { } } -fn repository_local_path(source: &workspace_api::RepositorySource) -> Option { +fn repository_local_path(source: &server_api::RepositorySource) -> Option { match source.kind { - workspace_api::RepositorySourceKind::LocalPath => Some(PathBuf::from(&source.uri)), - workspace_api::RepositorySourceKind::File => url::Url::parse(&source.uri) + server_api::RepositorySourceKind::LocalPath => Some(PathBuf::from(&source.uri)), + server_api::RepositorySourceKind::File => url::Url::parse(&source.uri) .ok() .and_then(|uri| uri.to_file_path().ok()), - workspace_api::RepositorySourceKind::Ssh - | workspace_api::RepositorySourceKind::Https - | workspace_api::RepositorySourceKind::Invalid => None, + server_api::RepositorySourceKind::Ssh + | server_api::RepositorySourceKind::Https + | server_api::RepositorySourceKind::Invalid => None, } } @@ -2814,7 +2814,7 @@ impl WorkspaceApi { .map_err(|error| error.into_error())?; if binding.state != WorkerOperationState::Accepted { return Ok(WorkerRestoreResult { - state: workspace_api::WorkerRestoreState::Rejected, + state: server_api::WorkerRestoreState::Rejected, worker: binding.worker, diagnostics: binding.diagnostics, }); @@ -2858,7 +2858,7 @@ impl WorkspaceApi { &self, repository_key: &str, ) -> ApiResult { - workspace_api::validate_repository_key(repository_key).map_err(|error| { + server_api::validate_repository_key(repository_key).map_err(|error| { ApiError::from(Error::InvalidInput(format!( "invalid Repository key: {error}" ))) @@ -3067,7 +3067,7 @@ impl server_api::ServerApi for WorkspaceApi { ) })?; Ok(server_api::WorkspaceWorkerSessionResponse { - subject: workspace_api::WorkspaceWorkerSubject::RuntimeWorker { + subject: server_api::WorkspaceWorkerSubject::RuntimeWorker { runtime_id, worker_id, }, @@ -4291,9 +4291,9 @@ async fn scoped_update_workspace_settings( let workspace = crate::profile_settings::workspace_metadata_settings(&workspace); Ok(Json(WorkspaceMetadataMutationResponse { workspace, - diagnostics: vec![workspace_api::Diagnostic { + diagnostics: vec![server_api::Diagnostic { code: "workspace_metadata_updated".to_string(), - severity: workspace_api::DiagnosticSeverity::Info, + severity: server_api::DiagnosticSeverity::Info, message: "Workspace display metadata was updated.".to_string(), }], })) @@ -4453,13 +4453,13 @@ struct WorkspaceConfigTreeResponse { async fn scoped_get_workspace_memory_settings( State(api): State, AxumPath(workspace_id): AxumPath, -) -> ApiResult> { +) -> ApiResult> { validate_workspace_scope(&api, &workspace_id)?; let settings = api .config_store .get_workspace_memory_settings(&workspace_id) .map_err(ApiError::from)?; - Ok(Json(workspace_api::WorkspaceMemorySettings { + Ok(Json(server_api::WorkspaceMemorySettings { workspace_id: settings.workspace_id, settings_revision: settings.settings_revision, language: settings.language, @@ -4469,8 +4469,8 @@ async fn scoped_get_workspace_memory_settings( async fn scoped_update_workspace_memory_settings( State(api): State, AxumPath(workspace_id): AxumPath, - Json(request): Json, -) -> ApiResult> { + Json(request): Json, +) -> ApiResult> { validate_workspace_scope(&api, &workspace_id)?; let settings = api .config_store @@ -4480,7 +4480,7 @@ async fn scoped_update_workspace_memory_settings( &request.language, ) .map_err(ApiError::from)?; - Ok(Json(workspace_api::WorkspaceMemorySettings { + Ok(Json(server_api::WorkspaceMemorySettings { workspace_id: settings.workspace_id, settings_revision: settings.settings_revision, language: settings.language, @@ -5617,7 +5617,7 @@ async fn scoped_edit_ticket_item( validate_workspace_scope(&api, &path.workspace_id)?; let mut target = request.target; if let Some(TicketTargetEdit::Set { repository_id, .. }) = target.as_mut() { - workspace_api::validate_repository_key(repository_id).map_err(|_| { + server_api::validate_repository_key(repository_id).map_err(|_| { settings_bad_request( "repository_key_invalid", "Repository key must contain 1-64 lowercase ASCII letters, digits, or hyphens without a leading or trailing hyphen", @@ -5816,7 +5816,7 @@ fn resolve_ticket_operation_repository_keys( let Some(repository_key) = submitted_key else { return Ok(()); }; - workspace_api::validate_repository_key(repository_key).map_err(|error| { + server_api::validate_repository_key(repository_key).map_err(|error| { ApiError::from(Error::InvalidInput(format!( "invalid Repository key: {error}" ))) @@ -8213,7 +8213,7 @@ async fn open_current_worker_workdir_session_locked( .require_configured_workspace_repository(&workdir.repository_id) .map_err(|error| error.error)?; let repository_requires_access = - repository.source.kind == workspace_api::RepositorySourceKind::Ssh; + repository.source.kind == server_api::RepositorySourceKind::Ssh; if repository_requires_access { close_current_worker_attachment_session_locked(api, worker).await?; let access = repository_access_request_for_workdir( @@ -9343,7 +9343,7 @@ async fn scoped_create_repository( ) -> ApiResult<(StatusCode, Json)> { require_workspace_owner(&api, &path.workspace_id, &actor, "ManageRepositories").await?; - workspace_api::validate_repository_key(&request.repository_key) + server_api::validate_repository_key(&request.repository_key) .map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?; let repository_key = request.repository_key; let default_ref = request @@ -9364,7 +9364,7 @@ async fn scoped_create_repository( source, default_ref, source_revision: 1, - observed_status: workspace_api::RepositoryObservedStatus::Unverified, + observed_status: server_api::RepositoryObservedStatus::Unverified, observed_at: None, created_at: now.clone(), updated_at: now, @@ -9684,7 +9684,7 @@ async fn get_latest_workspace_runtime_config( async fn scoped_list_runtimes( State(api): State, AxumPath(path): AxumPath, -) -> ApiResult>> { +) -> ApiResult>> { validate_workspace_scope(&api, &path.workspace_id)?; Ok(Json( workspace_runtime_resources_response(&api, &api.config.workspace_id).await?, @@ -9876,7 +9876,7 @@ async fn scoped_worker_remove_source_boundary( async fn scoped_get_workspace_worker( State(api): State, AxumPath(path): AxumPath, -) -> ApiResult> { +) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; let worker_id = api .store @@ -9972,7 +9972,7 @@ async fn scoped_discover_workspace_workers( } fn workspace_worker_discovery_page( - workers: Vec, + workers: Vec, query: Option<&str>, offset: usize, limit: usize, @@ -10043,7 +10043,7 @@ fn workspace_worker_discovery_filter_fingerprint(query: Option<&str>) -> u64 { async fn scoped_list_workers( State(api): State, AxumPath(path): AxumPath, -) -> ApiResult>> { +) -> ApiResult>> { validate_workspace_scope(&api, &path.workspace_id)?; list_workers(State(api)).await } @@ -10309,7 +10309,7 @@ async fn restore_known_worker( State(api): State, AxumPath(path): AxumPath, headers: HeaderMap, -) -> ApiResult> { +) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; let source = authenticate_worker_mutation_source(&api, &path.workspace_id, &headers)?; let subject = path.worker.clone(); @@ -10461,7 +10461,7 @@ async fn scoped_start_workspace_orchestrator( .runtime .restore_worker(&existing.worker) .map_err(|error| error.into_error())?; - if restored.state != workspace_api::WorkerRestoreState::Accepted { + if restored.state != server_api::WorkerRestoreState::Accepted { return Err(ApiError::with_diagnostics( Error::RuntimeOperationFailed { runtime_id: existing.worker.runtime_id.clone(), @@ -10537,7 +10537,7 @@ fn worker_launch_worker_summary(worker: WorkerSummary) -> WorkerLaunchWorkerSumm profile: worker.profile, singleton_key: worker.singleton_key, tags: worker.tags, - workspace: workspace_api::WorkerWorkspaceSummary { + workspace: server_api::WorkerWorkspaceSummary { visibility: worker.workspace.visibility, identity: worker.workspace.identity, workspace_id: worker.workspace.workspace_id, @@ -10546,19 +10546,19 @@ fn worker_launch_worker_summary(worker: WorkerSummary) -> WorkerLaunchWorkerSumm last_seen_at: worker.last_seen_at, pinned: worker.pinned, retention_state: worker.retention_state, - implementation: workspace_api::WorkerImplementationSummary { + implementation: server_api::WorkerImplementationSummary { kind: worker.implementation.kind, display_hint: worker.implementation.display_hint, }, - capabilities: workspace_api::WorkerCapabilitySummary { + capabilities: server_api::WorkerCapabilitySummary { can_stop: worker.capabilities.can_stop, can_spawn_followup: worker.capabilities.can_spawn_followup, }, - working_directory: worker.working_directory, + working_directory: worker.working_directory.map(Into::into), diagnostics: worker .diagnostics .into_iter() - .map(workspace_api::Diagnostic::from) + .map(server_api::Diagnostic::from) .collect(), } } @@ -10578,7 +10578,7 @@ fn workspace_orchestrator_response( .diagnostics .iter() .cloned() - .map(workspace_api::Diagnostic::from) + .map(server_api::Diagnostic::from) .collect() }) .unwrap_or_default(); @@ -10615,15 +10615,15 @@ async fn scoped_get_worker_launch_options( fn working_directory_diagnostics( diagnostics: Vec, -) -> Vec { +) -> Vec { diagnostics .into_iter() - .map(|diagnostic| workspace_api::Diagnostic { + .map(|diagnostic| server_api::Diagnostic { code: diagnostic.code, severity: match diagnostic.severity { - DiagnosticSeverity::Info => workspace_api::DiagnosticSeverity::Info, - DiagnosticSeverity::Warning => workspace_api::DiagnosticSeverity::Warning, - DiagnosticSeverity::Error => workspace_api::DiagnosticSeverity::Error, + DiagnosticSeverity::Info => server_api::DiagnosticSeverity::Info, + DiagnosticSeverity::Warning => server_api::DiagnosticSeverity::Warning, + DiagnosticSeverity::Error => server_api::DiagnosticSeverity::Error, }, message: diagnostic.message, }) @@ -10652,6 +10652,7 @@ async fn scoped_list_runtime_working_directories( validate_workspace_scope(&api, &path.workspace_id)?; require_active_workspace_runtime_binding(&api, &path.runtime_id).await?; let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?; + let items = items.into_iter().map(Into::into).collect(); Ok(Json(BrowserWorkingDirectoryListResponse { workspace_id: api.config.workspace_id.clone(), items, @@ -10723,6 +10724,7 @@ async fn scoped_list_working_directories( ) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; let items = working_directory_summaries(&api)?; + let items = items.into_iter().map(Into::into).collect(); Ok(Json(BrowserWorkingDirectoryListResponse { workspace_id: api.config.workspace_id.clone(), items, @@ -10889,11 +10891,11 @@ async fn create_workspace_working_directory( existing.source_fingerprint.clone(), ) { let kind = match kind { - "local_path" => workspace_api::RepositorySourceKind::LocalPath, - "file" => workspace_api::RepositorySourceKind::File, - "https" => workspace_api::RepositorySourceKind::Https, - "http" | "invalid" => workspace_api::RepositorySourceKind::Invalid, - "ssh" => workspace_api::RepositorySourceKind::Ssh, + "local_path" => server_api::RepositorySourceKind::LocalPath, + "file" => server_api::RepositorySourceKind::File, + "https" => server_api::RepositorySourceKind::Https, + "http" | "invalid" => server_api::RepositorySourceKind::Invalid, + "ssh" => server_api::RepositorySourceKind::Ssh, _ => { return Err(settings_bad_request( "working_directory_repository_source_invalid", @@ -10902,7 +10904,7 @@ async fn create_workspace_working_directory( } }; working_directory_request.repository.source = - workspace_api::RepositorySource { kind, uri }; + server_api::RepositorySource { kind, uri }; working_directory_request.repository.source_revision = revision; working_directory_request.repository.source_fingerprint = fingerprint; } @@ -11208,7 +11210,7 @@ async fn create_workspace_working_directory( Json(BrowserWorkingDirectoryCreateResponse { workspace_id: workspace_id.to_string(), runtime_id: reserved.resolved_runtime_id, - item: summary, + item: summary.into(), diagnostics: working_directory_diagnostics(result.diagnostics), }), )) @@ -11249,14 +11251,14 @@ fn working_directory_detail_for_runtime( return Ok(Json(BrowserWorkingDirectoryDetailResponse { workspace_id: api.config.workspace_id.clone(), runtime_id: runtime_id.to_string(), - item: summary, + item: summary.into(), diagnostics: working_directory_diagnostics(result.diagnostics), })); } Ok(Json(BrowserWorkingDirectoryDetailResponse { workspace_id: api.config.workspace_id.clone(), runtime_id: runtime_id.to_string(), - item: projected_workdir_summary_from_record(&api, &existing)?, + item: projected_workdir_summary_from_record(&api, &existing)?.into(), diagnostics: working_directory_diagnostics(result.diagnostics), })) } @@ -12369,7 +12371,7 @@ async fn scoped_test_runtime_connection( } if binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity { validate_runtime_connection_request(&CreateRemoteRuntimeRequest { - public_bundle: workspace_api::RuntimePublicIdentityBundle { + public_bundle: server_api::RuntimePublicIdentityBundle { identity_id: binding.runtime_id.clone(), public_key: binding.public_key.clone(), }, @@ -12421,7 +12423,7 @@ async fn scoped_list_runtime_workers( State(api): State, AxumPath(path): AxumPath, Query(query): Query, -) -> ApiResult>> { +) -> ApiResult>> { validate_workspace_scope(&api, &path.workspace_id)?; list_runtime_workers(State(api), AxumPath(path.runtime_id), Query(query)).await } @@ -12480,7 +12482,7 @@ async fn scoped_restore_runtime_worker( State(api): State, AxumPath(path): AxumPath, Query(query): Query, -) -> ApiResult> { +) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; let workspace_id = path.workspace_id.clone(); let runtime_id = path.worker.runtime_id.clone(); @@ -12535,12 +12537,12 @@ async fn scoped_restore_runtime_worker( assign_ticket_worker_from_lifecycle(&api, assignment, &runtime_id, &worker_id)?; accept_queued_ticket_after_worker_spawn(&api, assignment)?; let worker = project_workspace_worker(&api, worker)?; - return Ok(Json(workspace_api::WorkerRestoreResponse { + return Ok(Json(server_api::WorkerRestoreResponse { workspace_id, runtime_id: runtime_id.clone(), worker_id: worker_id.clone(), - result: workspace_api::WorkerRestoreResult { - state: workspace_api::WorkerRestoreState::Accepted, + result: server_api::WorkerRestoreResult { + state: server_api::WorkerRestoreState::Accepted, worker: Some(worker), diagnostics: Vec::new(), }, @@ -12924,7 +12926,7 @@ async fn scoped_worker_protocol_ws( async fn scoped_list_host_workers( State(api): State, AxumPath(path): AxumPath, -) -> ApiResult>> { +) -> ApiResult>> { validate_workspace_scope(&api, &path.workspace_id)?; list_host_workers(State(api), AxumPath(path.host_id)).await } @@ -13645,9 +13647,9 @@ fn companion_console_extension_point( status: &CompanionStatusResponse, ) -> WorkspaceExtensionPointState { let extension_status = match status.state { - workspace_api::CompanionLifecycleState::Idle => "idle", - workspace_api::CompanionLifecycleState::Running => "running", - workspace_api::CompanionLifecycleState::Stopped => "stopped", + server_api::CompanionLifecycleState::Idle => "idle", + server_api::CompanionLifecycleState::Running => "running", + server_api::CompanionLifecycleState::Stopped => "stopped", } .to_string(); let diagnostic_codes = status @@ -13759,7 +13761,7 @@ async fn repository_detail( State(api): State, AxumPath(repository_key): AxumPath, ) -> ApiResult> { - workspace_api::validate_repository_key(&repository_key).map_err(|error| { + server_api::validate_repository_key(&repository_key).map_err(|error| { ApiError::from(Error::InvalidInput(format!( "invalid Repository key: {error}" ))) @@ -13780,7 +13782,7 @@ async fn repository_log( AxumPath(repository_key): AxumPath, Query(query): Query, ) -> ApiResult> { - workspace_api::validate_repository_key(&repository_key).map_err(|error| { + server_api::validate_repository_key(&repository_key).map_err(|error| { ApiError::from(Error::InvalidInput(format!( "invalid Repository key: {error}" ))) @@ -13821,10 +13823,10 @@ async fn list_hosts( async fn list_runtimes( State(api): State, -) -> ApiResult>> { +) -> ApiResult>> { let limit = api.config.max_records.min(200); let runtimes = api.runtime.list_runtimes(limit); - Ok(Json(workspace_api::ListResponse { + Ok(Json(server_api::ListResponse { workspace_id: api.config.workspace_id, limit, items: runtimes.items.into_iter().map(Into::into).collect(), @@ -13835,7 +13837,7 @@ async fn list_runtimes( async fn list_workers( State(api): State, -) -> ApiResult>> { +) -> ApiResult>> { workers_response(api).map(Json) } @@ -15030,7 +15032,7 @@ fn browser_worker_response_from_summary( worker, diagnostics: diagnostics .into_iter() - .map(workspace_api::Diagnostic::from) + .map(server_api::Diagnostic::from) .collect(), }) } @@ -15177,7 +15179,7 @@ async fn post_companion_cancel( #[derive(Debug, Serialize)] struct WorkerShowProjection { #[serde(flatten)] - worker: workspace_api::WorkerSummary, + worker: server_api::WorkerSummary, updated_at: String, } @@ -15231,7 +15233,7 @@ async fn get_runtime_worker( async fn restore_runtime_worker( State(api): State, AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>, -) -> ApiResult> { +) -> ApiResult> { let worker = resolve_workspace_worker_reference(&api, &runtime_id, &worker_id)?; let result = api.restore_workspace_worker(&worker)?; let projected_worker = if let Some(worker) = result.worker.as_ref() { @@ -15247,11 +15249,11 @@ async fn restore_runtime_worker( } else { None }; - Ok(Json(workspace_api::WorkerRestoreResponse { + Ok(Json(server_api::WorkerRestoreResponse { workspace_id: api.workspace_id().to_string(), runtime_id: runtime_id.clone(), worker_id: worker_id.clone(), - result: workspace_api::WorkerRestoreResult { + result: server_api::WorkerRestoreResult { state: result.state, worker: projected_worker, diagnostics: result.diagnostics.into_iter().map(Into::into).collect(), @@ -15311,7 +15313,7 @@ async fn list_runtime_workers( State(api): State, AxumPath(runtime_id): AxumPath, Query(query): Query, -) -> ApiResult>> { +) -> ApiResult>> { let limit = api.config.max_records.min(200); let (runtime_workers, source) = match query.status { Some(RuntimeWorkersStatusFilter::Stopped) => ( @@ -15328,7 +15330,7 @@ async fn list_runtime_workers( ), }; let items = project_observed_workspace_workers(&api, runtime_workers.items)?; - Ok(Json(workspace_api::ListResponse { + Ok(Json(server_api::ListResponse { workspace_id: api.workspace_id().to_string(), limit, items, @@ -16505,14 +16507,14 @@ fn protocol_error_event(message: impl Into) -> protocol::Event { async fn list_host_workers( State(api): State, AxumPath(host_id): AxumPath, -) -> ApiResult>> { +) -> ApiResult>> { let limit = api.config.max_records.min(200); let runtime_workers = api .runtime .list_workers_for_host(&host_id, limit) .map_err(|err| err.into_error())?; let items = project_observed_workspace_workers(&api, runtime_workers.items)?; - Ok(Json(workspace_api::ListResponse { + Ok(Json(server_api::ListResponse { workspace_id: api.workspace_id().to_string(), limit, items, @@ -16528,7 +16530,7 @@ async fn list_host_workers( fn project_workspace_worker( api: &WorkspaceApi, summary: WorkerSummary, -) -> ApiResult { +) -> ApiResult { let resource_key = api .store .resource_key( @@ -16554,14 +16556,14 @@ fn project_workspace_worker( Ok(workspace_worker_summary( summary, resource_key, - working_directory, + working_directory.map(Into::into), )) } fn project_observed_workspace_workers( api: &WorkspaceApi, workers: Vec, -) -> ApiResult> { +) -> ApiResult> { let workdirs = api .store .list_workdir_registry(&api.config.workspace_id, 500)?; @@ -16581,7 +16583,7 @@ fn project_observed_workspace_workers( fn workers_response( api: WorkspaceApi, -) -> ApiResult> { +) -> ApiResult> { let limit = api.config.max_records.min(200); let runtime_workers = api.runtime.list_workers(limit); let mut observed = std::collections::BTreeMap::new(); @@ -16628,7 +16630,7 @@ fn workers_response( ); items.push(project_workspace_worker(&api, summary)?); } - Ok(workspace_api::ListResponse { + Ok(server_api::ListResponse { workspace_id: api.config.workspace_id, limit, items, @@ -16640,7 +16642,7 @@ fn workers_response( async fn workspace_runtime_resources_response( api: &WorkspaceApi, workspace_id: &str, -) -> ApiResult> { +) -> ApiResult> { let limit = api.config.max_records.min(200); let runtimes = api.runtime.list_runtimes(limit); let bindings = api @@ -16665,7 +16667,7 @@ async fn workspace_runtime_resources_response( .iter() .find(|binding| binding.runtime_id == runtime.runtime_id); let built_in = runtime.runtime_id == EMBEDDED_WORKER_RUNTIME_ID; - let mut runtime: workspace_api::RuntimeSummary = runtime.into(); + let mut runtime: server_api::RuntimeSummary = runtime.into(); if binding.is_some_and(|binding| binding.state != StoredRuntimeBindingState::Verified) { runtime.worker_creation_available = false; } @@ -16694,16 +16696,16 @@ async fn workspace_runtime_resources_response( continue; } items.push(WorkspaceRuntimeResource { - runtime: workspace_api::RuntimeSummary { + runtime: server_api::RuntimeSummary { runtime_id: binding.runtime_id.clone(), label: binding.display_name.clone(), kind: "remote_http".to_string(), status: "unavailable".to_string(), - source: workspace_api::RuntimeSourceSummary { - kind: workspace_api::RuntimeSourceKind::RemoteHttp, - status: workspace_api::RuntimeSourceStatus::Reserved, + source: server_api::RuntimeSourceSummary { + kind: server_api::RuntimeSourceKind::RemoteHttp, + status: server_api::RuntimeSourceStatus::Reserved, identity_authority: - workspace_api::RuntimeIdentityAuthority::ServerRuntimeConfiguration, + server_api::RuntimeIdentityAuthority::ServerRuntimeConfiguration, note: "The registered Runtime is not present in the active Runtime registry." .to_string(), }, @@ -16734,7 +16736,7 @@ async fn workspace_runtime_resources_response( }); } - Ok(workspace_api::ListResponse { + Ok(server_api::ListResponse { workspace_id: workspace_id.to_string(), limit, items, @@ -16788,15 +16790,15 @@ fn runtime_binding_summary( fn runtime_verification_summary( verification: &crate::store::WorkspaceRuntimeVerificationEvidence, -) -> Option { +) -> Option { let last_outcome = match verification.last_outcome.as_str() { - "verified" => workspace_api::RuntimeVerificationOutcome::Verified, - "challenge_issued" => workspace_api::RuntimeVerificationOutcome::ChallengeIssued, - "verification_failed" => workspace_api::RuntimeVerificationOutcome::VerificationFailed, - "connectivity_failed" => workspace_api::RuntimeVerificationOutcome::ConnectivityFailed, + "verified" => server_api::RuntimeVerificationOutcome::Verified, + "challenge_issued" => server_api::RuntimeVerificationOutcome::ChallengeIssued, + "verification_failed" => server_api::RuntimeVerificationOutcome::VerificationFailed, + "connectivity_failed" => server_api::RuntimeVerificationOutcome::ConnectivityFailed, _ => return None, }; - Some(workspace_api::RuntimeVerificationEvidenceSummary { + Some(server_api::RuntimeVerificationEvidenceSummary { verified_at: verification.verified_at.clone(), last_checked_at: verification.checked_at.clone(), last_outcome, @@ -16825,16 +16827,16 @@ async fn workspace_runtime_detail( .find(|resource| resource.runtime.runtime_id == runtime_id); if resource.is_none() { resource = binding.as_ref().map(|binding| WorkspaceRuntimeResource { - runtime: workspace_api::RuntimeSummary { + runtime: server_api::RuntimeSummary { runtime_id: binding.runtime_id.clone(), label: binding.display_name.clone(), kind: "remote_http".to_string(), status: "unavailable".to_string(), - source: workspace_api::RuntimeSourceSummary { - kind: workspace_api::RuntimeSourceKind::RemoteHttp, - status: workspace_api::RuntimeSourceStatus::Reserved, + source: server_api::RuntimeSourceSummary { + kind: server_api::RuntimeSourceKind::RemoteHttp, + status: server_api::RuntimeSourceStatus::Reserved, identity_authority: - workspace_api::RuntimeIdentityAuthority::ServerRuntimeConfiguration, + server_api::RuntimeIdentityAuthority::ServerRuntimeConfiguration, note: "The Runtime trust binding is not active in the Runtime registry." .to_string(), }, @@ -17186,7 +17188,7 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> ApiResult ApiResult ApiResult ApiResult<()> { - let context = if request.repository.source.kind == workspace_api::RepositorySourceKind::Ssh { + let context = if request.repository.source.kind == server_api::RepositorySourceKind::Ssh { if let ( Some(_credential_id), Some(_credential_revision), @@ -17944,8 +17950,8 @@ fn authorize_repository_materialization_operation( let primary_host_trust_revision = primary_lease.host_trust_revision; let known_hosts_entry = primary_lease.known_hosts_entry.clone(); let access = match access_mode { - "read_only" => workspace_api::RepositoryAccessMode::ReadOnly, - "read_write" => workspace_api::RepositoryAccessMode::ReadWrite, + "read_only" => server_api::RepositoryAccessMode::ReadOnly, + "read_write" => server_api::RepositoryAccessMode::ReadWrite, _ => { return Err(settings_bad_request( "working_directory_repository_access_invalid", @@ -18077,8 +18083,8 @@ fn authorize_repository_materialization_operation( &ssh.host_trust_id, ssh.host_trust_revision, match ssh.access { - workspace_api::RepositoryAccessMode::ReadOnly => "read_only", - workspace_api::RepositoryAccessMode::ReadWrite => "read_write", + server_api::RepositoryAccessMode::ReadOnly => "read_only", + server_api::RepositoryAccessMode::ReadWrite => "read_write", }, &credential_candidates, &now_registry_timestamp(), @@ -18179,8 +18185,8 @@ fn repository_ssh_lease_candidates_from_operation( .repository_access_mode .as_deref() .map(|access_mode| match access_mode { - "read_only" => Ok(workspace_api::RepositoryAccessMode::ReadOnly), - "read_write" => Ok(workspace_api::RepositoryAccessMode::ReadWrite), + "read_only" => Ok(server_api::RepositoryAccessMode::ReadOnly), + "read_write" => Ok(server_api::RepositoryAccessMode::ReadWrite), _other => Err(settings_bad_request( "working_directory_repository_access_snapshot_invalid", "persisted Repository access mode is invalid", @@ -18237,7 +18243,7 @@ fn authorize_repository_materialization( .find(|repository| repository.id == request.repository.id) .map(|repository| repository.repository_key.as_str()) .ok_or_else(|| Error::UnknownRepository(request.repository.id.clone()))?; - let ssh = if request.repository.source.kind == workspace_api::RepositorySourceKind::Ssh { + let ssh = if request.repository.source.kind == server_api::RepositorySourceKind::Ssh { let binding = match projection .bindings .iter() @@ -18367,7 +18373,7 @@ fn repository_access_request_for_workdir( )); } let repository = api.require_configured_workspace_repository(&record.repository_id)?; - if repository.source.kind != workspace_api::RepositorySourceKind::Ssh { + if repository.source.kind != server_api::RepositorySourceKind::Ssh { return Ok(None); } let projection = active_repository_access_projection(api, &api.config.workspace_id)?; @@ -19176,7 +19182,8 @@ mod tests { current_ref: Some("abc123".to_string()), current_tree: None, observed_at_epoch_seconds: Some(1_767_225_600), - materializer_kind: workspace_api::WorkingDirectoryMaterializerKind::RuntimeGitClone, + materializer_kind: + workdir::workspace::WorkingDirectoryMaterializerKind::RuntimeGitClone, cleanup_target: None, status: worker_runtime::catalog::WorkingDirectoryStatusKind::Active, cleanliness: Some("clean".to_string()), @@ -20054,11 +20061,11 @@ mod tests { "owner-account", ) .unwrap(); - let binding = workspace_api::RepositorySshAccessBinding { + let binding = server_api::RepositorySshAccessBinding { repository_key: "test-repository".to_string(), credential_id: specific.credential_id.clone(), host_trust_id: host_trust.host_trust_id, - access: workspace_api::RepositoryAccessMode::ReadOnly, + access: server_api::RepositoryAccessMode::ReadOnly, }; let primary = api .repository_secrets @@ -20082,8 +20089,8 @@ mod tests { candidates[1].known_hosts_entry ); - let source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Ssh, + let source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, uri: "ssh://git@example.test/org/repository.git".to_string(), }; let source_fingerprint = repository_source_fingerprint(&source); @@ -20165,7 +20172,7 @@ mod tests { .is_err() ); - let default_binding = workspace_api::RepositorySshAccessBinding { + let default_binding = server_api::RepositorySshAccessBinding { credential_id: crate::repository_access::WORKSPACE_DEFAULT_REPOSITORY_SSH_CREDENTIAL_ID .to_string(), ..binding @@ -20198,14 +20205,14 @@ mod tests { repository_key: "foreign".to_string(), kind: "git".to_string(), provider: Some("git".to_string()), - source: workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + source: server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: dir.path().join("foreign").display().to_string(), }, default_ref: Some("HEAD".to_string()), source_revision: 1, source_fingerprint: "sha256:test".to_string(), - observed_status: workspace_api::RepositoryObservedStatus::Unverified, + observed_status: server_api::RepositoryObservedStatus::Unverified, observed_at: None, created_at: "1".to_string(), updated_at: "1".to_string(), @@ -20321,7 +20328,7 @@ mod tests { ], host_trust_id: "host-trust-1".to_string(), host_trust_revision: 1, - access: workspace_api::RepositoryAccessMode::ReadOnly, + access: server_api::RepositoryAccessMode::ReadOnly, expires_at_epoch_seconds: u64::MAX, repository_id: working_directory.repository.id.clone(), repository_source_fingerprint: working_directory @@ -21519,7 +21526,7 @@ mod tests { State(api), Extension(actor), Json(CreateRemoteRuntimeRequest { - public_bundle: workspace_api::RuntimePublicIdentityBundle { + public_bundle: server_api::RuntimePublicIdentityBundle { identity_id: "verified-runtime".to_string(), public_key: replacement.public_key, }, @@ -21556,7 +21563,7 @@ mod tests { ); let runtime_identity = RuntimeIdentityMaterial::generate("configured-runtime").unwrap(); let request = CreateRemoteRuntimeRequest { - public_bundle: workspace_api::RuntimePublicIdentityBundle { + public_bundle: server_api::RuntimePublicIdentityBundle { identity_id: "configured-runtime".to_string(), public_key: runtime_identity.public_key, }, @@ -21659,7 +21666,7 @@ mod tests { State(api.clone()), Extension(test_owner_actor()), Json(CreateRemoteRuntimeRequest { - public_bundle: workspace_api::RuntimePublicIdentityBundle { + public_bundle: server_api::RuntimePublicIdentityBundle { identity_id: "unknown-runtime".to_string(), public_key: unknown_identity.public_key, }, @@ -21695,7 +21702,7 @@ mod tests { async fn runtime_connection_request_validation_bounds_browser_input() { let identity = RuntimeIdentityMaterial::generate("team-runtime_1").unwrap(); let ok = CreateRemoteRuntimeRequest { - public_bundle: workspace_api::RuntimePublicIdentityBundle { + public_bundle: server_api::RuntimePublicIdentityBundle { identity_id: "team-runtime_1".to_string(), public_key: identity.public_key, }, @@ -22034,8 +22041,8 @@ mod tests { created_at: "1".to_string(), updated_at: "1".to_string(), }; - let source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Https, + let source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Https, uri: "https://example.test/org/repository.git".to_string(), }; let repositories = vec![RepositoryRecord { @@ -22048,7 +22055,7 @@ mod tests { source, default_ref: Some("main".to_string()), source_revision: 1, - observed_status: workspace_api::RepositoryObservedStatus::Unverified, + observed_status: server_api::RepositoryObservedStatus::Unverified, observed_at: None, created_at: "1".to_string(), updated_at: "1".to_string(), @@ -22060,7 +22067,7 @@ mod tests { assert!(scoped.repositories[0].path.is_none()); assert_eq!( scoped.repositories[0].source.kind, - workspace_api::RepositorySourceKind::Https + server_api::RepositorySourceKind::Https ); assert_eq!( scoped.workspace_execution_root, @@ -22085,8 +22092,8 @@ mod tests { created_at: "1".to_string(), updated_at: "1".to_string(), }; - let source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::Https, + let source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Https, uri: "https://example.test/org/repository.git".to_string(), }; let repositories = vec![RepositoryRecord { @@ -22099,7 +22106,7 @@ mod tests { source, default_ref: Some("main".to_string()), source_revision: 1, - observed_status: workspace_api::RepositoryObservedStatus::Unverified, + observed_status: server_api::RepositoryObservedStatus::Unverified, observed_at: None, created_at: "1".to_string(), updated_at: "1".to_string(), @@ -22122,8 +22129,8 @@ mod tests { .with_embedded_runtime_store_root(store_root); config.database_path = workspace_root.join(".test-yoi-server.db"); config.backend_base_url = Some("http://127.0.0.1:8787".to_string()); - let source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + let source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: workspace_root.display().to_string(), }; config.repositories = vec![ConfiguredRepository { @@ -22133,7 +22140,7 @@ mod tests { source_fingerprint: crate::repository_source::repository_source_fingerprint(&source), source, source_revision: 1, - observed_status: workspace_api::RepositoryObservedStatus::Unverified, + observed_status: server_api::RepositoryObservedStatus::Unverified, observed_at: None, path: Some(workspace_root), default_selector: Some("HEAD".to_string()), @@ -22286,8 +22293,8 @@ mod tests { .unwrap(); assert_eq!(persisted.source, config.repositories[0].source); - let changed_source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + let changed_source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: dir.path().join("different-source").display().to_string(), }; config.repositories[0].source_fingerprint = @@ -22536,7 +22543,7 @@ mod tests { let workspace_body = to_bytes(authenticated_legacy.into_body(), usize::MAX) .await .unwrap(); - let typed_workspace: workspace_api::WorkspaceResponse = + let typed_workspace: server_api::WorkspaceResponse = serde_json::from_slice(&workspace_body).unwrap(); assert!(typed_workspace.permissions.manage_repositories); @@ -22566,7 +22573,7 @@ mod tests { let catalog_body = to_bytes(authenticated_catalog.into_body(), usize::MAX) .await .unwrap(); - let typed_catalog: workspace_api::WorkspaceCatalogListResponse = + let typed_catalog: server_api::WorkspaceCatalogListResponse = serde_json::from_slice(&catalog_body).unwrap(); assert_eq!( typed_catalog.0[0].workspace_id, @@ -22602,7 +22609,7 @@ mod tests { let created_body = to_bytes(created_response.into_body(), usize::MAX) .await .unwrap(); - let created: workspace_api::WorkspaceCreateResponse = + let created: server_api::WorkspaceCreateResponse = serde_json::from_slice(&created_body).unwrap(); assert_eq!(created.workspace.owner_account_id, "account-auth"); @@ -25314,7 +25321,7 @@ mod tests { assert_eq!(retried_restore.worker_id, first_worker.worker.worker_id); assert_eq!( retried_restore.result.state, - workspace_api::WorkerRestoreState::Accepted + server_api::WorkerRestoreState::Accepted ); let restored_assignment = api .store @@ -26230,7 +26237,7 @@ mod tests { scoped_update_workspace_memory_settings( State(api), AxumPath("workspace-foreign".to_string()), - Json(workspace_api::UpdateWorkspaceMemorySettingsRequest { + Json(server_api::UpdateWorkspaceMemorySettingsRequest { expected_revision: 1, language: "English".to_string(), }), @@ -27039,14 +27046,14 @@ mod tests { repository_key: repository_id.to_string(), kind: "git".to_string(), provider: Some("git".to_string()), - source: workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + source: server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: api.config.workspace_execution_root.display().to_string(), }, default_ref: Some("HEAD".to_string()), source_revision: 1, source_fingerprint: "sha256:test".to_string(), - observed_status: workspace_api::RepositoryObservedStatus::Unverified, + observed_status: server_api::RepositoryObservedStatus::Unverified, observed_at: None, created_at: "1".to_string(), updated_at: "1".to_string(), @@ -28412,8 +28419,8 @@ mod tests { worker_id: &str, name: &str, visibility: &str, - ) -> workspace_api::WorkerSummary { - workspace_api::WorkerSummary { + ) -> server_api::WorkerSummary { + server_api::WorkerSummary { runtime_id: "runtime-test".to_string(), worker_id: worker_id.to_string(), resource_key: key.to_string(), @@ -28423,7 +28430,7 @@ mod tests { profile: Some("builtin:coder".to_string()), singleton_key: None, tags: Vec::new(), - workspace: workspace_api::WorkerWorkspaceSummary { + workspace: server_api::WorkerWorkspaceSummary { visibility: visibility.to_string(), identity: "workspace-test".to_string(), workspace_id: Some(TEST_WORKSPACE_ID.to_string()), @@ -28433,11 +28440,11 @@ mod tests { last_seen_at: None, pinned: false, retention_state: "normal".to_string(), - implementation: workspace_api::WorkerImplementationSummary { + implementation: server_api::WorkerImplementationSummary { kind: "remote".to_string(), display_hint: "remote".to_string(), }, - capabilities: workspace_api::WorkerCapabilitySummary { + capabilities: server_api::WorkerCapabilitySummary { can_stop: true, can_spawn_followup: false, }, @@ -28510,7 +28517,7 @@ mod tests { assert!(before.is_empty()); let discovered = workspace_worker_discovery_page( - vec![workspace_api::WorkerSummary { + vec![server_api::WorkerSummary { runtime_id: target.runtime_id.clone(), worker_id: target.worker_id.clone(), resource_key: "W-2".to_string(), @@ -28520,7 +28527,7 @@ mod tests { profile: Some("builtin:coder".to_string()), singleton_key: None, tags: Vec::new(), - workspace: workspace_api::WorkerWorkspaceSummary { + workspace: server_api::WorkerWorkspaceSummary { visibility: "workspace_scoped".to_string(), identity: "workspace-test".to_string(), workspace_id: Some(TEST_WORKSPACE_ID.to_string()), @@ -28530,11 +28537,11 @@ mod tests { last_seen_at: None, pinned: false, retention_state: "normal".to_string(), - implementation: workspace_api::WorkerImplementationSummary { + implementation: server_api::WorkerImplementationSummary { kind: "remote".to_string(), display_hint: "remote".to_string(), }, - capabilities: workspace_api::WorkerCapabilitySummary { + capabilities: server_api::WorkerCapabilitySummary { can_stop: true, can_spawn_followup: false, }, @@ -29966,7 +29973,7 @@ mod tests { let runtime_workers = get_json(app.clone(), "/api/runtimes/embedded-worker-runtime/workers").await; let runtime_workers = serde_json::from_value::< - workspace_api::ListResponse, + server_api::ListResponse, >(runtime_workers) .expect("Runtime-scoped Worker list must use the shared Workspace API contract"); assert!( @@ -30288,7 +30295,7 @@ mod tests { let workspace = get_json(app.clone(), "/api/workspace").await; assert_eq!(workspace["workspace_id"], TEST_WORKSPACE_ID); assert_eq!(workspace["display_name"], "Test Workspace"); - let typed_workspace: workspace_api::WorkspaceResponse = + let typed_workspace: server_api::WorkspaceResponse = serde_json::from_value(workspace.clone()).unwrap(); assert!(!typed_workspace.permissions.manage_repositories); assert_eq!(workspace["record_authority"], "server_db"); @@ -30449,7 +30456,7 @@ mod tests { let memory_document = get_json(app.clone(), &format!("/api/w/{TEST_WORKSPACE_ID}/memory")).await; - let _: workspace_api::MemoryDocumentResponse = + let _: server_api::MemoryDocumentResponse = serde_json::from_value(memory_document.clone()).unwrap(); assert_eq!(memory_document["created_at"], "2026-01-01T00:00:00Z"); assert_eq!(memory_document["updated_at"], "2026-01-02T00:00:00Z"); @@ -30467,7 +30474,7 @@ mod tests { &format!("/api/w/{TEST_WORKSPACE_ID}/memory/staging?limit=10"), ) .await; - let _: workspace_api::MemoryStagingListResponse = + let _: server_api::MemoryStagingListResponse = serde_json::from_value(memory_staging.clone()).unwrap(); assert_eq!( memory_staging["record_authority"], @@ -30481,7 +30488,7 @@ mod tests { ); let repositories = get_json(app.clone(), "/api/repositories").await; - let typed_repositories: workspace_api::RepositoryListResponse = + let typed_repositories: server_api::RepositoryListResponse = serde_json::from_value(repositories.clone()).unwrap(); assert_eq!( typed_repositories.items[0].repository_key, @@ -30500,7 +30507,7 @@ mod tests { ); let repository_detail = get_json(app.clone(), "/api/repositories/test-repository").await; - let _: workspace_api::RepositoryDetailResponse = + let _: server_api::RepositoryDetailResponse = serde_json::from_value(repository_detail.clone()).unwrap(); assert_eq!( repository_detail["item"]["repository_key"], @@ -30520,7 +30527,7 @@ mod tests { let repository_log = get_json(app.clone(), "/api/repositories/test-repository/log?limit=3").await; - let _: workspace_api::RepositoryLogResponse = + let _: server_api::RepositoryLogResponse = serde_json::from_value(repository_log.clone()).unwrap(); assert_eq!(repository_log["repository_key"], "test-repository"); assert!(repository_log.get("repository_id").is_none()); @@ -30968,13 +30975,13 @@ mod tests { id: "files".to_string(), repository_key: "files".to_string(), provider: "local_fs".to_string(), - source: workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + source: server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: root.path().display().to_string(), }, source_revision: 1, source_fingerprint: "sha256:test".to_string(), - observed_status: workspace_api::RepositoryObservedStatus::Unverified, + observed_status: server_api::RepositoryObservedStatus::Unverified, observed_at: None, path: Some(root.path().to_path_buf()), default_selector: None, @@ -31929,28 +31936,31 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3); fn workspace_workdir_response_serializes_shared_occupied_contract() { let response = BrowserWorkingDirectoryListResponse { workspace_id: TEST_WORKSPACE_ID.to_string(), - items: vec![WorkingDirectorySummary { - working_directory_id: "wd-1".to_string(), - repository_key: "main".to_string(), - creation_selector: None, - creation_ref: None, - creation_tree: None, - current_selector: Some("work/ticket".to_string()), - current_ref: Some("abc123".to_string()), - current_tree: Some("tree123".to_string()), - observed_at_epoch_seconds: Some(1_777_777_777), - materializer_kind: MaterializerKind::RuntimeGitClone, - cleanup_target: None, - status: WorkingDirectoryStatusKind::Active, - cleanliness: Some("clean".to_string()), - primary_worker_id: None, - occupied_by: Some(WorkingDirectoryOccupancy { - runtime_id: "arcadia".to_string(), - worker_id: "worker-opaque-64".to_string(), - display_name: "Coder".to_string(), - linked_at: "2026-08-12T00:00:00Z".to_string(), - }), - }], + items: vec![ + WorkingDirectorySummary { + working_directory_id: "wd-1".to_string(), + repository_key: "main".to_string(), + creation_selector: None, + creation_ref: None, + creation_tree: None, + current_selector: Some("work/ticket".to_string()), + current_ref: Some("abc123".to_string()), + current_tree: Some("tree123".to_string()), + observed_at_epoch_seconds: Some(1_777_777_777), + materializer_kind: MaterializerKind::RuntimeGitClone, + cleanup_target: None, + status: WorkingDirectoryStatusKind::Active, + cleanliness: Some("clean".to_string()), + primary_worker_id: None, + occupied_by: Some(WorkingDirectoryOccupancy { + runtime_id: "arcadia".to_string(), + worker_id: "worker-opaque-64".to_string(), + display_name: "Coder".to_string(), + linked_at: "2026-08-12T00:00:00Z".to_string(), + }), + } + .into(), + ], diagnostics: vec![], }; diff --git a/crates/workspace-server/src/skills.rs b/crates/workspace-server/src/skills.rs index 917031084..fd3262816 100644 --- a/crates/workspace-server/src/skills.rs +++ b/crates/workspace-server/src/skills.rs @@ -4,12 +4,12 @@ use config_source::{ ConfigSchemaContribution, MarkdownDocumentProjection, VirtualPath, project_markdown_document, }; use serde::Deserialize; -use worker::skill::SkillActivationResponse; -use workspace_api::{ +use server_api::{ SKILL_CATALOG_AUTHORITY, SkillActivationStatus, SkillCatalogEntry, SkillCatalogResponse, SkillDetailResponse, SkillDiagnostic, SkillDiagnosticSeverity, SkillProjectionIdentity, SkillProjectionStatus, SkillProvenance, SkillResourceRef, SkillSourceKind, }; +use worker::skill::SkillActivationResponse; use crate::config_source::{ WorkspaceConfigSchemaProvider, WorkspaceConfigState, evaluate_workspace_config_state, diff --git a/crates/workspace-server/src/store.rs b/crates/workspace-server/src/store.rs index b574ce0ae..9ab5ffdfc 100644 --- a/crates/workspace-server/src/store.rs +++ b/crates/workspace-server/src/store.rs @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use uuid::Uuid; +use server_api::{RepositoryObservedStatus, RepositorySource}; use worker_runtime::identity::{RuntimeWorkerRef, WorkerId}; -use workspace_api::{RepositoryObservedStatus, RepositorySource}; use crate::workspace_deletion::WorkspaceDeletionStore; use crate::{Error, Result}; @@ -7496,15 +7496,15 @@ fn repository_registration_intent_matches( } fn validate_repository_record_identity(record: &RepositoryRecord) -> Result<()> { - workspace_api::validate_repository_key(&record.repository_key) + server_api::validate_repository_key(&record.repository_key) .map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?; Ok(()) } fn read_repository_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { let source_kind_value = row.get::<_, String>(5)?; - let source_kind = workspace_api::RepositorySourceKind::parse(&source_kind_value) - .unwrap_or(workspace_api::RepositorySourceKind::Invalid); + let source_kind = server_api::RepositorySourceKind::parse(&source_kind_value) + .unwrap_or(server_api::RepositorySourceKind::Invalid); let source_revision = row.get::<_, u64>(8)?; let source_fingerprint = row.get::<_, String>(9)?; let mut source = RepositorySource { @@ -7517,7 +7517,7 @@ fn read_repository_record(row: &rusqlite::Row<'_>) -> rusqlite::Result String { mod tests { use super::*; use crate::store::{AccountRecord, ControlPlaneStore, RepositoryRecord, WorkspaceRecord}; - use workspace_api::{RepositoryObservedStatus, RepositorySource, RepositorySourceKind}; + use server_api::{RepositoryObservedStatus, RepositorySource, RepositorySourceKind}; fn attempt_owner() -> WorkdirRemovalAttemptOwner { WorkdirRemovalAttemptOwner { diff --git a/crates/workspace-server/src/workspace_catalog.rs b/crates/workspace-server/src/workspace_catalog.rs index b62a8b42e..3dee38110 100644 --- a/crates/workspace-server/src/workspace_catalog.rs +++ b/crates/workspace-server/src/workspace_catalog.rs @@ -4,8 +4,8 @@ use chrono::{SecondsFormat, Utc}; use sha2::{Digest, Sha256}; use uuid::Uuid; -pub use workspace_api::{InitialRepositoryIntent, WorkspaceCreateRequest}; -use workspace_api::{RepositoryObservedStatus, RepositorySource}; +pub use server_api::{InitialRepositoryIntent, WorkspaceCreateRequest}; +use server_api::{RepositoryObservedStatus, RepositorySource}; use crate::repository_source::{parse_repository_source, repository_source_fingerprint}; use crate::store::{ @@ -96,7 +96,7 @@ impl WorkspaceCatalogService { normalize_required("display_name", request.display_name, MAX_DISPLAY_NAME_BYTES)?; let repository_source = validate_repository_source(&request.repository.uri)?; let repository_uri = repository_source.uri.clone(); - workspace_api::validate_repository_key(&request.repository.repository_key) + server_api::validate_repository_key(&request.repository.repository_key) .map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?; let repository_key = request.repository.repository_key.clone(); let default_ref = request @@ -228,7 +228,7 @@ mod tests { InMemoryWorkspaceSigningMaterialStore, WorkspaceSigningMaterialStore, WorkspaceSigningPrivateMaterial, identity_error, }; - use workspace_api::RepositorySourceKind; + use server_api::RepositorySourceKind; struct FailFirstMaterialWrite { inner: Arc, diff --git a/crates/workspace-server/src/workspace_deletion.rs b/crates/workspace-server/src/workspace_deletion.rs index a74f2c518..a17f829be 100644 --- a/crates/workspace-server/src/workspace_deletion.rs +++ b/crates/workspace-server/src/workspace_deletion.rs @@ -1,7 +1,6 @@ use chrono::Utc; use rusqlite::{OptionalExtension, params}; -use sha2::{Digest, Sha256}; -use workspace_api::{ +use server_api::{ WORKSPACE_DELETION_MAX_BLOCKER_MESSAGE_BYTES, WORKSPACE_DELETION_MAX_BLOCKERS, WORKSPACE_DELETION_MAX_CHILD_OPERATION_IDS, WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES, WORKSPACE_DELETION_MAX_RESOURCE_VALUE_BYTES, WORKSPACE_DELETION_MAX_REVISION_BYTES, @@ -9,6 +8,7 @@ use workspace_api::{ WorkspaceDeletionPreflightResponse, WorkspaceDeletionRequest, WorkspaceDeletionResourceCounts, WorkspaceDeletionState, }; +use sha2::{Digest, Sha256}; use crate::store::{SqliteWorkspaceStore, WorkspaceRecord}; use crate::{Error, Result}; @@ -200,7 +200,7 @@ impl WorkspaceDeletionStore for SqliteWorkspaceStore { ) -> Result { validate_operation_id(&request.operation_id)?; if request.expected_revision.len() > WORKSPACE_DELETION_MAX_REVISION_BYTES - || request.confirmation.len() > workspace_api::WORKSPACE_DELETION_MAX_CONFIRMATION_BYTES + || request.confirmation.len() > server_api::WORKSPACE_DELETION_MAX_CONFIRMATION_BYTES { return Err(Error::InvalidInput( "Workspace deletion request exceeds bounded field limits".to_string(), diff --git a/crates/yoi/Cargo.toml b/crates/yoi/Cargo.toml index 2cd9653e5..c959c4e70 100644 --- a/crates/yoi/Cargo.toml +++ b/crates/yoi/Cargo.toml @@ -15,7 +15,7 @@ client = { workspace = true } memory = { workspace = true } manifest = { workspace = true } worker = { workspace = true } -workspace-api = { workspace = true } +server-api = { workspace = true } session-store = { workspace = true } session-analytics = { workspace = true } ticket = { workspace = true } diff --git a/crates/yoi/src/objective_cli.rs b/crates/yoi/src/objective_cli.rs index b0fa3bb39..817c104b1 100644 --- a/crates/yoi/src/objective_cli.rs +++ b/crates/yoi/src/objective_cli.rs @@ -148,7 +148,7 @@ fn run_with_backend( return Err(ObjectiveCliError::new("create --title must not be empty")); } let objective = backend - .create_objective(&workspace_api::ObjectiveCreateRequest { + .create_objective(&server_api::ObjectiveCreateRequest { title: title.to_string(), body_md: objective_body_template(), state: "active".to_string(), diff --git a/crates/yoi/src/workspace_bootstrap.rs b/crates/yoi/src/workspace_bootstrap.rs index 898494a9e..6daf87c85 100644 --- a/crates/yoi/src/workspace_bootstrap.rs +++ b/crates/yoi/src/workspace_bootstrap.rs @@ -21,7 +21,7 @@ pub(crate) struct InitOptions { pub(crate) async fn run_init( options: InitOptions, -) -> Result { +) -> Result { let repository_uri = options .repository_root .to_str() @@ -82,8 +82,8 @@ pub(crate) fn discover_repository_root(path: &Path) -> Result, + server_api::WorkspaceSummary, + Vec, )], ) -> Result { let matches = catalog @@ -122,22 +122,21 @@ struct OpenRepositoryIdentity { } impl OpenRepositoryIdentity { - fn matches(&self, source: &workspace_api::RepositorySource) -> bool { + fn matches(&self, source: &server_api::RepositorySource) -> bool { match source.kind { - workspace_api::RepositorySourceKind::LocalPath => fs::canonicalize(&source.uri) + server_api::RepositorySourceKind::LocalPath => fs::canonicalize(&source.uri) .ok() .is_some_and(|path| path == self.root), - workspace_api::RepositorySourceKind::File => source + server_api::RepositorySourceKind::File => source .uri .strip_prefix("file://") .and_then(|path| fs::canonicalize(path).ok()) .is_some_and(|path| path == self.root), - workspace_api::RepositorySourceKind::Ssh - | workspace_api::RepositorySourceKind::Https => self + server_api::RepositorySourceKind::Ssh | server_api::RepositorySourceKind::Https => self .remote_uris .iter() .any(|uri| normalize_git_uri(uri) == normalize_git_uri(&source.uri)), - workspace_api::RepositorySourceKind::Invalid => false, + server_api::RepositorySourceKind::Invalid => false, } } } @@ -306,8 +305,8 @@ mod tests { use super::*; use crate::{ClientDefaultConnection, read_client_config_from_global_path}; - fn workspace_summary(id: &str, name: &str) -> workspace_api::WorkspaceSummary { - workspace_api::WorkspaceSummary { + fn workspace_summary(id: &str, name: &str) -> server_api::WorkspaceSummary { + server_api::WorkspaceSummary { workspace_id: id.to_string(), owner_account_id: "owner-account".to_string(), display_name: name.to_string(), @@ -317,17 +316,15 @@ mod tests { } } - fn repository_summary( - source: workspace_api::RepositorySource, - ) -> workspace_api::RepositorySummary { - workspace_api::RepositorySummary { + fn repository_summary(source: server_api::RepositorySource) -> server_api::RepositorySummary { + server_api::RepositorySummary { repository_key: "main".to_string(), kind: "git".to_string(), provider: "builtin:git".to_string(), source, source_revision: 1, source_fingerprint: "fingerprint".to_string(), - observed_status: workspace_api::RepositoryObservedStatus::Unverified, + observed_status: server_api::RepositoryObservedStatus::Unverified, observed_at: None, default_selector: Some("develop".to_string()), record_authority: "server_db".to_string(), @@ -345,8 +342,8 @@ mod tests { .status() .unwrap(); let identity = discover_open_repository(repository.path()).unwrap(); - let source = workspace_api::RepositorySource { - kind: workspace_api::RepositorySourceKind::LocalPath, + let source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::LocalPath, uri: identity.root.display().to_string(), }; let matching_repository = repository_summary(source); diff --git a/web/workspace/src/lib/generated/README.md b/web/workspace/src/lib/generated/README.md new file mode 100644 index 000000000..912e9f5f8 --- /dev/null +++ b/web/workspace/src/lib/generated/README.md @@ -0,0 +1,19 @@ +# Generated TypeScript inventory + +`server-api` is the Rust source authority for the checked-in Server HTTP DTO projections in this directory. +Regenerate them from the repository root with: + +```sh +cargo run -q -p server-api --features typescript --example generate_legacy_typescript > web/workspace/src/lib/generated/legacy-server-api.ts +cargo run -q -p server-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts +cargo run -q -p server-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts +cargo run -q -p server-api --features typescript --example generate_companion_api_types > web/workspace/src/lib/generated/companion-api.ts +cargo run -q -p server-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts +cargo run -q -p server-api --features typescript --example generate_skill_api_types > web/workspace/src/lib/generated/skill-api.ts +cargo run -q -p server-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts +cargo run -q -p server-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts +``` + +`legacy-server-api.ts` is the bounded aggregate projection retained during the OpenAPI migration. It is not the final OpenAPI-derived Frontend artifact and must not become a second contract authority. The other files are narrower projections generated from the same Rust source. + +`ticket-api.ts` and `protocol.ts` are owned by the separate `ticket` and `protocol` authorities respectively. diff --git a/web/workspace/src/lib/generated/auth-api.ts b/web/workspace/src/lib/generated/auth-api.ts index 9fa3291ff..2233ccf1f 100644 --- a/web/workspace/src/lib/generated/auth-api.ts +++ b/web/workspace/src/lib/generated/auth-api.ts @@ -1,107 +1,50 @@ -// Generated from workspace-api. Do not edit by hand. -// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts +// Generated from server-api. Do not edit by hand. +// Regenerate: cargo run -q -p server-api --features typescript --example generate_auth_api_types > web/workspace/src/lib/generated/auth-api.ts -export type AuthPublicConfig = { - rp_id: string; - origin: string; - public_base_url: string; - cookie_name: string; -}; +export type AuthPublicConfig = { rp_id: string, origin: string, public_base_url: string, cookie_name: string, }; export type ActorAuthMethod = "browser_session" | "api_token"; -export type AuthenticatedUser = { - user_id: string; - account_id: string; - handle: string; - display_name: string; -}; +export type AuthenticatedUser = { user_id: string, account_id: string, handle: string, display_name: string, }; -export type RequestActor = { - user_id: string; - account_id: string; - handle: string; - display_name: string; - auth_method: ActorAuthMethod; -}; +export type RequestActor = { user_id: string, account_id: string, handle: string, display_name: string, auth_method: ActorAuthMethod, }; -export type WhoamiResponse = { actor: RequestActor | null }; +export type WhoamiResponse = { actor: RequestActor | null, }; -export type AuthBootstrapUserRequest = { - handle: string; - display_name?: string | null; -}; +export type AuthBootstrapUserRequest = { handle: string, display_name?: string | null, }; -export type AuthUserResponse = { user: AuthenticatedUser }; +export type AuthUserResponse = { user: AuthenticatedUser, }; -export type PasskeyRegistrationOptionsRequest = { - handle: string; - display_name?: string | null; - browser_origin?: string | null; -}; +export type PasskeyRegistrationOptionsRequest = { handle: string, display_name?: string | null, browser_origin?: string | null, }; -export type PasskeyRegistrationOptionsResponse = { - challenge_id: string; - public_key: unknown; -}; +export type PasskeyRegistrationOptionsResponse = { challenge_id: string, public_key: unknown, }; -export type PasskeyRegistrationCompleteRequest = { - challenge_id: string; - credential: unknown; -}; +export type PasskeyRegistrationCompleteRequest = { challenge_id: string, credential: unknown, }; -export type PasskeyLoginOptionsRequest = { - handle?: string | null; - browser_origin?: string | null; -}; +export type PasskeyLoginOptionsRequest = { handle?: string | null, browser_origin?: string | null, }; -export type PasskeyLoginOptionsResponse = { - challenge_id: string; - public_key: unknown; -}; +export type PasskeyLoginOptionsResponse = { challenge_id: string, public_key: unknown, }; -export type PasskeyLoginCompleteRequest = { - challenge_id: string; - credential: unknown; -}; +export type PasskeyLoginCompleteRequest = { challenge_id: string, credential: unknown, }; -export type DeviceLoginStartRequest = { client_name?: string | null }; +export type DeviceLoginStartRequest = { client_name?: string | null, }; -export type DeviceLoginStartResponse = { - device_code: string; - user_code: string; - verification_uri: string; - verification_uri_complete: string; - expires_in: number; - interval: number; -}; +export type DeviceLoginStartResponse = { device_code: string, user_code: string, verification_uri: string, verification_uri_complete: string, expires_in: number, interval: number, }; -export type DeviceLoginApproveRequest = { user_code: string }; +export type DeviceLoginApproveRequest = { user_code: string, }; export type DeviceLoginApprovalStatus = "approved"; -export type DeviceLoginApproveResponse = { - status: DeviceLoginApprovalStatus; - user: AuthenticatedUser; -}; +export type DeviceLoginApproveResponse = { status: DeviceLoginApprovalStatus, user: AuthenticatedUser, }; -export type DeviceLoginPollRequest = { device_code: string }; +export type DeviceLoginPollRequest = { device_code: string, }; export type DeviceAccessTokenType = "Bearer"; -export type DeviceLoginPollStatus = - | "pending" - | "approved" - | "expired" - | "denied" - | "consumed"; +export type DeviceLoginPollStatus = "pending" | "approved" | "expired" | "denied" | "consumed"; -export type DeviceLoginPollResponse = { - status: DeviceLoginPollStatus; - access_token?: string | null; - token_type?: DeviceAccessTokenType | null; -}; +export type DeviceLoginPollResponse = { status: DeviceLoginPollStatus, access_token?: string | null, token_type?: DeviceAccessTokenType | null, }; export type LogoutStatus = "logged_out"; -export type LogoutResponse = { status: LogoutStatus }; +export type LogoutResponse = { status: LogoutStatus, }; diff --git a/web/workspace/src/lib/generated/companion-api.ts b/web/workspace/src/lib/generated/companion-api.ts index ad25a30ed..76cc6231c 100644 --- a/web/workspace/src/lib/generated/companion-api.ts +++ b/web/workspace/src/lib/generated/companion-api.ts @@ -1,64 +1,30 @@ -// Generated by `cargo run -p workspace-api --features typescript --example generate_companion_api_types`. +// Generated by `cargo run -p server-api --features typescript --example generate_companion_api_types`. // Do not edit manually. export type DiagnosticSeverity = "info" | "warning" | "error"; -export type Diagnostic = { - code: string; - severity: DiagnosticSeverity; - message: string; -}; +export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; -export type WorkspaceWorkerSubject = { - "kind": "runtime_worker"; - runtime_id: string; - worker_id: string; -}; +export type WorkspaceWorkerSubject = { "kind": "runtime_worker", runtime_id: string, worker_id: string, }; -export type WorkspaceWorkerDiscoveryItem = { - subject: WorkspaceWorkerSubject; - resource_key: string; - display_name: string; - profile: string | null; - status?: string | null; -}; +export type WorkspaceWorkerDiscoveryItem = { subject: WorkspaceWorkerSubject, resource_key: string, display_name: string, profile: string | null, status?: string | null, }; export type CompanionLifecycleState = "idle" | "running" | "stopped"; export type CompanionMessageDisposition = "accepted" | "rejected"; -export type CompanionTransportSummary = { mode: string; available: boolean }; +export type CompanionTransportSummary = { mode: string, available: boolean, }; -export type CompanionStatusResponse = { - state: CompanionLifecycleState; - worker: WorkspaceWorkerDiscoveryItem | null; - transport: CompanionTransportSummary; - diagnostics: Array; -}; +export type CompanionStatusResponse = { state: CompanionLifecycleState, worker: WorkspaceWorkerDiscoveryItem | null, transport: CompanionTransportSummary, diagnostics: Array, }; -export type CompanionMessageRequest = { content: string }; +export type CompanionMessageRequest = { content: string, }; -export type CompanionCancelRequest = { reason?: string | null }; +export type CompanionCancelRequest = { reason?: string | null, }; -export type CompanionMessageResponse = { - state: CompanionMessageDisposition; - message: string; -}; +export type CompanionMessageResponse = { state: CompanionMessageDisposition, message: string, }; export type CompanionTranscriptRole = "user" | "assistant"; -export type CompanionTranscriptItem = { - sequence: number; - role: CompanionTranscriptRole; - content: string; - created_at: string; -}; +export type CompanionTranscriptItem = { sequence: number, role: CompanionTranscriptRole, content: string, created_at: string, }; -export type CompanionTranscriptProjection = { - state: CompanionLifecycleState; - start: number; - limit: number; - total: number; - next: number | null; - items: Array; -}; +export type CompanionTranscriptProjection = { state: CompanionLifecycleState, start: number, limit: number, total: number, next: number | null, items: Array, }; diff --git a/web/workspace/src/lib/generated/legacy-server-api.ts b/web/workspace/src/lib/generated/legacy-server-api.ts new file mode 100644 index 000000000..2648edfd8 --- /dev/null +++ b/web/workspace/src/lib/generated/legacy-server-api.ts @@ -0,0 +1,161 @@ +// This file is generated by `cargo run -p server-api --features typescript --example generate_legacy_typescript | deno fmt -`. +// Do not edit this file directly. + +export type WorkspaceSummary = { workspace_id: string, owner_account_id: string, display_name: string, state: string, created_at: string, updated_at: string, }; + +export type WorkspaceCatalogListResponse = Array; + +export type WorkspaceRepositoryRecord = { workspace_id: string, repository_key: string, kind: string, provider: string | null, source: RepositorySource, default_ref: string | null, source_revision: number, source_fingerprint: string, observed_status: RepositoryObservedStatus, observed_at: string | null, created_at: string, updated_at: string, }; + +export type WorkspaceCreateResponse = { workspace: WorkspaceSummary, repository: WorkspaceRepositoryRecord, config_revision: number, request_fingerprint: string, replayed: boolean, }; + +export type WorkspaceAuthConfig = { "Passkey": { rp_id: string, origin: string, public_base_url: string, cookie_name: string, } }; + +export type WorkspacePermissionSummary = { manage_repositories: boolean, manage_secrets: boolean, manage_runtimes: boolean, delete_workspace: boolean, }; + +export type WorkspaceDeletionState = "queued" | "running" | "blocked" | "failed" | "succeeded"; + +export type WorkspaceDeletionBlockerKind = "last_accessible_workspace" | "revision_conflict" | "dirty_workdir" | "worker_removal_blocked" | "workdir_removal_blocked" | "retention_hold" | "cleanup_unavailable"; + +export type WorkspaceDeletionBlocker = { kind: WorkspaceDeletionBlockerKind, resource_kind: string | null, resource_key: string | null, message: string, }; + +export type WorkspaceDeletionResourceCounts = { workers: number, workdirs: number, repositories: number, runtime_bindings: number, secrets: number, artifacts: number, }; + +export type WorkspaceDeletionPreflightResponse = { workspace_id: string, display_name: string, +/** + * Opaque persisted Workspace metadata revision used as a CAS fence. + */ +expected_revision: string, can_delete: boolean, resources: WorkspaceDeletionResourceCounts, blockers: Array, }; + +export type WorkspaceDeletionRequest = { operation_id: string, expected_revision: string, confirmation: string, }; + +export type WorkspaceDeletionOperationResponse = { operation_id: string, workspace_id: string, display_name: string, state: WorkspaceDeletionState, resources: WorkspaceDeletionResourceCounts, child_operation_ids: Array, blockers: Array, failure_category: string | null, created_at: string, updated_at: string, completed_at: string | null, }; + +export type DiagnosticSeverity = "info" | "warning" | "error"; + +export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; + +export type WorkspaceExtensionPointState = { status: string, note: string, diagnostics: Array, }; + +export type WorkspaceExtensionPoints = { store: string, event_stream: WorkspaceExtensionPointState, host_worker_bridge: WorkspaceExtensionPointState, companion_console: WorkspaceExtensionPointState, }; + +export type WorkspaceResponse = { workspace_id: string, display_name: string, record_authority: string, schema_version: number, auth: WorkspaceAuthConfig, permissions: WorkspacePermissionSummary, extension_points: WorkspaceExtensionPoints, }; + +export type WorkspaceMetadataSettingsResponse = { workspace_id: string, display_name: string, created_at: string, revision: string, source: string, diagnostics: Array, }; + +export type UpdateWorkspaceMetadataRequest = { display_name: string, revision: string, }; + +export type WorkspaceMetadataMutationResponse = { workspace: WorkspaceMetadataSettingsResponse, diagnostics: Array, }; + +export type WorkspaceSigningIdentityState = "pending_provisioning" | "active"; + +export type WorkspaceSigningIdentityPublic = { workspace_id: string, key_id: string, algorithm: string, public_key?: string, public_key_fingerprint?: string, revision: number, state: WorkspaceSigningIdentityState, created_at: string, provisioned_at?: string, }; + +export type WorkspacePublicIdentityBundle = { workspace_id: string, backend_url: string, key_id: string, algorithm: string, public_key: string, public_key_fingerprint: string, revision: number, }; + +export type WorkspaceSigningIdentityResponse = { identity: WorkspaceSigningIdentityPublic, public_bundle?: WorkspacePublicIdentityBundle, }; + +export type ProfileSettingsResponse = { workspace_id: string, registry_revision: string, config_revision?: number | null, tree_digest?: string | null, projection_digest?: string | null, default_profile?: string | null, profiles: Array, sources: Array, diagnostics: Array, }; + +export type WorkspaceProfileSummary = { profile_id: string, selector: string, label: string, source_kind: string, profile_source_id?: string | null, description?: string | null, editable: boolean, is_default: boolean, diagnostics: Array, }; + +export type WorkspaceProfileSourceSummary = { profile_source_id: string, display_path: string, kind: string, content_type: string, content_digest: string, provenance: WorkspaceProfileSourceProvenance, editable: boolean, revision: string, size_bytes: number, diagnostics: Array, }; + +export type WorkspaceProfileSourceProvenance = "project_profile_source_tree"; + +export type RepositorySourceKind = "local_path" | "file" | "ssh" | "https" | "invalid"; + +export type RepositorySource = { kind: RepositorySourceKind, +/** + * Canonical source representation. This is an absolute local path for + * `local_path`, and a normalized URI/remote specification otherwise. + */ +uri: string, }; + +export type RepositoryObservedStatus = "unverified" | "ready" | "invalid"; + +export type RepositoryDiagnostic = { severity: string, code: string, message: string, }; + +export type GitRemoteSummary = { name: string, fetch_url: string, }; + +export type GitRepositorySummary = { status: string, head: string | null, branch: string | null, dirty: boolean, remotes: Array, }; + +export type RepositorySummary = { repository_key: string, kind: string, provider: string, source: RepositorySource, source_revision: number, source_fingerprint: string, observed_status: RepositoryObservedStatus, observed_at?: string | null, default_selector?: string | null, record_authority: string, git?: GitRepositorySummary | null, diagnostics?: Array | null, }; + +export type GitCommitSummary = { hash: string, short_hash: string, summary: string, author_name: string, author_email: string, author_date: string, parents: Array, refs: Array, }; + +export type RepositoryListResponse = { workspace_id: string, items: Array, source: string, diagnostics: Array, }; + +export type RepositoryDetailResponse = { workspace_id: string, item: RepositorySummary, source: string, }; + +export type RepositorySshConnectionProbeRequest = { runtime_id: string, }; + +export type RepositorySshHostKeyCandidate = { algorithm: string, host_key: string, fingerprint: string, }; + +export type RepositorySshConnectionTrustState = "untrusted" | "verified" | "changed"; + +export type RepositorySshConnectionProbeResponse = { workspace_id: string, repository_key: string, runtime_id: string, hostname: string, port: number, trust_state: RepositorySshConnectionTrustState, host_trust_id: string, expected_host_trust_revision: number | null, candidates: Array, }; + +export type ConfirmRepositorySshHostTrustRequest = { operation_id: string, runtime_id: string, host_key: string, expected_host_trust_revision: number | null, }; + +export type RepositoryLogResponse = { workspace_id: string, repository_key: string, default_selector?: string | null, limit: number, items: Array, diagnostics: Array, }; + +export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http"; + +export type RuntimeSourceStatus = "active" | "reserved"; + +export type RuntimeIdentityAuthority = "runtime_registry_projection" | "server_runtime_configuration"; + +export type RuntimeSourceSummary = { kind: RuntimeSourceKind, status: RuntimeSourceStatus, identity_authority: RuntimeIdentityAuthority, note: string, }; + +export type RuntimeSummary = { runtime_id: string, label: string, kind: string, status: string, source: RuntimeSourceSummary, host_ids: Array, worker_creation_available: boolean, os: string, arch: string, diagnostics: Array, }; + +export type WorkspaceRuntimeBindingState = "configured" | "verified" | "revoked"; + +export type RuntimeConnectionDisplayState = "configured" | "verified" | "unavailable" | "revoked"; + +export type RuntimeVerificationOutcome = "verified" | "challenge_issued" | "verification_failed" | "connectivity_failed"; + +export type RuntimeVerificationEvidenceSummary = { verified_at: string | null, last_checked_at: string, last_outcome: RuntimeVerificationOutcome, binding_revision: number, workspace_key_id: string, workspace_identity_revision: number, workspace_trust_generation: number, runtime_public_key_fingerprint: string, runtime_identity_revision: number, }; + +export type WorkspaceRuntimeBindingSummary = { state: WorkspaceRuntimeBindingState, connection_state: RuntimeConnectionDisplayState, revision: number, workspace_key_id?: string | null, workspace_key_generation?: number | null, verification?: RuntimeVerificationEvidenceSummary | null, }; + +export type RuntimeManagementSummary = { built_in: boolean, config_managed: boolean, removable: boolean, endpoint_configured: boolean, token_ref_configured: boolean, binding?: WorkspaceRuntimeBindingSummary | null, }; + +export type WorkspaceRuntimeResource = { management: RuntimeManagementSummary, runtime_id: string, label: string, kind: string, status: string, source: RuntimeSourceSummary, host_ids: Array, worker_creation_available: boolean, os: string, arch: string, diagnostics: Array, }; + +export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked"; + +export type RuntimeTrustKeyState = { status: RuntimeTrustKeyStatus, fingerprint?: string | null, revision?: number | null, created_at?: string | null, updated_at?: string | null, revoked_at?: string | null, }; + +export type RuntimeTrustAuditAction = "created" | "replaced" | "reactivated" | "revoked"; + +export type RuntimeTrustAuditEntry = { action: RuntimeTrustAuditAction, actor_account_id: string, old_fingerprint?: string | null, new_fingerprint?: string | null, revision: number, at: string, }; + +export type WorkspaceRuntimeDetail = { workspace_id: string, runtime: WorkspaceRuntimeResource, endpoint?: string | null, trust_key: RuntimeTrustKeyState, recent_audit: Array, }; + +export type RuntimeTrustKeyRevealResponse = { public_key: string, }; + +export type RevokeRuntimeTrustKeyRequest = { expected_revision: number, }; + +export type RemoveRuntimeRequest = { operation_id: string, expected_binding_revision: number, }; + +export type RuntimeRemovalOperationState = "pending" | "cleanup_pending" | "succeeded" | "failed"; + +export type RuntimeRemovalOperationResponse = { operation_id: string, workspace_id: string, runtime_id: string, state: RuntimeRemovalOperationState, binding_removed: boolean, runtime_registration_removed: boolean | null, failure_category?: string | null, created_at: string, updated_at: string, completed_at?: string | null, }; + +export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use"; + +export type RuntimeTrustConflictResponse = { error: RuntimeTrustConflictKind, message: string, current_revision?: number, current_fingerprint?: string | null, }; + +export type RuntimePublicIdentityBundle = { identity_id: string, public_key: string, }; + +export type CreateRemoteRuntimeRequest = { public_bundle: RuntimePublicIdentityBundle, display_name?: string | null, endpoint: string, expected_revision?: number | null, }; + +export type UpdateRemoteRuntimeRequest = { display_name?: string | null, endpoint: string, }; + +export type RuntimeConnectionTestStatus = "compatible" | "failed"; + +export type RuntimeConnectionTestFailureKind = "authentication" | "authorization" | "network_unreachable" | "timeout" | "tls_or_transport" | "malformed_response" | "protocol_version_mismatch" | "runtime_identity_mismatch" | "configuration"; + +export type RuntimeConnectionTestResponse = { workspace_id: string, runtime_id: string, binding_revision: number, connection_state: RuntimeConnectionDisplayState, verification: RuntimeVerificationEvidenceSummary | null, checked_at: string, status: RuntimeConnectionTestStatus, failure_kind: RuntimeConnectionTestFailureKind | null, expected_protocol_version: number, actual_protocol_version: number | null, diagnostics: Array, }; diff --git a/web/workspace/src/lib/generated/memory-api.ts b/web/workspace/src/lib/generated/memory-api.ts index f62472801..d1fd56c6d 100644 --- a/web/workspace/src/lib/generated/memory-api.ts +++ b/web/workspace/src/lib/generated/memory-api.ts @@ -1,100 +1,26 @@ -// Generated from workspace-api. Do not edit by hand. -// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts +// Generated from server-api. Do not edit by hand. +// Regenerate: cargo run -q -p server-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts export type DiagnosticSeverity = "info" | "warning" | "error"; -export type Diagnostic = { - code: string; - severity: DiagnosticSeverity; - message: string; -}; +export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; -export type MemoryDocumentResponse = { - body_md: string; - created_at: string; - updated_at: string; - bytes: number; - record_source: string; -}; +export type MemoryDocumentResponse = { body_md: string, created_at: string, updated_at: string, bytes: number, record_source: string, }; -export type MemoryCandidateKind = - | "preference" - | "working_assumption" - | "constraint" - | "decision" - | "open_question" - | "lesson"; +export type MemoryCandidateKind = "preference" | "working_assumption" | "constraint" | "decision" | "open_question" | "lesson"; -export type MemoryEvidenceOriginKind = - | "human_input" - | "worker_input" - | "flow_instruction" - | "backend_instruction" - | "model_output" - | "tool_output" - | "derived_summary" - | "legacy_unknown"; +export type MemoryEvidenceOriginKind = "human_input" | "worker_input" | "flow_instruction" | "backend_instruction" | "model_output" | "tool_output" | "derived_summary" | "legacy_unknown"; -export type MemoryEvidenceOrigin = { - kind: MemoryEvidenceOriginKind; - account_id?: string | null; - workspace_id?: string | null; - runtime_id?: string | null; - worker_id?: string | null; - flow_selector?: string | null; - flow_definition_id?: string | null; - flow_definition_revision?: number | null; -}; +export type MemoryEvidenceOrigin = { kind: MemoryEvidenceOriginKind, account_id?: string | null, workspace_id?: string | null, runtime_id?: string | null, worker_id?: string | null, flow_selector?: string | null, flow_definition_id?: string | null, flow_definition_revision?: number | null, }; -export type MemorySourceRef = { segment_id: string; range: [number, number] }; +export type MemorySourceRef = { segment_id: string, range: [number, number], }; -export type MemoryStagingEvidence = { - id: string; - kind: string; - entry_range: [number, number] | null; - origin?: MemoryEvidenceOrigin | null; - excerpt: string | null; - summary: string | null; -}; +export type MemoryStagingEvidence = { id: string, kind: string, entry_range: [number, number] | null, origin?: MemoryEvidenceOrigin | null, excerpt: string | null, summary: string | null, }; -export type MemorySourceEvidenceRef = { - session_id: string | null; - segment_id: string | null; - entry_range: [number, number] | null; - evidence_id: string | null; - origin?: MemoryEvidenceOrigin | null; - evidence_kind: string | null; - label: string | null; - summary: string | null; -}; +export type MemorySourceEvidenceRef = { session_id: string | null, segment_id: string | null, entry_range: [number, number] | null, evidence_id: string | null, origin?: MemoryEvidenceOrigin | null, evidence_kind: string | null, label: string | null, summary: string | null, }; -export type MemoryStagingRecord = { - schema_version: number; - id: string; - extract_run_id: string; - source: MemorySourceRef; - kind: MemoryCandidateKind; - claim: string; - why_useful: string; - staleness: string | null; - evidence: Array; - source_refs: Array; -}; +export type MemoryStagingRecord = { schema_version: number, id: string, extract_run_id: string, source: MemorySourceRef, kind: MemoryCandidateKind, claim: string, why_useful: string, staleness: string | null, evidence: Array, source_refs: Array, }; -export type MemoryStagingEntry = { - id: string; - byte_len: number; - record: MemoryStagingRecord; -}; +export type MemoryStagingEntry = { id: string, byte_len: number, record: MemoryStagingRecord, }; -export type MemoryStagingListResponse = { - limit: number; - returned_count: number; - total_valid_count: number; - invalid_count: number; - truncated: boolean; - order: string; - record_authority: string; - items: Array; - diagnostics: Array; -}; +export type MemoryStagingListResponse = { limit: number, returned_count: number, total_valid_count: number, invalid_count: number, truncated: boolean, order: string, record_authority: string, items: Array, diagnostics: Array, }; diff --git a/web/workspace/src/lib/generated/repository-access-api.ts b/web/workspace/src/lib/generated/repository-access-api.ts index 958c76d7f..2d0eb9d07 100644 --- a/web/workspace/src/lib/generated/repository-access-api.ts +++ b/web/workspace/src/lib/generated/repository-access-api.ts @@ -1,93 +1,26 @@ -// Generated from workspace-api. Do not edit by hand. -// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts +// Generated from server-api. Do not edit by hand. +// Regenerate: cargo run -q -p server-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts -export type RepositorySshCredential = { - credential_id: string; - workspace_id: string; - name: string; - public_key_algorithm: string; - public_key_fingerprint: string; - current_revision: number; - status: string; - created_at: string; - rotated_at: string | null; - referenced_repositories: Array; -}; +export type RepositorySshCredential = { credential_id: string, workspace_id: string, name: string, public_key_algorithm: string, public_key_fingerprint: string, current_revision: number, status: string, created_at: string, rotated_at: string | null, referenced_repositories: Array, }; -export type CreateRepositorySshCredentialRequest = { - operation_id: string; - credential_id: string; - name: string; - private_key: string; - passphrase: string | null; -}; +export type CreateRepositorySshCredentialRequest = { operation_id: string, credential_id: string, name: string, private_key: string, passphrase: string | null, }; -export type GenerateRepositorySshCredentialRequest = { - operation_id: string; - credential_id: string; - name: string; -}; +export type GenerateRepositorySshCredentialRequest = { operation_id: string, credential_id: string, name: string, }; -export type RepositorySshPublicKey = { - credential_id: string; - current_revision: number; - public_key_algorithm: string; - public_key_fingerprint: string; - public_key: string; -}; +export type RepositorySshPublicKey = { credential_id: string, current_revision: number, public_key_algorithm: string, public_key_fingerprint: string, public_key: string, }; -export type RotateRepositorySshCredentialRequest = { - operation_id: string; - expected_revision: number; - private_key: string; - passphrase: string | null; -}; +export type RotateRepositorySshCredentialRequest = { operation_id: string, expected_revision: number, private_key: string, passphrase: string | null, }; -export type DeleteRepositorySshCredentialRequest = { - operation_id: string; - expected_revision: number; -}; +export type DeleteRepositorySshCredentialRequest = { operation_id: string, expected_revision: number, }; -export type RepositorySshHostTrust = { - host_trust_id: string; - workspace_id: string; - hostname: string; - port: number; - key_algorithm: string; - host_key: string; - fingerprint: string; - current_revision: number; - created_at: string; - updated_at: string; - referenced_repositories: Array; -}; +export type RepositorySshHostTrust = { host_trust_id: string, workspace_id: string, hostname: string, port: number, key_algorithm: string, host_key: string, fingerprint: string, current_revision: number, created_at: string, updated_at: string, referenced_repositories: Array, }; -export type PutRepositorySshHostTrustRequest = { - operation_id: string; - host_trust_id: string; - hostname: string; - port: number; - host_key: string; - expected_revision: number | null; -}; +export type PutRepositorySshHostTrustRequest = { operation_id: string, host_trust_id: string, hostname: string, port: number, host_key: string, expected_revision: number | null, }; -export type DeleteRepositorySshHostTrustRequest = { - operation_id: string; - expected_revision: number; -}; +export type DeleteRepositorySshHostTrustRequest = { operation_id: string, expected_revision: number, }; export type RepositoryAccessMode = "read_only" | "read_write"; -export type RepositorySshAccessBinding = { - repository_key: string; - credential_id: string; - host_trust_id: string; - access: RepositoryAccessMode; -}; +export type RepositorySshAccessBinding = { repository_key: string, credential_id: string, host_trust_id: string, access: RepositoryAccessMode, }; -export type RepositoryAccessProjection = { - workspace_id: string; - config_revision: number; - projection_digest: string; - bindings: Array; -}; +export type RepositoryAccessProjection = { workspace_id: string, config_revision: number, projection_digest: string, bindings: Array, }; diff --git a/web/workspace/src/lib/generated/skill-api.ts b/web/workspace/src/lib/generated/skill-api.ts index b7812b030..3f75a4e86 100644 --- a/web/workspace/src/lib/generated/skill-api.ts +++ b/web/workspace/src/lib/generated/skill-api.ts @@ -1,5 +1,5 @@ -// Generated from workspace-api. Do not edit by hand. -// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_skill_api_types > web/workspace/src/lib/generated/skill-api.ts +// Generated from server-api. Do not edit by hand. +// Regenerate: cargo run -q -p server-api --features typescript --example generate_skill_api_types > web/workspace/src/lib/generated/skill-api.ts export const SKILL_API_AUTHORITY = "workspace-config-skills-v1" as const; @@ -20,69 +20,22 @@ export const SKILL_API_LIMITS = { export type SkillDiagnosticSeverity = "error" | "warning"; -export type SkillDiagnostic = { - severity: SkillDiagnosticSeverity; - code: string; - message: string; - source?: string; -}; +export type SkillDiagnostic = { severity: SkillDiagnosticSeverity, code: string, message: string, source?: string, }; export type SkillSourceKind = "builtin" | "workspace"; -export type SkillProvenance = { - kind: SkillSourceKind; - id: string; - virtual_path?: string; - revision?: number; - source_digest?: string; - tree_digest?: string; -}; +export type SkillProvenance = { kind: SkillSourceKind, id: string, virtual_path?: string, revision?: number, source_digest?: string, tree_digest?: string, }; export type SkillActivationStatus = "active" | "inactive"; export type SkillProjectionStatus = "valid" | "invalid"; -export type SkillProjectionIdentity = { - config_revision: number; - tree_digest: string; -}; +export type SkillProjectionIdentity = { config_revision: number, tree_digest: string, }; -export type SkillResourceRef = { - kind: string; - name: string; - supported: boolean; - diagnostic?: string; -}; +export type SkillResourceRef = { kind: string, name: string, supported: boolean, diagnostic?: string, }; -export type SkillCatalogEntry = { - name: string; - description: string; - activation_status: SkillActivationStatus; - projection_status: SkillProjectionStatus; - provenance: SkillProvenance; - overrides: Array; - diagnostics: Array; -}; +export type SkillCatalogEntry = { name: string, description: string, activation_status: SkillActivationStatus, projection_status: SkillProjectionStatus, provenance: SkillProvenance, overrides: Array, diagnostics: Array, }; -export type SkillCatalogResponse = { - authority: string; - projection: SkillProjectionIdentity; - entries: Array; - diagnostics: Array; -}; +export type SkillCatalogResponse = { authority: string, projection: SkillProjectionIdentity, entries: Array, diagnostics: Array, }; -export type SkillDetailResponse = { - authority: string; - projection: SkillProjectionIdentity; - name: string; - description: string; - provenance: SkillProvenance; - overrides: Array; - diagnostics: Array; - activation_status: SkillActivationStatus; - projection_status: SkillProjectionStatus; - body: string; - allowed_tools: Array; - allowed_tools_status: string; - resources: Array; -}; +export type SkillDetailResponse = { authority: string, projection: SkillProjectionIdentity, name: string, description: string, provenance: SkillProvenance, overrides: Array, diagnostics: Array, activation_status: SkillActivationStatus, projection_status: SkillProjectionStatus, body: string, allowed_tools: Array, allowed_tools_status: string, resources: Array, }; diff --git a/web/workspace/src/lib/generated/workdir-api.ts b/web/workspace/src/lib/generated/workdir-api.ts index e665e600c..7673d592d 100644 --- a/web/workspace/src/lib/generated/workdir-api.ts +++ b/web/workspace/src/lib/generated/workdir-api.ts @@ -1,77 +1,24 @@ -// Generated from workspace-api. Do not edit by hand. -// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts +// Generated from server-api. Do not edit by hand. +// Regenerate: cargo run -q -p server-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts export type DiagnosticSeverity = "info" | "warning" | "error"; -export type Diagnostic = { - code: string; - severity: DiagnosticSeverity; - message: string; -}; +export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; export type WorkingDirectoryMaterializerKind = "runtime_git_clone"; -export type WorkingDirectoryStatusKind = - | "active" - | "cleanup_pending" - | "corrupted" - | "not_found" - | "unknown"; +export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown"; -export type WorkingDirectoryCleanupTarget = { - kind: string; - working_directory_id: string; - repository_key: string; -}; +export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_key: string, }; -export type WorkingDirectoryOccupancy = { - runtime_id: string; - worker_id: string; - display_name: string; - linked_at: string; -}; +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 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 WorkingDirectoryCreateRequest = { - runtime_id?: string | null; - repository_key: string; - selector?: string | null; - operation_id?: string | null; -}; +export type WorkingDirectoryCreateRequest = { runtime_id?: string | null, repository_key: string, selector?: string | null, operation_id?: string | null, }; -export type WorkingDirectoryListResponse = { - workspace_id: string; - items: Array; - diagnostics: Array; -}; +export type WorkingDirectoryListResponse = { workspace_id: string, items: Array, diagnostics: Array, }; -export type WorkingDirectoryDetailResponse = { - workspace_id: string; - runtime_id: string; - item: WorkingDirectorySummary; - diagnostics: Array; -}; +export type WorkingDirectoryDetailResponse = { workspace_id: string, runtime_id: string, item: WorkingDirectorySummary, diagnostics: Array, }; -export type WorkingDirectoryCreateResponse = { - workspace_id: string; - runtime_id: string; - item: WorkingDirectorySummary; - diagnostics: Array; -}; +export type WorkingDirectoryCreateResponse = { workspace_id: string, runtime_id: string, item: WorkingDirectorySummary, diagnostics: Array, }; diff --git a/web/workspace/src/lib/generated/worker-launch-api.ts b/web/workspace/src/lib/generated/worker-launch-api.ts index ba696b455..7ed93f50d 100644 --- a/web/workspace/src/lib/generated/worker-launch-api.ts +++ b/web/workspace/src/lib/generated/worker-launch-api.ts @@ -1,183 +1,52 @@ -// 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 +// Generated from server-api. Do not edit by hand. +// Regenerate: cargo run -q -p server-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts import type { Segment } from "./protocol"; export type DiagnosticSeverity = "info" | "warning" | "error"; -export type Diagnostic = { - code: string; - severity: DiagnosticSeverity; - message: string; -}; +export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; export type WorkingDirectoryMaterializerKind = "runtime_git_clone"; -export type WorkingDirectoryStatusKind = - | "active" - | "cleanup_pending" - | "corrupted" - | "not_found" - | "unknown"; +export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown"; -export type WorkingDirectoryCleanupTarget = { - kind: string; - working_directory_id: string; - repository_key: string; -}; +export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_key: string, }; -export type RuntimeWorkingDirectoryCleanupTarget = { - kind: string; - working_directory_id: string; - repository_id: 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 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 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 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 WorkerWorkspaceSummary = { visibility: string, identity: string, workspace_id?: string | null, }; -export type WorkerImplementationSummary = { - kind: string; - display_hint: string; -}; +export type WorkerImplementationSummary = { kind: string, display_hint: string, }; -export type WorkerCapabilitySummary = { - can_stop: boolean; - can_spawn_followup: boolean; -}; +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; - workspace: WorkerWorkspaceSummary; - state: string; - last_seen_at: string | null; - pinned: boolean; - retention_state: string; - implementation: WorkerImplementationSummary; - capabilities: WorkerCapabilitySummary; - working_directory?: RuntimeWorkingDirectorySummary | null; - diagnostics: Array; -}; +export type WorkerLaunchWorkerSummary = { runtime_id: string, worker_id: string, host_id: string, display_name: string, label: string, profile: string | null, singleton_key: string | null, tags: Array, workspace: WorkerWorkspaceSummary, state: string, last_seen_at: string | null, pinned: boolean, retention_state: string, implementation: WorkerImplementationSummary, capabilities: WorkerCapabilitySummary, working_directory?: RuntimeWorkingDirectorySummary | null, diagnostics: Array, }; -export type WorkerLaunchRuntimeOption = { - runtime_id: string; - display_name: string; - built_in: boolean; - worker_creation_available: boolean; - working_directory_required: boolean; - status: string; - diagnostics: Array; -}; +export type WorkerLaunchRuntimeOption = { runtime_id: string, display_name: string, built_in: boolean, worker_creation_available: boolean, working_directory_required: boolean, status: string, diagnostics: Array, }; -export type WorkerLaunchProfileCandidate = { - id: string; - label: string; - description: string; -}; +export type WorkerLaunchProfileCandidate = { id: string, label: string, description: string, }; -export type WorkingDirectoryRepositoryOption = { - repository_key: string; - default_selector?: string | null; -}; +export type WorkingDirectoryRepositoryOption = { repository_key: string, default_selector?: string | null, }; -export type WorkerLaunchOptionsResponse = { - workspace_id: string; - runtimes: Array; - default_profile: string | null; - profiles: Array; - repositories: Array; - working_directories: Array; - diagnostics: Array; -}; +export type WorkerLaunchOptionsResponse = { workspace_id: string, runtimes: Array, default_profile: string | null, profiles: Array, repositories: Array, working_directories: Array, diagnostics: Array, }; -export type BrowserWorkerWorkingDirectorySelection = { - working_directory_id: string; - relative_cwd: string | null; -}; +export type BrowserWorkerWorkingDirectorySelection = { working_directory_id: string, relative_cwd: string | null, }; -export type CreateWorkspaceWorkerTicketAssignmentRequest = { - ticket_id: string; - operation_id: string; -}; +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; - working_directory: BrowserWorkerWorkingDirectorySelection | null; - /** - * Backend idempotency key used only for authenticated Worker-owned spawn/control. - */ - control_operation_id: string | null; -}; +export type CreateWorkspaceWorkerRequest = { runtime_id: string, display_name: string, profile: string | null, ticket_assignment: CreateWorkspaceWorkerTicketAssignmentRequest | null, initial_submit: Array, 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; -}; +export type BrowserCreateWorkerResponse = { workspace_id: string, runtime_id: string, worker_id: string, console_href: string, worker: WorkerLaunchWorkerSummary, diagnostics: Array, }; -export type BrowserWorkspaceOrchestratorResponse = { - workspace_id: string; - online: boolean; - disposition: string; - worker?: WorkerLaunchWorkerSummary | null; - diagnostics: Array; -}; +export type BrowserWorkspaceOrchestratorResponse = { workspace_id: string, online: boolean, disposition: string, worker?: WorkerLaunchWorkerSummary | null, diagnostics: Array, }; diff --git a/web/workspace/src/lib/generated/workspace-api.ts b/web/workspace/src/lib/generated/workspace-api.ts deleted file mode 100644 index 7c3c42db1..000000000 --- a/web/workspace/src/lib/generated/workspace-api.ts +++ /dev/null @@ -1,552 +0,0 @@ -// This file is generated by `cargo run -p workspace-api --features typescript --example generate_typescript | deno fmt -`. -// Do not edit this file directly. - -export type WorkspaceSummary = { - workspace_id: string; - owner_account_id: string; - display_name: string; - state: string; - created_at: string; - updated_at: string; -}; - -export type WorkspaceCatalogListResponse = Array; - -export type WorkspaceRepositoryRecord = { - workspace_id: string; - repository_key: string; - kind: string; - provider: string | null; - source: RepositorySource; - default_ref: string | null; - source_revision: number; - source_fingerprint: string; - observed_status: RepositoryObservedStatus; - observed_at: string | null; - created_at: string; - updated_at: string; -}; - -export type WorkspaceCreateResponse = { - workspace: WorkspaceSummary; - repository: WorkspaceRepositoryRecord; - config_revision: number; - request_fingerprint: string; - replayed: boolean; -}; - -export type WorkspaceAuthConfig = { - "Passkey": { - rp_id: string; - origin: string; - public_base_url: string; - cookie_name: string; - }; -}; - -export type WorkspacePermissionSummary = { - manage_repositories: boolean; - manage_secrets: boolean; - manage_runtimes: boolean; - delete_workspace: boolean; -}; - -export type WorkspaceDeletionState = - | "queued" - | "running" - | "blocked" - | "failed" - | "succeeded"; - -export type WorkspaceDeletionBlockerKind = - | "last_accessible_workspace" - | "revision_conflict" - | "dirty_workdir" - | "worker_removal_blocked" - | "workdir_removal_blocked" - | "retention_hold" - | "cleanup_unavailable"; - -export type WorkspaceDeletionBlocker = { - kind: WorkspaceDeletionBlockerKind; - resource_kind: string | null; - resource_key: string | null; - message: string; -}; - -export type WorkspaceDeletionResourceCounts = { - workers: number; - workdirs: number; - repositories: number; - runtime_bindings: number; - secrets: number; - artifacts: number; -}; - -export type WorkspaceDeletionPreflightResponse = { - workspace_id: string; - display_name: string; - /** - * Opaque persisted Workspace metadata revision used as a CAS fence. - */ - expected_revision: string; - can_delete: boolean; - resources: WorkspaceDeletionResourceCounts; - blockers: Array; -}; - -export type WorkspaceDeletionRequest = { - operation_id: string; - expected_revision: string; - confirmation: string; -}; - -export type WorkspaceDeletionOperationResponse = { - operation_id: string; - workspace_id: string; - display_name: string; - state: WorkspaceDeletionState; - resources: WorkspaceDeletionResourceCounts; - child_operation_ids: Array; - blockers: Array; - failure_category: string | null; - created_at: string; - updated_at: string; - completed_at: string | null; -}; - -export type DiagnosticSeverity = "info" | "warning" | "error"; - -export type Diagnostic = { - code: string; - severity: DiagnosticSeverity; - message: string; -}; - -export type WorkspaceExtensionPointState = { - status: string; - note: string; - diagnostics: Array; -}; - -export type WorkspaceExtensionPoints = { - store: string; - event_stream: WorkspaceExtensionPointState; - host_worker_bridge: WorkspaceExtensionPointState; - companion_console: WorkspaceExtensionPointState; -}; - -export type WorkspaceResponse = { - workspace_id: string; - display_name: string; - record_authority: string; - schema_version: number; - auth: WorkspaceAuthConfig; - permissions: WorkspacePermissionSummary; - extension_points: WorkspaceExtensionPoints; -}; - -export type WorkspaceMetadataSettingsResponse = { - workspace_id: string; - display_name: string; - created_at: string; - revision: string; - source: string; - diagnostics: Array; -}; - -export type UpdateWorkspaceMetadataRequest = { - display_name: string; - revision: string; -}; - -export type WorkspaceMetadataMutationResponse = { - workspace: WorkspaceMetadataSettingsResponse; - diagnostics: Array; -}; - -export type WorkspaceSigningIdentityState = "pending_provisioning" | "active"; - -export type WorkspaceSigningIdentityPublic = { - workspace_id: string; - key_id: string; - algorithm: string; - public_key?: string; - public_key_fingerprint?: string; - revision: number; - state: WorkspaceSigningIdentityState; - created_at: string; - provisioned_at?: string; -}; - -export type WorkspacePublicIdentityBundle = { - workspace_id: string; - backend_url: string; - key_id: string; - algorithm: string; - public_key: string; - public_key_fingerprint: string; - revision: number; -}; - -export type WorkspaceSigningIdentityResponse = { - identity: WorkspaceSigningIdentityPublic; - public_bundle?: WorkspacePublicIdentityBundle; -}; - -export type ProfileSettingsResponse = { - workspace_id: string; - registry_revision: string; - config_revision?: number | null; - tree_digest?: string | null; - projection_digest?: string | null; - default_profile?: string | null; - profiles: Array; - sources: Array; - diagnostics: Array; -}; - -export type WorkspaceProfileSummary = { - profile_id: string; - selector: string; - label: string; - source_kind: string; - profile_source_id?: string | null; - description?: string | null; - editable: boolean; - is_default: boolean; - diagnostics: Array; -}; - -export type WorkspaceProfileSourceSummary = { - profile_source_id: string; - display_path: string; - kind: string; - content_type: string; - content_digest: string; - provenance: WorkspaceProfileSourceProvenance; - editable: boolean; - revision: string; - size_bytes: number; - diagnostics: Array; -}; - -export type WorkspaceProfileSourceProvenance = "project_profile_source_tree"; - -export type RepositorySourceKind = - | "local_path" - | "file" - | "ssh" - | "https" - | "invalid"; - -export type RepositorySource = { - kind: RepositorySourceKind; - /** - * Canonical source representation. This is an absolute local path for - * `local_path`, and a normalized URI/remote specification otherwise. - */ - uri: string; -}; - -export type RepositoryObservedStatus = "unverified" | "ready" | "invalid"; - -export type RepositoryDiagnostic = { - severity: string; - code: string; - message: string; -}; - -export type GitRemoteSummary = { name: string; fetch_url: string }; - -export type GitRepositorySummary = { - status: string; - head: string | null; - branch: string | null; - dirty: boolean; - remotes: Array; -}; - -export type RepositorySummary = { - repository_key: string; - kind: string; - provider: string; - source: RepositorySource; - source_revision: number; - source_fingerprint: string; - observed_status: RepositoryObservedStatus; - observed_at?: string | null; - default_selector?: string | null; - record_authority: string; - git?: GitRepositorySummary | null; - diagnostics?: Array | null; -}; - -export type GitCommitSummary = { - hash: string; - short_hash: string; - summary: string; - author_name: string; - author_email: string; - author_date: string; - parents: Array; - refs: Array; -}; - -export type RepositoryListResponse = { - workspace_id: string; - items: Array; - source: string; - diagnostics: Array; -}; - -export type RepositoryDetailResponse = { - workspace_id: string; - item: RepositorySummary; - source: string; -}; - -export type RepositorySshConnectionProbeRequest = { runtime_id: string }; - -export type RepositorySshHostKeyCandidate = { - algorithm: string; - host_key: string; - fingerprint: string; -}; - -export type RepositorySshConnectionTrustState = - | "untrusted" - | "verified" - | "changed"; - -export type RepositorySshConnectionProbeResponse = { - workspace_id: string; - repository_key: string; - runtime_id: string; - hostname: string; - port: number; - trust_state: RepositorySshConnectionTrustState; - host_trust_id: string; - expected_host_trust_revision: number | null; - candidates: Array; -}; - -export type ConfirmRepositorySshHostTrustRequest = { - operation_id: string; - runtime_id: string; - host_key: string; - expected_host_trust_revision: number | null; -}; - -export type RepositoryLogResponse = { - workspace_id: string; - repository_key: string; - default_selector?: string | null; - limit: number; - items: Array; - diagnostics: Array; -}; - -export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http"; - -export type RuntimeSourceStatus = "active" | "reserved"; - -export type RuntimeIdentityAuthority = - | "runtime_registry_projection" - | "server_runtime_configuration"; - -export type RuntimeSourceSummary = { - kind: RuntimeSourceKind; - status: RuntimeSourceStatus; - identity_authority: RuntimeIdentityAuthority; - note: string; -}; - -export type RuntimeSummary = { - runtime_id: string; - label: string; - kind: string; - status: string; - source: RuntimeSourceSummary; - host_ids: Array; - worker_creation_available: boolean; - os: string; - arch: string; - diagnostics: Array; -}; - -export type WorkspaceRuntimeBindingState = - | "configured" - | "verified" - | "revoked"; - -export type RuntimeConnectionDisplayState = - | "configured" - | "verified" - | "unavailable" - | "revoked"; - -export type RuntimeVerificationOutcome = - | "verified" - | "challenge_issued" - | "verification_failed" - | "connectivity_failed"; - -export type RuntimeVerificationEvidenceSummary = { - verified_at: string | null; - last_checked_at: string; - last_outcome: RuntimeVerificationOutcome; - binding_revision: number; - workspace_key_id: string; - workspace_identity_revision: number; - workspace_trust_generation: number; - runtime_public_key_fingerprint: string; - runtime_identity_revision: number; -}; - -export type WorkspaceRuntimeBindingSummary = { - state: WorkspaceRuntimeBindingState; - connection_state: RuntimeConnectionDisplayState; - revision: number; - workspace_key_id?: string | null; - workspace_key_generation?: number | null; - verification?: RuntimeVerificationEvidenceSummary | null; -}; - -export type RuntimeManagementSummary = { - built_in: boolean; - config_managed: boolean; - removable: boolean; - endpoint_configured: boolean; - token_ref_configured: boolean; - binding?: WorkspaceRuntimeBindingSummary | null; -}; - -export type WorkspaceRuntimeResource = { - management: RuntimeManagementSummary; - runtime_id: string; - label: string; - kind: string; - status: string; - source: RuntimeSourceSummary; - host_ids: Array; - worker_creation_available: boolean; - os: string; - arch: string; - diagnostics: Array; -}; - -export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked"; - -export type RuntimeTrustKeyState = { - status: RuntimeTrustKeyStatus; - fingerprint?: string | null; - revision?: number | null; - created_at?: string | null; - updated_at?: string | null; - revoked_at?: string | null; -}; - -export type RuntimeTrustAuditAction = - | "created" - | "replaced" - | "reactivated" - | "revoked"; - -export type RuntimeTrustAuditEntry = { - action: RuntimeTrustAuditAction; - actor_account_id: string; - old_fingerprint?: string | null; - new_fingerprint?: string | null; - revision: number; - at: string; -}; - -export type WorkspaceRuntimeDetail = { - workspace_id: string; - runtime: WorkspaceRuntimeResource; - endpoint?: string | null; - trust_key: RuntimeTrustKeyState; - recent_audit: Array; -}; - -export type RuntimeTrustKeyRevealResponse = { public_key: string }; - -export type RevokeRuntimeTrustKeyRequest = { expected_revision: number }; - -export type RemoveRuntimeRequest = { - operation_id: string; - expected_binding_revision: number; -}; - -export type RuntimeRemovalOperationState = - | "pending" - | "cleanup_pending" - | "succeeded" - | "failed"; - -export type RuntimeRemovalOperationResponse = { - operation_id: string; - workspace_id: string; - runtime_id: string; - state: RuntimeRemovalOperationState; - binding_removed: boolean; - runtime_registration_removed: boolean | null; - failure_category?: string | null; - created_at: string; - updated_at: string; - completed_at?: string | null; -}; - -export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use"; - -export type RuntimeTrustConflictResponse = { - error: RuntimeTrustConflictKind; - message: string; - current_revision?: number; - current_fingerprint?: string | null; -}; - -export type RuntimePublicIdentityBundle = { - identity_id: string; - public_key: string; -}; - -export type CreateRemoteRuntimeRequest = { - public_bundle: RuntimePublicIdentityBundle; - display_name?: string | null; - endpoint: string; - expected_revision?: number | null; -}; - -export type UpdateRemoteRuntimeRequest = { - display_name?: string | null; - endpoint: string; -}; - -export type RuntimeConnectionTestStatus = "compatible" | "failed"; - -export type RuntimeConnectionTestFailureKind = - | "authentication" - | "authorization" - | "network_unreachable" - | "timeout" - | "tls_or_transport" - | "malformed_response" - | "protocol_version_mismatch" - | "runtime_identity_mismatch" - | "configuration"; - -export type RuntimeConnectionTestResponse = { - workspace_id: string; - runtime_id: string; - binding_revision: number; - connection_state: RuntimeConnectionDisplayState; - verification: RuntimeVerificationEvidenceSummary | null; - checked_at: string; - status: RuntimeConnectionTestStatus; - failure_kind: RuntimeConnectionTestFailureKind | null; - expected_protocol_version: number; - actual_protocol_version: number | null; - diagnostics: Array; -}; diff --git a/web/workspace/src/lib/workspace/api/runtime-connection.ts b/web/workspace/src/lib/workspace/api/runtime-connection.ts index 05ec74ce5..ada1ff6aa 100644 --- a/web/workspace/src/lib/workspace/api/runtime-connection.ts +++ b/web/workspace/src/lib/workspace/api/runtime-connection.ts @@ -3,7 +3,7 @@ import type { RuntimeConnectionTestFailureKind, RuntimeConnectionTestResponse, RuntimeVerificationEvidenceSummary, -} from "$lib/generated/workspace-api"; +} from "$lib/generated/legacy-server-api"; const RESPONSE_KEYS = [ "workspace_id", diff --git a/web/workspace/src/lib/workspace/api/runtime-management.ts b/web/workspace/src/lib/workspace/api/runtime-management.ts index d7e363cfe..92cd1ba0a 100644 --- a/web/workspace/src/lib/workspace/api/runtime-management.ts +++ b/web/workspace/src/lib/workspace/api/runtime-management.ts @@ -24,7 +24,7 @@ import type { WorkspaceRuntimeBindingSummary, WorkspaceRuntimeDetail, WorkspaceRuntimeResource, -} from "$lib/generated/workspace-api.ts"; +} from "$lib/generated/legacy-server-api.ts"; import type { ListResponse } from "$lib/workspace/sidebar/types"; import { workspaceApiPath } from "./http.ts"; diff --git a/web/workspace/src/lib/workspace/api/workspace-model.ts b/web/workspace/src/lib/workspace/api/workspace-model.ts index 38d5b0281..f4808df05 100644 --- a/web/workspace/src/lib/workspace/api/workspace-model.ts +++ b/web/workspace/src/lib/workspace/api/workspace-model.ts @@ -29,7 +29,7 @@ import type { WorkspaceRepositoryRecord, WorkspaceResponse, WorkspaceSummary, -} from "$lib/generated/workspace-api.ts"; +} from "$lib/generated/legacy-server-api.ts"; export type { GitCommitSummary, @@ -48,7 +48,7 @@ export type { WorkspacePermissionSummary, WorkspaceResponse, WorkspaceSummary, -} from "$lib/generated/workspace-api.ts"; +} from "$lib/generated/legacy-server-api.ts"; type JsonObject = Record; diff --git a/web/workspace/src/lib/workspace/repositories/ssh-connection.ts b/web/workspace/src/lib/workspace/repositories/ssh-connection.ts index d76e374f2..7b01e33f0 100644 --- a/web/workspace/src/lib/workspace/repositories/ssh-connection.ts +++ b/web/workspace/src/lib/workspace/repositories/ssh-connection.ts @@ -1,4 +1,4 @@ -import type { WorkspaceRuntimeResource } from "$lib/generated/workspace-api"; +import type { WorkspaceRuntimeResource } from "$lib/generated/legacy-server-api"; export type RepositorySshProbeSelection = Readonly<{ changed: boolean; diff --git a/web/workspace/src/lib/workspace/settings/model.ts b/web/workspace/src/lib/workspace/settings/model.ts index 49c9d0716..21b6da261 100644 --- a/web/workspace/src/lib/workspace/settings/model.ts +++ b/web/workspace/src/lib/workspace/settings/model.ts @@ -1,6 +1,6 @@ -import type { Diagnostic as WorkspaceApiDiagnostic } from "$lib/generated/workspace-api"; +import type { Diagnostic as ServerApiDiagnostic } from "$lib/generated/legacy-server-api"; -export type Diagnostic = WorkspaceApiDiagnostic; +export type Diagnostic = ServerApiDiagnostic; export type SettingsSectionId = | "runtimes" diff --git a/web/workspace/src/lib/workspace/settings/profile-api.ts b/web/workspace/src/lib/workspace/settings/profile-api.ts index 57881a3df..53383cc6c 100644 --- a/web/workspace/src/lib/workspace/settings/profile-api.ts +++ b/web/workspace/src/lib/workspace/settings/profile-api.ts @@ -12,7 +12,7 @@ import type { WorkspaceSigningIdentityPublic, WorkspaceSigningIdentityResponse, WorkspaceSigningIdentityState, -} from "$lib/generated/workspace-api"; +} from "$lib/generated/legacy-server-api"; export class ProfileApiError extends Error { constructor( diff --git a/web/workspace/src/lib/workspace/settings/workspace-deletion-api.ts b/web/workspace/src/lib/workspace/settings/workspace-deletion-api.ts index 449bff2e4..f35f090a0 100644 --- a/web/workspace/src/lib/workspace/settings/workspace-deletion-api.ts +++ b/web/workspace/src/lib/workspace/settings/workspace-deletion-api.ts @@ -2,7 +2,7 @@ import type { WorkspaceDeletionOperationResponse, WorkspaceDeletionPreflightResponse, WorkspaceDeletionRequest, -} from "$lib/generated/workspace-api"; +} from "$lib/generated/legacy-server-api"; import { loadJson } from "$lib/workspace/api/http"; import { parseWorkspaceDeletionOperationResponse, diff --git a/web/workspace/src/routes/w/[workspaceId]/repositories/[repositoryKey]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/repositories/[repositoryKey]/+page.svelte index 215e879a9..5d232c73b 100644 --- a/web/workspace/src/routes/w/[workspaceId]/repositories/[repositoryKey]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/repositories/[repositoryKey]/+page.svelte @@ -3,7 +3,7 @@ ConfirmRepositorySshHostTrustRequest, RepositorySshConnectionProbeRequest, RepositorySshConnectionProbeResponse - } from '$lib/generated/workspace-api'; + } from '$lib/generated/legacy-server-api'; import { parseRepositorySshHostTrust } from '$lib/workspace/api/repository-access'; import { formatDate, workspaceApiPath } from '$lib/workspace/api/http'; import { parseRepositorySshConnectionProbeResponse } from '$lib/workspace/api/workspace-model'; diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/+page.svelte index 0dfd5a68d..afb725a3e 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/+page.svelte @@ -6,7 +6,7 @@ WorkspaceDeletionRequest, WorkspaceMetadataSettingsResponse, WorkspaceSigningIdentityResponse, - } from '$lib/generated/workspace-api'; + } from '$lib/generated/legacy-server-api'; import { goto } from '$app/navigation'; import { onMount } from 'svelte'; import { disposeWorkspaceMultiplexer } from '$lib/workspace/multiplexer'; diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/profiles/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/profiles/+page.svelte index 71a1bf0b0..5bdb002ef 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/profiles/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/profiles/+page.svelte @@ -1,7 +1,7 @@