feat: migrate runtime worker domains to ServerApi

This commit is contained in:
2026-09-23 08:24:39 +09:00
parent 7a975765ac
commit ad12d77ad4
28 changed files with 10269 additions and 896 deletions
@@ -359,207 +359,3 @@ export type RepositoryLogResponse = {
items: Array<GitCommitSummary>;
diagnostics: Array<Diagnostic>;
};
export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http";
export type RuntimeSourceStatus = "active" | "reserved";
export type RuntimeIdentityAuthority =
| "runtime_registry_projection"
| "server_runtime_configuration";
export type RuntimeSourceSummary = {
kind: RuntimeSourceKind;
status: RuntimeSourceStatus;
identity_authority: RuntimeIdentityAuthority;
note: string;
};
export type RuntimeSummary = {
runtime_id: string;
label: string;
kind: string;
status: string;
source: RuntimeSourceSummary;
host_ids: Array<string>;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceRuntimeBindingState =
| "configured"
| "verified"
| "revoked";
export type RuntimeConnectionDisplayState =
| "configured"
| "verified"
| "unavailable"
| "revoked";
export type RuntimeVerificationOutcome =
| "verified"
| "challenge_issued"
| "verification_failed"
| "connectivity_failed";
export type RuntimeVerificationEvidenceSummary = {
verified_at: string | null;
last_checked_at: string;
last_outcome: RuntimeVerificationOutcome;
binding_revision: number;
workspace_key_id: string;
workspace_identity_revision: number;
workspace_trust_generation: number;
runtime_public_key_fingerprint: string;
runtime_identity_revision: number;
};
export type WorkspaceRuntimeBindingSummary = {
state: WorkspaceRuntimeBindingState;
connection_state: RuntimeConnectionDisplayState;
revision: number;
workspace_key_id?: string | null;
workspace_key_generation?: number | null;
verification?: RuntimeVerificationEvidenceSummary | null;
};
export type RuntimeManagementSummary = {
built_in: boolean;
config_managed: boolean;
removable: boolean;
endpoint_configured: boolean;
token_ref_configured: boolean;
binding?: WorkspaceRuntimeBindingSummary | null;
};
export type WorkspaceRuntimeResource = {
management: RuntimeManagementSummary;
runtime_id: string;
label: string;
kind: string;
status: string;
source: RuntimeSourceSummary;
host_ids: Array<string>;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Array<Diagnostic>;
};
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
export type RuntimeTrustKeyState = {
status: RuntimeTrustKeyStatus;
fingerprint?: string | null;
revision?: number | null;
created_at?: string | null;
updated_at?: string | null;
revoked_at?: string | null;
};
export type RuntimeTrustAuditAction =
| "created"
| "replaced"
| "reactivated"
| "revoked";
export type RuntimeTrustAuditEntry = {
action: RuntimeTrustAuditAction;
actor_account_id: string;
old_fingerprint?: string | null;
new_fingerprint?: string | null;
revision: number;
at: string;
};
export type WorkspaceRuntimeDetail = {
workspace_id: string;
runtime: WorkspaceRuntimeResource;
endpoint?: string | null;
trust_key: RuntimeTrustKeyState;
recent_audit: Array<RuntimeTrustAuditEntry>;
};
export type RuntimeTrustKeyRevealResponse = { public_key: string };
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number };
export type RemoveRuntimeRequest = {
operation_id: string;
expected_binding_revision: number;
};
export type RuntimeRemovalOperationState =
| "pending"
| "cleanup_pending"
| "succeeded"
| "failed";
export type RuntimeRemovalOperationResponse = {
operation_id: string;
workspace_id: string;
runtime_id: string;
state: RuntimeRemovalOperationState;
binding_removed: boolean;
runtime_registration_removed: boolean | null;
failure_category?: string | null;
created_at: string;
updated_at: string;
completed_at?: string | null;
};
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
export type RuntimeTrustConflictResponse = {
error: RuntimeTrustConflictKind;
message: string;
current_revision?: number;
current_fingerprint?: string | null;
};
export type RuntimePublicIdentityBundle = {
identity_id: string;
public_key: string;
};
export type CreateRemoteRuntimeRequest = {
public_bundle: RuntimePublicIdentityBundle;
display_name?: string | null;
endpoint: string;
expected_revision?: number | null;
};
export type UpdateRemoteRuntimeRequest = {
display_name?: string | null;
endpoint: string;
};
export type RuntimeConnectionTestStatus = "compatible" | "failed";
export type RuntimeConnectionTestFailureKind =
| "authentication"
| "authorization"
| "network_unreachable"
| "timeout"
| "tls_or_transport"
| "malformed_response"
| "protocol_version_mismatch"
| "runtime_identity_mismatch"
| "configuration";
export type RuntimeConnectionTestResponse = {
workspace_id: string;
runtime_id: string;
binding_revision: number;
connection_state: RuntimeConnectionDisplayState;
verification: RuntimeVerificationEvidenceSummary | null;
checked_at: string;
status: RuntimeConnectionTestStatus;
failure_kind: RuntimeConnectionTestFailureKind | null;
expected_protocol_version: number;
actual_protocol_version: number | null;
diagnostics: Array<Diagnostic>;
};
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:e3f776a17079ce202725bb97de607426f4bfad9afe390ba7bbf76186e871be59)
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -0,0 +1,172 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
export type AttachmentUploadCancelResponse = { cancelled: boolean; };
export type AttachmentUploadGrantResponse = { expires_at_ms: number; upload_id: string; };
export type CleanupTargetKind = "worker_delete" | "workdir_clean_cleanup" | "workdir_dirty_discard" | "workdir_record_delete";
export type CleanupWorkdirCandidate = { action: CleanupTargetKind; blocking_reason?: string | null; cleanliness: CleanupWorkdirCleanliness; estimated_reclaim_bytes?: number | null; file_status: CleanupWorkdirFileStatus; linked_running_worker_ids: Array<string>; linked_worker_ids: Array<string>; pinned_linked: boolean; reason: string; repository_key: string; running_linked: boolean; runtime_id: string; target_id: string; workdir_id: string; };
export type CleanupWorkdirCleanliness = "clean" | "dirty" | "unknown";
export type CleanupWorkdirFileStatus = "pending" | "present" | "active" | "cleanup_pending" | "not_found" | "corrupted" | "failed" | "unknown";
export type CleanupWorkerCandidate = { action: CleanupTargetKind; blocking_reason?: string | null; estimated_reclaim_bytes?: number | null; linked_workdir_ids: Array<string>; pinned: boolean; reason: string; retention_state: string; running_linked: boolean; runtime_id: string; runtime_worker_id: string; target_id: string; worker_id: string; };
export type CreateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; expected_revision?: number | null; public_bundle: RuntimePublicIdentityBundle; };
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
export type DiagnosticSeverity = "info" | "warning" | "error";
export type ExecuteRuntimeCleanupRequest = { confirm_dirty_discard_target_ids?: Array<string>; expected_plan_digest: string; expected_plan_revision: string; workdir_target_ids?: Array<string>; worker_target_ids?: Array<string>; };
export type RemoveRuntimeRequest = { expected_binding_revision: number; operation_id: string; };
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number; };
export type RuntimeCleanupExecutionResponse = { diagnostics: Array<Diagnostic>; executed_at: string; plan_after: RuntimeCleanupPlanResponse; results: Array<RuntimeCleanupExecutionResult>; runtime_id: string; workspace_id: string; };
export type RuntimeCleanupExecutionResult = { action: CleanupTargetKind; message: string; status: string; target_id: string; };
export type RuntimeCleanupPlanResponse = { diagnostics: Array<Diagnostic>; digest: string; generated_at: string; revision: string; runtime_id: string; workdirs: Array<CleanupWorkdirCandidate>; workers: Array<CleanupWorkerCandidate>; workspace_id: string; };
export type RuntimeConfigBundleResult = { availability?: unknown; diagnostics?: Array<Diagnostic>; state: WorkerOperationState; };
export type RuntimeConfigBundleSyncRequest = { bundle: unknown; };
export type RuntimeConnectionDisplayState = "configured" | "verified" | "unavailable" | "revoked";
export type RuntimeConnectionTestFailureKind = "authentication" | "authorization" | "network_unreachable" | "timeout" | "tls_or_transport" | "malformed_response" | "protocol_version_mismatch" | "runtime_identity_mismatch" | "configuration";
export type RuntimeConnectionTestResponse = { actual_protocol_version?: number | null; binding_revision: number; checked_at: string; connection_state: RuntimeConnectionDisplayState; diagnostics?: Array<Diagnostic>; expected_protocol_version: number; failure_kind?: RuntimeConnectionTestFailureKind | null; runtime_id: string; status: RuntimeConnectionTestStatus; verification?: RuntimeVerificationEvidenceSummary | null; workspace_id: string; };
export type RuntimeConnectionTestStatus = "compatible" | "failed";
export type RuntimeIdentityAuthority = "runtime_registry_projection" | "server_runtime_configuration";
export type RuntimeListResponse = { diagnostics?: Array<Diagnostic>; items: Array<RuntimeSummary>; limit: number; source: string; workspace_id: string; };
export type RuntimeManagementApiError = { current_fingerprint?: string | null; current_revision?: number | null; diagnostics?: Array<Diagnostic>; error: string; message: string; };
export type RuntimeManagementSummary = { binding?: WorkspaceRuntimeBindingSummary | null; built_in: boolean; config_managed: boolean; endpoint_configured: boolean; removable: boolean; token_ref_configured: boolean; };
export type RuntimeProfileSelector = { kind: "builtin"; value: string; } | { kind: "named"; value: string; };
export type RuntimePublicIdentityBundle = { identity_id: string; public_key: string; };
export type RuntimeRemovalOperationResponse = { binding_removed: boolean; completed_at?: string | null; created_at: string; failure_category?: string | null; operation_id: string; runtime_id: string; runtime_registration_removed?: boolean | null; state: RuntimeRemovalOperationState; updated_at: string; workspace_id: string; };
export type RuntimeRemovalOperationState = "pending" | "cleanup_pending" | "succeeded" | "failed";
export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http";
export type RuntimeSourceStatus = "active" | "reserved";
export type RuntimeSourceSummary = { identity_authority: RuntimeIdentityAuthority; kind: RuntimeSourceKind; note: string; status: RuntimeSourceStatus; };
export type RuntimeSummary = { arch: string; diagnostics?: Array<Diagnostic>; host_ids?: Array<string>; kind: string; label: string; os: string; runtime_id: string; source: RuntimeSourceSummary; status: string; worker_creation_available: boolean; };
export type RuntimeTicketWorkerRole = "intake" | "orchestrator" | "coder" | "reviewer";
export type RuntimeTrustAuditAction = "created" | "replaced" | "reactivated" | "revoked";
export type RuntimeTrustAuditEntry = { action: RuntimeTrustAuditAction; actor_account_id: string; at: string; new_fingerprint?: string | null; old_fingerprint?: string | null; revision: number; };
export type RuntimeTrustKeyRevealResponse = { public_key: string; };
export type RuntimeTrustKeyState = { created_at?: string | null; fingerprint?: string | null; revision?: number | null; revoked_at?: string | null; status: RuntimeTrustKeyStatus; updated_at?: string | null; };
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
export type RuntimeVerificationEvidenceSummary = { binding_revision: number; last_checked_at: string; last_outcome: RuntimeVerificationOutcome; runtime_identity_revision: number; runtime_public_key_fingerprint: string; verified_at?: string | null; workspace_identity_revision: number; workspace_key_id: string; workspace_trust_generation: number; };
export type RuntimeVerificationOutcome = "verified" | "challenge_issued" | "verification_failed" | "connectivity_failed";
export type RuntimeWorkerCompletionsRequest = { kind: unknown; prefix?: string; };
export type RuntimeWorkerCompletionsResult = { diagnostics?: Array<Diagnostic>; entries: Array<unknown>; kind: unknown; prefix: string; runtime_id: string; worker_id: string; };
export type RuntimeWorkerInputRequest = { content: string; kind?: string | null; segments?: Array<unknown> | null; };
export type RuntimeWorkerInputResult = { diagnostics?: Array<Diagnostic>; runtime_id: string; state: WorkerOperationState; worker_id: string; };
export type RuntimeWorkerLifecycleRequest = { reason?: string | null; ticket_assignment?: RuntimeWorkerTicketAssignmentRequest | null; };
export type RuntimeWorkerLifecycleResult = { diagnostics?: Array<Diagnostic>; runtime_id: string; state: WorkerOperationState; worker_id: string; };
export type RuntimeWorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
export type RuntimeWorkerShowResponse = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; updated_at: string; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
export type RuntimeWorkerSpawnAcceptanceEvidence = { detail: string; kind: string; };
export type RuntimeWorkerSpawnAcceptanceRequirement = "socket_ready" | { run_accepted: { expected_segments: number; }; };
export type RuntimeWorkerSpawnIntent = { kind: "workspace_companion"; } | { kind: "workspace_orchestrator"; } | { kind: "workspace_coding"; } | { kind: "ticket_role"; role: RuntimeTicketWorkerRole; ticket_id: string; };
export type RuntimeWorkerSpawnRequest = { acceptance: RuntimeWorkerSpawnAcceptanceRequirement; initial_submit?: Array<unknown>; intent: RuntimeWorkerSpawnIntent; profile: RuntimeProfileSelector; requested_worker_name?: string | null; ticket_assignment?: RuntimeWorkerTicketAssignmentRequest | null; workdir_attachment_requests?: Array<RuntimeWorkerSpawnWorkingDirectoryAttachmentRequest>; };
export type RuntimeWorkerSpawnResponse = { acceptance_evidence?: Array<RuntimeWorkerSpawnAcceptanceEvidence>; diagnostics?: Array<Diagnostic>; state: WorkerOperationState; worker?: RuntimeWorkerSummary | null; };
export type RuntimeWorkerSpawnWorkingDirectoryAttachmentRequest = { alias: string; working_directory: RuntimeWorkerSpawnWorkingDirectoryRequest; };
export type RuntimeWorkerSpawnWorkingDirectoryRequest = { repository_key: string; selector?: string | null; };
export type RuntimeWorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<unknown>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
export type RuntimeWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; };
export type UpdateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; };
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
export type WorkerFileDeleteResponse = { deleted: boolean; };
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
export type WorkerOperationState = "accepted" | "unsupported" | "rejected";
export type WorkerRestoreResponse = { result: WorkerRestoreResult; runtime_id: string; worker_id: string; workspace_id: string; };
export type WorkerRestoreResult = { diagnostics?: Array<Diagnostic>; state: WorkerRestoreState; worker?: WorkerSummary | null; };
export type WorkerRestoreState = "accepted" | "rejected" | "rolled_back" | "reconciliation_required";
export type WorkerRetentionResponse = { pinned: boolean; retention_state: string; runtime_id: string; worker_id: string; workspace_id: string; };
export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
export type 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; };
export type WorkspaceRuntimeBindingState = "configured" | "verified" | "revoked";
export type WorkspaceRuntimeBindingSummary = { connection_state: RuntimeConnectionDisplayState; revision: number; state: WorkspaceRuntimeBindingState; verification?: RuntimeVerificationEvidenceSummary | null; workspace_key_generation?: number | null; workspace_key_id?: string | null; };
export type WorkspaceRuntimeDetail = { endpoint?: string | null; recent_audit?: Array<RuntimeTrustAuditEntry>; runtime: WorkspaceRuntimeResource; trust_key: RuntimeTrustKeyState; workspace_id: string; };
export type WorkspaceRuntimeListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkspaceRuntimeResource>; limit: number; source: string; workspace_id: string; };
export type WorkspaceRuntimeResource = { arch: string; diagnostics?: Array<Diagnostic>; host_ids?: Array<string>; kind: string; label: string; management: RuntimeManagementSummary; os: string; runtime_id: string; source: RuntimeSourceSummary; status: string; worker_creation_available: boolean; };
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:e3f776a17079ce202725bb97de607426f4bfad9afe390ba7bbf76186e871be59)
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
// 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
@@ -3,7 +3,7 @@ import type {
RuntimeConnectionTestFailureKind,
RuntimeConnectionTestResponse,
RuntimeVerificationEvidenceSummary,
} from "$lib/generated/legacy-server-api";
} from "$lib/generated/runtime-api";
const RESPONSE_KEYS = [
"workspace_id",
@@ -5,6 +5,7 @@ import type {
RevokeRuntimeTrustKeyRequest,
RuntimeConnectionDisplayState,
RuntimeIdentityAuthority,
RuntimeManagementApiError,
RuntimeManagementSummary,
RuntimeRemovalOperationResponse,
RuntimeRemovalOperationState,
@@ -13,8 +14,6 @@ import type {
RuntimeSourceSummary,
RuntimeTrustAuditAction,
RuntimeTrustAuditEntry,
RuntimeTrustConflictKind,
RuntimeTrustConflictResponse,
RuntimeTrustKeyRevealResponse,
RuntimeTrustKeyState,
RuntimeTrustKeyStatus,
@@ -23,12 +22,24 @@ import type {
WorkspaceRuntimeBindingState,
WorkspaceRuntimeBindingSummary,
WorkspaceRuntimeDetail,
WorkspaceRuntimeListResponse,
WorkspaceRuntimeResource,
} from "$lib/generated/legacy-server-api.ts";
import type { ListResponse } from "$lib/workspace/sidebar/types";
} from "$lib/generated/runtime-api.ts";
import { workspaceApiPath } from "./http.ts";
export type WorkspaceRuntimeList = ListResponse<WorkspaceRuntimeResource>;
export type WorkspaceRuntimeList = Omit<WorkspaceRuntimeListResponse, "diagnostics"> & {
diagnostics: Diagnostic[];
};
export type ValidatedWorkspaceRuntimeDetail = Omit<
WorkspaceRuntimeDetail,
"recent_audit"
> & {
recent_audit: RuntimeTrustAuditEntry[];
};
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
export type RuntimeTrustConflictResponse = RuntimeManagementApiError & {
error: RuntimeTrustConflictKind;
};
const LIMITS = {
runtimeItems: 200,
@@ -690,7 +701,7 @@ export function parseWorkspaceRuntimeList(
export function parseWorkspaceRuntimeDetail(
value: unknown,
): WorkspaceRuntimeDetail {
): ValidatedWorkspaceRuntimeDetail {
const response = object(value, "Runtime detail response");
exactKeys(
response,
@@ -0,0 +1,356 @@
import type {
CleanupTargetKind,
CleanupWorkdirCandidate,
CleanupWorkdirCleanliness,
CleanupWorkdirFileStatus,
CleanupWorkerCandidate,
Diagnostic,
RuntimeCleanupExecutionResponse,
RuntimeCleanupExecutionResult,
RuntimeCleanupPlanResponse,
RuntimeWorkerLifecycleResult,
WorkerOperationState,
WorkerRetentionResponse,
} from "$lib/generated/runtime-api";
const MAX_STRING_BYTES = 4_096;
const MAX_CANDIDATES = 1_000;
const MAX_RESULTS = 2_000;
const MAX_DIAGNOSTICS = 64;
const encoder = new TextEncoder();
type JsonObject = Record<string, unknown>;
const TARGET_KINDS = new Set<CleanupTargetKind>([
"worker_delete",
"workdir_clean_cleanup",
"workdir_dirty_discard",
"workdir_record_delete",
]);
const FILE_STATUSES = new Set<CleanupWorkdirFileStatus>([
"pending",
"present",
"active",
"cleanup_pending",
"not_found",
"corrupted",
"failed",
"unknown",
]);
const CLEANLINESS = new Set<CleanupWorkdirCleanliness>([
"clean",
"dirty",
"unknown",
]);
export class RuntimeWorkerValidationError extends Error {
constructor(message: string) {
super(message.slice(0, 256));
this.name = "RuntimeWorkerValidationError";
}
}
function fail(path: string, message: string): never {
throw new RuntimeWorkerValidationError(`${path} ${message}`);
}
function object(value: unknown, path: string): JsonObject {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return fail(path, "must be an object");
}
return value as JsonObject;
}
function exactKeys(
value: JsonObject,
required: readonly string[],
optional: readonly string[],
path: string,
): void {
const allowed = new Set([...required, ...optional]);
for (const key of Object.keys(value)) {
if (!allowed.has(key)) fail(`${path}.${key}`, "is not part of the wire contract");
}
for (const key of required) {
if (!Object.hasOwn(value, key)) fail(`${path}.${key}`, "is required");
}
}
function boundedString(value: unknown, path: string, allowEmpty = false): string {
if (typeof value !== "string") return fail(path, "must be a string");
if (!allowEmpty && value.length === 0) return fail(path, "must not be empty");
if (encoder.encode(value).byteLength > MAX_STRING_BYTES) {
return fail(path, `must be at most ${MAX_STRING_BYTES} UTF-8 bytes`);
}
return value;
}
function boolean(value: unknown, path: string): boolean {
if (typeof value !== "boolean") return fail(path, "must be a boolean");
return value;
}
function array(value: unknown, path: string, max: number): unknown[] {
if (!Array.isArray(value)) return fail(path, "must be an array");
if (value.length > max) return fail(path, `must contain at most ${max} items`);
return value;
}
function strings(value: unknown, path: string): string[] {
return array(value, path, MAX_CANDIDATES).map((entry, index) =>
boundedString(entry, `${path}[${index}]`)
);
}
function optionalNullableString(value: unknown, path: string): string | null | undefined {
if (value === undefined || value === null) return value;
return boundedString(value, path);
}
function optionalNullableSafeInteger(value: unknown, path: string): number | null | undefined {
if (value === undefined || value === null) return value;
if (!Number.isSafeInteger(value) || (value as number) < 0) {
return fail(path, "must be a non-negative safe integer");
}
return value as number;
}
function targetKind(value: unknown, path: string): CleanupTargetKind {
if (typeof value !== "string" || !TARGET_KINDS.has(value as CleanupTargetKind)) {
return fail(path, "has an unknown cleanup action");
}
return value as CleanupTargetKind;
}
function diagnostic(value: unknown, path: string): Diagnostic {
const item = object(value, path);
exactKeys(item, ["code", "severity", "message"], [], path);
const severity = boundedString(item.severity, `${path}.severity`);
if (severity !== "info" && severity !== "warning" && severity !== "error") {
fail(`${path}.severity`, "has an unknown value");
}
return {
code: boundedString(item.code, `${path}.code`),
severity,
message: boundedString(item.message, `${path}.message`, true),
};
}
function diagnostics(value: unknown, path: string): Diagnostic[] {
return array(value, path, MAX_DIAGNOSTICS).map((entry, index) =>
diagnostic(entry, `${path}[${index}]`)
);
}
function workerCandidate(value: unknown, path: string): CleanupWorkerCandidate {
const item = object(value, path);
exactKeys(
item,
[
"target_id",
"action",
"worker_id",
"runtime_worker_id",
"runtime_id",
"reason",
"pinned",
"retention_state",
"linked_workdir_ids",
"running_linked",
],
["blocking_reason", "estimated_reclaim_bytes"],
path,
);
return {
target_id: boundedString(item.target_id, `${path}.target_id`),
action: targetKind(item.action, `${path}.action`),
worker_id: boundedString(item.worker_id, `${path}.worker_id`),
runtime_worker_id: boundedString(item.runtime_worker_id, `${path}.runtime_worker_id`),
runtime_id: boundedString(item.runtime_id, `${path}.runtime_id`),
reason: boundedString(item.reason, `${path}.reason`, true),
blocking_reason: optionalNullableString(item.blocking_reason, `${path}.blocking_reason`),
pinned: boolean(item.pinned, `${path}.pinned`),
retention_state: boundedString(item.retention_state, `${path}.retention_state`),
linked_workdir_ids: strings(item.linked_workdir_ids, `${path}.linked_workdir_ids`),
running_linked: boolean(item.running_linked, `${path}.running_linked`),
estimated_reclaim_bytes: optionalNullableSafeInteger(
item.estimated_reclaim_bytes,
`${path}.estimated_reclaim_bytes`,
),
};
}
function workdirCandidate(value: unknown, path: string): CleanupWorkdirCandidate {
const item = object(value, path);
exactKeys(
item,
[
"target_id",
"action",
"workdir_id",
"runtime_id",
"repository_key",
"reason",
"linked_worker_ids",
"linked_running_worker_ids",
"running_linked",
"pinned_linked",
"file_status",
"cleanliness",
],
["blocking_reason", "estimated_reclaim_bytes"],
path,
);
const fileStatus = boundedString(item.file_status, `${path}.file_status`);
const cleanliness = boundedString(item.cleanliness, `${path}.cleanliness`);
if (!FILE_STATUSES.has(fileStatus as CleanupWorkdirFileStatus)) {
fail(`${path}.file_status`, "has an unknown value");
}
if (!CLEANLINESS.has(cleanliness as CleanupWorkdirCleanliness)) {
fail(`${path}.cleanliness`, "has an unknown value");
}
return {
target_id: boundedString(item.target_id, `${path}.target_id`),
action: targetKind(item.action, `${path}.action`),
workdir_id: boundedString(item.workdir_id, `${path}.workdir_id`),
runtime_id: boundedString(item.runtime_id, `${path}.runtime_id`),
repository_key: boundedString(item.repository_key, `${path}.repository_key`),
reason: boundedString(item.reason, `${path}.reason`, true),
blocking_reason: optionalNullableString(item.blocking_reason, `${path}.blocking_reason`),
linked_worker_ids: strings(item.linked_worker_ids, `${path}.linked_worker_ids`),
linked_running_worker_ids: strings(
item.linked_running_worker_ids,
`${path}.linked_running_worker_ids`,
),
running_linked: boolean(item.running_linked, `${path}.running_linked`),
pinned_linked: boolean(item.pinned_linked, `${path}.pinned_linked`),
file_status: fileStatus as CleanupWorkdirFileStatus,
cleanliness: cleanliness as CleanupWorkdirCleanliness,
estimated_reclaim_bytes: optionalNullableSafeInteger(
item.estimated_reclaim_bytes,
`${path}.estimated_reclaim_bytes`,
),
};
}
export function parseWorkerRetentionResponse(value: unknown): WorkerRetentionResponse {
const item = object(value, "Worker retention response");
exactKeys(
item,
["workspace_id", "runtime_id", "worker_id", "pinned", "retention_state"],
[],
"Worker retention response",
);
return {
workspace_id: boundedString(item.workspace_id, "Worker retention response.workspace_id"),
runtime_id: boundedString(item.runtime_id, "Worker retention response.runtime_id"),
worker_id: boundedString(item.worker_id, "Worker retention response.worker_id"),
pinned: boolean(item.pinned, "Worker retention response.pinned"),
retention_state: boundedString(
item.retention_state,
"Worker retention response.retention_state",
),
};
}
const WORKER_OPERATION_STATES = new Set<WorkerOperationState>([
"accepted",
"unsupported",
"rejected",
]);
export function parseRuntimeWorkerLifecycleResult(
value: unknown,
): RuntimeWorkerLifecycleResult {
const item = object(value, "Runtime Worker lifecycle result");
exactKeys(
item,
["state", "runtime_id", "worker_id", "diagnostics"],
[],
"Runtime Worker lifecycle result",
);
const state = boundedString(item.state, "Runtime Worker lifecycle result.state");
if (!WORKER_OPERATION_STATES.has(state as WorkerOperationState)) {
fail("Runtime Worker lifecycle result.state", "has an unknown value");
}
return {
state: state as WorkerOperationState,
runtime_id: boundedString(
item.runtime_id,
"Runtime Worker lifecycle result.runtime_id",
),
worker_id: boundedString(
item.worker_id,
"Runtime Worker lifecycle result.worker_id",
),
diagnostics: diagnostics(
item.diagnostics,
"Runtime Worker lifecycle result.diagnostics",
),
};
}
export function parseRuntimeCleanupPlan(value: unknown): RuntimeCleanupPlanResponse {
const item = object(value, "Runtime cleanup plan");
exactKeys(
item,
[
"workspace_id",
"runtime_id",
"generated_at",
"revision",
"digest",
"workers",
"workdirs",
"diagnostics",
],
[],
"Runtime cleanup plan",
);
return {
workspace_id: boundedString(item.workspace_id, "Runtime cleanup plan.workspace_id"),
runtime_id: boundedString(item.runtime_id, "Runtime cleanup plan.runtime_id"),
generated_at: boundedString(item.generated_at, "Runtime cleanup plan.generated_at"),
revision: boundedString(item.revision, "Runtime cleanup plan.revision"),
digest: boundedString(item.digest, "Runtime cleanup plan.digest"),
workers: array(item.workers, "Runtime cleanup plan.workers", MAX_CANDIDATES).map(
(entry, index) => workerCandidate(entry, `Runtime cleanup plan.workers[${index}]`),
),
workdirs: array(item.workdirs, "Runtime cleanup plan.workdirs", MAX_CANDIDATES).map(
(entry, index) => workdirCandidate(entry, `Runtime cleanup plan.workdirs[${index}]`),
),
diagnostics: diagnostics(item.diagnostics, "Runtime cleanup plan.diagnostics"),
};
}
function executionResult(value: unknown, path: string): RuntimeCleanupExecutionResult {
const item = object(value, path);
exactKeys(item, ["target_id", "action", "status", "message"], [], path);
return {
target_id: boundedString(item.target_id, `${path}.target_id`),
action: targetKind(item.action, `${path}.action`),
status: boundedString(item.status, `${path}.status`),
message: boundedString(item.message, `${path}.message`, true),
};
}
export function parseRuntimeCleanupExecution(
value: unknown,
): RuntimeCleanupExecutionResponse {
const item = object(value, "Runtime cleanup execution");
exactKeys(
item,
["workspace_id", "runtime_id", "executed_at", "results", "plan_after", "diagnostics"],
[],
"Runtime cleanup execution",
);
return {
workspace_id: boundedString(item.workspace_id, "Runtime cleanup execution.workspace_id"),
runtime_id: boundedString(item.runtime_id, "Runtime cleanup execution.runtime_id"),
executed_at: boundedString(item.executed_at, "Runtime cleanup execution.executed_at"),
results: array(item.results, "Runtime cleanup execution.results", MAX_RESULTS).map(
(entry, index) => executionResult(entry, `Runtime cleanup execution.results[${index}]`),
),
plan_after: parseRuntimeCleanupPlan(item.plan_after),
diagnostics: diagnostics(item.diagnostics, "Runtime cleanup execution.diagnostics"),
};
}
@@ -1,3 +1,4 @@
import type { AttachmentUploadGrantResponse } from "$lib/generated/runtime-api.ts";
import type { UploadedFileRef } from "$lib/generated/protocol.ts";
export const MAX_UPLOADED_FILE_BYTES = 10 * 1024 * 1024;
@@ -133,10 +134,19 @@ export function uploadAttachment(
function isUploadGrantResponse(
value: unknown,
): value is { upload_id: string; expires_at_ms: number } {
return !!value && typeof value === "object" &&
"upload_id" in value && typeof value.upload_id === "string" &&
"expires_at_ms" in value && typeof value.expires_at_ms === "number";
): value is AttachmentUploadGrantResponse {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const keys = Object.keys(value);
if (
keys.length !== 2 || !keys.includes("upload_id") ||
!keys.includes("expires_at_ms")
) {
return false;
}
const grant = value as Record<string, unknown>;
return typeof grant.upload_id === "string" && grant.upload_id.length > 0 &&
grant.upload_id.length <= 256 && Number.isSafeInteger(grant.expires_at_ms) &&
(grant.expires_at_ms as number) >= 0;
}
function isUploadedFileResponse(
@@ -1,4 +1,4 @@
import type { WorkspaceRuntimeResource } from "$lib/generated/legacy-server-api";
import type { WorkspaceRuntimeResource } from "$lib/generated/runtime-api";
export type RepositorySshProbeSelection<T> = Readonly<{
changed: boolean;
@@ -1,3 +1,10 @@
import type {
CleanupTargetKind,
CleanupWorkdirCandidate,
CleanupWorkerCandidate,
RuntimeCleanupExecutionResponse,
RuntimeCleanupPlanResponse,
} from "$lib/generated/runtime-api";
import type {
BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse,
BrowserWorkerWorkingDirectorySelection
@@ -123,67 +130,12 @@ export type WorkerLaunchProfileCandidate = SharedWorkerLaunchProfileCandidate;
export type WorkingDirectoryRepositoryOption =
SharedWorkingDirectoryRepositoryOption;
export type CleanupTargetKind =
| "worker_delete"
| "workdir_clean_cleanup"
| "workdir_dirty_discard"
| "workdir_record_delete";
export type CleanupWorkerCandidate = {
target_id: string;
action: CleanupTargetKind;
worker_id: string;
runtime_worker_id: string;
runtime_id: string;
reason: string;
blocking_reason?: string | null;
pinned: boolean;
retention_state: string;
linked_workdir_ids: string[];
running_linked: boolean;
estimated_reclaim_bytes?: number | null;
};
export type CleanupWorkdirCandidate = {
target_id: string;
action: CleanupTargetKind;
workdir_id: string;
runtime_id: string;
repository_key: string;
reason: string;
blocking_reason?: string | null;
linked_worker_ids: string[];
linked_running_worker_ids: string[];
running_linked: boolean;
pinned_linked: boolean;
file_status: string;
cleanliness: string;
estimated_reclaim_bytes?: number | null;
};
export type RuntimeCleanupPlanResponse = {
workspace_id: string;
runtime_id: string;
generated_at: string;
revision: string;
digest: string;
workers: CleanupWorkerCandidate[];
workdirs: CleanupWorkdirCandidate[];
diagnostics: Diagnostic[];
};
export type RuntimeCleanupExecutionResponse = {
workspace_id: string;
runtime_id: string;
executed_at: string;
results: {
target_id: string;
action: CleanupTargetKind;
status: string;
message: string;
}[];
plan_after: RuntimeCleanupPlanResponse;
diagnostics: Diagnostic[];
export type {
CleanupTargetKind,
CleanupWorkdirCandidate,
CleanupWorkerCandidate,
RuntimeCleanupExecutionResponse,
RuntimeCleanupPlanResponse,
};
export type BrowserWorkerWorkingDirectorySelection =
@@ -1,19 +1,17 @@
import { workspaceApiPath } from "$lib/workspace/api/http";
import {
parseRuntimeCleanupExecution,
parseRuntimeCleanupPlan,
parseRuntimeWorkerLifecycleResult,
} from "$lib/workspace/api/runtime-workers";
import type {
Diagnostic,
RuntimeCleanupExecutionResponse,
RuntimeCleanupPlanResponse,
Worker,
} from "./types";
type FetchFn = typeof fetch;
type WorkerActionTarget = Pick<Worker, "runtime_id" | "worker_id" | "state">;
type WorkerLifecycleResponse = {
state: string;
diagnostics?: Diagnostic[];
};
function workerPath(workspaceId: string, worker: WorkerActionTarget): string {
return workspaceApiPath(
workspaceId,
@@ -62,7 +60,7 @@ export async function stopSidebarWorker(
});
if (!response.ok) throw new Error(await responseError(response));
const result = await response.json() as WorkerLifecycleResponse;
const result = parseRuntimeWorkerLifecycleResult(await response.json());
if (result.state !== "accepted") {
throw new Error(
diagnosticMessage(result.diagnostics, `Worker stop was ${result.state}`),
@@ -81,7 +79,7 @@ export async function deleteSidebarWorker(
);
if (!planResponse.ok) throw new Error(await responseError(planResponse));
const plan = await planResponse.json() as RuntimeCleanupPlanResponse;
const plan = parseRuntimeCleanupPlan(await planResponse.json());
const candidate = plan.workers.find((item) =>
item.runtime_id === worker.runtime_id &&
item.runtime_worker_id === worker.worker_id
@@ -107,8 +105,7 @@ export async function deleteSidebarWorker(
throw new Error(await responseError(executionResponse));
}
const execution = await executionResponse
.json() as RuntimeCleanupExecutionResponse;
const execution = parseRuntimeCleanupExecution(await executionResponse.json());
const outcome = execution.results.find((result) =>
result.target_id === candidate.target_id
);
@@ -4,7 +4,7 @@
RuntimeConnectionTestResponse,
RuntimePublicIdentityBundle,
WorkspaceRuntimeResource,
} from '$lib/generated/legacy-server-api';
} from '$lib/generated/runtime-api';
import {
createRemoteRuntime,
previewRuntimePublicKeyFingerprint,
@@ -397,12 +397,12 @@
</td>
</tr>
{@const currentResult = currentTestResult(runtime)}
{#if runtime.diagnostics.length > 0 || currentResult}
{#if (runtime.diagnostics ?? []).length > 0 || currentResult}
<tr class="settings-runtime-detail-row">
<td colspan="7">
{#if runtime.diagnostics.length > 0}
{#if (runtime.diagnostics ?? []).length > 0}
<ul class="settings-diagnostics-list">
{#each runtime.diagnostics as diagnostic}
{#each runtime.diagnostics ?? [] as diagnostic}
<li class:error={diagnostic.severity === 'error'} class:warning={diagnostic.severity === 'warning'}>
<strong>{diagnostic.code}</strong>
<span>{diagnostic.message}</span>
@@ -413,8 +413,8 @@
{#if currentResult}
<div class:failed={currentResult.status === 'failed'} class="settings-test-result">
<strong>Connection test: {connectionTestSummary(currentResult)}</strong>
{#if currentResult.diagnostics[0]}
<span>{currentResult.diagnostics[0].message}</span>
{#if (currentResult.diagnostics ?? [])[0]}
<span>{(currentResult.diagnostics ?? [])[0].message}</span>
{/if}
<small>Checked {new Date(currentResult.checked_at).toLocaleString()}</small>
</div>
@@ -3,7 +3,7 @@
import type {
RevokeRuntimeTrustKeyRequest,
RuntimeTrustKeyStatus,
} from '$lib/generated/legacy-server-api';
} from '$lib/generated/runtime-api';
import {
createRemoteRuntime,
removeRemoteRuntime,
@@ -414,9 +414,9 @@
<div><dt>Updated</dt><dd>{formatTimestamp(trust.updated_at)}</dd></div>
<div><dt>Revoked</dt><dd>{formatTimestamp(trust.revoked_at)}</dd></div>
</dl>
{#if runtime.diagnostics.length > 0}
{#if (runtime.diagnostics ?? []).length > 0}
<ul class="settings-diagnostics-list">
{#each runtime.diagnostics as diagnostic}
{#each runtime.diagnostics ?? [] as diagnostic}
<li class:error={diagnostic.severity === 'error'} class:warning={diagnostic.severity === 'warning'}>
<strong>{diagnostic.code}</strong>
<span>{diagnostic.message}</span>
@@ -1,10 +1,10 @@
<script lang="ts">
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
import { workspaceApiPath } from '$lib/workspace/api/http';
import { parseRuntimeCleanupExecution } from '$lib/workspace/api/runtime-workers';
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
import type {
CleanupWorkdirCandidate,
RuntimeCleanupExecutionResponse,
RuntimeCleanupPlanResponse,
WorkingDirectorySummary,
} from '$lib/workspace/sidebar/types';
@@ -83,14 +83,11 @@
}),
},
);
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | unknown;
const payload = await response.json().catch(() => null);
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
if (payload && typeof payload === 'object' && 'plan_after' in payload) {
cleanupPlan = (payload as RuntimeCleanupExecutionResponse).plan_after;
}
const result = payload && typeof payload === 'object' && 'results' in payload
? (payload as RuntimeCleanupExecutionResponse).results.find((entry) => entry.target_id === candidate.target_id)
: undefined;
const execution = parseRuntimeCleanupExecution(payload);
cleanupPlan = execution.plan_after;
const result = execution.results.find((entry) => entry.target_id === candidate.target_id);
if (!result || result.status !== 'deleted') {
throw new Error(result?.message ?? 'Runtime did not delete the selected Workdir');
}
@@ -1,18 +1,17 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseWorkspaceRuntimeList } from "$lib/workspace/api/runtime-management";
import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers";
import { parseWorkingDirectoryListResponse } from "$lib/workspace/api/workdirs";
import type {
ListResponse,
Runtime,
RuntimeCleanupPlanResponse,
} from "$lib/workspace/sidebar/types";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const runtimeId = params.runtimeId;
const [runtimes, workdirs, cleanupPlan] = await Promise.all([
loadJson<ListResponse<Runtime>>(
loadJson(
fetch,
workspaceApiPath(params.workspaceId, "/runtimes"),
undefined,
parseWorkspaceRuntimeList,
),
loadJson(
fetch,
@@ -23,12 +22,14 @@ export const load: PageLoad = async ({ fetch, params }) => {
undefined,
parseWorkingDirectoryListResponse,
),
loadJson<RuntimeCleanupPlanResponse>(
loadJson(
fetch,
workspaceApiPath(
params.workspaceId,
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
),
undefined,
parseRuntimeCleanupPlan,
),
]);
@@ -1,11 +1,16 @@
<script lang="ts">
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
import { workspaceApiPath } from '$lib/workspace/api/http';
import {
parseRuntimeCleanupExecution,
parseRuntimeCleanupPlan,
parseWorkerRetentionResponse,
} from '$lib/workspace/api/runtime-workers';
import { workerHref } from '$lib/workspace/resource-links';
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
import { canOpenWorkerConsole } from '$lib/workspace/sidebar/workers';
import { liveWorkerState } from '$lib/workspace/sidebar/worker-state';
import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace/sidebar/types';
import type { CleanupWorkerCandidate, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace/sidebar/types';
import type { PageProps } from './$types';
type WorkerActionKind = 'pin' | 'delete';
@@ -55,7 +60,7 @@
workspaceApiPath(data.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`),
);
if (!response.ok) return;
const plan = (await response.json()) as RuntimeCleanupPlanResponse;
const plan = parseRuntimeCleanupPlan(await response.json());
cleanupPlans = { ...cleanupPlans, [runtimeId]: plan };
}
@@ -75,8 +80,9 @@
pushWorkspaceAlert('error', errorMessage(payload, response.statusText), { title: 'Worker pin failed' });
return;
}
worker.pinned = Boolean(payload?.pinned);
worker.retention_state = payload?.retention_state ?? (worker.pinned ? 'pinned' : 'normal');
const retention = parseWorkerRetentionResponse(payload);
worker.pinned = retention.pinned;
worker.retention_state = retention.retention_state;
await refreshCleanupPlan(worker.runtime_id);
} catch (error) {
pushWorkspaceAlert('error', error instanceof Error ? error.message : 'Worker pin failed', {
@@ -112,15 +118,11 @@
}),
},
);
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | unknown;
const payload = await response.json().catch(() => null);
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
if (payload && typeof payload === 'object' && 'plan_after' in payload) {
const execution = payload as RuntimeCleanupExecutionResponse;
cleanupPlans = { ...cleanupPlans, [worker.runtime_id]: execution.plan_after };
}
const result = payload && typeof payload === 'object' && 'results' in payload
? (payload as RuntimeCleanupExecutionResponse).results.find((entry) => entry.target_id === candidate.target_id)
: undefined;
const execution = parseRuntimeCleanupExecution(payload);
cleanupPlans = { ...cleanupPlans, [worker.runtime_id]: execution.plan_after };
const result = execution.results.find((entry) => entry.target_id === candidate.target_id);
if (!result || result.status !== 'deleted') {
throw new Error(result?.message ?? 'Runtime did not delete the selected Worker');
}
@@ -1,4 +1,5 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers";
import type {
ListResponse,
RuntimeCleanupPlanResponse,
@@ -16,12 +17,14 @@ export const load: PageLoad = async ({ fetch, params }) => {
);
const cleanupPlanEntries = await Promise.all(
runtimeIds.map(async (runtimeId) => {
const cleanupPlan = await loadJson<RuntimeCleanupPlanResponse>(
const cleanupPlan = await loadJson(
fetch,
workspaceApiPath(
params.workspaceId,
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
),
undefined,
parseRuntimeCleanupPlan,
);
return [runtimeId, cleanupPlan] as const;
}),
@@ -1,5 +1,5 @@
import { assert, assertEquals } from "jsr:@std/assert";
import type { WorkspaceRuntimeResource } from "../src/lib/generated/legacy-server-api.ts";
import type { WorkspaceRuntimeResource } from "../src/lib/generated/runtime-api.ts";
import { parseRepositorySshConnectionProbeResponse } from "../src/lib/workspace/api/workspace-model.ts";
import {
changeRepositorySshProbeRuntime,
@@ -55,7 +55,12 @@ Deno.test("sidebar Stop uses the workspace-scoped Worker lifecycle endpoint", as
const requests: Array<{ url: string; init?: RequestInit }> = [];
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
requests.push({ url: input.toString(), init });
return jsonResponse({ state: "accepted", diagnostics: [] });
return jsonResponse({
state: "accepted",
runtime_id: worker.runtime_id,
worker_id: worker.worker_id,
diagnostics: [],
});
}) as typeof fetch;
await stopSidebarWorker("team space", worker, fetchFn);
@@ -76,7 +81,13 @@ Deno.test("sidebar Stop rejects non-accepted lifecycle responses", async () => {
Promise.resolve(
jsonResponse({
state: "rejected",
diagnostics: [{ severity: "error", message: "Worker cannot stop" }],
runtime_id: worker.runtime_id,
worker_id: worker.worker_id,
diagnostics: [{
code: "cannot_stop",
severity: "error",
message: "Worker cannot stop",
}],
}),
)) as typeof fetch;
@@ -92,25 +103,48 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
requests.push({ url: input.toString(), init });
if (requests.length === 1) {
return jsonResponse({
revision: 7,
workspace_id: "team",
runtime_id: worker.runtime_id,
generated_at: "2026-01-01T00:00:00Z",
revision: "7",
digest: "digest-7",
candidates: [],
workers: [{
target_id: "worker-target",
action: "worker_delete",
worker_id: worker.worker_id,
runtime_id: worker.runtime_id,
runtime_worker_id: worker.worker_id,
reason: "stopped worker",
blocking_reason: null,
pinned: false,
retention_state: "stopped",
linked_workdir_ids: [],
running_linked: false,
}],
workdirs: [],
diagnostics: [],
});
}
return jsonResponse({
workspace_id: "team",
runtime_id: worker.runtime_id,
executed_at: "2026-01-01T00:00:01Z",
results: [{
target_id: "worker-target",
action: "worker_delete",
status: "deleted",
message: null,
message: "",
}],
plan_after: {
workspace_id: "team",
runtime_id: worker.runtime_id,
generated_at: "2026-01-01T00:00:01Z",
revision: "8",
digest: "digest-8",
workers: [],
workdirs: [],
diagnostics: [],
},
diagnostics: [],
});
}) as typeof fetch;
@@ -123,7 +157,7 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
]);
assertEquals(requests[1]?.init?.method, "POST");
assertEquals(JSON.parse(String(requests[1]?.init?.body)), {
expected_plan_revision: 7,
expected_plan_revision: "7",
expected_plan_digest: "digest-7",
worker_target_ids: ["worker-target"],
workdir_target_ids: [],
@@ -135,14 +169,23 @@ Deno.test("sidebar Delete reports cleanup-plan blocking reasons", async () => {
const fetchFn = (() =>
Promise.resolve(
jsonResponse({
revision: 8,
workspace_id: "team",
runtime_id: worker.runtime_id,
generated_at: "2026-01-01T00:00:00Z",
revision: "8",
digest: "digest-8",
candidates: [],
workers: [{
target_id: "worker-target",
action: "worker_delete",
worker_id: worker.worker_id,
runtime_id: worker.runtime_id,
runtime_worker_id: worker.worker_id,
reason: "stopped worker",
blocking_reason: "Worker is pinned",
pinned: true,
retention_state: "pinned",
linked_workdir_ids: [],
running_linked: false,
}],
workdirs: [],
diagnostics: [],
+122
View File
@@ -0,0 +1,122 @@
declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
};
import {
parseRuntimeCleanupExecution,
parseRuntimeCleanupPlan,
parseRuntimeWorkerLifecycleResult,
parseWorkerRetentionResponse,
} from "../src/lib/workspace/api/runtime-workers.ts";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function assertThrows(operation: () => unknown, expected: string): void {
try {
operation();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes(expected)) return;
throw new Error(`expected error containing ${expected}, received ${message}`);
}
throw new Error("expected operation to throw");
}
function plan() {
return {
workspace_id: "workspace-a",
runtime_id: "runtime-a",
generated_at: "2026-09-01T00:00:00Z",
revision: "revision-a",
digest: "digest-a",
workers: [{
target_id: "worker-target",
action: "worker_delete",
worker_id: "W-1",
runtime_worker_id: "worker-1",
runtime_id: "runtime-a",
reason: "stopped",
blocking_reason: null,
pinned: false,
retention_state: "active",
linked_workdir_ids: [],
running_linked: false,
estimated_reclaim_bytes: 10,
}],
workdirs: [{
target_id: "workdir-target",
action: "workdir_clean_cleanup",
workdir_id: "workdir-1",
runtime_id: "runtime-a",
repository_key: "main",
reason: "unused",
blocking_reason: null,
linked_worker_ids: [],
linked_running_worker_ids: [],
running_linked: false,
pinned_linked: false,
file_status: "active",
cleanliness: "clean",
estimated_reclaim_bytes: null,
}],
diagnostics: [],
};
}
Deno.test("Runtime cleanup parsers accept the generated exact response shapes", () => {
const parsedPlan = parseRuntimeCleanupPlan(plan());
assert(parsedPlan.workers[0]?.runtime_worker_id === "worker-1", "worker candidate missing");
const execution = parseRuntimeCleanupExecution({
workspace_id: "workspace-a",
runtime_id: "runtime-a",
executed_at: "2026-09-01T00:01:00Z",
results: [{
target_id: "worker-target",
action: "worker_delete",
status: "deleted",
message: "deleted",
}],
plan_after: plan(),
diagnostics: [],
});
assert(execution.results[0]?.status === "deleted", "execution result missing");
const lifecycle = parseRuntimeWorkerLifecycleResult({
state: "accepted",
runtime_id: "runtime-a",
worker_id: "worker-1",
diagnostics: [],
});
assert(lifecycle.state === "accepted", "lifecycle state missing");
const retention = parseWorkerRetentionResponse({
workspace_id: "workspace-a",
runtime_id: "runtime-a",
worker_id: "worker-1",
pinned: true,
retention_state: "pinned",
});
assert(retention.pinned, "retention state missing");
});
Deno.test("Runtime cleanup parsers reject unknown fields and unsafe values", () => {
assertThrows(
() => parseRuntimeCleanupPlan({ ...plan(), unexpected: true }),
"not part of the wire contract",
);
assertThrows(
() => parseRuntimeCleanupPlan({ ...plan(), workers: new Array(1_001).fill(plan().workers[0]) }),
"at most 1000",
);
assertThrows(
() => parseRuntimeCleanupPlan({ ...plan(), workers: [{ ...plan().workers[0], estimated_reclaim_bytes: Number.MAX_SAFE_INTEGER + 1 }] }),
"safe integer",
);
assertThrows(
() => parseRuntimeWorkerLifecycleResult({ state: "future", runtime_id: "runtime-a", worker_id: "worker-1", diagnostics: [] }),
"unknown value",
);
});