diff --git a/Cargo.lock b/Cargo.lock index 3ad43461a..cc29a095e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1447,6 +1447,7 @@ dependencies = [ "grep-searcher", "ignore", "libc", + "schemars", "serde", "serde_json", "sha2 0.10.9", @@ -6104,6 +6105,7 @@ dependencies = [ "fs-operation", "manifest", "reqwest", + "schemars", "serde", "serde_json", "sha2 0.11.0", diff --git a/crates/fs-operation/Cargo.toml b/crates/fs-operation/Cargo.toml index f61b8be76..6d360b536 100644 --- a/crates/fs-operation/Cargo.toml +++ b/crates/fs-operation/Cargo.toml @@ -11,6 +11,7 @@ grep-regex = "0.1.14" grep-searcher = "0.1.16" ignore = "0.4.25" libc = "0.2" +schemars.workspace = true serde = { workspace = true, features = ["derive"] } sha2 = "0.10.9" tempfile.workspace = true diff --git a/crates/fs-operation/src/operation.rs b/crates/fs-operation/src/operation.rs index 4d74184b9..f5765b6f6 100644 --- a/crates/fs-operation/src/operation.rs +++ b/crates/fs-operation/src/operation.rs @@ -1,6 +1,7 @@ use std::fmt; use std::path::{Component, Path, PathBuf}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::FsError; @@ -11,9 +12,11 @@ use crate::FsError; /// while computing line and content metadata. `max_response_bytes` limits the /// bytes retained for the response independently of the caller's requested /// `ReadRequest::max_bytes`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)] pub struct BoundedReadLimits { + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub max_source_bytes: u64, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub max_response_bytes: usize, } @@ -72,7 +75,7 @@ impl<'de> Deserialize<'de> for BoundedReadLimits { /// Scope-checked filesystem path. Relative paths resolve below the bound /// Workdir root; absolute paths require an explicit matching scope rule. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, JsonSchema)] #[serde(transparent)] pub struct FsPath(String); @@ -153,21 +156,22 @@ impl fmt::Display for FsPath { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct StatRequest { pub path: FsPath, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct StatResult { pub path: FsPath, pub kind: EntryKind, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub size: u64, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum EntryKind { File, @@ -178,27 +182,32 @@ pub enum EntryKind { pub type ContentHash = [u8; 32]; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ReadRequest { pub path: FsPath, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub offset: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub limit: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub max_bytes: usize, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ReadResult { pub path: FsPath, pub bytes: Vec, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub start_line: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub total_lines: usize, pub content_hash: ContentHash, pub truncated: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WriteRequest { pub path: FsPath, @@ -206,13 +215,14 @@ pub struct WriteRequest { pub expected_hash: Option, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct WriteResult { + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub bytes_written: usize, pub created: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct EditRequest { pub path: FsPath, @@ -222,54 +232,61 @@ pub struct EditRequest { pub expected_hash: ContentHash, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct EditResult { + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub replacements: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub bytes_written: usize, pub content_hash: ContentHash, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ListRequest { pub path: FsPath, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub limit: usize, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ListEntry { pub path: FsPath, pub kind: EntryKind, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub size: u64, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ListResult { pub entries: Vec, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub total_entries: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub total_bytes: u64, pub truncated: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GlobRequest { pub pattern: String, pub path: FsPath, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub limit: usize, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GlobResult { pub paths: Vec, pub truncated: bool, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum GrepOutputMode { Content, @@ -283,7 +300,7 @@ impl Default for GrepOutputMode { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GrepRequest { pub pattern: String, @@ -291,21 +308,27 @@ pub struct GrepRequest { pub glob: Option, pub file_type: Option, pub case_insensitive: bool, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub before_context: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub after_context: usize, pub multiline: bool, pub output_mode: GrepOutputMode, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub limit: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub offset: usize, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GrepResult { /// Provider-rendered bounded grep report. Keeping rendering here avoids /// transferring candidate files across a remote provider boundary. pub output: String, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub match_count: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub matched_files: usize, pub truncated: bool, } diff --git a/crates/server-api/Cargo.toml b/crates/server-api/Cargo.toml index a74b4c020..47452a8f0 100644 --- a/crates/server-api/Cargo.toml +++ b/crates/server-api/Cargo.toml @@ -33,7 +33,6 @@ required-features = ["typescript"] [[example]] name = "generate_workdir_api_types" -required-features = ["typescript"] [[example]] name = "generate_worker_launch_api_types" diff --git a/crates/server-api/examples/generate_workdir_api_types.rs b/crates/server-api/examples/generate_workdir_api_types.rs index 13ed5f08b..ff8dff334 100644 --- a/crates/server-api/examples/generate_workdir_api_types.rs +++ b/crates/server-api/examples/generate_workdir_api_types.rs @@ -1,6 +1,59 @@ +use std::fs; +use std::path::Path; + +use server_api::repository_openapi_typescript::{ + INPUT_PATH, WORKDIR_OUTPUT_PATH, generate_workdir_typescript, +}; + fn main() { - print!( - "{}", - server_api::normalized_typescript(server_api::workdir_api_typescript()) - ); + if let Err(error) = run() { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), String> { + let mut check = false; + for argument in std::env::args().skip(1) { + match argument.as_str() { + "--check" => check = true, + "--help" | "-h" => { + println!( + "Generate the bounded Workdir API TypeScript artifact.\n\n\ + Usage: cargo run -q -p server-api --example generate_workdir_api_types -- [--check]" + ); + return Ok(()); + } + other => return Err(format!("unknown argument `{other}`")), + } + } + + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let input_path = root.join(INPUT_PATH); + let output_path = root.join(WORKDIR_OUTPUT_PATH); + let input = fs::read_to_string(&input_path) + .map_err(|error| format!("failed to read {}: {error}", input_path.display()))?; + let generated = generate_workdir_typescript(&input) + .map_err(|error| format!("failed to generate Workdir API types: {error}"))?; + + if check { + let checked_in = fs::read_to_string(&output_path) + .map_err(|error| format!("failed to read {}: {error}", output_path.display()))?; + if checked_in != generated { + return Err(format!( + "{} is stale; regenerate it without --check", + output_path.display() + )); + } + println!("{} is current", output_path.display()); + return Ok(()); + } + + let temporary_path = output_path.with_extension("ts.tmp"); + fs::write(&temporary_path, generated) + .map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?; + fs::rename(&temporary_path, &output_path) + .map_err(|error| format!("failed to replace {}: {error}", output_path.display()))?; + println!("generated {}", output_path.display()); + Ok(()) } diff --git a/crates/server-api/src/lib.rs b/crates/server-api/src/lib.rs index ba6632d7d..6943e3a87 100644 --- a/crates/server-api/src/lib.rs +++ b/crates/server-api/src/lib.rs @@ -77,6 +77,64 @@ impl api_macros::HttpRequestError for RepositoryApiError { } } +/// Error body for current-Worker Workdir operations. +/// +/// Domain/service failures preserve the existing Repository-style API envelope, +/// while provider failures preserve the established typed Workdir transport +/// envelope and HTTP status classification. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WorkdirOperationApiError { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub code: Option, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub diagnostics: Option>, + #[serde(skip, default = "default_repository_error_status")] + status: u16, +} + +impl WorkdirOperationApiError { + pub fn api(error: RepositoryApiError) -> Self { + Self { + error: Some(error.error), + code: None, + message: error.message, + diagnostics: Some(error.diagnostics), + status: error.status, + } + } + + pub fn provider(error: workdir::http::WorkdirTransportError) -> Self { + let status = error.code.http_status(); + Self { + error: None, + code: Some(error.code), + message: error.message, + diagnostics: None, + status, + } + } +} + +impl api_macros::HttpError for WorkdirOperationApiError { + fn status_code(&self) -> u16 { + self.code.map_or(self.status, |code| code.http_status()) + } +} + +impl api_macros::HttpRequestError for WorkdirOperationApiError { + fn from_request_rejection(status: u16, message: String) -> Self { + Self::api( + ::from_request_rejection( + status, message, + ), + ) + } +} + macro_rules! impl_openapi_schema { ($($ty:ty),+ $(,)?) => { $(impl api_macros::openapi::OpenApiSchema for $ty {})+ @@ -109,8 +167,29 @@ impl_openapi_schema!( CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryResponse, RepositoryApiError, + WorkdirOperationApiError, RepositoryListResponse, RepositoryDetailResponse, + RepositorySshConnectionProbeRequest, + RepositorySshConnectionProbeResponse, + ConfirmRepositorySshHostTrustRequest, + RepositoryLogQuery, + RepositoryLogResponse, + HostListResponse, + HostWorkerListResponse, + HostSummary, + WorkingDirectoryRemovalRequest, + WorkingDirectoryRemovalResponse, + WorkingDirectoryCreateRequest, + WorkingDirectoryListResponse, + WorkingDirectoryDetailResponse, + WorkingDirectoryCreateResponse, + CurrentWorkerWorkdirAttachRequest, + CurrentWorkerWorkdirAttachmentResponse, + CurrentWorkerWorkdirOperationRequest, + CurrentWorkerWorkdirOperationResponse, + ExternalWorkdirGrantCreateRequest, + ExternalWorkdirGrantResponse, WorkspaceResponse, WorkspaceMetadataSettingsResponse, UpdateWorkspaceMetadataRequest, @@ -980,6 +1059,324 @@ pub trait ServerApi { #[body] request: CreateWorkspaceRepositoryRequest, ) -> Result; + #[post( + "/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn repository_ssh_connection_probe( + &self, + #[extension] actor: RequestActor, + #[path] workspace_id: String, + #[path] repository_key: String, + #[body] request: RepositorySshConnectionProbeRequest, + ) -> Result; + + #[put( + "/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn repository_ssh_host_trust_confirm( + &self, + #[extension] actor: RequestActor, + #[path] workspace_id: String, + #[path] repository_key: String, + #[body] request: ConfirmRepositorySshHostTrustRequest, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/repositories/{repository_key}/log", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn repository_log( + &self, + #[path] workspace_id: String, + #[path] repository_key: String, + #[query] query: RepositoryLogQuery, + ) -> Result; + + #[get( + "/api/repositories/{repository_key}/log", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn repository_log_alias( + &self, + #[path] repository_key: String, + #[query] query: RepositoryLogQuery, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/hosts", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn host_list( + &self, + #[path] workspace_id: String, + ) -> Result; + + #[get( + "/api/hosts", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn host_list_alias(&self) -> Result; + + #[get( + "/api/w/{workspace_id}/hosts/{host_id}/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn host_worker_list( + &self, + #[path] workspace_id: String, + #[path] host_id: String, + ) -> Result; + + #[get( + "/api/hosts/{host_id}/workers", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn host_worker_list_alias( + &self, + #[path] host_id: String, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn runtime_workdir_list( + &self, + #[path] workspace_id: String, + #[path] runtime_id: String, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories", + status = 201, + alternate_status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn runtime_workdir_create( + &self, + #[path] workspace_id: String, + #[path] runtime_id: String, + #[body] request: WorkingDirectoryCreateRequest, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn runtime_workdir_detail( + &self, + #[path] workspace_id: String, + #[path] runtime_id: String, + #[path] working_directory_id: String, + ) -> Result; + + #[delete( + "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn runtime_workdir_cleanup( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[path] runtime_id: String, + #[path] working_directory_id: String, + #[body] request: WorkingDirectoryRemovalRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/workers/self/workdir-attachments", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true + )] + async fn current_worker_workdir_attach( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[body] request: CurrentWorkerWorkdirAttachRequest, + ) -> Result; + + #[delete( + "/api/w/{workspace_id}/workers/self/workdir-attachments/{alias}", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true + )] + async fn current_worker_workdir_detach( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[path] alias: String, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/workers/self/workdir-session/operations", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true + )] + async fn current_worker_workdir_operation( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[body] request: CurrentWorkerWorkdirOperationRequest, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/external-workdir-grants", + status = 201, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500], + bearer_auth = true, + browser_auth = true + )] + async fn external_workdir_grant_create( + &self, + #[extension] actor: RequestActor, + #[path] workspace_id: String, + #[body] request: ExternalWorkdirGrantCreateRequest, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/external-workdir-grants/{grant_id}", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500], + bearer_auth = true, + browser_auth = true + )] + async fn external_workdir_grant_get( + &self, + #[extension] actor: RequestActor, + #[path] workspace_id: String, + #[path] grant_id: String, + ) -> Result; + + #[delete( + "/api/w/{workspace_id}/external-workdir-grants/{grant_id}", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500], + bearer_auth = true, + browser_auth = true + )] + async fn external_workdir_grant_revoke( + &self, + #[extension] actor: RequestActor, + #[path] workspace_id: String, + #[path] grant_id: String, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/working-directories", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_workdir_list( + &self, + #[path] workspace_id: String, + ) -> Result; + + #[post( + "/api/w/{workspace_id}/working-directories", + status = 201, + alternate_status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_workdir_create( + &self, + #[path] workspace_id: String, + #[body] request: WorkingDirectoryCreateRequest, + ) -> Result; + + #[get( + "/api/w/{workspace_id}/working-directories/{working_directory_id}", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_workdir_detail( + &self, + #[path] workspace_id: String, + #[path] working_directory_id: String, + ) -> Result; + + #[delete( + "/api/w/{workspace_id}/working-directories/{working_directory_id}", + status = 200, + error_status = 400, + additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], + bearer_auth = true, + browser_auth = true + )] + async fn workspace_workdir_cleanup( + &self, + #[extension] context: ServerRequestContext, + #[path] workspace_id: String, + #[path] working_directory_id: String, + #[body] request: WorkingDirectoryRemovalRequest, + ) -> Result; + #[get("/api/tickets", status = 200, error_status = 400, additional_error_statuses = [500])] async fn ticket_list_alias( &self, @@ -1488,9 +1885,16 @@ pub fn canonical_openapi_document() /// /// Generated clients and OpenAPI omit extension parameters; operation implementations use this /// value instead of re-parsing transport headers inside domain handlers. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ServerWorkerSource { + pub runtime_id: String, + pub worker_id: String, +} + #[derive(Clone)] pub struct ServerRequestContext { pub actor: Option, + pub worker_source: Option, pub origin: Option, /// Transport headers retained for Server-side authentication adapters. /// @@ -2991,7 +3395,7 @@ pub struct RepositorySummary { pub diagnostics: Option>, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct GitCommitSummary { @@ -3024,14 +3428,14 @@ pub struct RepositoryDetailResponse { pub source: String, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[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)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct RepositorySshHostKeyCandidate { @@ -3040,7 +3444,7 @@ pub struct RepositorySshHostKeyCandidate { pub fingerprint: String, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum RepositorySshConnectionTrustState { @@ -3049,7 +3453,7 @@ pub enum RepositorySshConnectionTrustState { Changed, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct RepositorySshConnectionProbeResponse { @@ -3061,11 +3465,12 @@ pub struct RepositorySshConnectionProbeResponse { pub trust_state: RepositorySshConnectionTrustState, pub host_trust_id: String, #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub expected_host_trust_revision: Option, pub candidates: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct ConfirmRepositorySshHostTrustRequest { @@ -3073,10 +3478,19 @@ pub struct ConfirmRepositorySshHostTrustRequest { pub runtime_id: String, pub host_key: String, #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub expected_host_trust_revision: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct RepositoryLogQuery { + #[serde(default)] + #[schemars(range(min = 1, max = 200))] + pub limit: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct RepositoryLogResponse { @@ -3085,6 +3499,7 @@ pub struct RepositoryLogResponse { #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "typescript", ts(optional = nullable))] pub default_selector: Option, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub limit: usize, pub items: Vec, pub diagnostics: Vec, @@ -3115,7 +3530,7 @@ pub struct Diagnostic { /// /// 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)] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum WorkingDirectoryMaterializerKind { @@ -3124,7 +3539,7 @@ pub enum WorkingDirectoryMaterializerKind { ClientHostedExternal, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum WorkingDirectoryStatusKind { @@ -3153,7 +3568,7 @@ impl std::fmt::Display for WorkingDirectoryStatusKind { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryCleanupTarget { @@ -3162,14 +3577,14 @@ pub struct WorkingDirectoryCleanupTarget { pub repository_key: String, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[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)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum WorkingDirectoryRemovalDisposition { @@ -3178,7 +3593,7 @@ pub enum WorkingDirectoryRemovalDisposition { AttentionRequired, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[serde(deny_unknown_fields)] @@ -3191,7 +3606,7 @@ pub struct WorkingDirectoryRemovalResponse { } /// Durable Workspace occupancy projection for one Workdir. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryOccupancy { @@ -3204,7 +3619,7 @@ pub struct WorkingDirectoryOccupancy { /// 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)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[serde(deny_unknown_fields)] @@ -3217,7 +3632,7 @@ pub struct RuntimeWorkingDirectoryCleanupTarget { /// 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)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[serde(deny_unknown_fields)] @@ -3241,6 +3656,7 @@ pub struct RuntimeWorkingDirectorySummary { pub current_tree: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "typescript", ts(type = "number | null"))] + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub observed_at_epoch_seconds: Option, pub materializer_kind: WorkingDirectoryMaterializerKind, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -3253,7 +3669,7 @@ pub struct RuntimeWorkingDirectorySummary { } /// Public Workdir source identity. Provider routing details and host paths are intentionally absent. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum WorkingDirectorySource { @@ -3262,7 +3678,7 @@ pub enum WorkingDirectorySource { } /// Public, provider-neutral Workdir inventory projection. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[serde(deny_unknown_fields)] @@ -3286,6 +3702,7 @@ pub struct WorkingDirectorySummary { pub current_tree: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub observed_at_epoch_seconds: Option, pub materializer_kind: WorkingDirectoryMaterializerKind, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -3414,17 +3831,18 @@ impl From for WorkingDirectorySumma } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct ExternalWorkdirGrantCreateRequest { pub provider_instance_id: String, pub display_name: String, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub ttl_seconds: u64, pub read_only: bool, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct ExternalWorkdirGrantResponse { @@ -3435,6 +3853,7 @@ pub struct ExternalWorkdirGrantResponse { pub display_name: String, pub permissions: String, pub expires_at: String, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub generation: u64, pub status: String, } @@ -3444,7 +3863,7 @@ pub struct ExternalWorkdirGrantResponse { /// `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)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[serde(deny_unknown_fields)] @@ -3461,7 +3880,7 @@ pub struct WorkingDirectoryCreateRequest { pub operation_id: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryListResponse { @@ -3470,7 +3889,7 @@ pub struct WorkingDirectoryListResponse { pub diagnostics: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryDetailResponse { @@ -3481,7 +3900,72 @@ pub struct WorkingDirectoryDetailResponse { pub diagnostics: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct HostSummary { + pub runtime_id: String, + pub host_id: String, + pub label: String, + pub kind: String, + pub status: String, + pub observed_at: String, + pub last_seen_at: Option, + pub os: String, + pub arch: String, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct HostListResponse { + pub workspace_id: String, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] + pub limit: usize, + pub items: Vec, + pub source: String, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct HostWorkerListResponse { + pub workspace_id: String, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] + pub limit: usize, + pub items: Vec, + pub source: String, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CurrentWorkerWorkdirAttachRequest { + pub alias: String, + pub working_directory_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CurrentWorkerWorkdirAttachmentResponse { + pub workspace_id: String, + pub alias: String, + pub working_directory_id: String, + pub capabilities: workdir::WorkdirSessionCapabilities, + pub attached: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CurrentWorkerWorkdirOperationRequest { + pub target_workdir: String, + pub operation: workdir::http::WorkdirSessionOperation, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(transparent)] +pub struct CurrentWorkerWorkdirOperationResponse(pub workdir::http::WorkdirSessionOperationResult); + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryCreateResponse { @@ -3489,6 +3973,20 @@ pub struct WorkingDirectoryCreateResponse { pub runtime_id: String, pub item: WorkingDirectorySummary, pub diagnostics: Vec, + #[serde(skip, default = "default_workdir_create_status")] + #[cfg_attr(feature = "typescript", ts(skip))] + #[schemars(skip)] + pub http_status: u16, +} + +const fn default_workdir_create_status() -> u16 { + 201 +} + +impl api_macros::HttpSuccess for WorkingDirectoryCreateResponse { + fn status_code(&self) -> u16 { + self.http_status + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -4962,7 +5460,7 @@ pub struct RuntimeConnectionTestResponse { pub diagnostics: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct WorkerWorkspaceSummary { pub visibility: String, @@ -4971,14 +5469,14 @@ pub struct WorkerWorkspaceSummary { pub workspace_id: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[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)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct WorkerCapabilitySummary { pub can_stop: bool, @@ -5149,7 +5647,7 @@ pub struct WorkspaceWorkerDiscoveryPage { /// One Workspace Worker Workdir attachment. `alias` is the stable Worker-local /// routing key; the nested Workdir id and display name are inventory metadata. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] pub struct WorkerWorkdirAttachmentSummary { @@ -5170,7 +5668,7 @@ pub struct RuntimeWorkerWorkdirAttachmentSummary { /// `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)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct WorkerSummary { pub runtime_id: String, @@ -5190,6 +5688,7 @@ pub struct WorkerSummary { pub state: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "typescript", ts(optional))] + #[schemars(with = "Option")] pub worker_state: Option, pub last_seen_at: Option, #[serde(default)] diff --git a/crates/server-api/src/repository_openapi_typescript.rs b/crates/server-api/src/repository_openapi_typescript.rs index ac459aa91..fa374443f 100644 --- a/crates/server-api/src/repository_openapi_typescript.rs +++ b/crates/server-api/src/repository_openapi_typescript.rs @@ -7,6 +7,11 @@ pub const GENERATOR_VERSION: &str = "1"; pub const GENERATOR_OPTIONS: &str = "repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output"; pub const INPUT_PATH: &str = "openapi/server-api.json"; pub const OUTPUT_PATH: &str = "web/workspace/src/lib/generated/repository-api.ts"; +pub const WORKDIR_GENERATOR_NAME: &str = "yoi-workdir-openapi-typescript"; +pub const WORKDIR_GENERATOR_VERSION: &str = "1"; +pub const WORKDIR_GENERATOR_OPTIONS: &str = + "workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output"; +pub const WORKDIR_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/workdir-api.ts"; const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0; @@ -72,6 +77,48 @@ pub fn generate_repository_typescript(input: &str) -> Result Result { + let document: Value = serde_json::from_str(input) + .map_err(|error| GenerationError::invalid(format!("invalid OpenAPI JSON: {error}")))?; + let root = object(&document, "OpenAPI document")?; + if root.get("openapi").and_then(Value::as_str) != Some("3.1.0") { + return Err(GenerationError::invalid( + "Workdir TypeScript generation requires OpenAPI 3.1.0", + )); + } + + let info = object_field(root, "info", "OpenAPI document")?; + let contract_version = required_string(info, "version", "OpenAPI info")?; + let contract_digest = required_string(info, "x-yoi-source-digest", "OpenAPI info")?; + validate_contract_digest(contract_digest)?; + let schemas = object_field( + object_field(root, "components", "OpenAPI document")?, + "schemas", + "OpenAPI components", + )?; + let paths = object_field(root, "paths", "OpenAPI document")?; + let roots = validate_workdir_operations(paths)?; + let closure = schema_closure(schemas, &roots)?; + + let mut output = format!( + "// @generated by {WORKDIR_GENERATOR_NAME}@{WORKDIR_GENERATOR_VERSION}. Do not edit.\n\ +// Input: {INPUT_PATH}\n\ +// Input contract: {contract_version} ({contract_digest})\n\ +// Options: {WORKDIR_GENERATOR_OPTIONS}\n\ +// Output: {WORKDIR_OUTPUT_PATH}\n\ +// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check\n\n" + ); + for name in closure { + let schema = schemas + .get(&name) + .ok_or_else(|| GenerationError::invalid(format!("missing schema `{name}`")))?; + let declaration = render_schema(schema, &format!("schema `{name}`"))?; + output.push_str(&format!("export type {name} = {declaration};\n\n")); + } + output.pop(); + Ok(output) +} + fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> { let Some(hex) = digest.strip_prefix("sha256:") else { return Err(GenerationError::invalid( @@ -89,8 +136,7 @@ fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> { fn validate_repository_operations( paths: &Map, ) -> Result, GenerationError> { - let mut roots = BTreeSet::new(); - for operation in [ + let operations = [ OperationContract { path: "/api/repositories", method: "get", @@ -129,7 +175,184 @@ fn validate_repository_operations( request: None, successes: &[("200", "RepositoryDetailResponse")], }, - ] { + OperationContract { + path: "/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test", + method: "post", + operation_id: "repository_ssh_connection_probe", + request: Some("RepositorySshConnectionProbeRequest"), + successes: &[("200", "RepositorySshConnectionProbeResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test", + method: "put", + operation_id: "repository_ssh_host_trust_confirm", + request: Some("ConfirmRepositorySshHostTrustRequest"), + successes: &[("200", "RepositorySshHostTrust")], + }, + OperationContract { + path: "/api/w/{workspace_id}/repositories/{repository_key}/log", + method: "get", + operation_id: "repository_log", + request: None, + successes: &[("200", "RepositoryLogResponse")], + }, + OperationContract { + path: "/api/repositories/{repository_key}/log", + method: "get", + operation_id: "repository_log_alias", + request: None, + successes: &[("200", "RepositoryLogResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/hosts", + method: "get", + operation_id: "host_list", + request: None, + successes: &[("200", "HostListResponse")], + }, + OperationContract { + path: "/api/hosts", + method: "get", + operation_id: "host_list_alias", + request: None, + successes: &[("200", "HostListResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/hosts/{host_id}/workers", + method: "get", + operation_id: "host_worker_list", + request: None, + successes: &[("200", "HostWorkerListResponse")], + }, + OperationContract { + path: "/api/hosts/{host_id}/workers", + method: "get", + operation_id: "host_worker_list_alias", + request: None, + successes: &[("200", "HostWorkerListResponse")], + }, + ]; + validate_operation_contracts(paths, &operations) +} + +fn validate_workdir_operations( + paths: &Map, +) -> Result, GenerationError> { + let operations = [ + OperationContract { + path: "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories", + method: "get", + operation_id: "runtime_workdir_list", + request: None, + successes: &[("200", "WorkingDirectoryListResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories", + method: "post", + operation_id: "runtime_workdir_create", + request: Some("WorkingDirectoryCreateRequest"), + successes: &[ + ("200", "WorkingDirectoryCreateResponse"), + ("201", "WorkingDirectoryCreateResponse"), + ], + }, + OperationContract { + path: "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}", + method: "get", + operation_id: "runtime_workdir_detail", + request: None, + successes: &[("200", "WorkingDirectoryDetailResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}", + method: "delete", + operation_id: "runtime_workdir_cleanup", + request: Some("WorkingDirectoryRemovalRequest"), + successes: &[("200", "WorkingDirectoryRemovalResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/workers/self/workdir-attachments", + method: "post", + operation_id: "current_worker_workdir_attach", + request: Some("CurrentWorkerWorkdirAttachRequest"), + successes: &[("200", "CurrentWorkerWorkdirAttachmentResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/workers/self/workdir-attachments/{alias}", + method: "delete", + operation_id: "current_worker_workdir_detach", + request: None, + successes: &[("200", "CurrentWorkerWorkdirAttachmentResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/workers/self/workdir-session/operations", + method: "post", + operation_id: "current_worker_workdir_operation", + request: Some("CurrentWorkerWorkdirOperationRequest"), + successes: &[("200", "WorkdirSessionOperationResult")], + }, + OperationContract { + path: "/api/w/{workspace_id}/external-workdir-grants", + method: "post", + operation_id: "external_workdir_grant_create", + request: Some("ExternalWorkdirGrantCreateRequest"), + successes: &[("201", "ExternalWorkdirGrantResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/external-workdir-grants/{grant_id}", + method: "get", + operation_id: "external_workdir_grant_get", + request: None, + successes: &[("200", "ExternalWorkdirGrantResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/external-workdir-grants/{grant_id}", + method: "delete", + operation_id: "external_workdir_grant_revoke", + request: None, + successes: &[("200", "ExternalWorkdirGrantResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/working-directories", + method: "get", + operation_id: "workspace_workdir_list", + request: None, + successes: &[("200", "WorkingDirectoryListResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/working-directories", + method: "post", + operation_id: "workspace_workdir_create", + request: Some("WorkingDirectoryCreateRequest"), + successes: &[ + ("200", "WorkingDirectoryCreateResponse"), + ("201", "WorkingDirectoryCreateResponse"), + ], + }, + OperationContract { + path: "/api/w/{workspace_id}/working-directories/{working_directory_id}", + method: "get", + operation_id: "workspace_workdir_detail", + request: None, + successes: &[("200", "WorkingDirectoryDetailResponse")], + }, + OperationContract { + path: "/api/w/{workspace_id}/working-directories/{working_directory_id}", + method: "delete", + operation_id: "workspace_workdir_cleanup", + request: Some("WorkingDirectoryRemovalRequest"), + successes: &[("200", "WorkingDirectoryRemovalResponse")], + }, + ]; + validate_operation_contracts(paths, &operations) +} + +fn validate_operation_contracts( + paths: &Map, + operations: &[OperationContract], +) -> Result, GenerationError> { + let mut roots = BTreeSet::new(); + for operation in operations { let path = object_field(paths, operation.path, "OpenAPI paths")?; let value = object_field(path, operation.method, operation.path)?; if value.get("operationId").and_then(Value::as_str) != Some(operation.operation_id) { @@ -167,10 +390,13 @@ fn validate_repository_operations( &format!("{} response {status}", operation.operation_id), )?; let schema = json_content_schema(response, operation.operation_id)?; - let expected = successes - .get(status.as_str()) - .copied() - .unwrap_or("RepositoryApiError"); + let expected = successes.get(status.as_str()).copied().unwrap_or_else(|| { + if operation.operation_id == "current_worker_workdir_operation" { + "WorkdirOperationApiError" + } else { + "RepositoryApiError" + } + }); require_schema_ref(schema, expected, operation.operation_id)?; roots.insert(expected.to_owned()); } @@ -315,6 +541,27 @@ fn literal_matches_type(kind: &str, value: &Value) -> bool { ) } +fn tagged_object_discriminator(value: &Value) -> Option<(String, String)> { + let schema = value.as_object()?; + if schema.get("type")?.as_str()? != "object" + || schema.get("additionalProperties") != Some(&Value::Bool(false)) + { + return None; + } + let required = schema.get("required")?.as_array()?; + let properties = schema.get("properties")?.as_object()?; + properties.iter().find_map(|(name, property)| { + if !required + .iter() + .any(|required| required.as_str() == Some(name)) + { + return None; + } + let literal = property.as_object()?.get("const")?.as_str()?; + Some((name.clone(), literal.to_owned())) + }) +} + fn literal_schema_matches_type(schema: &Map, kind: &str) -> bool { if let Some(constant) = schema.get("const") { return literal_matches_type(kind, constant); @@ -328,6 +575,14 @@ fn literal_schema_matches_type(schema: &Map, kind: &str) -> bool } fn render_schema(value: &Value, context: &str) -> Result { + if value == &Value::Bool(true) { + return Ok("unknown".to_owned()); + } + if value == &Value::Bool(false) { + return Err(GenerationError::invalid(format!( + "{context} rejects every value" + ))); + } let schema = object(value, context)?; if schema.contains_key("$ref") { let reference = schema_reference(value, context)?; @@ -403,6 +658,34 @@ fn render_schema(value: &Value, context: &str) -> Result>>(); + if let Some(discriminators) = tagged_discriminators { + if outer_type.is_some() { + return Err(GenerationError::invalid(format!( + "{context} combines a tagged object union with an outer type" + ))); + } + let Some((discriminator_name, _)) = discriminators.first() else { + unreachable!("empty oneOf rejected above"); + }; + let mut seen = BTreeSet::new(); + for (name, literal) in &discriminators { + if name != discriminator_name || !seen.insert(literal.clone()) { + return Err(GenerationError::invalid(format!( + "{context} contains overlapping tagged oneOf branches" + ))); + } + } + return branches + .iter() + .map(|branch| render_schema(branch, context)) + .collect::, _>>() + .map(|branches| branches.join(" | ")); + } + let mut rendered = Vec::with_capacity(branches.len()); let mut seen_literals = BTreeSet::new(); for branch in branches { @@ -438,12 +721,12 @@ fn render_schema(value: &Value, context: &str) -> Result { ensure_schema_keywords(schema, &["format", "maximum", "minimum", "type"], context)?; let format = match schema.get("format") { - Some(Value::String(format)) if format == "uint64" => Some(format.as_str()), + Some(Value::String(format)) + if matches!( + format.as_str(), + "uint" | "uint8" | "uint16" | "uint32" | "uint64" | "int32" + ) => + { + Some(format.as_str()) + } Some(Value::String(format)) => { return Err(GenerationError::invalid(format!( "{context} uses unsupported integer format `{format}`" @@ -545,12 +835,16 @@ fn render_typed_schema( }; let minimum = schema.get("minimum").and_then(Value::as_f64); let maximum = schema.get("maximum").and_then(Value::as_f64); - let expected_minimum = if format == Some("uint64") { - 0.0 - } else { - MIN_SAFE_INTEGER + let (expected_minimum, expected_maximum) = match format { + Some("uint") | Some("uint64") => (0.0, MAX_SAFE_INTEGER), + Some("uint8") => (0.0, u8::MAX as f64), + Some("uint16") => (0.0, u16::MAX as f64), + Some("uint32") => (0.0, u32::MAX as f64), + Some("int32") => (i32::MIN as f64, i32::MAX as f64), + None => (MIN_SAFE_INTEGER, MAX_SAFE_INTEGER), + Some(_) => unreachable!("integer format validated above"), }; - if minimum != Some(expected_minimum) || maximum != Some(MAX_SAFE_INTEGER) { + if minimum != Some(expected_minimum) || maximum != Some(expected_maximum) { return Err(GenerationError::invalid(format!( "{context} integer range does not match its JavaScript safe-number mapping" ))); @@ -558,11 +852,35 @@ fn render_typed_schema( Ok("number".to_owned()) } "array" => { - ensure_schema_keywords(schema, &["items", "type"], context)?; + ensure_schema_keywords(schema, &["items", "maxItems", "minItems", "type"], context)?; let items = schema.get("items").ok_or_else(|| { GenerationError::invalid(format!("{context} array has no item schema")) })?; - Ok(format!("Array<{}>", render_schema(items, context)?)) + let item = render_schema(items, context)?; + match (schema.get("minItems"), schema.get("maxItems")) { + (None, None) => Ok(format!("Array<{item}>")), + (Some(minimum), Some(maximum)) if minimum == maximum => { + let length = minimum.as_u64().ok_or_else(|| { + GenerationError::invalid(format!( + "{context} fixed array length must be a non-negative integer" + )) + })?; + if length > 64 { + return Err(GenerationError::invalid(format!( + "{context} fixed array length exceeds the generator limit" + ))); + } + Ok(format!( + "[{}]", + std::iter::repeat_n(item, length as usize) + .collect::>() + .join(", ") + )) + } + _ => Err(GenerationError::invalid(format!( + "{context} contains unsupported variable array bounds" + ))), + } } "object" => { ensure_schema_keywords( @@ -778,13 +1096,13 @@ mod tests { ); let mut document: Value = serde_json::from_str(OPENAPI).unwrap(); - document["components"]["schemas"]["RepositoryListResponse"]["properties"]["items"]["maxItems"] = - serde_json::json!(1); + document["components"]["schemas"]["RepositoryListResponse"]["properties"]["items"]["uniqueItems"] = + serde_json::json!(true); let error = generate_repository_typescript(&document.to_string()).unwrap_err(); assert!( error .to_string() - .contains("unsupported schema keyword `maxItems`") + .contains("unsupported schema keyword `uniqueItems`") ); } diff --git a/crates/workdir/Cargo.toml b/crates/workdir/Cargo.toml index 22c70a495..b5b7a99c8 100644 --- a/crates/workdir/Cargo.toml +++ b/crates/workdir/Cargo.toml @@ -13,6 +13,7 @@ async-trait.workspace = true fs-operation.workspace = true manifest.workspace = true reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"], optional = true } +schemars.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sha2.workspace = true diff --git a/crates/workdir/src/http.rs b/crates/workdir/src/http.rs index 00fa2330f..0a3f7e8aa 100644 --- a/crates/workdir/src/http.rs +++ b/crates/workdir/src/http.rs @@ -5,6 +5,7 @@ //! optional so Runtime servers can share these DTOs without depending on a //! client stack. +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ @@ -16,7 +17,7 @@ use crate::{ }; /// Opaque Runtime-owned identifier for one ephemeral Workdir session. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[serde(transparent)] pub struct WorkdirSessionId(String); @@ -37,7 +38,7 @@ impl WorkdirSessionId { } /// Open a fresh session for a persisted Workdir identity. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct OpenWorkdirSessionRequest { /// Optional Runtime Worker whose persisted binding establishes workspace /// ownership of the Workdir. Runtime servers reject cross-workspace owners. @@ -45,7 +46,7 @@ pub struct OpenWorkdirSessionRequest { pub owner_worker_id: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct OpenWorkdirSessionResponse { pub session_id: WorkdirSessionId, pub workdir_id: WorkdirId, @@ -53,7 +54,7 @@ pub struct OpenWorkdirSessionResponse { } /// One provider-side Workdir operation. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde( tag = "operation", content = "request", @@ -77,14 +78,14 @@ pub enum WorkdirSessionOperation { } /// Wire envelope for one provider operation. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WorkdirSessionOperationRequest { pub operation: WorkdirSessionOperation, } /// Typed result paired with [`WorkdirSessionOperation`]. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde( tag = "operation", content = "result", @@ -174,7 +175,7 @@ pub async fn dispatch_workdir_session_operation( } /// Stable, host-path-free error code crossing the Runtime boundary. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum WorkdirTransportErrorCode { NotFound, @@ -238,7 +239,7 @@ impl WorkdirTransportErrorCode { } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct WorkdirTransportError { pub code: WorkdirTransportErrorCode, pub message: String, diff --git a/crates/workdir/src/lib.rs b/crates/workdir/src/lib.rs index 6a13a5b46..e162b8d9e 100644 --- a/crates/workdir/src/lib.rs +++ b/crates/workdir/src/lib.rs @@ -20,6 +20,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use async_trait::async_trait; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; @@ -45,7 +46,7 @@ pub use scope::{ }; /// Persistent, opaque identity of one materialized Workdir. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct Workdir { id: WorkdirId, } @@ -63,7 +64,7 @@ impl Workdir { } /// Opaque Workdir identifier assigned by the materialization authority. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[serde(transparent)] pub struct WorkdirId(String); @@ -79,7 +80,7 @@ impl std::fmt::Display for WorkdirId { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum WorkdirSessionCapability { Read, @@ -90,7 +91,7 @@ pub enum WorkdirSessionCapability { Command, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct WorkdirSessionCapabilities { bits: u8, } diff --git a/crates/workdir/src/operation.rs b/crates/workdir/src/operation.rs index 5af8b54a3..c0c242774 100644 --- a/crates/workdir/src/operation.rs +++ b/crates/workdir/src/operation.rs @@ -1,15 +1,18 @@ use std::path::PathBuf; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[serde(transparent)] pub struct CommandHandle(pub String); -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct CommandRequest { pub command: String, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub timeout_secs: u64, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub output_limit: usize, /// Workdir-relative command directory. Providers validate it against the /// active session before process start. @@ -25,15 +28,17 @@ pub struct CommandRequest { pub tool_call_id: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct CommandOutputRequest { pub handle: CommandHandle, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub cursor: usize, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub limit: usize, pub wait: bool, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum CommandStatus { Running, @@ -43,35 +48,41 @@ pub enum CommandStatus { Cancelled, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum CommandStream { Stdout, Stderr, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)] pub struct CommandStreamSlice { + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub start_offset: u64, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub end_offset: u64, pub content: String, pub truncated: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct CommandSnapshot { pub command_id: String, pub tool_call_id: Option, pub status: CommandStatus, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub started_at_ms: u64, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub observed_at_ms: u64, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] pub last_output_at_ms: Option, pub stdout: CommandStreamSlice, pub stderr: CommandStreamSlice, + #[schemars(range(min = -2147483648_i32, max = 2147483647_i32))] pub exit_code: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum CommandEvent { Started { @@ -97,12 +108,14 @@ pub enum CommandEvent { }, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct CommandOutput { pub status: CommandStatus, + #[schemars(range(min = -2147483648_i32, max = 2147483647_i32))] pub exit_code: Option, pub timed_out: bool, pub content: String, + #[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))] pub next_cursor: Option, pub truncated: bool, /// Complete output retained by the provider when `truncated` is true. diff --git a/crates/workdir/src/scope.rs b/crates/workdir/src/scope.rs index 7eadad4e9..fb5aabe74 100644 --- a/crates/workdir/src/scope.rs +++ b/crates/workdir/src/scope.rs @@ -9,6 +9,7 @@ use fs_operation::{ ListResult, ReadRequest, ReadResult, StatRequest, StatResult, WriteRequest, WriteResult, }; use manifest::SymlinkPolicy; +use schemars::JsonSchema; use tokio::sync::broadcast; const MAX_SCOPED_COMMANDS: usize = 16; @@ -19,25 +20,26 @@ use crate::{ WorkdirSessionCapabilities, WorkdirSessionCapability, WorkdirSessionHandle, }; -#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum WorkdirToolScopePermission { Read, Write, } -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WorkdirToolScopeRule { pub target: FsPath, pub permission: WorkdirToolScopePermission, pub recursive: bool, #[serde(default)] + #[schemars(with = "String")] pub symlink_policy: SymlinkPolicy, } /// Provider-side check for one operation under an attenuated tool scope. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WorkdirScopeAuthorizationRequest { pub rules: Vec, @@ -46,14 +48,14 @@ pub struct WorkdirScopeAuthorizationRequest { } /// Provider-side overlap comparison that keeps resolved host paths private. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WorkdirScopeOverlapRequest { pub left: WorkdirToolScopeRule, pub right: WorkdirToolScopeRule, } -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WorkdirToolScope { pub rules: Vec, diff --git a/crates/workdir/src/workspace.rs b/crates/workdir/src/workspace.rs index c945accd4..2e7c57582 100644 --- a/crates/workdir/src/workspace.rs +++ b/crates/workdir/src/workspace.rs @@ -4,9 +4,10 @@ //! transport projections. They intentionally do not expose provider/session //! handles, host paths, Runtime URLs, or credentials. +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum WorkingDirectoryMaterializerKind { #[default] @@ -14,7 +15,7 @@ pub enum WorkingDirectoryMaterializerKind { ClientHostedExternal, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum WorkingDirectoryStatusKind { Active, @@ -42,7 +43,7 @@ impl std::fmt::Display for WorkingDirectoryStatusKind { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryCleanupTarget { pub kind: String, @@ -50,7 +51,7 @@ pub struct WorkingDirectoryCleanupTarget { pub repository_key: String, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryOccupancy { pub runtime_id: String, @@ -59,7 +60,7 @@ pub struct WorkingDirectoryOccupancy { pub linked_at: String, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct RuntimeWorkingDirectoryCleanupTarget { pub kind: String, @@ -67,7 +68,7 @@ pub struct RuntimeWorkingDirectoryCleanupTarget { pub repository_id: String, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct RuntimeWorkingDirectorySummary { pub working_directory_id: String, @@ -99,14 +100,14 @@ pub struct RuntimeWorkingDirectorySummary { pub occupied_by: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum WorkingDirectorySource { Repository { repository_key: String }, ExternalGrant { grant_id: String }, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct WorkingDirectorySummary { pub working_directory_id: String, @@ -146,7 +147,9 @@ impl WorkingDirectorySummary { 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)] +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] #[serde(deny_unknown_fields)] pub struct RuntimeWorkerRef { pub runtime_id: String, @@ -164,7 +167,7 @@ impl RuntimeWorkerRef { } /// Immutable materialization provenance retained by Workspace inventory. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryProvenance { #[serde(default, skip_serializing_if = "Option::is_none")] @@ -179,7 +182,7 @@ pub struct WorkingDirectoryProvenance { } /// Latest provider-neutral observation attached to Workspace inventory. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WorkingDirectoryCurrentObservation { #[serde(default, skip_serializing_if = "Option::is_none")] @@ -247,7 +250,7 @@ mod tests { } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct WorkspaceWorkdirSessionOperationRequest { /// Worker-local attachment alias selected by the calling tool router. diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index f1d80a494..dfe5d72b9 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -291,6 +291,23 @@ impl From for server_api::Diagnostic { } } +impl From for server_api::HostSummary { + fn from(host: HostSummary) -> Self { + Self { + runtime_id: host.runtime_id, + host_id: host.host_id, + label: host.label, + kind: host.kind, + status: host.status, + observed_at: host.observed_at, + last_seen_at: host.last_seen_at, + os: host.os, + arch: host.arch, + diagnostics: host.diagnostics.into_iter().map(Into::into).collect(), + } + } +} + impl From for server_api::RuntimeSourceSummary { fn from(source: RuntimeSourceSummary) -> Self { let kind = match source.kind { diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 4d5bb7ee4..91e79e95a 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -3641,11 +3641,13 @@ fn build_server_auth_router(api: ServerAuthApi) -> Router { fn server_request_context( actor: Option, + worker_source: Option, origin: Option, headers: &HeaderMap, ) -> server_api::ServerRequestContext { server_api::ServerRequestContext { actor, + worker_source, origin, transport_headers: headers .iter() @@ -3683,7 +3685,7 @@ fn contract_request_headers( async fn attach_server_origin_context(mut request: Request, next: Next) -> Response { let actor = request.extensions().get::().cloned(); let origin = request_origin(request.headers()); - let context = server_request_context(actor, origin, request.headers()); + let context = server_request_context(actor, None, origin, request.headers()); request.extensions_mut().insert(context); next.run(request).await } @@ -3708,8 +3710,15 @@ async fn attach_server_request_context( Err(error) => return server_error_response(error), } }; + let worker_source = request + .extensions() + .get::() + .map(|source| server_api::ServerWorkerSource { + runtime_id: source.runtime_id.clone(), + worker_id: source.worker_id.clone(), + }); let origin = request_origin(request.headers()); - let context = server_request_context(actor, origin, request.headers()); + let context = server_request_context(actor, worker_source, origin, request.headers()); request.extensions_mut().insert(context); next.run(request).await } @@ -3865,6 +3874,62 @@ fn generated_workspace_contract_router(service: ServerApiContractService) -> Rou .merge(server_api::server_api_axum::repository_create( service.clone(), )) + .merge(server_api::server_api_axum::repository_ssh_connection_probe(service.clone())) + .merge(server_api::server_api_axum::repository_ssh_host_trust_confirm(service.clone())) + .merge(server_api::server_api_axum::repository_log(service.clone())) + .merge(server_api::server_api_axum::repository_log_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::host_list(service.clone())) + .merge(server_api::server_api_axum::host_list_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::host_worker_list( + service.clone(), + )) + .merge(server_api::server_api_axum::host_worker_list_alias( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_workdir_list( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_workdir_create( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_workdir_detail( + service.clone(), + )) + .merge(server_api::server_api_axum::runtime_workdir_cleanup( + service.clone(), + )) + .merge(server_api::server_api_axum::current_worker_workdir_attach( + service.clone(), + )) + .merge(server_api::server_api_axum::current_worker_workdir_detach( + service.clone(), + )) + .merge(server_api::server_api_axum::current_worker_workdir_operation(service.clone())) + .merge(server_api::server_api_axum::external_workdir_grant_create( + service.clone(), + )) + .merge(server_api::server_api_axum::external_workdir_grant_get( + service.clone(), + )) + .merge(server_api::server_api_axum::external_workdir_grant_revoke( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_workdir_list( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_workdir_create( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_workdir_detail( + service.clone(), + )) + .merge(server_api::server_api_axum::workspace_workdir_cleanup( + service.clone(), + )) .merge(server_api::server_api_axum::ticket_list_alias( service.clone(), )) @@ -6103,6 +6168,451 @@ impl server_api::ServerApi for ServerApiContractService { .map_err(ApiError::into_repository_api_error)?; project_contract_dto(&response) } + + async fn repository_ssh_connection_probe( + &self, + actor: RequestActor, + workspace_id: String, + repository_key: String, + request: server_api::RepositorySshConnectionProbeRequest, + ) -> std::result::Result< + server_api::RepositorySshConnectionProbeResponse, + server_api::RepositoryApiError, + > { + let Json(response) = scoped_probe_repository_ssh_connection( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRepositoryPath { + workspace_id, + repository_key, + }), + Extension(actor), + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn repository_ssh_host_trust_confirm( + &self, + actor: RequestActor, + workspace_id: String, + repository_key: String, + request: server_api::ConfirmRepositorySshHostTrustRequest, + ) -> std::result::Result + { + let Json(response) = scoped_confirm_repository_ssh_host_trust( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRepositoryPath { + workspace_id, + repository_key, + }), + Extension(actor), + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn repository_log( + &self, + workspace_id: String, + repository_key: String, + query: server_api::RepositoryLogQuery, + ) -> std::result::Result + { + let Json(response) = scoped_repository_log( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRepositoryPath { + workspace_id, + repository_key, + }), + Query(LogQuery { limit: query.limit }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn repository_log_alias( + &self, + repository_key: String, + query: server_api::RepositoryLogQuery, + ) -> std::result::Result + { + let Json(response) = repository_log( + State(self.workspace_api()?.clone()), + AxumPath(repository_key), + Query(LogQuery { limit: query.limit }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn host_list( + &self, + workspace_id: String, + ) -> std::result::Result { + let Json(response) = scoped_list_hosts( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(server_api::HostListResponse { + workspace_id: response.workspace_id, + limit: response.limit, + items: response.items.into_iter().map(Into::into).collect(), + source: response.source, + diagnostics: response.diagnostics.into_iter().map(Into::into).collect(), + }) + } + + async fn host_list_alias( + &self, + ) -> std::result::Result { + let Json(response) = list_hosts(State(self.workspace_api()?.clone())) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(server_api::HostListResponse { + workspace_id: response.workspace_id, + limit: response.limit, + items: response.items.into_iter().map(Into::into).collect(), + source: response.source, + diagnostics: response.diagnostics.into_iter().map(Into::into).collect(), + }) + } + + async fn host_worker_list( + &self, + workspace_id: String, + host_id: String, + ) -> std::result::Result + { + let Json(response) = scoped_list_host_workers( + State(self.workspace_api()?.clone()), + AxumPath(ScopedHostPath { + workspace_id, + host_id, + }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn host_worker_list_alias( + &self, + host_id: String, + ) -> std::result::Result + { + let Json(response) = + list_host_workers(State(self.workspace_api()?.clone()), AxumPath(host_id)) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn runtime_workdir_list( + &self, + workspace_id: String, + runtime_id: String, + ) -> std::result::Result + { + let Json(response) = scoped_list_runtime_working_directories( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRuntimePath { + workspace_id, + runtime_id, + }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn runtime_workdir_create( + &self, + workspace_id: String, + runtime_id: String, + request: server_api::WorkingDirectoryCreateRequest, + ) -> std::result::Result< + server_api::WorkingDirectoryCreateResponse, + server_api::RepositoryApiError, + > { + let (_, Json(response)) = scoped_create_runtime_working_directory( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRuntimePath { + workspace_id, + runtime_id, + }), + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn runtime_workdir_detail( + &self, + workspace_id: String, + runtime_id: String, + working_directory_id: String, + ) -> std::result::Result< + server_api::WorkingDirectoryDetailResponse, + server_api::RepositoryApiError, + > { + let Json(response) = scoped_runtime_working_directory_detail( + State(self.workspace_api()?.clone()), + AxumPath(ScopedRuntimeWorkingDirectoryPath { + workspace_id, + runtime_id, + working_directory_id, + }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn runtime_workdir_cleanup( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + runtime_id: String, + working_directory_id: String, + request: server_api::WorkingDirectoryRemovalRequest, + ) -> std::result::Result< + server_api::WorkingDirectoryRemovalResponse, + server_api::RepositoryApiError, + > { + cleanup_runtime_working_directory_contract( + self.workspace_api()?.clone(), + workspace_id, + runtime_id, + working_directory_id, + context, + request, + ) + .await + .map_err(ApiError::into_repository_api_error) + } + + async fn current_worker_workdir_attach( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::CurrentWorkerWorkdirAttachRequest, + ) -> std::result::Result< + server_api::CurrentWorkerWorkdirAttachmentResponse, + server_api::RepositoryApiError, + > { + let Json(response) = scoped_attach_current_worker_workdir( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + contract_request_headers(&context)?, + Json(AttachCurrentWorkerWorkdirRequest { + alias: request.alias, + working_directory_id: request.working_directory_id, + }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(server_api::CurrentWorkerWorkdirAttachmentResponse { + workspace_id: response.workspace_id, + alias: response.alias, + working_directory_id: response.working_directory_id, + capabilities: response.capabilities, + attached: response.attached, + }) + } + + async fn current_worker_workdir_detach( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + alias: String, + ) -> std::result::Result< + server_api::CurrentWorkerWorkdirAttachmentResponse, + server_api::RepositoryApiError, + > { + let Json(response) = scoped_detach_current_worker_workdir( + State(self.workspace_api()?.clone()), + AxumPath((workspace_id, alias)), + contract_request_headers(&context)?, + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(server_api::CurrentWorkerWorkdirAttachmentResponse { + workspace_id: response.workspace_id, + alias: response.alias, + working_directory_id: response.working_directory_id, + capabilities: response.capabilities, + attached: response.attached, + }) + } + + async fn current_worker_workdir_operation( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + request: server_api::CurrentWorkerWorkdirOperationRequest, + ) -> std::result::Result< + server_api::CurrentWorkerWorkdirOperationResponse, + server_api::WorkdirOperationApiError, + > { + let Json(response) = scoped_execute_current_worker_workdir_operation( + State( + self.workspace_api() + .map_err(server_api::WorkdirOperationApiError::api)? + .clone(), + ), + AxumPath(ScopedWorkspacePath { workspace_id }), + contract_request_headers(&context) + .map_err(server_api::WorkdirOperationApiError::api)?, + Json(WorkspaceWorkdirSessionOperationRequest { + target_workdir: request.target_workdir, + operation: request.operation, + }), + ) + .await + .map_err(|error| match error { + WorkdirOperationApiError::Api(error) => { + server_api::WorkdirOperationApiError::api(error.into_repository_api_error()) + } + WorkdirOperationApiError::Provider(error) => { + server_api::WorkdirOperationApiError::provider(error) + } + })?; + Ok(server_api::CurrentWorkerWorkdirOperationResponse(response)) + } + + async fn external_workdir_grant_create( + &self, + actor: RequestActor, + workspace_id: String, + request: server_api::ExternalWorkdirGrantCreateRequest, + ) -> std::result::Result + { + let (_, Json(response)) = scoped_create_external_workdir_grant( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + Extension(actor), + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn external_workdir_grant_get( + &self, + actor: RequestActor, + workspace_id: String, + grant_id: String, + ) -> std::result::Result + { + let Json(response) = scoped_get_external_workdir_grant( + State(self.workspace_api()?.clone()), + Extension(actor), + AxumPath((workspace_id, grant_id)), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn external_workdir_grant_revoke( + &self, + actor: RequestActor, + workspace_id: String, + grant_id: String, + ) -> std::result::Result + { + let Json(response) = scoped_revoke_external_workdir_grant( + State(self.workspace_api()?.clone()), + Extension(actor), + AxumPath((workspace_id, grant_id)), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn workspace_workdir_list( + &self, + workspace_id: String, + ) -> std::result::Result + { + let Json(response) = scoped_list_working_directories( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn workspace_workdir_create( + &self, + workspace_id: String, + request: server_api::WorkingDirectoryCreateRequest, + ) -> std::result::Result< + server_api::WorkingDirectoryCreateResponse, + server_api::RepositoryApiError, + > { + let (_, Json(response)) = scoped_create_working_directory( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkspacePath { workspace_id }), + Json(request), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn workspace_workdir_detail( + &self, + workspace_id: String, + working_directory_id: String, + ) -> std::result::Result< + server_api::WorkingDirectoryDetailResponse, + server_api::RepositoryApiError, + > { + let Json(response) = scoped_working_directory_detail( + State(self.workspace_api()?.clone()), + AxumPath(ScopedWorkingDirectoryPath { + workspace_id, + working_directory_id, + }), + ) + .await + .map_err(ApiError::into_repository_api_error)?; + Ok(response) + } + + async fn workspace_workdir_cleanup( + &self, + context: server_api::ServerRequestContext, + workspace_id: String, + working_directory_id: String, + request: server_api::WorkingDirectoryRemovalRequest, + ) -> std::result::Result< + server_api::WorkingDirectoryRemovalResponse, + server_api::RepositoryApiError, + > { + cleanup_working_directory_contract( + self.workspace_api()?.clone(), + workspace_id, + working_directory_id, + context, + request, + ) + .await + .map_err(ApiError::into_repository_api_error) + } } fn build_inner_router(api: WorkspaceApi) -> Router { @@ -6110,62 +6620,15 @@ fn build_inner_router(api: WorkspaceApi) -> Router { let auth = build_server_auth_router(ServerAuthApi::from(&api)) .merge(generated_auth_contract_router(contract_service.clone())); let workspace = Router::new() - .route( - "/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test", - post(scoped_probe_repository_ssh_connection) - .put(scoped_confirm_repository_ssh_host_trust), - ) - .route("/api/repositories/{repository_key}/log", get(repository_log)) - .route( - "/api/w/{workspace_id}/repositories/{repository_key}/log", - get(scoped_repository_log), - ) - .route("/api/hosts", get(list_hosts)) - .route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts)) - .route( - "/api/w/{workspace_id}/runtime-config", - get(get_latest_workspace_runtime_config), - ) - .route( - "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories", - get(scoped_list_runtime_working_directories).post(scoped_create_runtime_working_directory), - ) - .route( - "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}", - get(scoped_runtime_working_directory_detail).delete(scoped_cleanup_runtime_working_directory), - ) - .route( - "/api/w/{workspace_id}/workers/self/workdir-attachments", - post(scoped_attach_current_worker_workdir), - ) - .route( - "/api/w/{workspace_id}/workers/self/workdir-attachments/{alias}", - delete(scoped_detach_current_worker_workdir), - ) - .route( - "/api/w/{workspace_id}/workers/self/workdir-session/operations", - post(scoped_execute_current_worker_workdir_operation), - ) - .route( - "/api/w/{workspace_id}/external-workdir-grants", - post(scoped_create_external_workdir_grant), - ) - .route( - "/api/w/{workspace_id}/external-workdir-grants/{grant_id}", - get(scoped_get_external_workdir_grant) - .delete(scoped_revoke_external_workdir_grant), - ) + // WebSocket provider transport is intentionally outside the generated + // JSON REST contract. .route( "/api/w/{workspace_id}/external-workdir-grants/{grant_id}/provider", get(scoped_external_workdir_provider_ws), ) .route( - "/api/w/{workspace_id}/working-directories", - get(scoped_list_working_directories).post(scoped_create_working_directory), - ) - .route( - "/api/w/{workspace_id}/working-directories/{working_directory_id}", - get(scoped_working_directory_detail).delete(scoped_cleanup_working_directory), + "/api/w/{workspace_id}/runtime-config", + get(get_latest_workspace_runtime_config), ) .route("/api/runtimes", get(list_runtimes)) .route( @@ -6381,11 +6844,6 @@ fn build_inner_router(api: WorkspaceApi) -> Router { "/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/protocol/ws", get(scoped_worker_protocol_ws), ) - .route("/api/hosts/{host_id}/workers", get(list_host_workers)) - .route( - "/api/w/{workspace_id}/hosts/{host_id}/workers", - get(scoped_list_host_workers), - ) .fallback(get(static_or_spa_fallback)) .with_state(api) .merge(generated_workspace_contract_router(contract_service)); @@ -13759,13 +14217,81 @@ async fn scoped_runtime_working_directory_detail( working_directory_detail_for_runtime(api, &path.runtime_id, &path.working_directory_id) } -async fn scoped_cleanup_runtime_working_directory( - State(api): State, - AxumPath(path): AxumPath, - worker_source: Option>, - request_actor: Option>, - Json(request): Json, -) -> ApiResult> { +fn workdir_removal_source_actor( + worker_source: Option<(&str, &str)>, + request_actor: Option<&RequestActor>, +) -> ApiResult { + if let Some((runtime_id, worker_id)) = worker_source { + Ok(format!("worker:{runtime_id}:{worker_id}")) + } else if let Some(actor) = request_actor { + Ok(format!("account:{}", actor.account_id)) + } else { + Err(ApiError::from(Error::WorkspacePermissionDenied( + "Workdir removal requires authenticated source authority".to_string(), + ))) + } +} + +fn contract_workdir_removal_source_actor( + context: &server_api::ServerRequestContext, +) -> ApiResult { + workdir_removal_source_actor( + context + .worker_source + .as_ref() + .map(|source| (source.runtime_id.as_str(), source.worker_id.as_str())), + context.actor.as_ref(), + ) +} + +async fn cleanup_runtime_working_directory_contract( + api: WorkspaceApi, + workspace_id: String, + runtime_id: String, + working_directory_id: String, + context: server_api::ServerRequestContext, + request: WorkingDirectoryRemovalRequest, +) -> ApiResult { + let source_actor = contract_workdir_removal_source_actor(&context)?; + cleanup_runtime_working_directory( + api, + ScopedRuntimeWorkingDirectoryPath { + workspace_id, + runtime_id, + working_directory_id, + }, + source_actor, + request, + ) + .await +} + +async fn cleanup_working_directory_contract( + api: WorkspaceApi, + workspace_id: String, + working_directory_id: String, + context: server_api::ServerRequestContext, + request: WorkingDirectoryRemovalRequest, +) -> ApiResult { + let source_actor = contract_workdir_removal_source_actor(&context)?; + cleanup_working_directory( + api, + ScopedWorkingDirectoryPath { + workspace_id, + working_directory_id, + }, + source_actor, + request, + ) + .await +} + +async fn cleanup_runtime_working_directory( + api: WorkspaceApi, + path: ScopedRuntimeWorkingDirectoryPath, + source_actor: String, + request: WorkingDirectoryRemovalRequest, +) -> ApiResult { validate_workspace_scope(&api, &path.workspace_id)?; require_active_workspace_runtime_binding(&api, &path.runtime_id).await?; let registered_runtime = registered_workdir_runtime_id(&api, &path.working_directory_id)?; @@ -13774,22 +14300,12 @@ async fn scoped_cleanup_runtime_working_directory( "Workdir does not belong to the requested Runtime".to_string(), ))); } - let source_actor = if let Some(Extension(source)) = worker_source { - format!("worker:{}:{}", source.runtime_id, source.worker_id) - } else if let Some(Extension(actor)) = request_actor { - format!("account:{}", actor.account_id) - } else { - return Err(ApiError::from(Error::WorkspacePermissionDenied( - "Workdir removal requires authenticated source authority".to_string(), - ))); - }; execute_workdir_removal( &api, &path.working_directory_id, &source_actor, &request.reason, ) - .map(Json) .map_err(ApiError::from) } @@ -14736,30 +15252,19 @@ async fn scoped_working_directory_detail( } } -async fn scoped_cleanup_working_directory( - State(api): State, - AxumPath(path): AxumPath, - worker_source: Option>, - request_actor: Option>, - Json(request): Json, -) -> ApiResult> { +async fn cleanup_working_directory( + api: WorkspaceApi, + path: ScopedWorkingDirectoryPath, + source_actor: String, + request: WorkingDirectoryRemovalRequest, +) -> ApiResult { validate_workspace_scope(&api, &path.workspace_id)?; - let source_actor = if let Some(Extension(source)) = worker_source { - format!("worker:{}:{}", source.runtime_id, source.worker_id) - } else if let Some(Extension(actor)) = request_actor { - format!("account:{}", actor.account_id) - } else { - return Err(ApiError::from(Error::WorkspacePermissionDenied( - "Workdir removal requires authenticated source authority".to_string(), - ))); - }; execute_workdir_removal( &api, &path.working_directory_id, &source_actor, &request.reason, ) - .map(Json) .map_err(ApiError::from) } @@ -15022,6 +15527,7 @@ async fn create_workspace_working_directory( .expect("Repository Workdir detail has Runtime"), item: response.item, diagnostics: response.diagnostics, + http_status: StatusCode::OK.as_u16(), }), ) }); @@ -15217,6 +15723,7 @@ async fn create_workspace_working_directory( runtime_id: reserved.resolved_runtime_id, item: summary.into(), diagnostics: working_directory_diagnostics(result.diagnostics), + http_status: StatusCode::CREATED.as_u16(), }), )) } @@ -16894,7 +17401,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 } @@ -20394,14 +20901,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(server_api::ListResponse { + Ok(Json(server_api::HostWorkerListResponse { workspace_id: api.workspace_id().to_string(), limit, items, diff --git a/openapi/server-api.json b/openapi/server-api.json index b2eaccb9b..19327a623 100644 --- a/openapi/server-api.json +++ b/openapi/server-api.json @@ -473,6 +473,143 @@ }, "type": "object" }, + "CommandOutput": { + "properties": { + "content": { + "type": "string" + }, + "exit_code": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": [ + "integer", + "null" + ] + }, + "next_cursor": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "output_path": { + "description": "Complete output retained by the provider when `truncated` is true.", + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/components/schemas/CommandStatus" + }, + "timed_out": { + "type": "boolean" + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "status", + "timed_out", + "content", + "truncated" + ], + "type": "object" + }, + "CommandOutputRequest": { + "properties": { + "cursor": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "wait": { + "type": "boolean" + } + }, + "required": [ + "handle", + "cursor", + "limit", + "wait" + ], + "type": "object" + }, + "CommandRequest": { + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "anyOf": [ + { + "$ref": "#/components/schemas/FsPath" + }, + { + "type": "null" + } + ], + "description": "Workdir-relative command directory. Providers validate it against the\nactive session before process start." + }, + "output_limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "spill_dir": { + "description": "Provider-local directory where complete output is retained when the\ninline result exceeds `output_limit`.", + "type": [ + "string", + "null" + ] + }, + "timeout_secs": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "tool_call_id": { + "description": "Optional caller-owned correlation id. Bash supplies its tool-call id so\nuser-facing command telemetry can update the corresponding Console row\nwithout exposing provider/session handles.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "command", + "timeout_secs", + "output_limit" + ], + "type": "object" + }, + "CommandStatus": { + "enum": [ + "running", + "completed", + "failed", + "timed_out", + "cancelled" + ], + "type": "string" + }, "CompiledFlowDefinition": { "additionalProperties": false, "properties": { @@ -844,6 +981,35 @@ ], "type": "object" }, + "ConfirmRepositorySshHostTrustRequest": { + "additionalProperties": false, + "properties": { + "expected_host_trust_revision": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "host_key": { + "type": "string" + }, + "operation_id": { + "type": "string" + }, + "runtime_id": { + "type": "string" + } + }, + "required": [ + "operation_id", + "runtime_id", + "host_key" + ], + "type": "object" + }, "ConflictResolution": { "enum": [ "none", @@ -968,6 +1134,66 @@ ], "type": "object" }, + "CurrentWorkerWorkdirAttachRequest": { + "additionalProperties": false, + "properties": { + "alias": { + "type": "string" + }, + "working_directory_id": { + "type": "string" + } + }, + "required": [ + "alias", + "working_directory_id" + ], + "type": "object" + }, + "CurrentWorkerWorkdirAttachmentResponse": { + "additionalProperties": false, + "properties": { + "alias": { + "type": "string" + }, + "attached": { + "type": "boolean" + }, + "capabilities": { + "$ref": "#/components/schemas/WorkdirSessionCapabilities" + }, + "working_directory_id": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "alias", + "working_directory_id", + "capabilities", + "attached" + ], + "type": "object" + }, + "CurrentWorkerWorkdirOperationRequest": { + "additionalProperties": false, + "properties": { + "operation": { + "$ref": "#/components/schemas/WorkdirSessionOperation" + }, + "target_workdir": { + "type": "string" + } + }, + "required": [ + "target_workdir", + "operation" + ], + "type": "object" + }, "DefaultIntakeReadyBodyRequest": { "additionalProperties": false, "properties": { @@ -1219,6 +1445,76 @@ ], "type": "string" }, + "EditRequest": { + "additionalProperties": false, + "properties": { + "expected_hash": { + "items": { + "format": "uint8", + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "maxItems": 32, + "minItems": 32, + "type": "array" + }, + "new_string": { + "type": "string" + }, + "old_string": { + "type": "string" + }, + "path": { + "$ref": "#/components/schemas/FsPath" + }, + "replace_all": { + "type": "boolean" + } + }, + "required": [ + "path", + "old_string", + "new_string", + "replace_all", + "expected_hash" + ], + "type": "object" + }, + "EditResult": { + "additionalProperties": false, + "properties": { + "bytes_written": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "content_hash": { + "items": { + "format": "uint8", + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "maxItems": 32, + "minItems": 32, + "type": "array" + }, + "replacements": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "replacements", + "bytes_written", + "content_hash" + ], + "type": "object" + }, "EffectivePromptCatalog": { "additionalProperties": false, "properties": { @@ -1256,6 +1552,15 @@ ], "type": "object" }, + "EntryKind": { + "enum": [ + "file", + "directory", + "symlink", + "other" + ], + "type": "string" + }, "EvidenceKind": { "description": "Extensible evidence kind tag used by staging source anchors.\n\nKnown values include `message`, `tool_call`, `tool_result`, `file_ref`,\n`ticket_ref`, and `objective_ref`, but callers may use newer bounded tags\nwithout changing the schema.", "type": "string" @@ -1354,6 +1659,80 @@ } ] }, + "ExternalWorkdirGrantCreateRequest": { + "additionalProperties": false, + "properties": { + "display_name": { + "type": "string" + }, + "provider_instance_id": { + "type": "string" + }, + "read_only": { + "type": "boolean" + }, + "ttl_seconds": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "provider_instance_id", + "display_name", + "ttl_seconds", + "read_only" + ], + "type": "object" + }, + "ExternalWorkdirGrantResponse": { + "additionalProperties": false, + "properties": { + "display_name": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "generation": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "grant_id": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "provider_instance_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "working_directory_id": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "grant_id", + "workspace_id", + "working_directory_id", + "provider_instance_id", + "display_name", + "permissions", + "expires_at", + "generation", + "status" + ], + "type": "object" + }, "ExtractAudit": { "properties": { "entry_range": { @@ -1542,6 +1921,10 @@ ], "type": "object" }, + "FsPath": { + "description": "Scope-checked filesystem path. Relative paths resolve below the bound\nWorkdir root; absolute paths require an explicit matching scope rule.", + "type": "string" + }, "GenerateRepositorySshCredentialRequest": { "additionalProperties": false, "properties": { @@ -1562,6 +1945,52 @@ ], "type": "object" }, + "GitCommitSummary": { + "additionalProperties": false, + "properties": { + "author_date": { + "type": "string" + }, + "author_email": { + "type": "string" + }, + "author_name": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "parents": { + "items": { + "type": "string" + }, + "type": "array" + }, + "refs": { + "items": { + "type": "string" + }, + "type": "array" + }, + "short_hash": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": [ + "hash", + "short_hash", + "summary", + "author_name", + "author_email", + "author_date", + "parents", + "refs" + ], + "type": "object" + }, "GitRemoteSummary": { "additionalProperties": false, "properties": { @@ -1613,6 +2042,155 @@ ], "type": "object" }, + "GlobRequest": { + "additionalProperties": false, + "properties": { + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "path": { + "$ref": "#/components/schemas/FsPath" + }, + "pattern": { + "type": "string" + } + }, + "required": [ + "pattern", + "path", + "limit" + ], + "type": "object" + }, + "GlobResult": { + "additionalProperties": false, + "properties": { + "paths": { + "items": { + "$ref": "#/components/schemas/FsPath" + }, + "type": "array" + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "paths", + "truncated" + ], + "type": "object" + }, + "GrepOutputMode": { + "enum": [ + "content", + "files_with_matches", + "count" + ], + "type": "string" + }, + "GrepRequest": { + "additionalProperties": false, + "properties": { + "after_context": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "before_context": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "case_insensitive": { + "type": "boolean" + }, + "file_type": { + "type": [ + "string", + "null" + ] + }, + "glob": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "multiline": { + "type": "boolean" + }, + "offset": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "output_mode": { + "$ref": "#/components/schemas/GrepOutputMode" + }, + "path": { + "$ref": "#/components/schemas/FsPath" + }, + "pattern": { + "type": "string" + } + }, + "required": [ + "pattern", + "path", + "case_insensitive", + "before_context", + "after_context", + "multiline", + "output_mode", + "limit", + "offset" + ], + "type": "object" + }, + "GrepResult": { + "additionalProperties": false, + "properties": { + "match_count": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "matched_files": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "output": { + "description": "Provider-rendered bounded grep report. Keeping rendering here avoids\ntransferring candidate files across a remote provider boundary.", + "type": "string" + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "match_count", + "matched_files", + "truncated" + ], + "type": "object" + }, "HealthResponse": { "additionalProperties": false, "properties": { @@ -1625,6 +2203,133 @@ ], "type": "object" }, + "HostListResponse": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "items": { + "items": { + "$ref": "#/components/schemas/HostSummary" + }, + "type": "array" + }, + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "source": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "limit", + "items", + "source", + "diagnostics" + ], + "type": "object" + }, + "HostSummary": { + "additionalProperties": false, + "properties": { + "arch": { + "type": "string" + }, + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "host_id": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "label": { + "type": "string" + }, + "last_seen_at": { + "type": [ + "string", + "null" + ] + }, + "observed_at": { + "type": "string" + }, + "os": { + "type": "string" + }, + "runtime_id": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "runtime_id", + "host_id", + "label", + "kind", + "status", + "observed_at", + "os", + "arch", + "diagnostics" + ], + "type": "object" + }, + "HostWorkerListResponse": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "items": { + "items": { + "$ref": "#/components/schemas/WorkerSummary" + }, + "type": "array" + }, + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "source": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "limit", + "items", + "source", + "diagnostics" + ], + "type": "object" + }, "InitialRepositoryIntent": { "additionalProperties": false, "description": "Initial Repository registration intent for Workspace creation.", @@ -1666,6 +2371,81 @@ ], "type": "object" }, + "ListEntry": { + "additionalProperties": false, + "properties": { + "kind": { + "$ref": "#/components/schemas/EntryKind" + }, + "path": { + "$ref": "#/components/schemas/FsPath" + }, + "size": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "path", + "kind", + "size" + ], + "type": "object" + }, + "ListRequest": { + "additionalProperties": false, + "properties": { + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "path": { + "$ref": "#/components/schemas/FsPath" + } + }, + "required": [ + "path", + "limit" + ], + "type": "object" + }, + "ListResult": { + "additionalProperties": false, + "properties": { + "entries": { + "items": { + "$ref": "#/components/schemas/ListEntry" + }, + "type": "array" + }, + "total_bytes": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "total_entries": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "entries", + "total_entries", + "total_bytes", + "truncated" + ], + "type": "object" + }, "MarkdownText": { "type": "string" }, @@ -4461,6 +5241,91 @@ ], "type": "object" }, + "ReadRequest": { + "additionalProperties": false, + "properties": { + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "max_bytes": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "offset": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "path": { + "$ref": "#/components/schemas/FsPath" + } + }, + "required": [ + "path", + "offset", + "limit", + "max_bytes" + ], + "type": "object" + }, + "ReadResult": { + "additionalProperties": false, + "properties": { + "bytes": { + "items": { + "format": "uint8", + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "content_hash": { + "items": { + "format": "uint8", + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "maxItems": 32, + "minItems": 32, + "type": "array" + }, + "path": { + "$ref": "#/components/schemas/FsPath" + }, + "start_line": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "total_lines": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "path", + "bytes", + "start_line", + "total_lines", + "content_hash", + "truncated" + ], + "type": "object" + }, "RecordOperationAudit": { "properties": { "after_hash": { @@ -4738,6 +5603,65 @@ ], "type": "object" }, + "RepositoryLogQuery": { + "additionalProperties": false, + "properties": { + "limit": { + "default": null, + "format": "uint", + "maximum": 200, + "minimum": 1, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, + "RepositoryLogResponse": { + "additionalProperties": false, + "properties": { + "default_selector": { + "type": [ + "string", + "null" + ] + }, + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "items": { + "items": { + "$ref": "#/components/schemas/GitCommitSummary" + }, + "type": "array" + }, + "limit": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "repository_key": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "repository_key", + "limit", + "items", + "diagnostics" + ], + "type": "object" + }, "RepositoryObservedStatus": { "enum": [ "unverified", @@ -4806,6 +5730,81 @@ ], "type": "object" }, + "RepositorySshConnectionProbeRequest": { + "additionalProperties": false, + "properties": { + "runtime_id": { + "type": "string" + } + }, + "required": [ + "runtime_id" + ], + "type": "object" + }, + "RepositorySshConnectionProbeResponse": { + "additionalProperties": false, + "properties": { + "candidates": { + "items": { + "$ref": "#/components/schemas/RepositorySshHostKeyCandidate" + }, + "type": "array" + }, + "expected_host_trust_revision": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "host_trust_id": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "port": { + "format": "uint16", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "repository_key": { + "type": "string" + }, + "runtime_id": { + "type": "string" + }, + "trust_state": { + "$ref": "#/components/schemas/RepositorySshConnectionTrustState" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "repository_key", + "runtime_id", + "hostname", + "port", + "trust_state", + "host_trust_id", + "candidates" + ], + "type": "object" + }, + "RepositorySshConnectionTrustState": { + "enum": [ + "untrusted", + "verified", + "changed" + ], + "type": "string" + }, "RepositorySshCredential": { "additionalProperties": false, "description": "Public metadata for one Workspace-scoped Repository SSH credential.\n\nSecret references and secret material are deliberately not part of this DTO.", @@ -4863,6 +5862,26 @@ ], "type": "object" }, + "RepositorySshHostKeyCandidate": { + "additionalProperties": false, + "properties": { + "algorithm": { + "type": "string" + }, + "fingerprint": { + "type": "string" + }, + "host_key": { + "type": "string" + } + }, + "required": [ + "algorithm", + "host_key", + "fingerprint" + ], + "type": "object" + }, "RepositorySshHostTrust": { "additionalProperties": false, "description": "Public metadata for an explicitly pinned SSH host key.", @@ -5826,6 +6845,41 @@ ], "type": "object" }, + "StatRequest": { + "additionalProperties": false, + "properties": { + "path": { + "$ref": "#/components/schemas/FsPath" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "StatResult": { + "additionalProperties": false, + "properties": { + "kind": { + "$ref": "#/components/schemas/EntryKind" + }, + "path": { + "$ref": "#/components/schemas/FsPath" + }, + "size": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "path", + "kind", + "size" + ], + "type": "object" + }, "SubmitMergeRequestReviewRequest": { "additionalProperties": false, "properties": { @@ -8271,6 +9325,636 @@ }, "type": "object" }, + "WorkdirOperationApiError": { + "additionalProperties": false, + "description": "Error body for current-Worker Workdir operations.\n\nDomain/service failures preserve the existing Repository-style API envelope,\nwhile provider failures preserve the established typed Workdir transport\nenvelope and HTTP status classification.", + "properties": { + "code": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkdirTransportErrorCode" + }, + { + "type": "null" + } + ] + }, + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": [ + "array", + "null" + ] + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "WorkdirScopeAuthorizationRequest": { + "additionalProperties": false, + "description": "Provider-side check for one operation under an attenuated tool scope.", + "properties": { + "path": { + "$ref": "#/components/schemas/FsPath" + }, + "permission": { + "$ref": "#/components/schemas/WorkdirToolScopePermission" + }, + "rules": { + "items": { + "$ref": "#/components/schemas/WorkdirToolScopeRule" + }, + "type": "array" + } + }, + "required": [ + "rules", + "path", + "permission" + ], + "type": "object" + }, + "WorkdirScopeOverlapRequest": { + "additionalProperties": false, + "description": "Provider-side overlap comparison that keeps resolved host paths private.", + "properties": { + "left": { + "$ref": "#/components/schemas/WorkdirToolScopeRule" + }, + "right": { + "$ref": "#/components/schemas/WorkdirToolScopeRule" + } + }, + "required": [ + "left", + "right" + ], + "type": "object" + }, + "WorkdirSessionCapabilities": { + "properties": { + "bits": { + "format": "uint8", + "maximum": 255, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "bits" + ], + "type": "object" + }, + "WorkdirSessionOperation": { + "description": "One provider-side Workdir operation.", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "authorize_scope", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/WorkdirScopeAuthorizationRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "scope_rules_overlap", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/WorkdirScopeOverlapRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "stat", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/StatRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "read", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/ReadRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "write", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/WriteRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "edit", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/EditRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "list", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/ListRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "glob", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/GlobRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "grep", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/GrepRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "command_start", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/CommandRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "command_status", + "type": "string" + }, + "request": { + "type": "string" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "command_output", + "type": "string" + }, + "request": { + "$ref": "#/components/schemas/CommandOutputRequest" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "command_cancel", + "type": "string" + }, + "request": { + "type": "string" + } + }, + "required": [ + "operation", + "request" + ], + "type": "object" + } + ] + }, + "WorkdirSessionOperationResult": { + "description": "Typed result paired with [`WorkdirSessionOperation`].", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "authorize_scope", + "type": "string" + } + }, + "required": [ + "operation" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "scope_rules_overlap", + "type": "string" + }, + "result": { + "additionalProperties": false, + "properties": { + "overlaps": { + "type": "boolean" + } + }, + "required": [ + "overlaps" + ], + "type": "object" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "stat", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/StatResult" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "read", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/ReadResult" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "write", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/WriteResult" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "edit", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/EditResult" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "list", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/ListResult" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "glob", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/GlobResult" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "grep", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/GrepResult" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "command_start", + "type": "string" + }, + "result": { + "type": "string" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "command_status", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/CommandStatus" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "command_output", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/CommandOutput" + } + }, + "required": [ + "operation", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "operation": { + "const": "command_cancel", + "type": "string" + } + }, + "required": [ + "operation" + ], + "type": "object" + } + ] + }, + "WorkdirToolScopePermission": { + "enum": [ + "read", + "write" + ], + "type": "string" + }, + "WorkdirToolScopeRule": { + "additionalProperties": false, + "properties": { + "permission": { + "$ref": "#/components/schemas/WorkdirToolScopePermission" + }, + "recursive": { + "type": "boolean" + }, + "symlink_policy": { + "default": "resolved", + "type": "string" + }, + "target": { + "$ref": "#/components/schemas/FsPath" + } + }, + "required": [ + "target", + "permission", + "recursive" + ], + "type": "object" + }, + "WorkdirTransportErrorCode": { + "description": "Stable, host-path-free error code crossing the Runtime boundary.", + "enum": [ + "not_found", + "conflict", + "unsupported", + "invalid_request", + "denied", + "out_of_scope", + "symlink_out_of_scope", + "broken_symlink", + "symlink_target_is_directory", + "read_only", + "is_directory", + "symlink_directory_not_traversed", + "unknown_command", + "unavailable", + "io", + "transport", + "internal" + ], + "type": "string" + }, + "WorkerCapabilitySummary": { + "properties": { + "can_spawn_followup": { + "type": "boolean" + }, + "can_stop": { + "type": "boolean" + } + }, + "required": [ + "can_stop", + "can_spawn_followup" + ], + "type": "object" + }, + "WorkerImplementationSummary": { + "properties": { + "display_hint": { + "type": "string" + }, + "kind": { + "type": "string" + } + }, + "required": [ + "kind", + "display_hint" + ], + "type": "object" + }, "WorkerLifecycleAudit": { "properties": { "consolidation": { @@ -8358,6 +10042,511 @@ ], "type": "string" }, + "WorkerSummary": { + "description": "Workspace-authoritative Worker projection.\n\n`resource_key` is required here even though Runtime-internal Worker summaries\ndo not carry one. The Workspace Server must resolve it from Workspace\nauthority before constructing this response.", + "properties": { + "capabilities": { + "$ref": "#/components/schemas/WorkerCapabilitySummary" + }, + "diagnostics": { + "default": [], + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "display_name": { + "default": "", + "type": "string" + }, + "host_id": { + "type": "string" + }, + "implementation": { + "$ref": "#/components/schemas/WorkerImplementationSummary" + }, + "label": { + "type": "string" + }, + "last_seen_at": { + "type": [ + "string", + "null" + ] + }, + "pinned": { + "default": false, + "type": "boolean" + }, + "profile": { + "type": [ + "string", + "null" + ] + }, + "resource_key": { + "type": "string" + }, + "retention_state": { + "default": "", + "type": "string" + }, + "runtime_id": { + "type": "string" + }, + "singleton_key": { + "type": [ + "string", + "null" + ] + }, + "state": { + "description": "Runtime catalog lifecycle compatibility state. Live foreground state, when\navailable, is carried separately in `worker_state`.", + "type": "string" + }, + "tags": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "workdir_attachments": { + "items": { + "$ref": "#/components/schemas/WorkerWorkdirAttachmentSummary" + }, + "type": "array" + }, + "worker_id": { + "type": "string" + }, + "worker_state": true, + "workspace": { + "$ref": "#/components/schemas/WorkerWorkspaceSummary" + } + }, + "required": [ + "runtime_id", + "worker_id", + "resource_key", + "host_id", + "label", + "workspace", + "state", + "implementation", + "capabilities" + ], + "type": "object" + }, + "WorkerWorkdirAttachmentSummary": { + "additionalProperties": false, + "description": "One Workspace Worker Workdir attachment. `alias` is the stable Worker-local\nrouting key; the nested Workdir id and display name are inventory metadata.", + "properties": { + "alias": { + "type": "string" + }, + "working_directory": { + "$ref": "#/components/schemas/WorkingDirectorySummary" + } + }, + "required": [ + "alias", + "working_directory" + ], + "type": "object" + }, + "WorkerWorkspaceSummary": { + "properties": { + "identity": { + "type": "string" + }, + "visibility": { + "type": "string" + }, + "workspace_id": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "visibility", + "identity" + ], + "type": "object" + }, + "WorkingDirectoryCleanupTarget": { + "additionalProperties": false, + "properties": { + "kind": { + "type": "string" + }, + "repository_key": { + "type": "string" + }, + "working_directory_id": { + "type": "string" + } + }, + "required": [ + "kind", + "working_directory_id", + "repository_key" + ], + "type": "object" + }, + "WorkingDirectoryCreateRequest": { + "additionalProperties": false, + "description": "Browser/Rust-client Workdir materialization request.\n\n`runtime_id = None` requests Workspace default Runtime resolution and\n`operation_id = Some(_)` fences exact replay. All four fields deliberately\npreserve the Server's existing optionality.", + "properties": { + "display_name": { + "description": "Optional human-facing label, independent of every Worker attachment alias.", + "type": [ + "string", + "null" + ] + }, + "operation_id": { + "type": [ + "string", + "null" + ] + }, + "repository_key": { + "type": "string" + }, + "runtime_id": { + "type": [ + "string", + "null" + ] + }, + "selector": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "repository_key" + ], + "type": "object" + }, + "WorkingDirectoryCreateResponse": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "item": { + "$ref": "#/components/schemas/WorkingDirectorySummary" + }, + "runtime_id": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "runtime_id", + "item", + "diagnostics" + ], + "type": "object" + }, + "WorkingDirectoryDetailResponse": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "item": { + "$ref": "#/components/schemas/WorkingDirectorySummary" + }, + "runtime_id": { + "type": [ + "string", + "null" + ] + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "item", + "diagnostics" + ], + "type": "object" + }, + "WorkingDirectoryListResponse": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "$ref": "#/components/schemas/Diagnostic" + }, + "type": "array" + }, + "items": { + "items": { + "$ref": "#/components/schemas/WorkingDirectorySummary" + }, + "type": "array" + }, + "workspace_id": { + "type": "string" + } + }, + "required": [ + "workspace_id", + "items", + "diagnostics" + ], + "type": "object" + }, + "WorkingDirectoryMaterializerKind": { + "description": "Public Workdir materializer classification.\n\nThe value identifies stable materialization provenance without exposing a\nprovider path, Runtime handle, or session identity.", + "enum": [ + "runtime_git_clone", + "client_hosted_external" + ], + "type": "string" + }, + "WorkingDirectoryOccupancy": { + "additionalProperties": false, + "description": "Durable Workspace occupancy projection for one Workdir.", + "properties": { + "display_name": { + "type": "string" + }, + "linked_at": { + "type": "string" + }, + "runtime_id": { + "type": "string" + }, + "worker_id": { + "type": "string" + } + }, + "required": [ + "runtime_id", + "worker_id", + "display_name", + "linked_at" + ], + "type": "object" + }, + "WorkingDirectoryRemovalDisposition": { + "enum": [ + "removed", + "retained", + "attention_required" + ], + "type": "string" + }, + "WorkingDirectoryRemovalRequest": { + "additionalProperties": false, + "properties": { + "reason": { + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "WorkingDirectoryRemovalResponse": { + "additionalProperties": false, + "properties": { + "disposition": { + "$ref": "#/components/schemas/WorkingDirectoryRemovalDisposition" + }, + "failure_category": { + "type": [ + "string", + "null" + ] + }, + "retryable": { + "type": "boolean" + }, + "working_directory_id": { + "type": "string" + } + }, + "required": [ + "working_directory_id", + "disposition", + "retryable" + ], + "type": "object" + }, + "WorkingDirectorySource": { + "description": "Public Workdir source identity. Provider routing details and host paths are intentionally absent.", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "repository", + "type": "string" + }, + "repository_key": { + "type": "string" + } + }, + "required": [ + "kind", + "repository_key" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "grant_id": { + "type": "string" + }, + "kind": { + "const": "external_grant", + "type": "string" + } + }, + "required": [ + "kind", + "grant_id" + ], + "type": "object" + } + ] + }, + "WorkingDirectoryStatusKind": { + "enum": [ + "active", + "cleanup_pending", + "corrupted", + "not_found", + "unknown" + ], + "type": "string" + }, + "WorkingDirectorySummary": { + "additionalProperties": false, + "description": "Public, provider-neutral Workdir inventory projection.", + "properties": { + "cleanliness": { + "type": [ + "string", + "null" + ] + }, + "cleanup_target": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkingDirectoryCleanupTarget" + }, + { + "type": "null" + } + ] + }, + "creation_ref": { + "type": [ + "string", + "null" + ] + }, + "creation_selector": { + "type": [ + "string", + "null" + ] + }, + "creation_tree": { + "type": [ + "string", + "null" + ] + }, + "current_ref": { + "type": [ + "string", + "null" + ] + }, + "current_selector": { + "type": [ + "string", + "null" + ] + }, + "current_tree": { + "type": [ + "string", + "null" + ] + }, + "display_name": { + "description": "Optional human-facing label, never a routing key.", + "type": [ + "string", + "null" + ] + }, + "materializer_kind": { + "$ref": "#/components/schemas/WorkingDirectoryMaterializerKind" + }, + "observed_at_epoch_seconds": { + "format": "uint64", + "maximum": 9007199254740991, + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "occupied_by": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkingDirectoryOccupancy" + }, + { + "type": "null" + } + ] + }, + "source": { + "$ref": "#/components/schemas/WorkingDirectorySource" + }, + "status": { + "$ref": "#/components/schemas/WorkingDirectoryStatusKind" + }, + "working_directory_id": { + "type": "string" + } + }, + "required": [ + "working_directory_id", + "source", + "materializer_kind", + "status" + ], + "type": "object" + }, "WorkspaceAuthConfig": { "description": "Browser authentication configuration exposed by the scoped Workspace summary.", "oneOf": [ @@ -9303,6 +11492,60 @@ ], "type": "object" }, + "WriteRequest": { + "additionalProperties": false, + "properties": { + "content": { + "items": { + "format": "uint8", + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "expected_hash": { + "items": { + "format": "uint8", + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "maxItems": 32, + "minItems": 32, + "type": [ + "array", + "null" + ] + }, + "path": { + "$ref": "#/components/schemas/FsPath" + } + }, + "required": [ + "path", + "content" + ], + "type": "object" + }, + "WriteResult": { + "properties": { + "bytes_written": { + "format": "uint", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "created": { + "type": "boolean" + } + }, + "required": [ + "bytes_written", + "created" + ], + "type": "object" + }, "string": { "type": "string" } @@ -9322,7 +11565,7 @@ "info": { "title": "Yoi Server API", "version": "0.1.0", - "x-yoi-source-digest": "sha256:0026ff3d0f1d267e4de3f154a14f4ef3f7554b603a376820f0775e5deb16a083" + "x-yoi-source-digest": "sha256:e3f776a17079ce202725bb97de607426f4bfad9afe390ba7bbf76186e871be59" }, "openapi": "3.1.0", "paths": { @@ -9740,6 +11983,188 @@ } } }, + "/api/hosts": { + "get": { + "operationId": "host_list_alias", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/hosts/{host_id}/workers": { + "get": { + "operationId": "host_worker_list_alias", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "host_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostWorkerListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/objectives": { "get": { "operationId": "objective_list_alias", @@ -10010,6 +12435,103 @@ ] } }, + "/api/repositories/{repository_key}/log": { + "get": { + "operationId": "repository_log_alias", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "repository_key", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": true, + "in": "query", + "name": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/RepositoryLogQuery" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryLogResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/tickets": { "get": { "operationId": "ticket_list_alias", @@ -10585,6 +13107,315 @@ ] } }, + "/api/w/{workspace_id}/external-workdir-grants": { + "post": { + "operationId": "external_workdir_grant_create", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalWorkdirGrantCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalWorkdirGrantResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/external-workdir-grants/{grant_id}": { + "delete": { + "operationId": "external_workdir_grant_revoke", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "grant_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalWorkdirGrantResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "get": { + "operationId": "external_workdir_grant_get", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "grant_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalWorkdirGrantResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/w/{workspace_id}/flows": { "get": { "operationId": "flow_list", @@ -10939,6 +13770,210 @@ } } }, + "/api/w/{workspace_id}/hosts": { + "get": { + "operationId": "host_list", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/hosts/{host_id}/workers": { + "get": { + "operationId": "host_worker_list", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "host_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostWorkerListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/w/{workspace_id}/internal/reviewer-child-sessions": { "post": { "operationId": "merge_request_reviewer_child_register", @@ -12909,6 +15944,919 @@ ] } }, + "/api/w/{workspace_id}/repositories/{repository_key}/log": { + "get": { + "operationId": "repository_log", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "repository_key", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": true, + "in": "query", + "name": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/RepositoryLogQuery" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryLogResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test": { + "post": { + "operationId": "repository_ssh_connection_probe", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "repository_key", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositorySshConnectionProbeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositorySshConnectionProbeResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "put": { + "operationId": "repository_ssh_host_trust_confirm", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "repository_key", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfirmRepositorySshHostTrustRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositorySshHostTrust" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories": { + "get": { + "operationId": "runtime_workdir_list", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "runtime_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "post": { + "operationId": "runtime_workdir_create", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "runtime_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryCreateResponse" + } + } + }, + "description": "Alternate successful response" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryCreateResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}": { + "delete": { + "operationId": "runtime_workdir_cleanup", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "runtime_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "working_directory_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryRemovalRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryRemovalResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "get": { + "operationId": "runtime_workdir_detail", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "runtime_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "working_directory_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryDetailResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/w/{workspace_id}/settings": { "get": { "operationId": "workspace_metadata_settings", @@ -19626,6 +23574,872 @@ } } }, + "/api/w/{workspace_id}/workers/self/workdir-attachments": { + "post": { + "operationId": "current_worker_workdir_attach", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWorkerWorkdirAttachRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWorkerWorkdirAttachmentResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/w/{workspace_id}/workers/self/workdir-attachments/{alias}": { + "delete": { + "operationId": "current_worker_workdir_detach", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "alias", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWorkerWorkdirAttachmentResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/w/{workspace_id}/workers/self/workdir-session/operations": { + "post": { + "operationId": "current_worker_workdir_operation", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWorkerWorkdirOperationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirSessionOperationResult" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirOperationApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirOperationApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirOperationApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirOperationApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirOperationApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirOperationApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirOperationApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkdirOperationApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/w/{workspace_id}/working-directories": { + "get": { + "operationId": "workspace_workdir_list", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryListResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "post": { + "operationId": "workspace_workdir_create", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryCreateResponse" + } + } + }, + "description": "Alternate successful response" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryCreateResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, + "/api/w/{workspace_id}/working-directories/{working_directory_id}": { + "delete": { + "operationId": "workspace_workdir_cleanup", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "working_directory_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryRemovalRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryRemovalResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + }, + "get": { + "operationId": "workspace_workdir_detail", + "parameters": [ + { + "explode": false, + "in": "path", + "name": "workspace_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + }, + { + "explode": false, + "in": "path", + "name": "working_directory_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkingDirectoryDetailResponse" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepositoryApiError" + } + } + }, + "description": "Error response" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ] + } + }, "/api/w/{workspace_id}/workspace": { "get": { "operationId": "workspace_scoped", diff --git a/web/workspace/src/lib/generated/repository-api.ts b/web/workspace/src/lib/generated/repository-api.ts index c450bf1e9..2da845e3a 100644 --- a/web/workspace/src/lib/generated/repository-api.ts +++ b/web/workspace/src/lib/generated/repository-api.ts @@ -1,10 +1,12 @@ // @generated by yoi-repository-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:0026ff3d0f1d267e4de3f154a14f4ef3f7554b603a376820f0775e5deb16a083) +// Input contract: 0.1.0 (sha256:e3f776a17079ce202725bb97de607426f4bfad9afe390ba7bbf76186e871be59) // Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output // Output: web/workspace/src/lib/generated/repository-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check +export type ConfirmRepositorySshHostTrustRequest = { expected_host_trust_revision?: number | null; host_key: string; operation_id: string; runtime_id: string; }; + export type CreateWorkspaceRepositoryRequest = { default_ref?: string | null; repository_key: string; source: string; }; export type CreateWorkspaceRepositoryResponse = { replayed: boolean; repository_key: string; workspace_id: string; }; @@ -13,10 +15,18 @@ export type Diagnostic = { code: string; message: string; severity: DiagnosticSe export type DiagnosticSeverity = "info" | "warning" | "error"; +export type GitCommitSummary = { author_date: string; author_email: string; author_name: string; hash: string; parents: Array; refs: Array; short_hash: string; summary: string; }; + export type GitRemoteSummary = { fetch_url: string; name: string; }; export type GitRepositorySummary = { branch?: string | null; dirty: boolean; head?: string | null; remotes: Array; status: string; }; +export type HostListResponse = { diagnostics: Array; items: Array; limit: number; source: string; workspace_id: string; }; + +export type HostSummary = { arch: string; diagnostics: Array; host_id: string; kind: string; label: string; last_seen_at?: string | null; observed_at: string; os: string; runtime_id: string; status: string; }; + +export type HostWorkerListResponse = { diagnostics: Array; items: Array; limit: number; source: string; workspace_id: string; }; + export type RepositoryApiError = { diagnostics?: Array; error: string; message: string; }; export type RepositoryDetailResponse = { item: RepositorySummary; source: string; workspace_id: string; }; @@ -25,10 +35,44 @@ export type RepositoryDiagnostic = { code: string; message: string; severity: st export type RepositoryListResponse = { diagnostics: Array; items: Array; source: string; workspace_id: string; }; +export type RepositoryLogResponse = { default_selector?: string | null; diagnostics: Array; items: Array; limit: number; repository_key: string; workspace_id: string; }; + export type RepositoryObservedStatus = "unverified" | "ready" | "invalid"; export type RepositorySource = { kind: RepositorySourceKind; uri: string; }; export type RepositorySourceKind = "local_path" | "file" | "ssh" | "https" | "invalid"; +export type RepositorySshConnectionProbeRequest = { runtime_id: string; }; + +export type RepositorySshConnectionProbeResponse = { candidates: Array; expected_host_trust_revision?: number | null; host_trust_id: string; hostname: string; port: number; repository_key: string; runtime_id: string; trust_state: RepositorySshConnectionTrustState; workspace_id: string; }; + +export type RepositorySshConnectionTrustState = "untrusted" | "verified" | "changed"; + +export type RepositorySshHostKeyCandidate = { algorithm: string; fingerprint: string; host_key: string; }; + +export type RepositorySshHostTrust = { created_at: string; current_revision: number; fingerprint: string; host_key: string; host_trust_id: string; hostname: string; key_algorithm: string; port: number; referenced_repositories?: Array; updated_at: string; workspace_id: string; }; + export type RepositorySummary = { default_selector?: string | null; diagnostics?: Array | null; git?: GitRepositorySummary | null; kind: string; observed_at?: string | null; observed_status: RepositoryObservedStatus; provider: string; record_authority: string; repository_key: string; source: RepositorySource; source_fingerprint: string; source_revision: number; }; + +export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; }; + +export type WorkerImplementationSummary = { display_hint: string; kind: string; }; + +export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; }; + +export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; + +export type WorkerWorkspaceSummary = { identity: string; visibility: string; workspace_id?: string | null; }; + +export type WorkingDirectoryCleanupTarget = { kind: string; repository_key: string; working_directory_id: string; }; + +export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external"; + +export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; }; + +export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; kind: "external_grant"; }; + +export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown"; + +export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; }; diff --git a/web/workspace/src/lib/generated/workdir-api.ts b/web/workspace/src/lib/generated/workdir-api.ts index 385624fec..c9b61fc1c 100644 --- a/web/workspace/src/lib/generated/workdir-api.ts +++ b/web/workspace/src/lib/generated/workdir-api.ts @@ -1,34 +1,110 @@ -// 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 +// @generated by yoi-workdir-openapi-typescript@1. Do not edit. +// Input: openapi/server-api.json +// Input contract: 0.1.0 (sha256:e3f776a17079ce202725bb97de607426f4bfad9afe390ba7bbf76186e871be59) +// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output +// Output: web/workspace/src/lib/generated/workdir-api.ts +// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check + +export type CommandOutput = { content: string; exit_code?: number | null; next_cursor?: number | null; output_path?: string | null; status: CommandStatus; timed_out: boolean; truncated: boolean; }; + +export type CommandOutputRequest = { cursor: number; handle: string; limit: number; wait: boolean; }; + +export type CommandRequest = { command: string; cwd?: FsPath | null; output_limit: number; spill_dir?: string | null; timeout_secs: number; tool_call_id?: string | null; }; + +export type CommandStatus = "running" | "completed" | "failed" | "timed_out" | "cancelled"; + +export type CurrentWorkerWorkdirAttachRequest = { alias: string; working_directory_id: string; }; + +export type CurrentWorkerWorkdirAttachmentResponse = { alias: string; attached: boolean; capabilities: WorkdirSessionCapabilities; working_directory_id: string; workspace_id: string; }; + +export type CurrentWorkerWorkdirOperationRequest = { operation: WorkdirSessionOperation; target_workdir: string; }; + +export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; }; export type DiagnosticSeverity = "info" | "warning" | "error"; -export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; +export type EditRequest = { expected_hash: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]; new_string: string; old_string: string; path: FsPath; replace_all: boolean; }; + +export type EditResult = { bytes_written: number; content_hash: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]; replacements: number; }; + +export type EntryKind = "file" | "directory" | "symlink" | "other"; + +export type ExternalWorkdirGrantCreateRequest = { display_name: string; provider_instance_id: string; read_only: boolean; ttl_seconds: number; }; + +export type ExternalWorkdirGrantResponse = { display_name: string; expires_at: string; generation: number; grant_id: string; permissions: string; provider_instance_id: string; status: string; working_directory_id: string; workspace_id: string; }; + +export type FsPath = string; + +export type GlobRequest = { limit: number; path: FsPath; pattern: string; }; + +export type GlobResult = { paths: Array; truncated: boolean; }; + +export type GrepOutputMode = "content" | "files_with_matches" | "count"; + +export type GrepRequest = { after_context: number; before_context: number; case_insensitive: boolean; file_type?: string | null; glob?: string | null; limit: number; multiline: boolean; offset: number; output_mode: GrepOutputMode; path: FsPath; pattern: string; }; + +export type GrepResult = { match_count: number; matched_files: number; output: string; truncated: boolean; }; + +export type ListEntry = { kind: EntryKind; path: FsPath; size: number; }; + +export type ListRequest = { limit: number; path: FsPath; }; + +export type ListResult = { entries: Array; total_bytes: number; total_entries: number; truncated: boolean; }; + +export type ReadRequest = { limit: number; max_bytes: number; offset: number; path: FsPath; }; + +export type ReadResult = { bytes: Array; content_hash: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]; path: FsPath; start_line: number; total_lines: number; truncated: boolean; }; + +export type RepositoryApiError = { diagnostics?: Array; error: string; message: string; }; + +export type StatRequest = { path: FsPath; }; + +export type StatResult = { kind: EntryKind; path: FsPath; size: number; }; + +export type WorkdirOperationApiError = { code?: WorkdirTransportErrorCode | null; diagnostics?: Array | null; error?: string | null; message: string; }; + +export type WorkdirScopeAuthorizationRequest = { path: FsPath; permission: WorkdirToolScopePermission; rules: Array; }; + +export type WorkdirScopeOverlapRequest = { left: WorkdirToolScopeRule; right: WorkdirToolScopeRule; }; + +export type WorkdirSessionCapabilities = { bits: number; }; + +export type WorkdirSessionOperation = { operation: "authorize_scope"; request: WorkdirScopeAuthorizationRequest; } | { operation: "scope_rules_overlap"; request: WorkdirScopeOverlapRequest; } | { operation: "stat"; request: StatRequest; } | { operation: "read"; request: ReadRequest; } | { operation: "write"; request: WriteRequest; } | { operation: "edit"; request: EditRequest; } | { operation: "list"; request: ListRequest; } | { operation: "glob"; request: GlobRequest; } | { operation: "grep"; request: GrepRequest; } | { operation: "command_start"; request: CommandRequest; } | { operation: "command_status"; request: string; } | { operation: "command_output"; request: CommandOutputRequest; } | { operation: "command_cancel"; request: string; }; + +export type WorkdirSessionOperationResult = { operation: "authorize_scope"; } | { operation: "scope_rules_overlap"; result: { overlaps: boolean; }; } | { operation: "stat"; result: StatResult; } | { operation: "read"; result: ReadResult; } | { operation: "write"; result: WriteResult; } | { operation: "edit"; result: EditResult; } | { operation: "list"; result: ListResult; } | { operation: "glob"; result: GlobResult; } | { operation: "grep"; result: GrepResult; } | { operation: "command_start"; result: string; } | { operation: "command_status"; result: CommandStatus; } | { operation: "command_output"; result: CommandOutput; } | { operation: "command_cancel"; }; + +export type WorkdirToolScopePermission = "read" | "write"; + +export type WorkdirToolScopeRule = { permission: WorkdirToolScopePermission; recursive: boolean; symlink_policy?: string; target: FsPath; }; + +export type WorkdirTransportErrorCode = "not_found" | "conflict" | "unsupported" | "invalid_request" | "denied" | "out_of_scope" | "symlink_out_of_scope" | "broken_symlink" | "symlink_target_is_directory" | "read_only" | "is_directory" | "symlink_directory_not_traversed" | "unknown_command" | "unavailable" | "io" | "transport" | "internal"; + +export type WorkingDirectoryCleanupTarget = { kind: string; repository_key: string; working_directory_id: string; }; + +export type WorkingDirectoryCreateRequest = { display_name?: string | null; operation_id?: string | null; repository_key: string; runtime_id?: string | null; selector?: string | null; }; + +export type WorkingDirectoryCreateResponse = { diagnostics: Array; item: WorkingDirectorySummary; runtime_id: string; workspace_id: string; }; + +export type WorkingDirectoryDetailResponse = { diagnostics: Array; item: WorkingDirectorySummary; runtime_id?: string | null; workspace_id: string; }; + +export type WorkingDirectoryListResponse = { diagnostics: Array; items: Array; workspace_id: string; }; export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external"; +export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; }; + +export type WorkingDirectoryRemovalDisposition = "removed" | "retained" | "attention_required"; + +export type WorkingDirectoryRemovalRequest = { reason: string; }; + +export type WorkingDirectoryRemovalResponse = { disposition: WorkingDirectoryRemovalDisposition; failure_category?: string | null; retryable: boolean; working_directory_id: string; }; + +export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; kind: "external_grant"; }; + export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown"; -export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_key: string, }; +export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; }; -export type WorkingDirectoryOccupancy = { runtime_id: string, worker_id: string, display_name: string, linked_at: string, }; +export type WriteRequest = { content: Array; expected_hash?: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number] | null; path: FsPath; }; -export type WorkingDirectorySource = { "kind": "repository", repository_key: string, } | { "kind": "external_grant", grant_id: string, }; - -export type WorkingDirectorySummary = { working_directory_id: string, -/** - * Optional human-facing label, never a routing key. - */ -display_name?: string | null, source: WorkingDirectorySource, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, }; - -export type WorkingDirectoryCreateRequest = { runtime_id?: string | null, -/** - * Optional human-facing label, independent of every Worker attachment alias. - */ -display_name?: string | null, repository_key: string, selector?: string | null, operation_id?: string | null, }; - -export type WorkingDirectoryListResponse = { workspace_id: string, items: Array, diagnostics: Array, }; - -export type WorkingDirectoryDetailResponse = { workspace_id: string, runtime_id?: string | null, item: WorkingDirectorySummary, diagnostics: Array, }; - -export type WorkingDirectoryCreateResponse = { workspace_id: string, runtime_id: string, item: WorkingDirectorySummary, diagnostics: Array, }; +export type WriteResult = { bytes_written: number; created: boolean; }; diff --git a/web/workspace/src/lib/workspace/api/workspace-model.ts b/web/workspace/src/lib/workspace/api/workspace-model.ts index 6449d53b3..e7e7af8e8 100644 --- a/web/workspace/src/lib/workspace/api/workspace-model.ts +++ b/web/workspace/src/lib/workspace/api/workspace-model.ts @@ -1,10 +1,5 @@ import type { ApiResult } from "$lib/workspace/api/http"; import type { - GitCommitSummary, - RepositoryLogResponse, - RepositorySshConnectionProbeResponse, - RepositorySshConnectionTrustState, - RepositorySshHostKeyCandidate, WorkspaceAuthConfig, WorkspaceCatalogListResponse, WorkspaceCreateResponse, @@ -24,22 +19,25 @@ import type { import type { CreateWorkspaceRepositoryResponse, Diagnostic, + GitCommitSummary, GitRemoteSummary, GitRepositorySummary, + HostListResponse, + HostSummary, RepositoryApiError, RepositoryDetailResponse, RepositoryDiagnostic, RepositoryListResponse, + RepositoryLogResponse, RepositorySource, RepositorySourceKind, + RepositorySshConnectionProbeResponse, + RepositorySshConnectionTrustState, + RepositorySshHostKeyCandidate, RepositorySummary, } from "$lib/generated/repository-api.ts"; export type { - GitCommitSummary, - RepositoryLogResponse, - RepositorySshConnectionProbeResponse, - RepositorySshHostKeyCandidate, WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkspaceDeletionOperationResponse, @@ -51,13 +49,19 @@ export type { export type { CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryResponse, + GitCommitSummary, GitRemoteSummary, GitRepositorySummary, + HostListResponse, + HostSummary, RepositoryApiError, RepositoryDetailResponse, RepositoryListResponse, + RepositoryLogResponse, RepositorySource, RepositorySourceKind, + RepositorySshConnectionProbeResponse, + RepositorySshHostKeyCandidate, RepositorySummary, } from "$lib/generated/repository-api.ts"; @@ -1094,6 +1098,77 @@ export function parseWorkspaceDeletionOperationResponse( }; } +export function parseHostListResponse(value: unknown): HostListResponse { + const response = object(value, "host list response"); + exactKeys( + response, + ["workspace_id", "limit", "items", "source", "diagnostics"], + "host list response", + ); + const limit = repositorySourceRevision( + response.limit, + "host list response.limit", + ); + return { + workspace_id: repositoryString( + response.workspace_id, + "host list response.workspace_id", + ), + limit, + items: repositoryArray(response.items, "host list response.items").map( + (item, index) => parseHostSummary(item, `host list response.items[${index}]`), + ), + source: repositoryString(response.source, "host list response.source"), + diagnostics: repositoryArray( + response.diagnostics, + "host list response.diagnostics", + ).map((item, index) => + repositoryApiDiagnostic( + item, + `host list response.diagnostics[${index}]`, + ) + ), + }; +} + +function parseHostSummary(value: unknown, path: string): HostSummary { + const host = object(value, path); + exactKeys( + host, + [ + "runtime_id", + "host_id", + "label", + "kind", + "status", + "observed_at", + "last_seen_at", + "os", + "arch", + "diagnostics", + ], + path, + ); + return { + runtime_id: repositoryString(host.runtime_id, `${path}.runtime_id`), + host_id: repositoryString(host.host_id, `${path}.host_id`), + label: repositoryString(host.label, `${path}.label`), + kind: repositoryString(host.kind, `${path}.kind`), + status: repositoryString(host.status, `${path}.status`), + observed_at: repositoryString(host.observed_at, `${path}.observed_at`), + last_seen_at: optionalNullableRepositoryString( + host.last_seen_at, + `${path}.last_seen_at`, + ), + os: repositoryString(host.os, `${path}.os`), + arch: repositoryString(host.arch, `${path}.arch`), + diagnostics: repositoryArray(host.diagnostics, `${path}.diagnostics`).map( + (item, index) => + repositoryApiDiagnostic(item, `${path}.diagnostics[${index}]`), + ), + }; +} + export function parseRepositoryLogResponse( value: unknown, ): RepositoryLogResponse { diff --git a/web/workspace/src/lib/workspace/sidebar/types.ts b/web/workspace/src/lib/workspace/sidebar/types.ts index b5734b243..bf33a3742 100644 --- a/web/workspace/src/lib/workspace/sidebar/types.ts +++ b/web/workspace/src/lib/workspace/sidebar/types.ts @@ -71,19 +71,6 @@ export type Runtime = { }; }; -export type Host = { - runtime_id: string; - host_id: string; - label: string; - kind: string; - status: string; - observed_at: string; - last_seen_at: string | null; - os: string; - arch: string; - diagnostics: Diagnostic[]; -}; - export type WorkerCapabilities = { can_stop: boolean; can_spawn_followup: boolean; diff --git a/web/workspace/src/routes/w/[workspaceId]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/+page.ts index 7ebfb3374..daf3fc39f 100644 --- a/web/workspace/src/routes/w/[workspaceId]/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/+page.ts @@ -1,10 +1,24 @@ import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; -import type { Host, ListResponse } from "$lib/workspace/sidebar/types"; +import { + parseHostListResponse, + type HostListResponse, +} from "$lib/workspace/api/workspace-model"; import type { PageLoad } from "./$types"; +const HOST_API_LOAD_POLICY = { + diagnosticLabel: "Host API", + maxResponseBytes: 4 * 1024 * 1024, +} as const; + export const load: PageLoad = async ({ fetch, params }) => { const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path); - const hosts = await loadJson>(fetch, apiPath("/hosts")); + const hosts = await loadJson( + fetch, + apiPath("/hosts"), + undefined, + parseHostListResponse, + HOST_API_LOAD_POLICY, + ); return { workspaceId: params.workspaceId, 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 5d232c73b..7b8a9d7bc 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/legacy-server-api'; + } from '$lib/generated/repository-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';