feat: migrate repository and workdir domains to ServerApi

This commit is contained in:
2026-09-23 06:51:05 +09:00
parent 6d65d50ec1
commit 7a975765ac
22 changed files with 6711 additions and 261 deletions
Generated
+2
View File
@@ -1447,6 +1447,7 @@ dependencies = [
"grep-searcher", "grep-searcher",
"ignore", "ignore",
"libc", "libc",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9", "sha2 0.10.9",
@@ -6104,6 +6105,7 @@ dependencies = [
"fs-operation", "fs-operation",
"manifest", "manifest",
"reqwest", "reqwest",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.11.0", "sha2 0.11.0",
+1
View File
@@ -11,6 +11,7 @@ grep-regex = "0.1.14"
grep-searcher = "0.1.16" grep-searcher = "0.1.16"
ignore = "0.4.25" ignore = "0.4.25"
libc = "0.2" libc = "0.2"
schemars.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
sha2 = "0.10.9" sha2 = "0.10.9"
tempfile.workspace = true tempfile.workspace = true
+42 -19
View File
@@ -1,6 +1,7 @@
use std::fmt; use std::fmt;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::FsError; use crate::FsError;
@@ -11,9 +12,11 @@ use crate::FsError;
/// while computing line and content metadata. `max_response_bytes` limits the /// while computing line and content metadata. `max_response_bytes` limits the
/// bytes retained for the response independently of the caller's requested /// bytes retained for the response independently of the caller's requested
/// `ReadRequest::max_bytes`. /// `ReadRequest::max_bytes`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
pub struct BoundedReadLimits { pub struct BoundedReadLimits {
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub max_source_bytes: u64, pub max_source_bytes: u64,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub max_response_bytes: 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 /// Scope-checked filesystem path. Relative paths resolve below the bound
/// Workdir root; absolute paths require an explicit matching scope rule. /// 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)] #[serde(transparent)]
pub struct FsPath(String); 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)] #[serde(deny_unknown_fields)]
pub struct StatRequest { pub struct StatRequest {
pub path: FsPath, pub path: FsPath,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct StatResult { pub struct StatResult {
pub path: FsPath, pub path: FsPath,
pub kind: EntryKind, pub kind: EntryKind,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub size: 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")] #[serde(rename_all = "snake_case")]
pub enum EntryKind { pub enum EntryKind {
File, File,
@@ -178,27 +182,32 @@ pub enum EntryKind {
pub type ContentHash = [u8; 32]; pub type ContentHash = [u8; 32];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ReadRequest { pub struct ReadRequest {
pub path: FsPath, pub path: FsPath,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub offset: usize, pub offset: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub limit: usize, pub limit: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub max_bytes: usize, pub max_bytes: usize,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ReadResult { pub struct ReadResult {
pub path: FsPath, pub path: FsPath,
pub bytes: Vec<u8>, pub bytes: Vec<u8>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub start_line: usize, pub start_line: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub total_lines: usize, pub total_lines: usize,
pub content_hash: ContentHash, pub content_hash: ContentHash,
pub truncated: bool, pub truncated: bool,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WriteRequest { pub struct WriteRequest {
pub path: FsPath, pub path: FsPath,
@@ -206,13 +215,14 @@ pub struct WriteRequest {
pub expected_hash: Option<ContentHash>, pub expected_hash: Option<ContentHash>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct WriteResult { pub struct WriteResult {
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub bytes_written: usize, pub bytes_written: usize,
pub created: bool, pub created: bool,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct EditRequest { pub struct EditRequest {
pub path: FsPath, pub path: FsPath,
@@ -222,54 +232,61 @@ pub struct EditRequest {
pub expected_hash: ContentHash, pub expected_hash: ContentHash,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct EditResult { pub struct EditResult {
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub replacements: usize, pub replacements: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub bytes_written: usize, pub bytes_written: usize,
pub content_hash: ContentHash, pub content_hash: ContentHash,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ListRequest { pub struct ListRequest {
pub path: FsPath, pub path: FsPath,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub limit: usize, pub limit: usize,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ListEntry { pub struct ListEntry {
pub path: FsPath, pub path: FsPath,
pub kind: EntryKind, pub kind: EntryKind,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub size: u64, pub size: u64,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ListResult { pub struct ListResult {
pub entries: Vec<ListEntry>, pub entries: Vec<ListEntry>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub total_entries: usize, pub total_entries: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub total_bytes: u64, pub total_bytes: u64,
pub truncated: bool, pub truncated: bool,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct GlobRequest { pub struct GlobRequest {
pub pattern: String, pub pattern: String,
pub path: FsPath, pub path: FsPath,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub limit: usize, pub limit: usize,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct GlobResult { pub struct GlobResult {
pub paths: Vec<FsPath>, pub paths: Vec<FsPath>,
pub truncated: bool, 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")] #[serde(rename_all = "snake_case")]
pub enum GrepOutputMode { pub enum GrepOutputMode {
Content, 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)] #[serde(deny_unknown_fields)]
pub struct GrepRequest { pub struct GrepRequest {
pub pattern: String, pub pattern: String,
@@ -291,21 +308,27 @@ pub struct GrepRequest {
pub glob: Option<String>, pub glob: Option<String>,
pub file_type: Option<String>, pub file_type: Option<String>,
pub case_insensitive: bool, pub case_insensitive: bool,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub before_context: usize, pub before_context: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub after_context: usize, pub after_context: usize,
pub multiline: bool, pub multiline: bool,
pub output_mode: GrepOutputMode, pub output_mode: GrepOutputMode,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub limit: usize, pub limit: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub offset: usize, pub offset: usize,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct GrepResult { pub struct GrepResult {
/// Provider-rendered bounded grep report. Keeping rendering here avoids /// Provider-rendered bounded grep report. Keeping rendering here avoids
/// transferring candidate files across a remote provider boundary. /// transferring candidate files across a remote provider boundary.
pub output: String, pub output: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub match_count: usize, pub match_count: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub matched_files: usize, pub matched_files: usize,
pub truncated: bool, pub truncated: bool,
} }
-1
View File
@@ -33,7 +33,6 @@ required-features = ["typescript"]
[[example]] [[example]]
name = "generate_workdir_api_types" name = "generate_workdir_api_types"
required-features = ["typescript"]
[[example]] [[example]]
name = "generate_worker_launch_api_types" name = "generate_worker_launch_api_types"
@@ -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() { fn main() {
print!( if let Err(error) = run() {
"{}", eprintln!("{error}");
server_api::normalized_typescript(server_api::workdir_api_typescript()) std::process::exit(1);
); }
}
fn run() -> Result<(), String> {
let mut check = false;
for argument in std::env::args().skip(1) {
match argument.as_str() {
"--check" => check = true,
"--help" | "-h" => {
println!(
"Generate the bounded 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(())
} }
+528 -29
View File
@@ -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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code: Option<workdir::http::WorkdirTransportErrorCode>,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diagnostics: Option<Vec<Diagnostic>>,
#[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(
<RepositoryApiError as api_macros::HttpRequestError>::from_request_rejection(
status, message,
),
)
}
}
macro_rules! impl_openapi_schema { macro_rules! impl_openapi_schema {
($($ty:ty),+ $(,)?) => { ($($ty:ty),+ $(,)?) => {
$(impl api_macros::openapi::OpenApiSchema for $ty {})+ $(impl api_macros::openapi::OpenApiSchema for $ty {})+
@@ -109,8 +167,29 @@ impl_openapi_schema!(
CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryRequest,
CreateWorkspaceRepositoryResponse, CreateWorkspaceRepositoryResponse,
RepositoryApiError, RepositoryApiError,
WorkdirOperationApiError,
RepositoryListResponse, RepositoryListResponse,
RepositoryDetailResponse, RepositoryDetailResponse,
RepositorySshConnectionProbeRequest,
RepositorySshConnectionProbeResponse,
ConfirmRepositorySshHostTrustRequest,
RepositoryLogQuery,
RepositoryLogResponse,
HostListResponse,
HostWorkerListResponse,
HostSummary,
WorkingDirectoryRemovalRequest,
WorkingDirectoryRemovalResponse,
WorkingDirectoryCreateRequest,
WorkingDirectoryListResponse,
WorkingDirectoryDetailResponse,
WorkingDirectoryCreateResponse,
CurrentWorkerWorkdirAttachRequest,
CurrentWorkerWorkdirAttachmentResponse,
CurrentWorkerWorkdirOperationRequest,
CurrentWorkerWorkdirOperationResponse,
ExternalWorkdirGrantCreateRequest,
ExternalWorkdirGrantResponse,
WorkspaceResponse, WorkspaceResponse,
WorkspaceMetadataSettingsResponse, WorkspaceMetadataSettingsResponse,
UpdateWorkspaceMetadataRequest, UpdateWorkspaceMetadataRequest,
@@ -980,6 +1059,324 @@ pub trait ServerApi {
#[body] request: CreateWorkspaceRepositoryRequest, #[body] request: CreateWorkspaceRepositoryRequest,
) -> Result<CreateWorkspaceRepositoryResponse, RepositoryApiError>; ) -> Result<CreateWorkspaceRepositoryResponse, RepositoryApiError>;
#[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<RepositorySshConnectionProbeResponse, RepositoryApiError>;
#[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<RepositorySshHostTrust, RepositoryApiError>;
#[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<RepositoryLogResponse, RepositoryApiError>;
#[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<RepositoryLogResponse, RepositoryApiError>;
#[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<HostListResponse, RepositoryApiError>;
#[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<HostListResponse, RepositoryApiError>;
#[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<HostWorkerListResponse, RepositoryApiError>;
#[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<HostWorkerListResponse, RepositoryApiError>;
#[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<WorkingDirectoryListResponse, RepositoryApiError>;
#[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<WorkingDirectoryCreateResponse, RepositoryApiError>;
#[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<WorkingDirectoryDetailResponse, RepositoryApiError>;
#[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<WorkingDirectoryRemovalResponse, RepositoryApiError>;
#[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<CurrentWorkerWorkdirAttachmentResponse, RepositoryApiError>;
#[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<CurrentWorkerWorkdirAttachmentResponse, RepositoryApiError>;
#[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<CurrentWorkerWorkdirOperationResponse, WorkdirOperationApiError>;
#[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<ExternalWorkdirGrantResponse, RepositoryApiError>;
#[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<ExternalWorkdirGrantResponse, RepositoryApiError>;
#[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<ExternalWorkdirGrantResponse, RepositoryApiError>;
#[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<WorkingDirectoryListResponse, RepositoryApiError>;
#[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<WorkingDirectoryCreateResponse, RepositoryApiError>;
#[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<WorkingDirectoryDetailResponse, RepositoryApiError>;
#[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<WorkingDirectoryRemovalResponse, RepositoryApiError>;
#[get("/api/tickets", status = 200, error_status = 400, additional_error_statuses = [500])] #[get("/api/tickets", status = 200, error_status = 400, additional_error_statuses = [500])]
async fn ticket_list_alias( async fn ticket_list_alias(
&self, &self,
@@ -1488,9 +1885,16 @@ pub fn canonical_openapi_document()
/// ///
/// Generated clients and OpenAPI omit extension parameters; operation implementations use this /// Generated clients and OpenAPI omit extension parameters; operation implementations use this
/// value instead of re-parsing transport headers inside domain handlers. /// 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)] #[derive(Clone)]
pub struct ServerRequestContext { pub struct ServerRequestContext {
pub actor: Option<RequestActor>, pub actor: Option<RequestActor>,
pub worker_source: Option<ServerWorkerSource>,
pub origin: Option<String>, pub origin: Option<String>,
/// Transport headers retained for Server-side authentication adapters. /// Transport headers retained for Server-side authentication adapters.
/// ///
@@ -2991,7 +3395,7 @@ pub struct RepositorySummary {
pub diagnostics: Option<Vec<RepositoryDiagnostic>>, pub diagnostics: Option<Vec<RepositoryDiagnostic>>,
} }
#[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", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct GitCommitSummary { pub struct GitCommitSummary {
@@ -3024,14 +3428,14 @@ pub struct RepositoryDetailResponse {
pub source: String, 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RepositorySshConnectionProbeRequest { pub struct RepositorySshConnectionProbeRequest {
pub runtime_id: String, 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RepositorySshHostKeyCandidate { pub struct RepositorySshHostKeyCandidate {
@@ -3040,7 +3444,7 @@ pub struct RepositorySshHostKeyCandidate {
pub fingerprint: String, 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RepositorySshConnectionTrustState { pub enum RepositorySshConnectionTrustState {
@@ -3049,7 +3453,7 @@ pub enum RepositorySshConnectionTrustState {
Changed, Changed,
} }
#[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", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RepositorySshConnectionProbeResponse { pub struct RepositorySshConnectionProbeResponse {
@@ -3061,11 +3465,12 @@ pub struct RepositorySshConnectionProbeResponse {
pub trust_state: RepositorySshConnectionTrustState, pub trust_state: RepositorySshConnectionTrustState,
pub host_trust_id: String, pub host_trust_id: String,
#[cfg_attr(feature = "typescript", ts(type = "number | null"))] #[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<u64>, pub expected_host_trust_revision: Option<u64>,
pub candidates: Vec<RepositorySshHostKeyCandidate>, pub candidates: Vec<RepositorySshHostKeyCandidate>,
} }
#[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", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ConfirmRepositorySshHostTrustRequest { pub struct ConfirmRepositorySshHostTrustRequest {
@@ -3073,10 +3478,19 @@ pub struct ConfirmRepositorySshHostTrustRequest {
pub runtime_id: String, pub runtime_id: String,
pub host_key: String, pub host_key: String,
#[cfg_attr(feature = "typescript", ts(type = "number | null"))] #[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<u64>, pub expected_host_trust_revision: Option<u64>,
} }
#[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<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RepositoryLogResponse { pub struct RepositoryLogResponse {
@@ -3085,6 +3499,7 @@ pub struct RepositoryLogResponse {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))] #[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub default_selector: Option<String>, pub default_selector: Option<String>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub limit: usize, pub limit: usize,
pub items: Vec<GitCommitSummary>, pub items: Vec<GitCommitSummary>,
pub diagnostics: Vec<Diagnostic>, pub diagnostics: Vec<Diagnostic>,
@@ -3115,7 +3530,7 @@ pub struct Diagnostic {
/// ///
/// The value identifies stable materialization provenance without exposing a /// The value identifies stable materialization provenance without exposing a
/// provider path, Runtime handle, or session identity. /// 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryMaterializerKind { pub enum WorkingDirectoryMaterializerKind {
@@ -3124,7 +3539,7 @@ pub enum WorkingDirectoryMaterializerKind {
ClientHostedExternal, 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryStatusKind { 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryCleanupTarget { pub struct WorkingDirectoryCleanupTarget {
@@ -3162,14 +3577,14 @@ pub struct WorkingDirectoryCleanupTarget {
pub repository_key: String, 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryRemovalRequest { pub struct WorkingDirectoryRemovalRequest {
pub reason: String, 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryRemovalDisposition { pub enum WorkingDirectoryRemovalDisposition {
@@ -3178,7 +3593,7 @@ pub enum WorkingDirectoryRemovalDisposition {
AttentionRequired, 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", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@@ -3191,7 +3606,7 @@ pub struct WorkingDirectoryRemovalResponse {
} }
/// Durable Workspace occupancy projection for one Workdir. /// 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryOccupancy { pub struct WorkingDirectoryOccupancy {
@@ -3204,7 +3619,7 @@ pub struct WorkingDirectoryOccupancy {
/// Runtime-internal Workdir cleanup authority. This transport intentionally /// Runtime-internal Workdir cleanup authority. This transport intentionally
/// retains the Backend-generated Repository id and is never a Workspace public /// retains the Backend-generated Repository id and is never a Workspace public
/// projection. /// projection.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@@ -3217,7 +3632,7 @@ pub struct RuntimeWorkingDirectoryCleanupTarget {
/// Runtime-internal Workdir inventory transport. Workspace REST and model-facing /// Runtime-internal Workdir inventory transport. Workspace REST and model-facing
/// surfaces must project this through [`WorkingDirectorySummary`] so the UUID is /// surfaces must project this through [`WorkingDirectorySummary`] so the UUID is
/// replaced with `repository_key`. /// replaced with `repository_key`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@@ -3241,6 +3656,7 @@ pub struct RuntimeWorkingDirectorySummary {
pub current_tree: Option<String>, pub current_tree: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(type = "number | null"))] #[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<u64>, pub observed_at_epoch_seconds: Option<u64>,
pub materializer_kind: WorkingDirectoryMaterializerKind, pub materializer_kind: WorkingDirectoryMaterializerKind,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -3253,7 +3669,7 @@ pub struct RuntimeWorkingDirectorySummary {
} }
/// Public Workdir source identity. Provider routing details and host paths are intentionally absent. /// 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum WorkingDirectorySource { pub enum WorkingDirectorySource {
@@ -3262,7 +3678,7 @@ pub enum WorkingDirectorySource {
} }
/// Public, provider-neutral Workdir inventory projection. /// 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", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@@ -3286,6 +3702,7 @@ pub struct WorkingDirectorySummary {
pub current_tree: Option<String>, pub current_tree: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] #[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<u64>, pub observed_at_epoch_seconds: Option<u64>,
pub materializer_kind: WorkingDirectoryMaterializerKind, pub materializer_kind: WorkingDirectoryMaterializerKind,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -3414,17 +3831,18 @@ impl From<workdir::workspace::WorkingDirectorySummary> 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ExternalWorkdirGrantCreateRequest { pub struct ExternalWorkdirGrantCreateRequest {
pub provider_instance_id: String, pub provider_instance_id: String,
pub display_name: String, pub display_name: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub ttl_seconds: u64, pub ttl_seconds: u64,
pub read_only: bool, 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ExternalWorkdirGrantResponse { pub struct ExternalWorkdirGrantResponse {
@@ -3435,6 +3853,7 @@ pub struct ExternalWorkdirGrantResponse {
pub display_name: String, pub display_name: String,
pub permissions: String, pub permissions: String,
pub expires_at: String, pub expires_at: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub generation: u64, pub generation: u64,
pub status: String, pub status: String,
} }
@@ -3444,7 +3863,7 @@ pub struct ExternalWorkdirGrantResponse {
/// `runtime_id = None` requests Workspace default Runtime resolution and /// `runtime_id = None` requests Workspace default Runtime resolution and
/// `operation_id = Some(_)` fences exact replay. All four fields deliberately /// `operation_id = Some(_)` fences exact replay. All four fields deliberately
/// preserve the Server's existing optionality. /// 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", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] #[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@@ -3461,7 +3880,7 @@ pub struct WorkingDirectoryCreateRequest {
pub operation_id: Option<String>, pub operation_id: Option<String>,
} }
#[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", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryListResponse { pub struct WorkingDirectoryListResponse {
@@ -3470,7 +3889,7 @@ pub struct WorkingDirectoryListResponse {
pub diagnostics: Vec<Diagnostic>, pub diagnostics: Vec<Diagnostic>,
} }
#[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", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryDetailResponse { pub struct WorkingDirectoryDetailResponse {
@@ -3481,7 +3900,72 @@ pub struct WorkingDirectoryDetailResponse {
pub diagnostics: Vec<Diagnostic>, pub diagnostics: Vec<Diagnostic>,
} }
#[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<String>,
pub os: String,
pub arch: String,
pub diagnostics: Vec<Diagnostic>,
}
#[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<HostSummary>,
pub source: String,
pub diagnostics: Vec<Diagnostic>,
}
#[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<WorkerSummary>,
pub source: String,
pub diagnostics: Vec<Diagnostic>,
}
#[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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryCreateResponse { pub struct WorkingDirectoryCreateResponse {
@@ -3489,6 +3973,20 @@ pub struct WorkingDirectoryCreateResponse {
pub runtime_id: String, pub runtime_id: String,
pub item: WorkingDirectorySummary, pub item: WorkingDirectorySummary,
pub diagnostics: Vec<Diagnostic>, pub diagnostics: Vec<Diagnostic>,
#[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)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -4962,7 +5460,7 @@ pub struct RuntimeConnectionTestResponse {
pub diagnostics: Vec<Diagnostic>, pub diagnostics: Vec<Diagnostic>,
} }
#[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", derive(ts_rs::TS))]
pub struct WorkerWorkspaceSummary { pub struct WorkerWorkspaceSummary {
pub visibility: String, pub visibility: String,
@@ -4971,14 +5469,14 @@ pub struct WorkerWorkspaceSummary {
pub workspace_id: Option<String>, pub workspace_id: Option<String>,
} }
#[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", derive(ts_rs::TS))]
pub struct WorkerImplementationSummary { pub struct WorkerImplementationSummary {
pub kind: String, pub kind: String,
pub display_hint: String, pub display_hint: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkerCapabilitySummary { pub struct WorkerCapabilitySummary {
pub can_stop: bool, pub can_stop: bool,
@@ -5149,7 +5647,7 @@ pub struct WorkspaceWorkerDiscoveryPage {
/// One Workspace Worker Workdir attachment. `alias` is the stable Worker-local /// One Workspace Worker Workdir attachment. `alias` is the stable Worker-local
/// routing key; the nested Workdir id and display name are inventory metadata. /// 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))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkerWorkdirAttachmentSummary { pub struct WorkerWorkdirAttachmentSummary {
@@ -5170,7 +5668,7 @@ pub struct RuntimeWorkerWorkdirAttachmentSummary {
/// `resource_key` is required here even though Runtime-internal Worker summaries /// `resource_key` is required here even though Runtime-internal Worker summaries
/// do not carry one. The Workspace Server must resolve it from Workspace /// do not carry one. The Workspace Server must resolve it from Workspace
/// authority before constructing this response. /// authority before constructing this response.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkerSummary { pub struct WorkerSummary {
pub runtime_id: String, pub runtime_id: String,
@@ -5190,6 +5688,7 @@ pub struct WorkerSummary {
pub state: String, pub state: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional))] #[cfg_attr(feature = "typescript", ts(optional))]
#[schemars(with = "Option<serde_json::Value>")]
pub worker_state: Option<protocol::WorkerStateSnapshot>, pub worker_state: Option<protocol::WorkerStateSnapshot>,
pub last_seen_at: Option<String>, pub last_seen_at: Option<String>,
#[serde(default)] #[serde(default)]
@@ -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 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 INPUT_PATH: &str = "openapi/server-api.json";
pub const OUTPUT_PATH: &str = "web/workspace/src/lib/generated/repository-api.ts"; 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 MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0; const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0;
@@ -72,6 +77,48 @@ pub fn generate_repository_typescript(input: &str) -> Result<String, GenerationE
Ok(output) Ok(output)
} }
pub fn generate_workdir_typescript(input: &str) -> Result<String, GenerationError> {
let document: Value = serde_json::from_str(input)
.map_err(|error| GenerationError::invalid(format!("invalid OpenAPI JSON: {error}")))?;
let root = object(&document, "OpenAPI document")?;
if root.get("openapi").and_then(Value::as_str) != Some("3.1.0") {
return Err(GenerationError::invalid(
"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> { fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> {
let Some(hex) = digest.strip_prefix("sha256:") else { let Some(hex) = digest.strip_prefix("sha256:") else {
return Err(GenerationError::invalid( return Err(GenerationError::invalid(
@@ -89,8 +136,7 @@ fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> {
fn validate_repository_operations( fn validate_repository_operations(
paths: &Map<String, Value>, paths: &Map<String, Value>,
) -> Result<BTreeSet<String>, GenerationError> { ) -> Result<BTreeSet<String>, GenerationError> {
let mut roots = BTreeSet::new(); let operations = [
for operation in [
OperationContract { OperationContract {
path: "/api/repositories", path: "/api/repositories",
method: "get", method: "get",
@@ -129,7 +175,184 @@ fn validate_repository_operations(
request: None, request: None,
successes: &[("200", "RepositoryDetailResponse")], 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<String, Value>,
) -> Result<BTreeSet<String>, 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<String, Value>,
operations: &[OperationContract],
) -> Result<BTreeSet<String>, GenerationError> {
let mut roots = BTreeSet::new();
for operation in operations {
let path = object_field(paths, operation.path, "OpenAPI paths")?; let path = object_field(paths, operation.path, "OpenAPI paths")?;
let value = object_field(path, operation.method, operation.path)?; let value = object_field(path, operation.method, operation.path)?;
if value.get("operationId").and_then(Value::as_str) != Some(operation.operation_id) { 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), &format!("{} response {status}", operation.operation_id),
)?; )?;
let schema = json_content_schema(response, operation.operation_id)?; let schema = json_content_schema(response, operation.operation_id)?;
let expected = successes let expected = successes.get(status.as_str()).copied().unwrap_or_else(|| {
.get(status.as_str()) if operation.operation_id == "current_worker_workdir_operation" {
.copied() "WorkdirOperationApiError"
.unwrap_or("RepositoryApiError"); } else {
"RepositoryApiError"
}
});
require_schema_ref(schema, expected, operation.operation_id)?; require_schema_ref(schema, expected, operation.operation_id)?;
roots.insert(expected.to_owned()); 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<String, Value>, kind: &str) -> bool { fn literal_schema_matches_type(schema: &Map<String, Value>, kind: &str) -> bool {
if let Some(constant) = schema.get("const") { if let Some(constant) = schema.get("const") {
return literal_matches_type(kind, constant); return literal_matches_type(kind, constant);
@@ -328,6 +575,14 @@ fn literal_schema_matches_type(schema: &Map<String, Value>, kind: &str) -> bool
} }
fn render_schema(value: &Value, context: &str) -> Result<String, GenerationError> { fn render_schema(value: &Value, context: &str) -> Result<String, GenerationError> {
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)?; let schema = object(value, context)?;
if schema.contains_key("$ref") { if schema.contains_key("$ref") {
let reference = schema_reference(value, context)?; let reference = schema_reference(value, context)?;
@@ -403,6 +658,34 @@ fn render_schema(value: &Value, context: &str) -> Result<String, GenerationError
"{context} contains an empty oneOf" "{context} contains an empty oneOf"
))); )));
} }
let tagged_discriminators = branches
.iter()
.map(tagged_object_discriminator)
.collect::<Option<Vec<_>>>();
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::<Result<Vec<_>, _>>()
.map(|branches| branches.join(" | "));
}
let mut rendered = Vec::with_capacity(branches.len()); let mut rendered = Vec::with_capacity(branches.len());
let mut seen_literals = BTreeSet::new(); let mut seen_literals = BTreeSet::new();
for branch in branches { for branch in branches {
@@ -438,12 +721,12 @@ fn render_schema(value: &Value, context: &str) -> Result<String, GenerationError
))); )));
} }
} }
if let Some(kind) = outer_type { if let Some(kind) = outer_type
if !literal_schema_matches_type(branch_object, kind) { && !literal_schema_matches_type(branch_object, kind)
return Err(GenerationError::invalid(format!( {
"{context} oneOf branch does not match outer type `{kind}`" return Err(GenerationError::invalid(format!(
))); "{context} oneOf branch does not match outer type `{kind}`"
} )));
} }
rendered.push(render_schema(branch, context)?); rendered.push(render_schema(branch, context)?);
} }
@@ -530,7 +813,14 @@ fn render_typed_schema(
"integer" => { "integer" => {
ensure_schema_keywords(schema, &["format", "maximum", "minimum", "type"], context)?; ensure_schema_keywords(schema, &["format", "maximum", "minimum", "type"], context)?;
let format = match schema.get("format") { 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)) => { Some(Value::String(format)) => {
return Err(GenerationError::invalid(format!( return Err(GenerationError::invalid(format!(
"{context} uses unsupported integer format `{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 minimum = schema.get("minimum").and_then(Value::as_f64);
let maximum = schema.get("maximum").and_then(Value::as_f64); let maximum = schema.get("maximum").and_then(Value::as_f64);
let expected_minimum = if format == Some("uint64") { let (expected_minimum, expected_maximum) = match format {
0.0 Some("uint") | Some("uint64") => (0.0, MAX_SAFE_INTEGER),
} else { Some("uint8") => (0.0, u8::MAX as f64),
MIN_SAFE_INTEGER 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!( return Err(GenerationError::invalid(format!(
"{context} integer range does not match its JavaScript safe-number mapping" "{context} integer range does not match its JavaScript safe-number mapping"
))); )));
@@ -558,11 +852,35 @@ fn render_typed_schema(
Ok("number".to_owned()) Ok("number".to_owned())
} }
"array" => { "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(|| { let items = schema.get("items").ok_or_else(|| {
GenerationError::invalid(format!("{context} array has no item schema")) 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::<Vec<_>>()
.join(", ")
))
}
_ => Err(GenerationError::invalid(format!(
"{context} contains unsupported variable array bounds"
))),
}
} }
"object" => { "object" => {
ensure_schema_keywords( ensure_schema_keywords(
@@ -778,13 +1096,13 @@ mod tests {
); );
let mut document: Value = serde_json::from_str(OPENAPI).unwrap(); let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
document["components"]["schemas"]["RepositoryListResponse"]["properties"]["items"]["maxItems"] = document["components"]["schemas"]["RepositoryListResponse"]["properties"]["items"]["uniqueItems"] =
serde_json::json!(1); serde_json::json!(true);
let error = generate_repository_typescript(&document.to_string()).unwrap_err(); let error = generate_repository_typescript(&document.to_string()).unwrap_err();
assert!( assert!(
error error
.to_string() .to_string()
.contains("unsupported schema keyword `maxItems`") .contains("unsupported schema keyword `uniqueItems`")
); );
} }
+1
View File
@@ -13,6 +13,7 @@ async-trait.workspace = true
fs-operation.workspace = true fs-operation.workspace = true
manifest.workspace = true manifest.workspace = true
reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"], optional = true } reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"], optional = true }
schemars.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
sha2.workspace = true sha2.workspace = true
+9 -8
View File
@@ -5,6 +5,7 @@
//! optional so Runtime servers can share these DTOs without depending on a //! optional so Runtime servers can share these DTOs without depending on a
//! client stack. //! client stack.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
@@ -16,7 +17,7 @@ use crate::{
}; };
/// Opaque Runtime-owned identifier for one ephemeral Workdir session. /// 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)] #[serde(transparent)]
pub struct WorkdirSessionId(String); pub struct WorkdirSessionId(String);
@@ -37,7 +38,7 @@ impl WorkdirSessionId {
} }
/// Open a fresh session for a persisted Workdir identity. /// 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 { pub struct OpenWorkdirSessionRequest {
/// Optional Runtime Worker whose persisted binding establishes workspace /// Optional Runtime Worker whose persisted binding establishes workspace
/// ownership of the Workdir. Runtime servers reject cross-workspace owners. /// ownership of the Workdir. Runtime servers reject cross-workspace owners.
@@ -45,7 +46,7 @@ pub struct OpenWorkdirSessionRequest {
pub owner_worker_id: Option<String>, pub owner_worker_id: Option<String>,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct OpenWorkdirSessionResponse { pub struct OpenWorkdirSessionResponse {
pub session_id: WorkdirSessionId, pub session_id: WorkdirSessionId,
pub workdir_id: WorkdirId, pub workdir_id: WorkdirId,
@@ -53,7 +54,7 @@ pub struct OpenWorkdirSessionResponse {
} }
/// One provider-side Workdir operation. /// One provider-side Workdir operation.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde( #[serde(
tag = "operation", tag = "operation",
content = "request", content = "request",
@@ -77,14 +78,14 @@ pub enum WorkdirSessionOperation {
} }
/// Wire envelope for one provider operation. /// 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)] #[serde(deny_unknown_fields)]
pub struct WorkdirSessionOperationRequest { pub struct WorkdirSessionOperationRequest {
pub operation: WorkdirSessionOperation, pub operation: WorkdirSessionOperation,
} }
/// Typed result paired with [`WorkdirSessionOperation`]. /// Typed result paired with [`WorkdirSessionOperation`].
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde( #[serde(
tag = "operation", tag = "operation",
content = "result", content = "result",
@@ -174,7 +175,7 @@ pub async fn dispatch_workdir_session_operation(
} }
/// Stable, host-path-free error code crossing the Runtime boundary. /// 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")] #[serde(rename_all = "snake_case")]
pub enum WorkdirTransportErrorCode { pub enum WorkdirTransportErrorCode {
NotFound, 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 struct WorkdirTransportError {
pub code: WorkdirTransportErrorCode, pub code: WorkdirTransportErrorCode,
pub message: String, pub message: String,
+5 -4
View File
@@ -20,6 +20,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::broadcast; use tokio::sync::broadcast;
@@ -45,7 +46,7 @@ pub use scope::{
}; };
/// Persistent, opaque identity of one materialized Workdir. /// 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 { pub struct Workdir {
id: WorkdirId, id: WorkdirId,
} }
@@ -63,7 +64,7 @@ impl Workdir {
} }
/// Opaque Workdir identifier assigned by the materialization authority. /// 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)] #[serde(transparent)]
pub struct WorkdirId(String); 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")] #[serde(rename_all = "snake_case")]
pub enum WorkdirSessionCapability { pub enum WorkdirSessionCapability {
Read, Read,
@@ -90,7 +91,7 @@ pub enum WorkdirSessionCapability {
Command, Command,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct WorkdirSessionCapabilities { pub struct WorkdirSessionCapabilities {
bits: u8, bits: u8,
} }
+22 -9
View File
@@ -1,15 +1,18 @@
use std::path::PathBuf; use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)] #[serde(transparent)]
pub struct CommandHandle(pub String); pub struct CommandHandle(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandRequest { pub struct CommandRequest {
pub command: String, pub command: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub timeout_secs: u64, pub timeout_secs: u64,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub output_limit: usize, pub output_limit: usize,
/// Workdir-relative command directory. Providers validate it against the /// Workdir-relative command directory. Providers validate it against the
/// active session before process start. /// active session before process start.
@@ -25,15 +28,17 @@ pub struct CommandRequest {
pub tool_call_id: Option<String>, pub tool_call_id: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandOutputRequest { pub struct CommandOutputRequest {
pub handle: CommandHandle, pub handle: CommandHandle,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub cursor: usize, pub cursor: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub limit: usize, pub limit: usize,
pub wait: bool, 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")] #[serde(rename_all = "snake_case")]
pub enum CommandStatus { pub enum CommandStatus {
Running, Running,
@@ -43,35 +48,41 @@ pub enum CommandStatus {
Cancelled, Cancelled,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum CommandStream { pub enum CommandStream {
Stdout, Stdout,
Stderr, Stderr,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
pub struct CommandStreamSlice { pub struct CommandStreamSlice {
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub start_offset: u64, pub start_offset: u64,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub end_offset: u64, pub end_offset: u64,
pub content: String, pub content: String,
pub truncated: bool, pub truncated: bool,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandSnapshot { pub struct CommandSnapshot {
pub command_id: String, pub command_id: String,
pub tool_call_id: Option<String>, pub tool_call_id: Option<String>,
pub status: CommandStatus, pub status: CommandStatus,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub started_at_ms: u64, pub started_at_ms: u64,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub observed_at_ms: u64, pub observed_at_ms: u64,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub last_output_at_ms: Option<u64>, pub last_output_at_ms: Option<u64>,
pub stdout: CommandStreamSlice, pub stdout: CommandStreamSlice,
pub stderr: CommandStreamSlice, pub stderr: CommandStreamSlice,
#[schemars(range(min = -2147483648_i32, max = 2147483647_i32))]
pub exit_code: Option<i32>, pub exit_code: Option<i32>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum CommandEvent { pub enum CommandEvent {
Started { 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 struct CommandOutput {
pub status: CommandStatus, pub status: CommandStatus,
#[schemars(range(min = -2147483648_i32, max = 2147483647_i32))]
pub exit_code: Option<i32>, pub exit_code: Option<i32>,
pub timed_out: bool, pub timed_out: bool,
pub content: String, pub content: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub next_cursor: Option<usize>, pub next_cursor: Option<usize>,
pub truncated: bool, pub truncated: bool,
/// Complete output retained by the provider when `truncated` is true. /// Complete output retained by the provider when `truncated` is true.
+7 -5
View File
@@ -9,6 +9,7 @@ use fs_operation::{
ListResult, ReadRequest, ReadResult, StatRequest, StatResult, WriteRequest, WriteResult, ListResult, ReadRequest, ReadResult, StatRequest, StatResult, WriteRequest, WriteResult,
}; };
use manifest::SymlinkPolicy; use manifest::SymlinkPolicy;
use schemars::JsonSchema;
use tokio::sync::broadcast; use tokio::sync::broadcast;
const MAX_SCOPED_COMMANDS: usize = 16; const MAX_SCOPED_COMMANDS: usize = 16;
@@ -19,25 +20,26 @@ use crate::{
WorkdirSessionCapabilities, WorkdirSessionCapability, WorkdirSessionHandle, 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")] #[serde(rename_all = "snake_case")]
pub enum WorkdirToolScopePermission { pub enum WorkdirToolScopePermission {
Read, Read,
Write, Write,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkdirToolScopeRule { pub struct WorkdirToolScopeRule {
pub target: FsPath, pub target: FsPath,
pub permission: WorkdirToolScopePermission, pub permission: WorkdirToolScopePermission,
pub recursive: bool, pub recursive: bool,
#[serde(default)] #[serde(default)]
#[schemars(with = "String")]
pub symlink_policy: SymlinkPolicy, pub symlink_policy: SymlinkPolicy,
} }
/// Provider-side check for one operation under an attenuated tool scope. /// 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)] #[serde(deny_unknown_fields)]
pub struct WorkdirScopeAuthorizationRequest { pub struct WorkdirScopeAuthorizationRequest {
pub rules: Vec<WorkdirToolScopeRule>, pub rules: Vec<WorkdirToolScopeRule>,
@@ -46,14 +48,14 @@ pub struct WorkdirScopeAuthorizationRequest {
} }
/// Provider-side overlap comparison that keeps resolved host paths private. /// 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)] #[serde(deny_unknown_fields)]
pub struct WorkdirScopeOverlapRequest { pub struct WorkdirScopeOverlapRequest {
pub left: WorkdirToolScopeRule, pub left: WorkdirToolScopeRule,
pub right: 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)] #[serde(deny_unknown_fields)]
pub struct WorkdirToolScope { pub struct WorkdirToolScope {
pub rules: Vec<WorkdirToolScopeRule>, pub rules: Vec<WorkdirToolScopeRule>,
+15 -12
View File
@@ -4,9 +4,10 @@
//! transport projections. They intentionally do not expose provider/session //! transport projections. They intentionally do not expose provider/session
//! handles, host paths, Runtime URLs, or credentials. //! handles, host paths, Runtime URLs, or credentials.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; 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")] #[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryMaterializerKind { pub enum WorkingDirectoryMaterializerKind {
#[default] #[default]
@@ -14,7 +15,7 @@ pub enum WorkingDirectoryMaterializerKind {
ClientHostedExternal, ClientHostedExternal,
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryStatusKind { pub enum WorkingDirectoryStatusKind {
Active, 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)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryCleanupTarget { pub struct WorkingDirectoryCleanupTarget {
pub kind: String, pub kind: String,
@@ -50,7 +51,7 @@ pub struct WorkingDirectoryCleanupTarget {
pub repository_key: String, pub repository_key: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryOccupancy { pub struct WorkingDirectoryOccupancy {
pub runtime_id: String, pub runtime_id: String,
@@ -59,7 +60,7 @@ pub struct WorkingDirectoryOccupancy {
pub linked_at: String, pub linked_at: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RuntimeWorkingDirectoryCleanupTarget { pub struct RuntimeWorkingDirectoryCleanupTarget {
pub kind: String, pub kind: String,
@@ -67,7 +68,7 @@ pub struct RuntimeWorkingDirectoryCleanupTarget {
pub repository_id: String, pub repository_id: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RuntimeWorkingDirectorySummary { pub struct RuntimeWorkingDirectorySummary {
pub working_directory_id: String, pub working_directory_id: String,
@@ -99,14 +100,14 @@ pub struct RuntimeWorkingDirectorySummary {
pub occupied_by: Option<WorkingDirectoryOccupancy>, pub occupied_by: Option<WorkingDirectoryOccupancy>,
} }
#[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)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum WorkingDirectorySource { pub enum WorkingDirectorySource {
Repository { repository_key: String }, Repository { repository_key: String },
ExternalGrant { grant_id: String }, ExternalGrant { grant_id: String },
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectorySummary { pub struct WorkingDirectorySummary {
pub working_directory_id: String, pub working_directory_id: String,
@@ -146,7 +147,9 @@ impl WorkingDirectorySummary {
pub use WorkingDirectoryMaterializerKind as MaterializerKind; pub use WorkingDirectoryMaterializerKind as MaterializerKind;
/// Stable Workspace identity for a Worker hosted by a Runtime. /// 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)] #[serde(deny_unknown_fields)]
pub struct RuntimeWorkerRef { pub struct RuntimeWorkerRef {
pub runtime_id: String, pub runtime_id: String,
@@ -164,7 +167,7 @@ impl RuntimeWorkerRef {
} }
/// Immutable materialization provenance retained by Workspace inventory. /// 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)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryProvenance { pub struct WorkingDirectoryProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -179,7 +182,7 @@ pub struct WorkingDirectoryProvenance {
} }
/// Latest provider-neutral observation attached to Workspace inventory. /// 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)] #[serde(deny_unknown_fields)]
pub struct WorkingDirectoryCurrentObservation { pub struct WorkingDirectoryCurrentObservation {
#[serde(default, skip_serializing_if = "Option::is_none")] #[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)] #[serde(deny_unknown_fields)]
pub struct WorkspaceWorkdirSessionOperationRequest { pub struct WorkspaceWorkdirSessionOperationRequest {
/// Worker-local attachment alias selected by the calling tool router. /// Worker-local attachment alias selected by the calling tool router.
+17
View File
@@ -291,6 +291,23 @@ impl From<RuntimeDiagnostic> for server_api::Diagnostic {
} }
} }
impl From<HostSummary> 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<RuntimeSourceSummary> for server_api::RuntimeSourceSummary { impl From<RuntimeSourceSummary> for server_api::RuntimeSourceSummary {
fn from(source: RuntimeSourceSummary) -> Self { fn from(source: RuntimeSourceSummary) -> Self {
let kind = match source.kind { let kind = match source.kind {
+602 -95
View File
@@ -3641,11 +3641,13 @@ fn build_server_auth_router(api: ServerAuthApi) -> Router {
fn server_request_context( fn server_request_context(
actor: Option<RequestActor>, actor: Option<RequestActor>,
worker_source: Option<server_api::ServerWorkerSource>,
origin: Option<String>, origin: Option<String>,
headers: &HeaderMap, headers: &HeaderMap,
) -> server_api::ServerRequestContext { ) -> server_api::ServerRequestContext {
server_api::ServerRequestContext { server_api::ServerRequestContext {
actor, actor,
worker_source,
origin, origin,
transport_headers: headers transport_headers: headers
.iter() .iter()
@@ -3683,7 +3685,7 @@ fn contract_request_headers(
async fn attach_server_origin_context(mut request: Request, next: Next) -> Response { async fn attach_server_origin_context(mut request: Request, next: Next) -> Response {
let actor = request.extensions().get::<RequestActor>().cloned(); let actor = request.extensions().get::<RequestActor>().cloned();
let origin = request_origin(request.headers()); 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); request.extensions_mut().insert(context);
next.run(request).await next.run(request).await
} }
@@ -3708,8 +3710,15 @@ async fn attach_server_request_context(
Err(error) => return server_error_response(error), Err(error) => return server_error_response(error),
} }
}; };
let worker_source = request
.extensions()
.get::<crate::worker_source::VerifiedWorkerMutationSource>()
.map(|source| server_api::ServerWorkerSource {
runtime_id: source.runtime_id.clone(),
worker_id: source.worker_id.clone(),
});
let origin = request_origin(request.headers()); 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); request.extensions_mut().insert(context);
next.run(request).await next.run(request).await
} }
@@ -3865,6 +3874,62 @@ fn generated_workspace_contract_router(service: ServerApiContractService) -> Rou
.merge(server_api::server_api_axum::repository_create( .merge(server_api::server_api_axum::repository_create(
service.clone(), 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( .merge(server_api::server_api_axum::ticket_list_alias(
service.clone(), service.clone(),
)) ))
@@ -6103,6 +6168,451 @@ impl server_api::ServerApi for ServerApiContractService {
.map_err(ApiError::into_repository_api_error)?; .map_err(ApiError::into_repository_api_error)?;
project_contract_dto(&response) 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<server_api::RepositorySshHostTrust, server_api::RepositoryApiError>
{
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<server_api::RepositoryLogResponse, server_api::RepositoryApiError>
{
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<server_api::RepositoryLogResponse, server_api::RepositoryApiError>
{
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<server_api::HostListResponse, server_api::RepositoryApiError> {
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<server_api::HostListResponse, server_api::RepositoryApiError> {
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<server_api::HostWorkerListResponse, server_api::RepositoryApiError>
{
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<server_api::HostWorkerListResponse, server_api::RepositoryApiError>
{
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<server_api::WorkingDirectoryListResponse, server_api::RepositoryApiError>
{
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<server_api::ExternalWorkdirGrantResponse, server_api::RepositoryApiError>
{
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<server_api::ExternalWorkdirGrantResponse, server_api::RepositoryApiError>
{
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<server_api::ExternalWorkdirGrantResponse, server_api::RepositoryApiError>
{
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<server_api::WorkingDirectoryListResponse, server_api::RepositoryApiError>
{
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 { 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)) let auth = build_server_auth_router(ServerAuthApi::from(&api))
.merge(generated_auth_contract_router(contract_service.clone())); .merge(generated_auth_contract_router(contract_service.clone()));
let workspace = Router::new() let workspace = Router::new()
.route( // WebSocket provider transport is intentionally outside the generated
"/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test", // JSON REST contract.
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),
)
.route( .route(
"/api/w/{workspace_id}/external-workdir-grants/{grant_id}/provider", "/api/w/{workspace_id}/external-workdir-grants/{grant_id}/provider",
get(scoped_external_workdir_provider_ws), get(scoped_external_workdir_provider_ws),
) )
.route( .route(
"/api/w/{workspace_id}/working-directories", "/api/w/{workspace_id}/runtime-config",
get(scoped_list_working_directories).post(scoped_create_working_directory), get(get_latest_workspace_runtime_config),
)
.route(
"/api/w/{workspace_id}/working-directories/{working_directory_id}",
get(scoped_working_directory_detail).delete(scoped_cleanup_working_directory),
) )
.route("/api/runtimes", get(list_runtimes)) .route("/api/runtimes", get(list_runtimes))
.route( .route(
@@ -6381,11 +6844,6 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/protocol/ws", "/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/protocol/ws",
get(scoped_worker_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)) .fallback(get(static_or_spa_fallback))
.with_state(api) .with_state(api)
.merge(generated_workspace_contract_router(contract_service)); .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) working_directory_detail_for_runtime(api, &path.runtime_id, &path.working_directory_id)
} }
async fn scoped_cleanup_runtime_working_directory( fn workdir_removal_source_actor(
State(api): State<WorkspaceApi>, worker_source: Option<(&str, &str)>,
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>, request_actor: Option<&RequestActor>,
worker_source: Option<Extension<crate::worker_source::VerifiedWorkerMutationSource>>, ) -> ApiResult<String> {
request_actor: Option<Extension<RequestActor>>, if let Some((runtime_id, worker_id)) = worker_source {
Json(request): Json<WorkingDirectoryRemovalRequest>, Ok(format!("worker:{runtime_id}:{worker_id}"))
) -> ApiResult<Json<WorkingDirectoryRemovalResponse>> { } 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<String> {
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<WorkingDirectoryRemovalResponse> {
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<WorkingDirectoryRemovalResponse> {
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<WorkingDirectoryRemovalResponse> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
require_active_workspace_runtime_binding(&api, &path.runtime_id).await?; require_active_workspace_runtime_binding(&api, &path.runtime_id).await?;
let registered_runtime = registered_workdir_runtime_id(&api, &path.working_directory_id)?; 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(), "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( execute_workdir_removal(
&api, &api,
&path.working_directory_id, &path.working_directory_id,
&source_actor, &source_actor,
&request.reason, &request.reason,
) )
.map(Json)
.map_err(ApiError::from) .map_err(ApiError::from)
} }
@@ -14736,30 +15252,19 @@ async fn scoped_working_directory_detail(
} }
} }
async fn scoped_cleanup_working_directory( async fn cleanup_working_directory(
State(api): State<WorkspaceApi>, api: WorkspaceApi,
AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>, path: ScopedWorkingDirectoryPath,
worker_source: Option<Extension<crate::worker_source::VerifiedWorkerMutationSource>>, source_actor: String,
request_actor: Option<Extension<RequestActor>>, request: WorkingDirectoryRemovalRequest,
Json(request): Json<WorkingDirectoryRemovalRequest>, ) -> ApiResult<WorkingDirectoryRemovalResponse> {
) -> ApiResult<Json<WorkingDirectoryRemovalResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?; 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( execute_workdir_removal(
&api, &api,
&path.working_directory_id, &path.working_directory_id,
&source_actor, &source_actor,
&request.reason, &request.reason,
) )
.map(Json)
.map_err(ApiError::from) .map_err(ApiError::from)
} }
@@ -15022,6 +15527,7 @@ async fn create_workspace_working_directory(
.expect("Repository Workdir detail has Runtime"), .expect("Repository Workdir detail has Runtime"),
item: response.item, item: response.item,
diagnostics: response.diagnostics, 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, runtime_id: reserved.resolved_runtime_id,
item: summary.into(), item: summary.into(),
diagnostics: working_directory_diagnostics(result.diagnostics), 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( async fn scoped_list_host_workers(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedHostPath>, AxumPath(path): AxumPath<ScopedHostPath>,
) -> ApiResult<Json<server_api::ListResponse<server_api::WorkerSummary>>> { ) -> ApiResult<Json<server_api::HostWorkerListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
list_host_workers(State(api), AxumPath(path.host_id)).await list_host_workers(State(api), AxumPath(path.host_id)).await
} }
@@ -20394,14 +20901,14 @@ fn protocol_error_event(message: impl Into<String>) -> protocol::Event {
async fn list_host_workers( async fn list_host_workers(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(host_id): AxumPath<String>, AxumPath(host_id): AxumPath<String>,
) -> ApiResult<Json<server_api::ListResponse<server_api::WorkerSummary>>> { ) -> ApiResult<Json<server_api::HostWorkerListResponse>> {
let limit = api.config.max_records.min(200); let limit = api.config.max_records.min(200);
let runtime_workers = api let runtime_workers = api
.runtime .runtime
.list_workers_for_host(&host_id, limit) .list_workers_for_host(&host_id, limit)
.map_err(|err| err.into_error())?; .map_err(|err| err.into_error())?;
let items = project_observed_workspace_workers(&api, runtime_workers.items)?; 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(), workspace_id: api.workspace_id().to_string(),
limit, limit,
items, items,
+4815 -1
View File
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,12 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit. // @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json // Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256: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 // Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts // Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check // Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
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 CreateWorkspaceRepositoryRequest = { default_ref?: string | null; repository_key: string; source: string; };
export type CreateWorkspaceRepositoryResponse = { replayed: boolean; repository_key: string; workspace_id: 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 DiagnosticSeverity = "info" | "warning" | "error";
export type GitCommitSummary = { author_date: string; author_email: string; author_name: string; hash: string; parents: Array<string>; refs: Array<string>; short_hash: string; summary: string; };
export type GitRemoteSummary = { fetch_url: string; name: string; }; export type GitRemoteSummary = { fetch_url: string; name: string; };
export type GitRepositorySummary = { branch?: string | null; dirty: boolean; head?: string | null; remotes: Array<GitRemoteSummary>; status: string; }; export type GitRepositorySummary = { branch?: string | null; dirty: boolean; head?: string | null; remotes: Array<GitRemoteSummary>; status: string; };
export type HostListResponse = { diagnostics: Array<Diagnostic>; items: Array<HostSummary>; limit: number; source: string; workspace_id: string; };
export type HostSummary = { arch: string; diagnostics: Array<Diagnostic>; 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<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; }; export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
export type RepositoryDetailResponse = { item: RepositorySummary; source: string; workspace_id: 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<Diagnostic>; items: Array<RepositorySummary>; source: string; workspace_id: string; }; export type RepositoryListResponse = { diagnostics: Array<Diagnostic>; items: Array<RepositorySummary>; source: string; workspace_id: string; };
export type RepositoryLogResponse = { default_selector?: string | null; diagnostics: Array<Diagnostic>; items: Array<GitCommitSummary>; limit: number; repository_key: string; workspace_id: string; };
export type RepositoryObservedStatus = "unverified" | "ready" | "invalid"; export type RepositoryObservedStatus = "unverified" | "ready" | "invalid";
export type RepositorySource = { kind: RepositorySourceKind; uri: string; }; export type RepositorySource = { kind: RepositorySourceKind; uri: string; };
export type RepositorySourceKind = "local_path" | "file" | "ssh" | "https" | "invalid"; export type RepositorySourceKind = "local_path" | "file" | "ssh" | "https" | "invalid";
export type RepositorySshConnectionProbeRequest = { runtime_id: string; };
export type RepositorySshConnectionProbeResponse = { candidates: Array<RepositorySshHostKeyCandidate>; 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<string>; updated_at: string; workspace_id: string; };
export type RepositorySummary = { default_selector?: string | null; diagnostics?: Array<RepositoryDiagnostic> | null; git?: GitRepositorySummary | null; kind: string; observed_at?: string | null; observed_status: RepositoryObservedStatus; provider: string; record_authority: string; repository_key: string; source: RepositorySource; source_fingerprint: string; source_revision: number; }; export type RepositorySummary = { default_selector?: string | null; diagnostics?: Array<RepositoryDiagnostic> | null; git?: GitRepositorySummary | null; kind: string; observed_at?: string | null; observed_status: RepositoryObservedStatus; provider: string; record_authority: string; repository_key: string; source: RepositorySource; source_fingerprint: string; source_revision: number; };
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
export type 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; };
+100 -24
View File
@@ -1,34 +1,110 @@
// Generated from server-api. Do not edit by hand. // @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Regenerate: cargo run -q -p server-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts // 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 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<FsPath>; 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<ListEntry>; 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<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]; path: FsPath; start_line: number; total_lines: number; truncated: boolean; };
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; 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<Diagnostic> | null; error?: string | null; message: string; };
export type WorkdirScopeAuthorizationRequest = { path: FsPath; permission: WorkdirToolScopePermission; rules: Array<WorkdirToolScopeRule>; };
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<Diagnostic>; item: WorkingDirectorySummary; runtime_id: string; workspace_id: string; };
export type WorkingDirectoryDetailResponse = { diagnostics: Array<Diagnostic>; item: WorkingDirectorySummary; runtime_id?: string | null; workspace_id: string; };
export type WorkingDirectoryListResponse = { diagnostics: Array<Diagnostic>; items: Array<WorkingDirectorySummary>; workspace_id: string; };
export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external"; export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; };
export type 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 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<number>; 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 WriteResult = { bytes_written: number; created: boolean; };
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<WorkingDirectorySummary>, diagnostics: Array<Diagnostic>, };
export type WorkingDirectoryDetailResponse = { workspace_id: string, runtime_id?: string | null, item: WorkingDirectorySummary, diagnostics: Array<Diagnostic>, };
export type WorkingDirectoryCreateResponse = { workspace_id: string, runtime_id: string, item: WorkingDirectorySummary, diagnostics: Array<Diagnostic>, };
@@ -1,10 +1,5 @@
import type { ApiResult } from "$lib/workspace/api/http"; import type { ApiResult } from "$lib/workspace/api/http";
import type { import type {
GitCommitSummary,
RepositoryLogResponse,
RepositorySshConnectionProbeResponse,
RepositorySshConnectionTrustState,
RepositorySshHostKeyCandidate,
WorkspaceAuthConfig, WorkspaceAuthConfig,
WorkspaceCatalogListResponse, WorkspaceCatalogListResponse,
WorkspaceCreateResponse, WorkspaceCreateResponse,
@@ -24,22 +19,25 @@ import type {
import type { import type {
CreateWorkspaceRepositoryResponse, CreateWorkspaceRepositoryResponse,
Diagnostic, Diagnostic,
GitCommitSummary,
GitRemoteSummary, GitRemoteSummary,
GitRepositorySummary, GitRepositorySummary,
HostListResponse,
HostSummary,
RepositoryApiError, RepositoryApiError,
RepositoryDetailResponse, RepositoryDetailResponse,
RepositoryDiagnostic, RepositoryDiagnostic,
RepositoryListResponse, RepositoryListResponse,
RepositoryLogResponse,
RepositorySource, RepositorySource,
RepositorySourceKind, RepositorySourceKind,
RepositorySshConnectionProbeResponse,
RepositorySshConnectionTrustState,
RepositorySshHostKeyCandidate,
RepositorySummary, RepositorySummary,
} from "$lib/generated/repository-api.ts"; } from "$lib/generated/repository-api.ts";
export type { export type {
GitCommitSummary,
RepositoryLogResponse,
RepositorySshConnectionProbeResponse,
RepositorySshHostKeyCandidate,
WorkspaceCatalogListResponse, WorkspaceCatalogListResponse,
WorkspaceCreateResponse, WorkspaceCreateResponse,
WorkspaceDeletionOperationResponse, WorkspaceDeletionOperationResponse,
@@ -51,13 +49,19 @@ export type {
export type { export type {
CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryRequest,
CreateWorkspaceRepositoryResponse, CreateWorkspaceRepositoryResponse,
GitCommitSummary,
GitRemoteSummary, GitRemoteSummary,
GitRepositorySummary, GitRepositorySummary,
HostListResponse,
HostSummary,
RepositoryApiError, RepositoryApiError,
RepositoryDetailResponse, RepositoryDetailResponse,
RepositoryListResponse, RepositoryListResponse,
RepositoryLogResponse,
RepositorySource, RepositorySource,
RepositorySourceKind, RepositorySourceKind,
RepositorySshConnectionProbeResponse,
RepositorySshHostKeyCandidate,
RepositorySummary, RepositorySummary,
} from "$lib/generated/repository-api.ts"; } 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( export function parseRepositoryLogResponse(
value: unknown, value: unknown,
): RepositoryLogResponse { ): RepositoryLogResponse {
@@ -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 = { export type WorkerCapabilities = {
can_stop: boolean; can_stop: boolean;
can_spawn_followup: boolean; can_spawn_followup: boolean;
@@ -1,10 +1,24 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; 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"; 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 }) => { export const load: PageLoad = async ({ fetch, params }) => {
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path); const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
const hosts = await loadJson<ListResponse<Host>>(fetch, apiPath("/hosts")); const hosts = await loadJson<HostListResponse>(
fetch,
apiPath("/hosts"),
undefined,
parseHostListResponse,
HOST_API_LOAD_POLICY,
);
return { return {
workspaceId: params.workspaceId, workspaceId: params.workspaceId,
@@ -3,7 +3,7 @@
ConfirmRepositorySshHostTrustRequest, ConfirmRepositorySshHostTrustRequest,
RepositorySshConnectionProbeRequest, RepositorySshConnectionProbeRequest,
RepositorySshConnectionProbeResponse RepositorySshConnectionProbeResponse
} from '$lib/generated/legacy-server-api'; } from '$lib/generated/repository-api';
import { parseRepositorySshHostTrust } from '$lib/workspace/api/repository-access'; import { parseRepositorySshHostTrust } from '$lib/workspace/api/repository-access';
import { formatDate, workspaceApiPath } from '$lib/workspace/api/http'; import { formatDate, workspaceApiPath } from '$lib/workspace/api/http';
import { parseRepositorySshConnectionProbeResponse } from '$lib/workspace/api/workspace-model'; import { parseRepositorySshConnectionProbeResponse } from '$lib/workspace/api/workspace-model';