chore: merge develop into hare/develop
This commit is contained in:
@@ -6,12 +6,14 @@
|
||||
"dev": "deno run -A npm:vite@7.2.7 dev",
|
||||
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
|
||||
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
|
||||
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
|
||||
"build": "deno run -A npm:vite@7.2.7 build",
|
||||
"preview": "deno run -A npm:vite@7.2.7 preview",
|
||||
"generate:repository-api": "cd ../.. && cargo run -q -p server-api --example generate_repository_openapi_types",
|
||||
"check:repository-api": "cd ../.. && cargo run -q -p server-api --example generate_repository_openapi_types -- --check"
|
||||
"check:repository-api": "cd ../.. && cargo run -q -p server-api --example generate_repository_openapi_types -- --check",
|
||||
"generate:ticket-api": "cd ../.. && cargo run -q -p server-api --example generate_ticket_api_types",
|
||||
"check:ticket-api": "cd ../.. && cargo run -q -p server-api --example generate_ticket_api_types -- --check"
|
||||
},
|
||||
"imports": {
|
||||
"$lib/": "./src/lib/",
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
// Generated by `cargo run -p server-api --features typescript --example generate_companion_api_types`.
|
||||
// Do not edit manually.
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
|
||||
|
||||
export type WorkspaceWorkerSubject = { "kind": "runtime_worker", runtime_id: string, worker_id: string, };
|
||||
|
||||
export type WorkspaceWorkerDiscoveryItem = { subject: WorkspaceWorkerSubject, resource_key: string, display_name: string, profile: string | null, status?: string | null, };
|
||||
export type CompanionCancelRequest = { reason?: string | null; };
|
||||
|
||||
export type CompanionLifecycleState = "idle" | "running" | "stopped";
|
||||
|
||||
export type CompanionMessageDisposition = "accepted" | "rejected";
|
||||
|
||||
export type CompanionTransportSummary = { mode: string, available: boolean, };
|
||||
export type CompanionMessageRequest = { content: string; };
|
||||
|
||||
export type CompanionStatusResponse = { state: CompanionLifecycleState, worker: WorkspaceWorkerDiscoveryItem | null, transport: CompanionTransportSummary, diagnostics: Array<Diagnostic>, };
|
||||
export type CompanionMessageResponse = { message: string; state: CompanionMessageDisposition; };
|
||||
|
||||
export type CompanionMessageRequest = { content: string, };
|
||||
export type CompanionStatusResponse = { diagnostics?: Array<Diagnostic>; state: CompanionLifecycleState; transport: CompanionTransportSummary; worker?: WorkspaceWorkerDiscoveryItem | null; };
|
||||
|
||||
export type CompanionCancelRequest = { reason?: string | null, };
|
||||
export type CompanionTranscriptItem = { content: string; created_at: string; role: CompanionTranscriptRole; sequence: number; };
|
||||
|
||||
export type CompanionMessageResponse = { state: CompanionMessageDisposition, message: string, };
|
||||
export type CompanionTranscriptProjection = { items: Array<CompanionTranscriptItem>; limit: number; next?: number | null; start: number; state: CompanionLifecycleState; total: number; };
|
||||
|
||||
export type CompanionTranscriptRole = "user" | "assistant";
|
||||
|
||||
export type CompanionTranscriptItem = { sequence: number, role: CompanionTranscriptRole, content: string, created_at: string, };
|
||||
export type CompanionTransportSummary = { available: boolean; mode: string; };
|
||||
|
||||
export type CompanionTranscriptProjection = { state: CompanionLifecycleState, start: number, limit: number, total: number, next: number | null, items: Array<CompanionTranscriptItem>, };
|
||||
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
|
||||
|
||||
export type WorkspaceWorkerDiscoveryItem = { display_name: string; profile?: string | null; resource_key: string; status?: string | null; subject: WorkspaceWorkerSubject; };
|
||||
|
||||
export type WorkspaceWorkerSubject = { kind: "runtime_worker"; runtime_id: string; worker_id: string; };
|
||||
|
||||
@@ -1,140 +1,361 @@
|
||||
// This file is generated by `cargo run -p server-api --features typescript --example generate_legacy_typescript | deno fmt -`.
|
||||
// Do not edit this file directly.
|
||||
|
||||
import type { RepositoryObservedStatus, RepositorySource } from "./repository-api";
|
||||
import type {
|
||||
RepositoryObservedStatus,
|
||||
RepositorySource,
|
||||
} from "./repository-api";
|
||||
|
||||
export type WorkspaceSummary = { workspace_id: string, owner_account_id: string, display_name: string, state: string, created_at: string, updated_at: string, };
|
||||
export type WorkspaceSummary = {
|
||||
workspace_id: string;
|
||||
owner_account_id: string;
|
||||
display_name: string;
|
||||
state: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type WorkspaceCatalogListResponse = Array<WorkspaceSummary>;
|
||||
|
||||
export type WorkspaceRepositoryRecord = { workspace_id: string, repository_key: string, kind: string, provider: string | null, source: RepositorySource, default_ref: string | null, source_revision: number, source_fingerprint: string, observed_status: RepositoryObservedStatus, observed_at: string | null, created_at: string, updated_at: string, };
|
||||
export type WorkspaceRepositoryRecord = {
|
||||
workspace_id: string;
|
||||
repository_key: string;
|
||||
kind: string;
|
||||
provider: string | null;
|
||||
source: RepositorySource;
|
||||
default_ref: string | null;
|
||||
source_revision: number;
|
||||
source_fingerprint: string;
|
||||
observed_status: RepositoryObservedStatus;
|
||||
observed_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type WorkspaceCreateResponse = { workspace: WorkspaceSummary, repository: WorkspaceRepositoryRecord, config_revision: number, request_fingerprint: string, replayed: boolean, };
|
||||
export type WorkspaceCreateResponse = {
|
||||
workspace: WorkspaceSummary;
|
||||
repository: WorkspaceRepositoryRecord;
|
||||
config_revision: number;
|
||||
request_fingerprint: string;
|
||||
replayed: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceAuthConfig = { "Passkey": { rp_id: string, origin: string, public_base_url: string, cookie_name: string, } };
|
||||
export type WorkspaceAuthConfig = {
|
||||
"Passkey": {
|
||||
rp_id: string;
|
||||
origin: string;
|
||||
public_base_url: string;
|
||||
cookie_name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkspacePermissionSummary = { manage_repositories: boolean, manage_secrets: boolean, manage_runtimes: boolean, delete_workspace: boolean, };
|
||||
export type WorkspacePermissionSummary = {
|
||||
manage_repositories: boolean;
|
||||
manage_secrets: boolean;
|
||||
manage_runtimes: boolean;
|
||||
delete_workspace: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceDeletionState = "queued" | "running" | "blocked" | "failed" | "succeeded";
|
||||
export type WorkspaceDeletionState =
|
||||
| "queued"
|
||||
| "running"
|
||||
| "blocked"
|
||||
| "failed"
|
||||
| "succeeded";
|
||||
|
||||
export type WorkspaceDeletionBlockerKind = "last_accessible_workspace" | "revision_conflict" | "dirty_workdir" | "worker_removal_blocked" | "workdir_removal_blocked" | "retention_hold" | "cleanup_unavailable";
|
||||
export type WorkspaceDeletionBlockerKind =
|
||||
| "last_accessible_workspace"
|
||||
| "revision_conflict"
|
||||
| "dirty_workdir"
|
||||
| "worker_removal_blocked"
|
||||
| "workdir_removal_blocked"
|
||||
| "retention_hold"
|
||||
| "cleanup_unavailable";
|
||||
|
||||
export type WorkspaceDeletionBlocker = { kind: WorkspaceDeletionBlockerKind, resource_kind: string | null, resource_key: string | null, message: string, };
|
||||
export type WorkspaceDeletionBlocker = {
|
||||
kind: WorkspaceDeletionBlockerKind;
|
||||
resource_kind: string | null;
|
||||
resource_key: string | null;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type WorkspaceDeletionResourceCounts = { workers: number, workdirs: number, repositories: number, runtime_bindings: number, secrets: number, artifacts: number, };
|
||||
export type WorkspaceDeletionResourceCounts = {
|
||||
workers: number;
|
||||
workdirs: number;
|
||||
repositories: number;
|
||||
runtime_bindings: number;
|
||||
secrets: number;
|
||||
artifacts: number;
|
||||
};
|
||||
|
||||
export type WorkspaceDeletionPreflightResponse = { workspace_id: string, display_name: string,
|
||||
/**
|
||||
* Opaque persisted Workspace metadata revision used as a CAS fence.
|
||||
*/
|
||||
expected_revision: string, can_delete: boolean, resources: WorkspaceDeletionResourceCounts, blockers: Array<WorkspaceDeletionBlocker>, };
|
||||
export type WorkspaceDeletionPreflightResponse = {
|
||||
workspace_id: string;
|
||||
display_name: string;
|
||||
/**
|
||||
* Opaque persisted Workspace metadata revision used as a CAS fence.
|
||||
*/
|
||||
expected_revision: string;
|
||||
can_delete: boolean;
|
||||
resources: WorkspaceDeletionResourceCounts;
|
||||
blockers: Array<WorkspaceDeletionBlocker>;
|
||||
};
|
||||
|
||||
export type WorkspaceDeletionRequest = { operation_id: string, expected_revision: string, confirmation: string, };
|
||||
export type WorkspaceDeletionRequest = {
|
||||
operation_id: string;
|
||||
expected_revision: string;
|
||||
confirmation: string;
|
||||
};
|
||||
|
||||
export type WorkspaceDeletionOperationResponse = { operation_id: string, workspace_id: string, display_name: string, state: WorkspaceDeletionState, resources: WorkspaceDeletionResourceCounts, child_operation_ids: Array<string>, blockers: Array<WorkspaceDeletionBlocker>, failure_category: string | null, created_at: string, updated_at: string, completed_at: string | null, };
|
||||
export type WorkspaceDeletionOperationResponse = {
|
||||
operation_id: string;
|
||||
workspace_id: string;
|
||||
display_name: string;
|
||||
state: WorkspaceDeletionState;
|
||||
resources: WorkspaceDeletionResourceCounts;
|
||||
child_operation_ids: Array<string>;
|
||||
blockers: Array<WorkspaceDeletionBlocker>;
|
||||
failure_category: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
completed_at: string | null;
|
||||
};
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
|
||||
export type Diagnostic = {
|
||||
code: string;
|
||||
severity: DiagnosticSeverity;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type WorkspaceExtensionPointState = { status: string, note: string, diagnostics: Array<Diagnostic>, };
|
||||
export type WorkspaceExtensionPointState = {
|
||||
status: string;
|
||||
note: string;
|
||||
diagnostics: Array<Diagnostic>;
|
||||
};
|
||||
|
||||
export type WorkspaceExtensionPoints = { store: string, event_stream: WorkspaceExtensionPointState, host_worker_bridge: WorkspaceExtensionPointState, companion_console: WorkspaceExtensionPointState, };
|
||||
export type WorkspaceExtensionPoints = {
|
||||
store: string;
|
||||
event_stream: WorkspaceExtensionPointState;
|
||||
host_worker_bridge: WorkspaceExtensionPointState;
|
||||
companion_console: WorkspaceExtensionPointState;
|
||||
};
|
||||
|
||||
export type WorkspaceResponse = { workspace_id: string, display_name: string, record_authority: string, schema_version: number, auth: WorkspaceAuthConfig, permissions: WorkspacePermissionSummary, extension_points: WorkspaceExtensionPoints, };
|
||||
export type WorkspaceResponse = {
|
||||
workspace_id: string;
|
||||
display_name: string;
|
||||
record_authority: string;
|
||||
schema_version: number;
|
||||
auth: WorkspaceAuthConfig;
|
||||
permissions: WorkspacePermissionSummary;
|
||||
extension_points: WorkspaceExtensionPoints;
|
||||
};
|
||||
|
||||
export type WorkspaceMetadataSettingsResponse = { workspace_id: string, display_name: string, created_at: string, revision: string, source: string, diagnostics: Array<Diagnostic>, };
|
||||
export type WorkspaceMetadataSettingsResponse = {
|
||||
workspace_id: string;
|
||||
display_name: string;
|
||||
created_at: string;
|
||||
revision: string;
|
||||
source: string;
|
||||
diagnostics: Array<Diagnostic>;
|
||||
};
|
||||
|
||||
export type UpdateWorkspaceMetadataRequest = { display_name: string, revision: string, };
|
||||
export type UpdateWorkspaceMetadataRequest = {
|
||||
display_name: string;
|
||||
revision: string;
|
||||
};
|
||||
|
||||
export type WorkspaceMetadataMutationResponse = { workspace: WorkspaceMetadataSettingsResponse, diagnostics: Array<Diagnostic>, };
|
||||
export type WorkspaceMetadataMutationResponse = {
|
||||
workspace: WorkspaceMetadataSettingsResponse;
|
||||
diagnostics: Array<Diagnostic>;
|
||||
};
|
||||
|
||||
export type WorkspaceSigningIdentityState = "pending_provisioning" | "active";
|
||||
|
||||
export type WorkspaceSigningIdentityPublic = { workspace_id: string, key_id: string, algorithm: string, public_key?: string, public_key_fingerprint?: string, revision: number, state: WorkspaceSigningIdentityState, created_at: string, provisioned_at?: string, };
|
||||
export type WorkspaceSigningIdentityPublic = {
|
||||
workspace_id: string;
|
||||
key_id: string;
|
||||
algorithm: string;
|
||||
public_key?: string;
|
||||
public_key_fingerprint?: string;
|
||||
revision: number;
|
||||
state: WorkspaceSigningIdentityState;
|
||||
created_at: string;
|
||||
provisioned_at?: string;
|
||||
};
|
||||
|
||||
export type WorkspacePublicIdentityBundle = { workspace_id: string, backend_url: string, key_id: string, algorithm: string, public_key: string, public_key_fingerprint: string, revision: number, };
|
||||
export type WorkspacePublicIdentityBundle = {
|
||||
workspace_id: string;
|
||||
backend_url: string;
|
||||
key_id: string;
|
||||
algorithm: string;
|
||||
public_key: string;
|
||||
public_key_fingerprint: string;
|
||||
revision: number;
|
||||
};
|
||||
|
||||
export type WorkspaceSigningIdentityResponse = { identity: WorkspaceSigningIdentityPublic, public_bundle?: WorkspacePublicIdentityBundle, };
|
||||
export type WorkspaceSigningIdentityResponse = {
|
||||
identity: WorkspaceSigningIdentityPublic;
|
||||
public_bundle?: WorkspacePublicIdentityBundle;
|
||||
};
|
||||
|
||||
export type ProfileSettingsResponse = { workspace_id: string, registry_revision: string, config_revision?: number | null, tree_digest?: string | null, projection_digest?: string | null, default_profile?: string | null, profiles: Array<WorkspaceProfileSummary>, sources: Array<WorkspaceProfileSourceSummary>, diagnostics: Array<Diagnostic>, };
|
||||
export type ConfigContentType = "decodal" | "text";
|
||||
|
||||
export type WorkspaceProfileSummary = { profile_id: string, selector: string, label: string, source_kind: string, profile_source_id?: string | null, description?: string | null, editable: boolean, is_default: boolean, diagnostics: Array<Diagnostic>, };
|
||||
export type ConfigEntry = {
|
||||
path: string;
|
||||
content_type: ConfigContentType;
|
||||
content: string;
|
||||
content_digest: string;
|
||||
};
|
||||
|
||||
export type WorkspaceProfileSourceSummary = { profile_source_id: string, display_path: string, kind: string, content_type: string, content_digest: string, provenance: WorkspaceProfileSourceProvenance, editable: boolean, revision: string, size_bytes: number, diagnostics: Array<Diagnostic>, };
|
||||
export type ConfigTreeSnapshot = {
|
||||
revision: number;
|
||||
digest: string;
|
||||
entries: { [key in string]: ConfigEntry };
|
||||
};
|
||||
|
||||
export type ConfigTreeChange =
|
||||
| {
|
||||
"kind": "create";
|
||||
path: string;
|
||||
content_type: ConfigContentType;
|
||||
content: string;
|
||||
}
|
||||
| { "kind": "update"; path: string; expected_digest: string; content: string }
|
||||
| { "kind": "rename"; from: string; to: string; expected_digest: string }
|
||||
| { "kind": "delete"; path: string; expected_digest: string };
|
||||
|
||||
export type ConfigProjectionValidator = {
|
||||
"kind": "static_template_catalog";
|
||||
namespace: string;
|
||||
key_aliases?: { [key in string]: string };
|
||||
};
|
||||
|
||||
export type ConfigSchemaContribution = {
|
||||
provider_id: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
source: string;
|
||||
projection_validator?: ConfigProjectionValidator;
|
||||
source_digest: string;
|
||||
};
|
||||
|
||||
export type WorkspaceConfigSchemaBundle = {
|
||||
contributions: Array<ConfigSchemaContribution>;
|
||||
source: string;
|
||||
fingerprint: string;
|
||||
};
|
||||
|
||||
export type ToolchainContract = {
|
||||
contract_version: number;
|
||||
decodal_version: string;
|
||||
schema_version: number;
|
||||
entrypoints: Array<string>;
|
||||
import_policy_version: number;
|
||||
schema_bundle: WorkspaceConfigSchemaBundle;
|
||||
fingerprint: string;
|
||||
};
|
||||
|
||||
export type WorkspaceConfigTreeResponse = {
|
||||
snapshot: ConfigTreeSnapshot;
|
||||
contract: ToolchainContract;
|
||||
projection_digest: string;
|
||||
};
|
||||
|
||||
export type ConfigCommitRequest = {
|
||||
base_revision: number;
|
||||
base_digest: string;
|
||||
changes: Array<ConfigTreeChange>;
|
||||
entrypoints: Array<string>;
|
||||
};
|
||||
|
||||
export type ProfileSettingsResponse = {
|
||||
workspace_id: string;
|
||||
registry_revision: string;
|
||||
config_revision?: number | null;
|
||||
tree_digest?: string | null;
|
||||
projection_digest?: string | null;
|
||||
default_profile?: string | null;
|
||||
profiles: Array<WorkspaceProfileSummary>;
|
||||
sources: Array<WorkspaceProfileSourceSummary>;
|
||||
diagnostics: Array<Diagnostic>;
|
||||
};
|
||||
|
||||
export type WorkspaceProfileSummary = {
|
||||
profile_id: string;
|
||||
selector: string;
|
||||
label: string;
|
||||
source_kind: string;
|
||||
profile_source_id?: string | null;
|
||||
description?: string | null;
|
||||
editable: boolean;
|
||||
is_default: boolean;
|
||||
diagnostics: Array<Diagnostic>;
|
||||
};
|
||||
|
||||
export type WorkspaceProfileSourceSummary = {
|
||||
profile_source_id: string;
|
||||
display_path: string;
|
||||
kind: string;
|
||||
content_type: string;
|
||||
content_digest: string;
|
||||
provenance: WorkspaceProfileSourceProvenance;
|
||||
editable: boolean;
|
||||
revision: string;
|
||||
size_bytes: number;
|
||||
diagnostics: Array<Diagnostic>;
|
||||
};
|
||||
|
||||
export type WorkspaceProfileSourceProvenance = "project_profile_source_tree";
|
||||
|
||||
export type GitCommitSummary = { hash: string, short_hash: string, summary: string, author_name: string, author_email: string, author_date: string, parents: Array<string>, refs: Array<string>, };
|
||||
export type GitCommitSummary = {
|
||||
hash: string;
|
||||
short_hash: string;
|
||||
summary: string;
|
||||
author_name: string;
|
||||
author_email: string;
|
||||
author_date: string;
|
||||
parents: Array<string>;
|
||||
refs: Array<string>;
|
||||
};
|
||||
|
||||
export type RepositorySshConnectionProbeRequest = { runtime_id: string, };
|
||||
export type RepositorySshConnectionProbeRequest = { runtime_id: string };
|
||||
|
||||
export type RepositorySshHostKeyCandidate = { algorithm: string, host_key: string, fingerprint: string, };
|
||||
export type RepositorySshHostKeyCandidate = {
|
||||
algorithm: string;
|
||||
host_key: string;
|
||||
fingerprint: string;
|
||||
};
|
||||
|
||||
export type RepositorySshConnectionTrustState = "untrusted" | "verified" | "changed";
|
||||
export type RepositorySshConnectionTrustState =
|
||||
| "untrusted"
|
||||
| "verified"
|
||||
| "changed";
|
||||
|
||||
export type RepositorySshConnectionProbeResponse = { workspace_id: string, repository_key: string, runtime_id: string, hostname: string, port: number, trust_state: RepositorySshConnectionTrustState, host_trust_id: string, expected_host_trust_revision: number | null, candidates: Array<RepositorySshHostKeyCandidate>, };
|
||||
export type RepositorySshConnectionProbeResponse = {
|
||||
workspace_id: string;
|
||||
repository_key: string;
|
||||
runtime_id: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
trust_state: RepositorySshConnectionTrustState;
|
||||
host_trust_id: string;
|
||||
expected_host_trust_revision: number | null;
|
||||
candidates: Array<RepositorySshHostKeyCandidate>;
|
||||
};
|
||||
|
||||
export type ConfirmRepositorySshHostTrustRequest = { operation_id: string, runtime_id: string, host_key: string, expected_host_trust_revision: number | null, };
|
||||
export type ConfirmRepositorySshHostTrustRequest = {
|
||||
operation_id: string;
|
||||
runtime_id: string;
|
||||
host_key: string;
|
||||
expected_host_trust_revision: number | null;
|
||||
};
|
||||
|
||||
export type RepositoryLogResponse = { workspace_id: string, repository_key: string, default_selector?: string | null, limit: number, items: Array<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>, };
|
||||
export type RepositoryLogResponse = {
|
||||
workspace_id: string;
|
||||
repository_key: string;
|
||||
default_selector?: string | null;
|
||||
limit: number;
|
||||
items: Array<GitCommitSummary>;
|
||||
diagnostics: Array<Diagnostic>;
|
||||
};
|
||||
|
||||
@@ -1,26 +1,108 @@
|
||||
// Generated from server-api. Do not edit by hand.
|
||||
// Regenerate: cargo run -q -p server-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts
|
||||
|
||||
export const MEMORY_API_LIMITS = {
|
||||
maxResponseBytes: 8388608,
|
||||
maxDocumentBytes: 4194304,
|
||||
maxCollectionItems: 500,
|
||||
maxStringBytes: 1048576,
|
||||
maxIdentifierBytes: 512,
|
||||
} as const;
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
|
||||
export type Diagnostic = {
|
||||
code: string;
|
||||
severity: DiagnosticSeverity;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type MemoryDocumentResponse = { body_md: string, created_at: string, updated_at: string, bytes: number, record_source: string, };
|
||||
export type MemoryDocumentResponse = {
|
||||
body_md: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
bytes: number;
|
||||
record_source: string;
|
||||
};
|
||||
|
||||
export type MemoryCandidateKind = "preference" | "working_assumption" | "constraint" | "decision" | "open_question" | "lesson";
|
||||
export type MemoryCandidateKind =
|
||||
| "preference"
|
||||
| "working_assumption"
|
||||
| "constraint"
|
||||
| "decision"
|
||||
| "open_question"
|
||||
| "lesson";
|
||||
|
||||
export type MemoryEvidenceOriginKind = "human_input" | "worker_input" | "flow_instruction" | "backend_instruction" | "model_output" | "tool_output" | "derived_summary" | "legacy_unknown";
|
||||
export type MemoryEvidenceOriginKind =
|
||||
| "human_input"
|
||||
| "worker_input"
|
||||
| "flow_instruction"
|
||||
| "backend_instruction"
|
||||
| "model_output"
|
||||
| "tool_output"
|
||||
| "derived_summary"
|
||||
| "legacy_unknown";
|
||||
|
||||
export type MemoryEvidenceOrigin = { kind: MemoryEvidenceOriginKind, account_id?: string | null, workspace_id?: string | null, runtime_id?: string | null, worker_id?: string | null, flow_selector?: string | null, flow_definition_id?: string | null, flow_definition_revision?: number | null, };
|
||||
export type MemoryEvidenceOrigin = {
|
||||
kind: MemoryEvidenceOriginKind;
|
||||
account_id?: string | null;
|
||||
workspace_id?: string | null;
|
||||
runtime_id?: string | null;
|
||||
worker_id?: string | null;
|
||||
flow_selector?: string | null;
|
||||
flow_definition_id?: string | null;
|
||||
flow_definition_revision?: number | null;
|
||||
};
|
||||
|
||||
export type MemorySourceRef = { segment_id: string, range: [number, number], };
|
||||
export type MemorySourceRef = { segment_id: string; range: [number, number] };
|
||||
|
||||
export type MemoryStagingEvidence = { id: string, kind: string, entry_range: [number, number] | null, origin?: MemoryEvidenceOrigin | null, excerpt: string | null, summary: string | null, };
|
||||
export type MemoryStagingEvidence = {
|
||||
id: string;
|
||||
kind: string;
|
||||
entry_range: [number, number] | null;
|
||||
origin?: MemoryEvidenceOrigin | null;
|
||||
excerpt: string | null;
|
||||
summary: string | null;
|
||||
};
|
||||
|
||||
export type MemorySourceEvidenceRef = { session_id: string | null, segment_id: string | null, entry_range: [number, number] | null, evidence_id: string | null, origin?: MemoryEvidenceOrigin | null, evidence_kind: string | null, label: string | null, summary: string | null, };
|
||||
export type MemorySourceEvidenceRef = {
|
||||
session_id: string | null;
|
||||
segment_id: string | null;
|
||||
entry_range: [number, number] | null;
|
||||
evidence_id: string | null;
|
||||
origin?: MemoryEvidenceOrigin | null;
|
||||
evidence_kind: string | null;
|
||||
label: string | null;
|
||||
summary: string | null;
|
||||
};
|
||||
|
||||
export type MemoryStagingRecord = { schema_version: number, id: string, extract_run_id: string, source: MemorySourceRef, kind: MemoryCandidateKind, claim: string, why_useful: string, staleness: string | null, evidence: Array<MemoryStagingEvidence>, source_refs: Array<MemorySourceEvidenceRef>, };
|
||||
export type MemoryStagingRecord = {
|
||||
schema_version: number;
|
||||
id: string;
|
||||
extract_run_id: string;
|
||||
source: MemorySourceRef;
|
||||
kind: MemoryCandidateKind;
|
||||
claim: string;
|
||||
why_useful: string;
|
||||
staleness: string | null;
|
||||
evidence: Array<MemoryStagingEvidence>;
|
||||
source_refs: Array<MemorySourceEvidenceRef>;
|
||||
};
|
||||
|
||||
export type MemoryStagingEntry = { id: string, byte_len: number, record: MemoryStagingRecord, };
|
||||
export type MemoryStagingEntry = {
|
||||
id: string;
|
||||
byte_len: number;
|
||||
record: MemoryStagingRecord;
|
||||
};
|
||||
|
||||
export type MemoryStagingListResponse = { limit: number, returned_count: number, total_valid_count: number, invalid_count: number, truncated: boolean, order: string, record_authority: string, items: Array<MemoryStagingEntry>, diagnostics: Array<Diagnostic>, };
|
||||
export type MemoryStagingListResponse = {
|
||||
limit: number;
|
||||
returned_count: number;
|
||||
total_valid_count: number;
|
||||
invalid_count: number;
|
||||
truncated: boolean;
|
||||
order: string;
|
||||
record_authority: string;
|
||||
items: Array<MemoryStagingEntry>;
|
||||
diagnostics: Array<Diagnostic>;
|
||||
};
|
||||
|
||||
@@ -1,26 +1,93 @@
|
||||
// Generated from server-api. Do not edit by hand.
|
||||
// Regenerate: cargo run -q -p server-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts
|
||||
|
||||
export type RepositorySshCredential = { credential_id: string, workspace_id: string, name: string, public_key_algorithm: string, public_key_fingerprint: string, current_revision: number, status: string, created_at: string, rotated_at: string | null, referenced_repositories: Array<string>, };
|
||||
export type RepositorySshCredential = {
|
||||
credential_id: string;
|
||||
workspace_id: string;
|
||||
name: string;
|
||||
public_key_algorithm: string;
|
||||
public_key_fingerprint: string;
|
||||
current_revision: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
rotated_at: string | null;
|
||||
referenced_repositories: Array<string>;
|
||||
};
|
||||
|
||||
export type CreateRepositorySshCredentialRequest = { operation_id: string, credential_id: string, name: string, private_key: string, passphrase: string | null, };
|
||||
export type CreateRepositorySshCredentialRequest = {
|
||||
operation_id: string;
|
||||
credential_id: string;
|
||||
name: string;
|
||||
private_key: string;
|
||||
passphrase: string | null;
|
||||
};
|
||||
|
||||
export type GenerateRepositorySshCredentialRequest = { operation_id: string, credential_id: string, name: string, };
|
||||
export type GenerateRepositorySshCredentialRequest = {
|
||||
operation_id: string;
|
||||
credential_id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type RepositorySshPublicKey = { credential_id: string, current_revision: number, public_key_algorithm: string, public_key_fingerprint: string, public_key: string, };
|
||||
export type RepositorySshPublicKey = {
|
||||
credential_id: string;
|
||||
current_revision: number;
|
||||
public_key_algorithm: string;
|
||||
public_key_fingerprint: string;
|
||||
public_key: string;
|
||||
};
|
||||
|
||||
export type RotateRepositorySshCredentialRequest = { operation_id: string, expected_revision: number, private_key: string, passphrase: string | null, };
|
||||
export type RotateRepositorySshCredentialRequest = {
|
||||
operation_id: string;
|
||||
expected_revision: number;
|
||||
private_key: string;
|
||||
passphrase: string | null;
|
||||
};
|
||||
|
||||
export type DeleteRepositorySshCredentialRequest = { operation_id: string, expected_revision: number, };
|
||||
export type DeleteRepositorySshCredentialRequest = {
|
||||
operation_id: string;
|
||||
expected_revision: number;
|
||||
};
|
||||
|
||||
export type RepositorySshHostTrust = { host_trust_id: string, workspace_id: string, hostname: string, port: number, key_algorithm: string, host_key: string, fingerprint: string, current_revision: number, created_at: string, updated_at: string, referenced_repositories: Array<string>, };
|
||||
export type RepositorySshHostTrust = {
|
||||
host_trust_id: string;
|
||||
workspace_id: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
key_algorithm: string;
|
||||
host_key: string;
|
||||
fingerprint: string;
|
||||
current_revision: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
referenced_repositories: Array<string>;
|
||||
};
|
||||
|
||||
export type PutRepositorySshHostTrustRequest = { operation_id: string, host_trust_id: string, hostname: string, port: number, host_key: string, expected_revision: number | null, };
|
||||
export type PutRepositorySshHostTrustRequest = {
|
||||
operation_id: string;
|
||||
host_trust_id: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
host_key: string;
|
||||
expected_revision: number | null;
|
||||
};
|
||||
|
||||
export type DeleteRepositorySshHostTrustRequest = { operation_id: string, expected_revision: number, };
|
||||
export type DeleteRepositorySshHostTrustRequest = {
|
||||
operation_id: string;
|
||||
expected_revision: number;
|
||||
};
|
||||
|
||||
export type RepositoryAccessMode = "read_only" | "read_write";
|
||||
|
||||
export type RepositorySshAccessBinding = { repository_key: string, credential_id: string, host_trust_id: string, access: RepositoryAccessMode, };
|
||||
export type RepositorySshAccessBinding = {
|
||||
repository_key: string;
|
||||
credential_id: string;
|
||||
host_trust_id: string;
|
||||
access: RepositoryAccessMode;
|
||||
};
|
||||
|
||||
export type RepositoryAccessProjection = { workspace_id: string, config_revision: number, projection_digest: string, bindings: Array<RepositorySshAccessBinding>, };
|
||||
export type RepositoryAccessProjection = {
|
||||
workspace_id: string;
|
||||
config_revision: number;
|
||||
projection_digest: string;
|
||||
bindings: Array<RepositorySshAccessBinding>;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:bac8ba9340cf541183e96715dd4ccd8367f965cdcfc398e6d5091f26a903e1ef)
|
||||
// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
|
||||
export type ConfirmRepositorySshHostTrustRequest = { expected_host_trust_revision?: number | null; host_key: string; operation_id: string; runtime_id: string; };
|
||||
|
||||
export type CreateWorkspaceRepositoryRequest = { default_ref?: string | null; repository_key: string; source: string; };
|
||||
|
||||
export type CreateWorkspaceRepositoryResponse = { replayed: boolean; repository_key: string; workspace_id: string; };
|
||||
@@ -13,10 +15,18 @@ export type Diagnostic = { code: string; message: string; severity: DiagnosticSe
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type GitCommitSummary = { author_date: string; author_email: string; author_name: string; hash: string; parents: Array<string>; refs: Array<string>; short_hash: string; summary: string; };
|
||||
|
||||
export type GitRemoteSummary = { fetch_url: string; name: string; };
|
||||
|
||||
export type GitRepositorySummary = { branch?: string | null; dirty: boolean; head?: string | null; remotes: Array<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 RepositoryDetailResponse = { item: RepositorySummary; source: string; workspace_id: string; };
|
||||
@@ -25,10 +35,54 @@ 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 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 RepositorySource = { kind: RepositorySourceKind; uri: string; };
|
||||
|
||||
export type RepositorySourceKind = "local_path" | "file" | "ssh" | "https" | "invalid";
|
||||
|
||||
export type RepositorySshConnectionProbeRequest = { runtime_id: string; };
|
||||
|
||||
export type RepositorySshConnectionProbeResponse = { candidates: Array<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 WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
|
||||
|
||||
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
|
||||
|
||||
export type WorkerMaintenanceState = "compacting";
|
||||
|
||||
export type WorkerRunState = "running" | "pausing" | "paused" | "cancelling";
|
||||
|
||||
export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; };
|
||||
|
||||
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
|
||||
|
||||
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?: WorkerStateSnapshot | null; 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; };
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c)
|
||||
// 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?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
|
||||
export type RuntimeWorkerSpawnAcceptanceEvidence = { detail: string; kind: string; };
|
||||
|
||||
export type RuntimeWorkerSpawnAcceptanceRequirement = { kind: "socket_ready"; } | { expected_segments: number; kind: "run_accepted"; };
|
||||
|
||||
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 UploadedFileAvailability = "available" | "unavailable" | "integrity_failed";
|
||||
|
||||
export type UploadedFileRef = { artifact_id: string; availability: UploadedFileAvailability; byte_len: number; created_at_ms: number; file_name: string; media_type: string; sha256: string; source_entry_id?: string | null; };
|
||||
|
||||
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
|
||||
|
||||
export type WorkerFileDeleteResponse = { deleted: boolean; };
|
||||
|
||||
export type WorkerFileUploadResponse = { file: UploadedFileRef; };
|
||||
|
||||
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
|
||||
|
||||
export type WorkerMaintenanceState = "compacting";
|
||||
|
||||
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 WorkerRunState = "running" | "pausing" | "paused" | "cancelling";
|
||||
|
||||
export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; };
|
||||
|
||||
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
|
||||
|
||||
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?: WorkerStateSnapshot | null; 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; };
|
||||
@@ -20,22 +20,76 @@ export const SKILL_API_LIMITS = {
|
||||
|
||||
export type SkillDiagnosticSeverity = "error" | "warning";
|
||||
|
||||
export type SkillDiagnostic = { severity: SkillDiagnosticSeverity, code: string, message: string, source?: string, };
|
||||
export type SkillDiagnostic = {
|
||||
severity: SkillDiagnosticSeverity;
|
||||
code: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type SkillSourceKind = "builtin" | "workspace";
|
||||
|
||||
export type SkillProvenance = { kind: SkillSourceKind, id: string, virtual_path?: string, revision?: number, source_digest?: string, tree_digest?: string, };
|
||||
export type SkillProvenance = {
|
||||
kind: SkillSourceKind;
|
||||
id: string;
|
||||
virtual_path?: string;
|
||||
revision?: number;
|
||||
source_digest?: string;
|
||||
tree_digest?: string;
|
||||
};
|
||||
|
||||
export type SkillActivationStatus = "active" | "inactive";
|
||||
|
||||
export type SkillProjectionStatus = "valid" | "invalid";
|
||||
|
||||
export type SkillProjectionIdentity = { config_revision: number, tree_digest: string, };
|
||||
export type SkillProjectionIdentity = {
|
||||
config_revision: number;
|
||||
tree_digest: string;
|
||||
};
|
||||
|
||||
export type SkillResourceRef = { kind: string, name: string, supported: boolean, diagnostic?: string, };
|
||||
export type SkillResourceRef = {
|
||||
kind: string;
|
||||
name: string;
|
||||
supported: boolean;
|
||||
diagnostic?: string;
|
||||
};
|
||||
|
||||
export type SkillCatalogEntry = { name: string, description: string, activation_status: SkillActivationStatus, projection_status: SkillProjectionStatus, provenance: SkillProvenance, overrides: Array<SkillProvenance>, diagnostics: Array<SkillDiagnostic>, };
|
||||
export type SkillCatalogEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
activation_status: SkillActivationStatus;
|
||||
projection_status: SkillProjectionStatus;
|
||||
provenance: SkillProvenance;
|
||||
overrides: Array<SkillProvenance>;
|
||||
diagnostics: Array<SkillDiagnostic>;
|
||||
};
|
||||
|
||||
export type SkillCatalogResponse = { authority: string, projection: SkillProjectionIdentity, entries: Array<SkillCatalogEntry>, diagnostics: Array<SkillDiagnostic>, };
|
||||
export type SkillCatalogResponse = {
|
||||
authority: string;
|
||||
projection: SkillProjectionIdentity;
|
||||
entries: Array<SkillCatalogEntry>;
|
||||
diagnostics: Array<SkillDiagnostic>;
|
||||
};
|
||||
|
||||
export type SkillDetailResponse = { authority: string, projection: SkillProjectionIdentity, name: string, description: string, provenance: SkillProvenance, overrides: Array<SkillProvenance>, diagnostics: Array<SkillDiagnostic>, activation_status: SkillActivationStatus, projection_status: SkillProjectionStatus, body: string, allowed_tools: Array<string>, allowed_tools_status: string, resources: Array<SkillResourceRef>, };
|
||||
export type SkillDetailResponse = {
|
||||
authority: string;
|
||||
projection: SkillProjectionIdentity;
|
||||
name: string;
|
||||
description: string;
|
||||
provenance: SkillProvenance;
|
||||
overrides: Array<SkillProvenance>;
|
||||
diagnostics: Array<SkillDiagnostic>;
|
||||
activation_status: SkillActivationStatus;
|
||||
projection_status: SkillProjectionStatus;
|
||||
body: string;
|
||||
allowed_tools: Array<string>;
|
||||
allowed_tools_status: string;
|
||||
resources: Array<SkillResourceRef>;
|
||||
};
|
||||
|
||||
export type SkillActivationResponse = {
|
||||
name: string;
|
||||
provenance: SkillProvenance;
|
||||
diagnostics: Array<SkillDiagnostic>;
|
||||
body: string;
|
||||
};
|
||||
|
||||
@@ -1,54 +1,138 @@
|
||||
// Generated from yoi-workspace-server. Do not edit by hand.
|
||||
// Regenerate: cargo run -q -p yoi-workspace-server --features typescript --example generate_ticket_api_types > web/workspace/src/lib/generated/ticket-api.ts
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c)
|
||||
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/ticket-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
|
||||
|
||||
export type InvalidProjectRecord = { label: string, reason: string, };
|
||||
export type BrowserAppendTicketEventRequest = { author?: string | null; body: string; role: BrowserTicketThreadRole; };
|
||||
|
||||
export type TicketSummary = { id: string, resource_key: string, title: string, state: string, priority: string, updated_at: string | null, queued_by: string | null, queued_at: string | null, workspace_action_priority: string, record_source: string, };
|
||||
export type BrowserCloseTicketRequest = { resolution: string; };
|
||||
|
||||
export type TicketListResponse = { workspace_id: string, limit: number, items: Array<TicketSummary>, page: QueryPage, invalid_records: Array<InvalidProjectRecord>, record_authority: string, };
|
||||
export type BrowserEditTicketRequest = { author?: string | null; body?: string | null; new_string?: string | null; old_string?: string | null; replace_all?: boolean; target?: BrowserTicketTargetEdit | null; title?: string | null; };
|
||||
|
||||
export type QueryPage = { limit: number, returned: number, has_more: boolean, next_cursor: string | null, sort: string, source_limit: number | null, source_truncated: boolean, };
|
||||
export type BrowserQueueTicketRequest = { [key: string]: never; };
|
||||
|
||||
export type TicketEventDetail = { sequence: number, event_ref: string, kind: string, author: string | null, at: string | null, status: string | null, from: string | null, to: string | null, reason: string | null, state_field: string | null, heading: string | null, body: string | null, attributes: { [key in string]: string }, references: Array<string>, };
|
||||
export type BrowserTicketTargetEdit = { action: "set"; targets: Array<TicketTarget>; } | { action: "clear"; };
|
||||
|
||||
export type ObjectiveLinkSummary = { id: string, resource_key: string, title: string, state: string, };
|
||||
export type BrowserTicketThreadRole = "comment" | "plan" | "decision" | "implementation_report";
|
||||
|
||||
export type TicketEvidenceEvent = { event_ref: string, sequence: number, kind: string, at: string | null, author: string | null, excerpt: string, };
|
||||
export type BrowserTicketWorkflowState = "planning" | "ready" | "queued" | "inprogress" | "done" | "closed";
|
||||
|
||||
export type TicketAssignmentSummary = { assignment_id: string, runtime_id: string, worker_id: string, worker_resource_key?: string | null, };
|
||||
export type BrowserTransitionTicketStateRequest = { author?: string | null; body?: string | null; reason?: string | null; state: BrowserTicketWorkflowState; };
|
||||
|
||||
export type TicketRoleAssignmentSummary = { assignment_id: string, role: string, principal: TicketAssignmentPrincipalSummary, assigned_by: string, assigned_at: string, };
|
||||
export type CancelTicketImplementationRequest = { assignment_id: string; operation_id: string; reason: string; };
|
||||
|
||||
export type TicketAssignmentPrincipalSummary = { "kind": "user", account_id: string, } | { "kind": "worker", runtime_id: string, worker_id: string, } | { "kind": "workspace_agent", agent_key: string, };
|
||||
export type ConflictResolution = "none" | "clean" | "conflicts_resolved";
|
||||
|
||||
export type TicketActionEligibility = { can_assign_orchestrator: boolean, can_unassign_orchestrator: boolean, can_queue: boolean, can_start_manual_coder: boolean, queue_tickets: Array<string>, blockers: Array<string>, };
|
||||
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
|
||||
|
||||
export type TicketMergeRequestSummary = { merge_request_id: string, repository_key: string, state: string, review_status: string, selector_from: string | null, selector_to: string, updated_at: string, current_subject_ref: string | null, review_subject_ref: string | null, review_requested_at: string | null, review_submitted_at: string | null, review_excerpt: string | null, };
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type MergeRequestRefDiagnostic = { code: string, message: string, };
|
||||
export type FindingSeverity = "blocker" | "major" | "minor" | "note";
|
||||
|
||||
export type MergeRequestListItem = { summary: TicketMergeRequestSummary, ticket_ids: Array<string>, thread_event_count: number, ref_diagnostics?: Array<MergeRequestRefDiagnostic>, };
|
||||
export type InvalidProjectRecord = { label: string; reason: string; };
|
||||
|
||||
export type MergeRequestListResponse = { items: Array<MergeRequestListItem>, next_cursor: string | null, };
|
||||
export type MarkdownText = string;
|
||||
|
||||
export type TicketEvidenceSummary = { has_merge_request: boolean, has_current_subject_ref: boolean, has_review_request: boolean, has_commit: boolean, review_status: string | null, approved_current_subject: boolean, review_after_rescope: boolean, unresolved_request_changes: boolean, complete_for_integration: boolean, missing: Array<string>, };
|
||||
export type MergeRequestDetailResponse = { created_at: string; linked_tickets: Array<MergeRequestLinkedTicketResponse>; merge_request_id: string; repository_key: string; selector_from?: string | null; selector_to: string; source: MergeRequestRefResponse; state: MergeRequestState; target: MergeRequestRefResponse; thread: Array<MergeRequestThreadEvent>; ticket_ids: Array<string>; updated_at: string; workspace_id: string; };
|
||||
|
||||
export type TicketQueryRequest = { query: string | null, states: Array<string>, event_kinds: Array<string>, evidence: Array<string>, review_status: string | null, attention: Array<string>, related_ticket_id: string | null, relation_kind: string | null, linked_objective_id: string | null, updated_after: string | null, updated_before: string | null, sort: string | null, limit: number | null, cursor: string | null, };
|
||||
export type MergeRequestLinkedTicketResponse = { key?: string | null; ticket_id: string; };
|
||||
|
||||
export type TicketQueryItem = { id: string, resource_key: string, title: string, state: string, readiness: string | null, priority: string, created_at: string | null, updated_at: string | null, item_revision: string, workspace_action_priority: string, matched_fields: Array<string>, snippet: string | null, matching_event: TicketEvidenceEvent | null, linked_objective_ids: Array<string>, relation_count: number, blocker_count: number, unresolved_blocker_count: number, unresolved_review_count: number, evidence: TicketEvidenceSummary, merge_request: TicketMergeRequestSummary | null, };
|
||||
export type MergeRequestListItem = { ref_diagnostics?: Array<MergeRequestRefDiagnostic>; summary: TicketMergeRequestSummary; thread_event_count: number; ticket_ids: Array<string>; };
|
||||
|
||||
export type TicketQueryResponse = { items: Array<TicketQueryItem>, page: QueryPage, record_authority: string, };
|
||||
export type MergeRequestListResponse = { items: Array<MergeRequestListItem>; next_cursor?: string | null; };
|
||||
|
||||
export type TicketShowRequest = { event_limit: number | null, event_cursor: string | null, };
|
||||
export type MergeRequestRefDiagnostic = { code: string; message: string; };
|
||||
|
||||
export type TicketRelation = { ticket_id: string, kind: string, target: string, target_resource_key?: string | null, note: string | null, author: string, at: string, };
|
||||
export type MergeRequestRefResponse = { diagnostic?: MergeRequestRefDiagnostic | null; observed_at: string; ref?: string | null; status: string; };
|
||||
|
||||
export type DerivedTicketRelation = { source_ticket: string, source_resource_key?: string | null, inverse_kind: string, forward_kind: string, note: string | null, author: string, at: string, };
|
||||
export type MergeRequestState = "open" | "merged" | "closed";
|
||||
|
||||
export type TicketRelationBlocker = { blocking_ticket: string, blocking_resource_key?: string | null, reason_kind: string, relation_kind: string, note: string | null, blocking_state: string, };
|
||||
export type MergeRequestThreadEvent = { created_at: string; event_id: string; kind: "review_requested"; requested_by: MergeRequestWorkerIdentity; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { body: string; created_at: string; decision: ReviewDecision; event_id: string; findings: Array<ReviewFinding>; kind: "review"; request_event_id: string; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_revoked"; reason: string; review_event_id: string; revoked_by: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_cancelled"; reason: string; request_event_id: string; sequence: number; subject_ref: string; } | { author: MergeRequestWorkerIdentity; body: string; created_at: string; event_id: string; kind: "comment"; sequence: number; } | { approval_event_id: string; approved_source_ref: string; created_at: string; event_id: string; kind: "merge"; merged_by: MergeRequestWorkerIdentity; operation_id: string; resolution: ConflictResolution; sequence: number; strategy: MergeStrategy; target_ref_after: string; target_ref_before: string; };
|
||||
|
||||
export type TicketRelationNotice = { related_ticket: string, kind: string, message: string, };
|
||||
export type MergeRequestWorkerIdentity = { runtime_id: string; worker_id: string; };
|
||||
|
||||
export type TicketRelationView = { outgoing: Array<TicketRelation>, incoming: Array<DerivedTicketRelation>, blockers: Array<TicketRelationBlocker>, notices: Array<TicketRelationNotice>, };
|
||||
export type MergeStrategy = "fast_forward" | "merge";
|
||||
|
||||
export type TicketDetail = { id: string, resource_key: string, title: string, state: string, readiness: string | null, priority: string, created_at: string | null, updated_at: string | null, item_revision: string, queued_by: string | null, queued_at: string | null, repository_key: string | null, ref_selector: string | null, risk_flags: Array<string>, body: string, body_truncated: boolean, event_count: number, events: Array<TicketEventDetail>, event_page: QueryPage, artifact_count: number, artifacts: Array<string>, relations: TicketRelationView, linked_objectives: Array<ObjectiveLinkSummary>, implementation_reports: Array<TicketEvidenceEvent>, assignments: Array<TicketRoleAssignmentSummary>, current_coder: TicketAssignmentSummary | null, assignment_diagnostics: Array<string>, action_eligibility: TicketActionEligibility, merge_request: TicketMergeRequestSummary | null, evidence: TicketEvidenceSummary, resolution: string | null, record_source: string, };
|
||||
export type NewTicket = { assignee?: string | null; author?: string | null; body: MarkdownText; kind: string; labels: Array<string>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; risk_flags: Array<string>; slug?: string | null; targets?: Array<TicketTarget>; title: string; workflow_state?: TicketWorkflowState | null; };
|
||||
|
||||
export type ObjectiveDetail = { body: string; body_truncated: boolean; created_at?: string | null; event_page: QueryPage; events: Array<ObjectiveEventDetail>; id: string; linked_ticket_summaries: Array<ObjectiveLinkedTicketSummary>; linked_tickets: Array<string>; record_source: string; resource_key: string; resources: Array<ObjectiveResourceSummary>; revision: string; state: string; title: string; updated_at?: string | null; };
|
||||
|
||||
export type ObjectiveEventDetail = { body?: string | null; created_at: string; event_ref: string; kind: string; };
|
||||
|
||||
export type ObjectiveLinkSummary = { id: string; resource_key: string; state: string; title: string; };
|
||||
|
||||
export type ObjectiveLinkedTicketSummary = { id: string; resource_key: string; state: string; title: string; };
|
||||
|
||||
export type ObjectiveListResponse = { invalid_records: Array<InvalidProjectRecord>; items: Array<ObjectiveSummary>; limit: number & { readonly "__yoiIntegerRange": "0..1000"; }; record_authority: string; workspace_id: string; };
|
||||
|
||||
export type ObjectiveResourceSummary = { bytes: number; media_type?: string | null; path: string; updated_at: string; };
|
||||
|
||||
export type ObjectiveSummary = { created_at?: string | null; id: string; linked_tickets: Array<string>; record_source: string; resource_key: string; state: string; summary: string; title: string; updated_at?: string | null; };
|
||||
|
||||
export type QueryPage = { has_more: boolean; limit: number; next_cursor?: string | null; returned: number; sort: string; source_limit?: number | null; source_truncated: boolean; };
|
||||
|
||||
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
|
||||
|
||||
export type ReviewDecision = "approve" | "request_changes";
|
||||
|
||||
export type ReviewFinding = { body: string; code?: string | null; line?: number | null; path?: string | null; severity: FindingSeverity; };
|
||||
|
||||
export type SetTicketRoleAssignmentRequest = { expected_assignment_id?: string | null; operation_id: string; principal: TicketAssignmentPrincipal; };
|
||||
|
||||
export type TicketActionEligibility = { blockers: Array<string>; can_assign_orchestrator: boolean; can_queue: boolean; can_start_manual_coder: boolean; can_unassign_orchestrator: boolean; queue_tickets: Array<string>; };
|
||||
|
||||
export type TicketAssignmentPrincipal = { account_id: string; kind: "user"; } | { kind: "worker"; runtime_id: string; worker_id: string; } | { agent_key: string; kind: "workspace_agent"; };
|
||||
|
||||
export type TicketAssignmentRole = "orchestrator" | "coder" | "owner" | "contributor";
|
||||
|
||||
export type TicketAssignmentSummary = { assignment_id: string; runtime_id: string; worker_id: string; worker_resource_key?: string | null; };
|
||||
|
||||
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; created_at?: string | null; current_coder?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; item_revision: string; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: string; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
|
||||
|
||||
export type TicketDetailDerivedRelation = { at: string; author: string; forward_kind: string; inverse_kind: string; note?: string | null; source_resource_key?: string | null; source_ticket: string; };
|
||||
|
||||
export type TicketDetailRelation = { at: string; author: string; kind: string; note?: string | null; target: string; target_resource_key?: string | null; ticket_id: string; };
|
||||
|
||||
export type TicketDetailRelationBlocker = { blocking_resource_key?: string | null; blocking_state: string; blocking_ticket: string; note?: string | null; reason_kind: string; relation_kind: string; };
|
||||
|
||||
export type TicketDetailRelationNotice = { kind: string; message: string; related_ticket: string; };
|
||||
|
||||
export type TicketDetailRelationView = { blockers: Array<TicketDetailRelationBlocker>; incoming: Array<TicketDetailDerivedRelation>; notices: Array<TicketDetailRelationNotice>; outgoing: Array<TicketDetailRelation>; };
|
||||
|
||||
export type TicketEventDetail = { at?: string | null; attributes: { [key: string]: string; }; author?: string | null; body?: string | null; event_ref: string; from?: string | null; heading?: string | null; kind: string; reason?: string | null; references: Array<string>; sequence: number; state_field?: string | null; status?: string | null; to?: string | null; };
|
||||
|
||||
export type TicketEvidenceEvent = { at?: string | null; author?: string | null; event_ref: string; excerpt: string; kind: string; sequence: number; };
|
||||
|
||||
export type TicketEvidenceSummary = { approved_current_subject: boolean; complete_for_integration: boolean; has_commit: boolean; has_current_subject_ref: boolean; has_merge_request: boolean; has_review_request: boolean; missing: Array<string>; review_after_rescope: boolean; review_status?: string | null; unresolved_request_changes: boolean; };
|
||||
|
||||
export type TicketIntakeSummary = { author?: string | null; body: MarkdownText; references: Array<TicketReference>; };
|
||||
|
||||
export type TicketListItemSummary = { id: string; priority: string; queued_at?: string | null; queued_by?: string | null; record_source: string; resource_key: string; state: string; title: string; updated_at?: string | null; workspace_action_priority: string; };
|
||||
|
||||
export type TicketListResponse = { invalid_records: Array<InvalidProjectRecord>; items: Array<TicketListItemSummary>; limit: number & { readonly "__yoiIntegerRange": "0..1000"; }; page: QueryPage; record_authority: string; workspace_id: string; };
|
||||
|
||||
export type TicketMarkReadyRequest = { intake_summary?: TicketIntakeSummary | null; operation_key: string; reason?: string | null; };
|
||||
|
||||
export type TicketMergeRequestSummary = { current_subject_ref?: string | null; merge_request_id: string; repository_key: string; review_excerpt?: string | null; review_requested_at?: string | null; review_status: string; review_subject_ref?: string | null; review_submitted_at?: string | null; selector_from?: string | null; selector_to: string; state: string; updated_at: string; };
|
||||
|
||||
export type TicketQueueOutcome = { queued_tickets: Array<string>; requested_ticket: string; };
|
||||
|
||||
export type TicketRef = { id: string; resource_key?: string | null; slug: string; status: TicketStatus; };
|
||||
|
||||
export type TicketReference = { kind: string; target: string; };
|
||||
|
||||
export type TicketRoleAssignmentMutationResponse = { assignment?: TicketRoleAssignmentRecord | null; ticket_id: string; workspace_id: string; };
|
||||
|
||||
export type TicketRoleAssignmentRecord = { assigned_at: string; assigned_by: string; assignment_id: string; principal: TicketAssignmentPrincipal; role: TicketAssignmentRole; ticket_id: string; workspace_id: string; };
|
||||
|
||||
export type TicketRoleAssignmentSummary = { assigned_at: string; assigned_by: string; assignment_id: string; principal: TicketAssignmentPrincipal; role: string; };
|
||||
|
||||
export type TicketStatus = "Open" | "Closed";
|
||||
|
||||
export type TicketTarget = { access: TicketTargetAccess; ref_selector?: string | null; repository_key: string; };
|
||||
|
||||
export type TicketTargetAccess = "read_only" | "read_write";
|
||||
|
||||
export type TicketWorkflowState = "planning" | "ready" | "queued" | "in_progress" | "done" | "closed";
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
// Generated from server-api. Do not edit by hand.
|
||||
// Regenerate: cargo run -q -p server-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts
|
||||
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:7a9d11f50cc41eca55a6dded4b8f55aa6d23b1beb2dc69b0a9b6c8349efc894f)
|
||||
// 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 Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
|
||||
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 RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
|
||||
|
||||
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 WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; };
|
||||
|
||||
export type WorkingDirectoryRemovalDisposition = "removed" | "retained" | "attention_required";
|
||||
|
||||
export type WorkingDirectoryRemovalRequest = { reason: string; };
|
||||
|
||||
export type WorkingDirectoryRemovalResponse = { disposition: WorkingDirectoryRemovalDisposition; failure_category?: string | null; retryable: boolean; working_directory_id: string; };
|
||||
|
||||
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; kind: "external_grant"; };
|
||||
|
||||
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
|
||||
|
||||
export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_key: string, };
|
||||
|
||||
export type WorkingDirectoryOccupancy = { runtime_id: string, worker_id: string, display_name: string, linked_at: string, };
|
||||
|
||||
export type WorkingDirectorySource = { "kind": "repository", repository_key: string, } | { "kind": "external_grant", grant_id: string, };
|
||||
|
||||
export type WorkingDirectorySummary = { working_directory_id: string,
|
||||
/**
|
||||
* Optional human-facing label, never a routing key.
|
||||
*/
|
||||
display_name?: string | null, source: WorkingDirectorySource, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
|
||||
export type WorkingDirectoryCreateRequest = { runtime_id?: string | null,
|
||||
/**
|
||||
* Optional human-facing label, independent of every Worker attachment alias.
|
||||
*/
|
||||
display_name?: string | null, repository_key: string, selector?: string | null, operation_id?: string | null, };
|
||||
|
||||
export type WorkingDirectoryListResponse = { workspace_id: string, items: Array<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>, };
|
||||
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; };
|
||||
|
||||
@@ -1,68 +1,84 @@
|
||||
// Generated from server-api. Do not edit by hand.
|
||||
// Regenerate: cargo run -q -p server-api --features typescript --example generate_worker_launch_api_types > web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
|
||||
import type { Segment } from "./protocol";
|
||||
export type BrowserCreateWorkerResponse = { console_href: string; diagnostics: Array<Diagnostic>; runtime_id: string; worker: WorkerLaunchWorkerSummary; worker_id: string; workspace_id: string; };
|
||||
|
||||
export type BrowserWorkerWorkingDirectorySelection = { alias: string; relative_cwd?: string | null; working_directory_id: string; };
|
||||
|
||||
export type BrowserWorkspaceOrchestratorResponse = { diagnostics: Array<Diagnostic>; disposition: string; online: boolean; worker?: WorkerLaunchWorkerSummary | null; workspace_id: string; };
|
||||
|
||||
export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; initial_submit?: Array<Segment>; profile?: string | null; runtime_id: string; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array<BrowserWorkerWorkingDirectorySelection>; };
|
||||
|
||||
export type CreateWorkspaceWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; };
|
||||
|
||||
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
|
||||
export type PasteArtifactAvailability = "available" | "unavailable" | "integrity_failed";
|
||||
|
||||
export type PasteArtifactMediaType = "text_plain_utf8";
|
||||
|
||||
export type PasteArtifactRef = { artifact_id: string; availability: PasteArtifactAvailability; byte_len: number; char_count: number; created_at_ms: number; line_count: number; media_type: PasteArtifactMediaType; sha256: string; source_entry_id: string; };
|
||||
|
||||
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
|
||||
|
||||
export type RuntimeWorkerWorkdirAttachmentSummary = { alias: string; working_directory: RuntimeWorkingDirectorySummary; };
|
||||
|
||||
export type RuntimeWorkingDirectoryCleanupTarget = { kind: string; repository_id: string; working_directory_id: string; };
|
||||
|
||||
export type RuntimeWorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: RuntimeWorkingDirectoryCleanupTarget | 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; repository_id: string; status: WorkingDirectoryStatusKind; working_directory_id: string; };
|
||||
|
||||
export type Segment = { content: string; kind: "text"; } | { chars: number; content: string; id: number; kind: "paste"; lines: number; } | { artifact: PasteArtifactRef; kind: "paste_artifact"; } | { file: UploadedFileRef; kind: "uploaded_file"; } | { kind: "file_ref"; path: string; } | { kind: "flow"; selector: string; } | { kind: "unknown"; };
|
||||
|
||||
export type UploadedFileAvailability = "available" | "unavailable" | "integrity_failed";
|
||||
|
||||
export type UploadedFileRef = { artifact_id: string; availability: UploadedFileAvailability; byte_len: number; created_at_ms: number; file_name: string; media_type: string; sha256: string; source_entry_id?: string | null; };
|
||||
|
||||
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
|
||||
|
||||
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
|
||||
|
||||
export type WorkerLaunchOptionsResponse = { default_profile?: string | null; diagnostics: Array<Diagnostic>; profiles: Array<WorkerLaunchProfileCandidate>; repositories: Array<WorkingDirectoryRepositoryOption>; runtimes: Array<WorkerLaunchRuntimeOption>; working_directories: Array<WorkingDirectorySummary>; workspace_id: string; };
|
||||
|
||||
export type WorkerLaunchProfileCandidate = { description: string; id: string; label: string; };
|
||||
|
||||
export type WorkerLaunchRuntimeOption = { built_in: boolean; diagnostics: Array<Diagnostic>; display_name: string; runtime_id: string; status: string; worker_creation_available: boolean; working_directory_required: boolean; };
|
||||
|
||||
export type WorkerLaunchWorkerSummary = { 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<RuntimeWorkerWorkdirAttachmentSummary>; worker_id: string; workspace: WorkerWorkspaceSummary; };
|
||||
|
||||
export type WorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
|
||||
|
||||
export type WorkerMaintenanceState = "compacting";
|
||||
|
||||
export type WorkerRunState = "running" | "pausing" | "paused" | "cancelling";
|
||||
|
||||
export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; };
|
||||
|
||||
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
|
||||
|
||||
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?: WorkerStateSnapshot | null; 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 WorkingDirectoryRepositoryOption = { default_selector?: string | null; repository_key: string; };
|
||||
|
||||
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; kind: "external_grant"; };
|
||||
|
||||
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
|
||||
|
||||
export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_key: string, };
|
||||
|
||||
export type RuntimeWorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_id: string, };
|
||||
|
||||
export type RuntimeWorkingDirectorySummary = { working_directory_id: string,
|
||||
/**
|
||||
* Optional human-facing label, never a routing key.
|
||||
*/
|
||||
display_name?: string | null, repository_id: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: RuntimeWorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
|
||||
export type WorkingDirectoryOccupancy = { runtime_id: string, worker_id: string, display_name: string, linked_at: string, };
|
||||
|
||||
export type WorkingDirectorySource = { "kind": "repository", repository_key: string, } | { "kind": "external_grant", grant_id: string, };
|
||||
|
||||
export type WorkingDirectorySummary = { working_directory_id: string,
|
||||
/**
|
||||
* Optional human-facing label, never a routing key.
|
||||
*/
|
||||
display_name?: string | null, source: WorkingDirectorySource, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
|
||||
export type WorkerWorkspaceSummary = { visibility: string, identity: string, workspace_id?: string | null, };
|
||||
|
||||
export type WorkerImplementationSummary = { kind: string, display_hint: string, };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_stop: boolean, can_spawn_followup: boolean, };
|
||||
|
||||
export type RuntimeWorkerWorkdirAttachmentSummary = { alias: string, working_directory: RuntimeWorkingDirectorySummary, };
|
||||
|
||||
export type WorkerLaunchWorkerSummary = { runtime_id: string, worker_id: string, host_id: string, display_name: string, label: string, profile: string | null, singleton_key: string | null, tags: Array<string>, workspace: WorkerWorkspaceSummary, state: string, last_seen_at: string | null, pinned: boolean, retention_state: string, implementation: WorkerImplementationSummary, capabilities: WorkerCapabilitySummary, workdir_attachments?: Array<RuntimeWorkerWorkdirAttachmentSummary>, diagnostics: Array<Diagnostic>, };
|
||||
|
||||
export type WorkerLaunchRuntimeOption = { runtime_id: string, display_name: string, built_in: boolean, worker_creation_available: boolean, working_directory_required: boolean, status: string, diagnostics: Array<Diagnostic>, };
|
||||
|
||||
export type WorkerLaunchProfileCandidate = { id: string, label: string, description: string, };
|
||||
|
||||
export type WorkingDirectoryRepositoryOption = { repository_key: string, default_selector?: string | null, };
|
||||
|
||||
export type WorkerLaunchOptionsResponse = { workspace_id: string, runtimes: Array<WorkerLaunchRuntimeOption>, default_profile: string | null, profiles: Array<WorkerLaunchProfileCandidate>, repositories: Array<WorkingDirectoryRepositoryOption>, working_directories: Array<WorkingDirectorySummary>, diagnostics: Array<Diagnostic>, };
|
||||
|
||||
export type BrowserWorkerWorkingDirectorySelection = {
|
||||
/**
|
||||
* Stable Worker-local routing alias.
|
||||
*/
|
||||
alias: string, working_directory_id: string, relative_cwd: string | null, };
|
||||
|
||||
export type CreateWorkspaceWorkerTicketAssignmentRequest = { ticket_id: string, operation_id: string, };
|
||||
|
||||
export type CreateWorkspaceWorkerRequest = { runtime_id: string, display_name: string, profile: string | null, ticket_assignment: CreateWorkspaceWorkerTicketAssignmentRequest | null, initial_submit: Array<Segment>, workdir_attachments: Array<BrowserWorkerWorkingDirectorySelection>,
|
||||
/**
|
||||
* Backend idempotency key used only for authenticated Worker-owned spawn/control.
|
||||
*/
|
||||
control_operation_id: string | null, };
|
||||
|
||||
export type BrowserCreateWorkerResponse = { workspace_id: string, runtime_id: string, worker_id: string, console_href: string, worker: WorkerLaunchWorkerSummary, diagnostics: Array<Diagnostic>, };
|
||||
|
||||
export type BrowserWorkspaceOrchestratorResponse = { workspace_id: string, online: boolean, disposition: string, worker?: WorkerLaunchWorkerSummary | null, diagnostics: Array<Diagnostic>, };
|
||||
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; };
|
||||
|
||||
@@ -177,23 +177,41 @@ export async function readBoundedJson(
|
||||
return JSON.parse(text) as unknown;
|
||||
}
|
||||
|
||||
async function requireJson<T>(response: Response, path: string): Promise<T> {
|
||||
async function requireJson<T>(
|
||||
response: Response,
|
||||
path: string,
|
||||
parse: (value: unknown) => T,
|
||||
maxResponseBytes?: number,
|
||||
): Promise<T> {
|
||||
if (!response.ok) {
|
||||
if (maxResponseBytes !== undefined) {
|
||||
await response.body?.cancel();
|
||||
throw new Error(`${path} request failed (${response.status})`);
|
||||
}
|
||||
const text = await response.text();
|
||||
throw new Error(text || `${path} request failed (${response.status})`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
const value: unknown = maxResponseBytes === undefined
|
||||
? await response.json()
|
||||
: await readBoundedJson(response, maxResponseBytes);
|
||||
return parse(value);
|
||||
}
|
||||
|
||||
export async function workspaceApiJson<T>(path: string): Promise<T> {
|
||||
return requireJson<T>(await fetch(path), path);
|
||||
export async function workspaceApiJson<T>(
|
||||
path: string,
|
||||
parse: (value: unknown) => T = (value) => value as T,
|
||||
maxResponseBytes?: number,
|
||||
): Promise<T> {
|
||||
return requireJson(await fetch(path), path, parse, maxResponseBytes);
|
||||
}
|
||||
|
||||
export async function workspaceApiJsonWithBody<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
parse: (value: unknown) => T = (value) => value as T,
|
||||
maxResponseBytes?: number,
|
||||
): Promise<T> {
|
||||
return requireJson<T>(
|
||||
return requireJson(
|
||||
await fetch(path, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
@@ -202,6 +220,8 @@ export async function workspaceApiJsonWithBody<T>(
|
||||
...init,
|
||||
}),
|
||||
path,
|
||||
parse,
|
||||
maxResponseBytes,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +1,13 @@
|
||||
import type { MergeRequestListResponse } from "$lib/generated/ticket-api";
|
||||
import type {
|
||||
MergeRequestDetailResponse,
|
||||
MergeRequestListResponse,
|
||||
MergeRequestThreadEvent,
|
||||
} from "$lib/generated/ticket-api";
|
||||
import { workspaceApiPath } from "./http";
|
||||
|
||||
export type MergeRequestState = "open" | "merged" | "closed";
|
||||
|
||||
export type MergeRequestActor = {
|
||||
runtime_id: string;
|
||||
worker_id: string;
|
||||
assignment_id: string;
|
||||
};
|
||||
|
||||
export type MergeRequestThreadEvent = {
|
||||
kind: string;
|
||||
sequence: number;
|
||||
at: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type MergeRequestRecord = {
|
||||
merge_request_id: string;
|
||||
workspace_id: string;
|
||||
repository_key: string;
|
||||
selector_from: string | null;
|
||||
selector_to: string;
|
||||
ticket_ids: string[];
|
||||
state: MergeRequestState;
|
||||
opened_by: MergeRequestActor;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
thread: MergeRequestThreadEvent[];
|
||||
};
|
||||
|
||||
export type MergeRequestRefObservation = {
|
||||
status: string;
|
||||
ref: string | null;
|
||||
observed_at: string;
|
||||
};
|
||||
|
||||
export type MergeRequestDetail = MergeRequestRecord & {
|
||||
source: MergeRequestRefObservation;
|
||||
target: MergeRequestRefObservation;
|
||||
linked_tickets: Array<{ ticket_id: string; key: string | null }>;
|
||||
};
|
||||
|
||||
export type MergeRequestDetail = MergeRequestDetailResponse;
|
||||
export type MergeRequestListPage = MergeRequestListResponse;
|
||||
export type { MergeRequestThreadEvent };
|
||||
|
||||
export function mergeRequestCollectionPath(workspaceId: string): string {
|
||||
return workspaceApiPath(workspaceId, "/merge-requests");
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { RepositoryAccessSchemaError } from "./repository-access.ts";
|
||||
import { readBoundedJson } from "./http.ts";
|
||||
import {
|
||||
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
|
||||
RepositoryAccessSchemaError,
|
||||
} from "./repository-access.ts";
|
||||
|
||||
export async function loadRepositoryAccessJson<T>(
|
||||
fetcher: typeof fetch,
|
||||
@@ -27,7 +31,10 @@ export async function loadRepositoryAccessJson<T>(
|
||||
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await response.json();
|
||||
payload = await readBoundedJson(
|
||||
response,
|
||||
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
} catch {
|
||||
error(502, {
|
||||
message: "Repository Access returned an invalid JSON response.",
|
||||
|
||||
@@ -5,6 +5,10 @@ import type {
|
||||
RepositorySshPublicKey,
|
||||
} from "../../generated/repository-access-api.ts";
|
||||
|
||||
export const REPOSITORY_ACCESS_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
||||
const MAX_COLLECTION_ITEMS = 1000;
|
||||
const MAX_STRING_BYTES = 1024 * 1024;
|
||||
|
||||
export class RepositoryAccessSchemaError extends Error {
|
||||
constructor(path: string, expected: string) {
|
||||
super(
|
||||
@@ -165,7 +169,7 @@ function readRecord(
|
||||
);
|
||||
if (unknownKey !== undefined) {
|
||||
throw new RepositoryAccessSchemaError(
|
||||
`${path}.${unknownKey}`,
|
||||
`${path}.${unknownKey.slice(0, 128)}`,
|
||||
"no unknown field",
|
||||
);
|
||||
}
|
||||
@@ -173,7 +177,7 @@ function readRecord(
|
||||
}
|
||||
|
||||
function readArray(value: unknown, path: string): unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
if (!Array.isArray(value) || value.length > MAX_COLLECTION_ITEMS) {
|
||||
throw new RepositoryAccessSchemaError(path, "an array");
|
||||
}
|
||||
return value;
|
||||
@@ -185,8 +189,11 @@ function readString(
|
||||
path: string,
|
||||
): string {
|
||||
const value = record[key];
|
||||
if (typeof value !== "string") {
|
||||
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a string");
|
||||
if (
|
||||
typeof value !== "string" ||
|
||||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES
|
||||
) {
|
||||
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a bounded string");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -197,8 +204,15 @@ function readNullableString(
|
||||
path: string,
|
||||
): string | null {
|
||||
const value = record[key];
|
||||
if (value !== null && typeof value !== "string") {
|
||||
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a string or null");
|
||||
if (
|
||||
value !== null &&
|
||||
(typeof value !== "string" ||
|
||||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES)
|
||||
) {
|
||||
throw new RepositoryAccessSchemaError(
|
||||
`${path}.${key}`,
|
||||
"a bounded string or null",
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -210,10 +224,13 @@ function readStringArray(
|
||||
): string[] {
|
||||
const values = readArray(record[key], `${path}.${key}`);
|
||||
values.forEach((value, index) => {
|
||||
if (typeof value !== "string") {
|
||||
if (
|
||||
typeof value !== "string" ||
|
||||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES
|
||||
) {
|
||||
throw new RepositoryAccessSchemaError(
|
||||
`${path}.${key}[${index}]`,
|
||||
"a string",
|
||||
"a bounded string",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => Promise<void> | void): void;
|
||||
};
|
||||
|
||||
import {
|
||||
parseMergeRequestDetailResponse,
|
||||
parseObjectiveListResponse,
|
||||
parseTicketDetail,
|
||||
parseTicketListResponse,
|
||||
parseTicketRecordRef,
|
||||
} from "./ticket-browser.ts";
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
const actualJson = JSON.stringify(actual);
|
||||
const expectedJson = JSON.stringify(expected);
|
||||
if (actualJson !== expectedJson) {
|
||||
throw new Error(`Expected ${expectedJson}, received ${actualJson}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertThrows(operation: () => unknown, message: string): void {
|
||||
try {
|
||||
operation();
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes(message)) return;
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Expected operation to throw: ${message}`);
|
||||
}
|
||||
|
||||
const page = {
|
||||
has_more: false,
|
||||
limit: 30,
|
||||
next_cursor: null,
|
||||
returned: 1,
|
||||
sort: "updated_desc",
|
||||
source_limit: null,
|
||||
source_truncated: false,
|
||||
};
|
||||
|
||||
Deno.test("Ticket Browser parser accepts the generated Ticket list shape", () => {
|
||||
const parsed = parseTicketListResponse({
|
||||
workspace_id: "workspace-a",
|
||||
limit: 30,
|
||||
items: [{
|
||||
id: "ticket-id",
|
||||
resource_key: "T-1",
|
||||
title: "Ticket",
|
||||
state: "ready",
|
||||
priority: "P2",
|
||||
updated_at: null,
|
||||
queued_by: null,
|
||||
queued_at: null,
|
||||
workspace_action_priority: "ready",
|
||||
record_source: "sqlite",
|
||||
}],
|
||||
page,
|
||||
invalid_records: [],
|
||||
record_authority: "sqlite",
|
||||
});
|
||||
assertEquals(parsed.items[0]?.resource_key, "T-1");
|
||||
});
|
||||
|
||||
Deno.test("Ticket Browser parser rejects unknown fields and out-of-range limits", () => {
|
||||
const fixture = {
|
||||
workspace_id: "workspace-a",
|
||||
limit: 30,
|
||||
items: [],
|
||||
page: { ...page, returned: 0 },
|
||||
invalid_records: [],
|
||||
record_authority: "sqlite",
|
||||
};
|
||||
assertThrows(
|
||||
() => parseTicketListResponse({ ...fixture, stale: true }),
|
||||
"unknown field stale",
|
||||
);
|
||||
assertThrows(
|
||||
() => parseTicketListResponse({ ...fixture, limit: 1001 }),
|
||||
"0 through 1000",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Ticket Browser parser accepts target collections and rejects singular target authority", () => {
|
||||
const fixture = {
|
||||
action_eligibility: {
|
||||
blockers: [],
|
||||
can_assign_orchestrator: false,
|
||||
can_queue: false,
|
||||
can_start_manual_coder: false,
|
||||
can_unassign_orchestrator: false,
|
||||
queue_tickets: [],
|
||||
},
|
||||
artifact_count: 0,
|
||||
artifacts: [],
|
||||
assignment_diagnostics: [],
|
||||
assignments: [],
|
||||
body: "Body",
|
||||
body_truncated: false,
|
||||
current_coder: null,
|
||||
event_count: 0,
|
||||
event_page: { ...page, returned: 0 },
|
||||
events: [],
|
||||
evidence: {
|
||||
approved_current_subject: false,
|
||||
complete_for_integration: false,
|
||||
has_commit: false,
|
||||
has_current_subject_ref: false,
|
||||
has_merge_request: false,
|
||||
has_review_request: false,
|
||||
missing: [],
|
||||
review_after_rescope: false,
|
||||
review_status: null,
|
||||
unresolved_request_changes: false,
|
||||
},
|
||||
id: "ticket-id",
|
||||
implementation_reports: [],
|
||||
item_revision: "revision-1",
|
||||
linked_objectives: [],
|
||||
merge_request: null,
|
||||
priority: "P2",
|
||||
queued_at: null,
|
||||
queued_by: null,
|
||||
readiness: null,
|
||||
record_source: "sqlite",
|
||||
relations: { blockers: [], incoming: [], notices: [], outgoing: [] },
|
||||
resolution: null,
|
||||
resource_key: "T-1",
|
||||
risk_flags: [],
|
||||
state: "planning",
|
||||
targets: [
|
||||
{
|
||||
repository_key: "docs",
|
||||
ref_selector: "main",
|
||||
access: "read_only",
|
||||
},
|
||||
{
|
||||
repository_key: "main",
|
||||
ref_selector: "develop",
|
||||
access: "read_write",
|
||||
},
|
||||
],
|
||||
title: "Ticket",
|
||||
};
|
||||
|
||||
const parsed = parseTicketDetail(fixture);
|
||||
assertEquals(parsed.targets.length, 2);
|
||||
assertEquals(parsed.targets[0]?.repository_key, "docs");
|
||||
assertEquals(parsed.targets[0]?.access, "read_only");
|
||||
assertEquals(parsed.targets[1]?.repository_key, "main");
|
||||
assertEquals(parsed.targets[1]?.access, "read_write");
|
||||
assertThrows(
|
||||
() =>
|
||||
parseTicketDetail({
|
||||
...fixture,
|
||||
targets: [{ ...fixture.targets[0], access: "write" }],
|
||||
}),
|
||||
"targets[0].access is invalid",
|
||||
);
|
||||
assertThrows(
|
||||
() =>
|
||||
parseTicketDetail({
|
||||
...fixture,
|
||||
repository_key: "main",
|
||||
ref_selector: "develop",
|
||||
}),
|
||||
"unknown field repository_key",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Ticket Browser parser validates Ticket create responses", () => {
|
||||
const parsed = parseTicketRecordRef({
|
||||
id: "ticket-id",
|
||||
resource_key: "T-1",
|
||||
slug: "ticket",
|
||||
status: "Open",
|
||||
});
|
||||
assertEquals(parsed.resource_key, "T-1");
|
||||
assertThrows(
|
||||
() => parseTicketRecordRef({ ...parsed, status: "open" }),
|
||||
"status is invalid",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Ticket Browser parser accepts the generated Objective list shape", () => {
|
||||
const parsed = parseObjectiveListResponse({
|
||||
workspace_id: "workspace-a",
|
||||
limit: 100,
|
||||
items: [{
|
||||
id: "objective-id",
|
||||
resource_key: "O-1",
|
||||
title: "Objective",
|
||||
state: "active",
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
summary: "Summary",
|
||||
linked_tickets: ["T-1"],
|
||||
record_source: "sqlite",
|
||||
}],
|
||||
invalid_records: [],
|
||||
record_authority: "sqlite",
|
||||
});
|
||||
assertEquals(parsed.items[0]?.resource_key, "O-1");
|
||||
});
|
||||
|
||||
Deno.test("Ticket Browser parser validates the canonical Merge Request thread", () => {
|
||||
const parsed = parseMergeRequestDetailResponse({
|
||||
workspace_id: "workspace-a",
|
||||
merge_request_id: "mr-1",
|
||||
repository_key: "main",
|
||||
state: "open",
|
||||
selector_from: "work/T-1",
|
||||
selector_to: "develop",
|
||||
ticket_ids: ["ticket-id"],
|
||||
created_at: "2026-09-22T00:00:00Z",
|
||||
updated_at: "2026-09-22T00:00:00Z",
|
||||
thread: [{
|
||||
kind: "review_requested",
|
||||
event_id: "event-1",
|
||||
sequence: 1,
|
||||
subject_ref: "abc123",
|
||||
requested_by: { runtime_id: "runtime-a", worker_id: "worker-a" },
|
||||
reviewer: { runtime_id: "runtime-a", worker_id: "reviewer-a" },
|
||||
created_at: "2026-09-22T00:00:00Z",
|
||||
}],
|
||||
source: {
|
||||
status: "resolved",
|
||||
ref: "abc123",
|
||||
observed_at: "2026-09-22T00:00:00Z",
|
||||
},
|
||||
target: {
|
||||
status: "resolved",
|
||||
ref: "def456",
|
||||
observed_at: "2026-09-22T00:00:00Z",
|
||||
},
|
||||
linked_tickets: [{ ticket_id: "ticket-id", key: "T-1" }],
|
||||
});
|
||||
assertEquals(parsed.thread[0]?.kind, "review_requested");
|
||||
assertThrows(
|
||||
() =>
|
||||
parseMergeRequestDetailResponse({
|
||||
...parsed,
|
||||
thread: [{ ...parsed.thread[0], unexpected: true }],
|
||||
}),
|
||||
"unknown field unexpected",
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,15 +6,20 @@ import type {
|
||||
CreateWorkspaceWorkerTicketAssignmentRequest,
|
||||
Diagnostic,
|
||||
DiagnosticSeverity,
|
||||
RepositoryApiError,
|
||||
RuntimeWorkerWorkdirAttachmentSummary,
|
||||
RuntimeWorkingDirectoryCleanupTarget,
|
||||
RuntimeWorkingDirectorySummary,
|
||||
RuntimeWorkerWorkdirAttachmentSummary,
|
||||
WorkerCapabilitySummary,
|
||||
WorkerImplementationSummary,
|
||||
WorkerLaunchOptionsResponse,
|
||||
WorkerLaunchProfileCandidate,
|
||||
WorkerLaunchRuntimeOption,
|
||||
WorkerLaunchWorkerSummary,
|
||||
WorkerListResponse,
|
||||
WorkerStateSnapshot,
|
||||
WorkerSummary,
|
||||
WorkerWorkdirAttachmentSummary,
|
||||
WorkerWorkspaceSummary,
|
||||
WorkingDirectoryRepositoryOption,
|
||||
} from "$lib/generated/worker-launch-api";
|
||||
@@ -45,8 +50,14 @@ function exact(
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_STRING_LENGTH = 65_536;
|
||||
const MAX_COLLECTION_LENGTH = 4_096;
|
||||
|
||||
function string(value: unknown, label: string): string {
|
||||
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
||||
if (value.length > MAX_STRING_LENGTH) {
|
||||
throw new Error(`${label} exceeds its length limit`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -56,8 +67,8 @@ function boolean(value: unknown, label: string): boolean {
|
||||
}
|
||||
|
||||
function number(value: unknown, label: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new Error(`${label} must be a finite number`);
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
||||
throw new Error(`${label} must be a safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -72,6 +83,9 @@ function array<T>(
|
||||
parse: (item: unknown, label: string) => T,
|
||||
): T[] {
|
||||
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
||||
if (value.length > MAX_COLLECTION_LENGTH) {
|
||||
throw new Error(`${label} exceeds its item limit`);
|
||||
}
|
||||
return value.map((item, index) => parse(item, `${label}[${index}]`));
|
||||
}
|
||||
|
||||
@@ -165,6 +179,18 @@ function repositoryOption(
|
||||
};
|
||||
}
|
||||
|
||||
export function parseWorkerApiError(value: unknown): RepositoryApiError {
|
||||
const item = record(value, "Worker API error");
|
||||
exact(item, ["error", "message", "diagnostics"], "Worker API error");
|
||||
return {
|
||||
error: string(item.error, "error"),
|
||||
message: string(item.message, "message"),
|
||||
diagnostics: item.diagnostics === undefined
|
||||
? []
|
||||
: array(item.diagnostics, "diagnostics", diagnostic),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseWorkerLaunchOptionsResponse(
|
||||
value: unknown,
|
||||
): WorkerLaunchOptionsResponse {
|
||||
@@ -237,6 +263,181 @@ function capabilitySummary(
|
||||
};
|
||||
}
|
||||
|
||||
function workerStateSnapshot(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): WorkerStateSnapshot {
|
||||
const snapshot = record(value, label);
|
||||
exact(snapshot, ["last_command_id", "state"], label);
|
||||
const state = record(snapshot.state, `${label}.state`);
|
||||
const kind = string(state.kind, `${label}.state.kind`);
|
||||
if (kind === "idle") {
|
||||
exact(state, ["kind"], `${label}.state`);
|
||||
return {
|
||||
last_command_id: unsignedInteger(
|
||||
snapshot.last_command_id,
|
||||
`${label}.last_command_id`,
|
||||
),
|
||||
state: { kind: "idle" },
|
||||
};
|
||||
}
|
||||
if (kind !== "busy") throw new Error(`${label}.state.kind is invalid`);
|
||||
exact(state, ["kind", "state"], `${label}.state`);
|
||||
const busy = record(state.state, `${label}.state.state`);
|
||||
exact(busy, ["kind", "state"], `${label}.state.state`);
|
||||
const busyKind = string(busy.kind, `${label}.state.state.kind`);
|
||||
const busyState = string(busy.state, `${label}.state.state.state`);
|
||||
if (
|
||||
(busyKind === "run" &&
|
||||
["running", "pausing", "paused", "cancelling"].includes(busyState)) ||
|
||||
(busyKind === "maintenance" && busyState === "compacting")
|
||||
) {
|
||||
return {
|
||||
last_command_id: unsignedInteger(
|
||||
snapshot.last_command_id,
|
||||
`${label}.last_command_id`,
|
||||
),
|
||||
state: {
|
||||
kind: "busy",
|
||||
state: busyKind === "run"
|
||||
? {
|
||||
kind: "run",
|
||||
state: busyState as "running" | "pausing" | "paused" | "cancelling",
|
||||
}
|
||||
: { kind: "maintenance", state: "compacting" },
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`${label}.state.state is invalid`);
|
||||
}
|
||||
|
||||
function workerWorkdirAttachment(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): WorkerWorkdirAttachmentSummary {
|
||||
const item = record(value, label);
|
||||
exact(item, ["alias", "working_directory"], label);
|
||||
return {
|
||||
alias: string(item.alias, `${label}.alias`),
|
||||
working_directory: parseWorkingDirectorySummary(item.working_directory),
|
||||
};
|
||||
}
|
||||
|
||||
export type ParsedWorkerSummary =
|
||||
& Omit<
|
||||
WorkerSummary,
|
||||
"display_name" | "tags" | "diagnostics"
|
||||
>
|
||||
& {
|
||||
display_name: string;
|
||||
tags: string[];
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type ParsedWorkerListResponse = Omit<WorkerListResponse, "items"> & {
|
||||
items: ParsedWorkerSummary[];
|
||||
};
|
||||
|
||||
export function parseWorkerSummary(
|
||||
value: unknown,
|
||||
label = "Worker summary",
|
||||
): ParsedWorkerSummary {
|
||||
const item = record(value, label);
|
||||
exact(
|
||||
item,
|
||||
[
|
||||
"runtime_id",
|
||||
"worker_id",
|
||||
"resource_key",
|
||||
"host_id",
|
||||
"display_name",
|
||||
"label",
|
||||
"profile",
|
||||
"singleton_key",
|
||||
"tags",
|
||||
"workspace",
|
||||
"state",
|
||||
"worker_state",
|
||||
"last_seen_at",
|
||||
"pinned",
|
||||
"retention_state",
|
||||
"implementation",
|
||||
"capabilities",
|
||||
"workdir_attachments",
|
||||
"diagnostics",
|
||||
],
|
||||
label,
|
||||
);
|
||||
return {
|
||||
runtime_id: string(item.runtime_id, `${label}.runtime_id`),
|
||||
worker_id: string(item.worker_id, `${label}.worker_id`),
|
||||
resource_key: string(item.resource_key, `${label}.resource_key`),
|
||||
host_id: string(item.host_id, `${label}.host_id`),
|
||||
display_name: item.display_name === undefined
|
||||
? ""
|
||||
: string(item.display_name, `${label}.display_name`),
|
||||
label: string(item.label, `${label}.label`),
|
||||
profile: optional(item.profile, `${label}.profile`, string),
|
||||
singleton_key: optional(
|
||||
item.singleton_key,
|
||||
`${label}.singleton_key`,
|
||||
string,
|
||||
),
|
||||
tags: item.tags === undefined
|
||||
? []
|
||||
: array(item.tags, `${label}.tags`, string),
|
||||
workspace: workspaceSummary(item.workspace, `${label}.workspace`),
|
||||
state: string(item.state, `${label}.state`),
|
||||
worker_state: optional(
|
||||
item.worker_state,
|
||||
`${label}.worker_state`,
|
||||
workerStateSnapshot,
|
||||
),
|
||||
last_seen_at: optional(item.last_seen_at, `${label}.last_seen_at`, string),
|
||||
pinned: item.pinned === undefined
|
||||
? false
|
||||
: boolean(item.pinned, `${label}.pinned`),
|
||||
retention_state: item.retention_state === undefined
|
||||
? ""
|
||||
: string(item.retention_state, `${label}.retention_state`),
|
||||
implementation: implementationSummary(
|
||||
item.implementation,
|
||||
`${label}.implementation`,
|
||||
),
|
||||
capabilities: capabilitySummary(item.capabilities, `${label}.capabilities`),
|
||||
workdir_attachments: item.workdir_attachments === undefined
|
||||
? undefined
|
||||
: array(
|
||||
item.workdir_attachments,
|
||||
`${label}.workdir_attachments`,
|
||||
workerWorkdirAttachment,
|
||||
),
|
||||
diagnostics: item.diagnostics === undefined
|
||||
? []
|
||||
: array(item.diagnostics, `${label}.diagnostics`, diagnostic),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseWorkerListResponse(
|
||||
value: unknown,
|
||||
): ParsedWorkerListResponse {
|
||||
const item = record(value, "Worker list response");
|
||||
exact(
|
||||
item,
|
||||
["workspace_id", "limit", "items", "source", "diagnostics"],
|
||||
"Worker list response",
|
||||
);
|
||||
return {
|
||||
workspace_id: string(item.workspace_id, "workspace_id"),
|
||||
limit: unsignedInteger(item.limit, "limit"),
|
||||
items: array(item.items, "items", parseWorkerSummary),
|
||||
source: string(item.source, "source"),
|
||||
diagnostics: item.diagnostics === undefined
|
||||
? []
|
||||
: array(item.diagnostics, "diagnostics", diagnostic),
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeCleanupTarget(
|
||||
value: unknown,
|
||||
label: string,
|
||||
|
||||
@@ -138,15 +138,18 @@ async function fetchJson(
|
||||
if (!response.ok) {
|
||||
let detail = `${response.status} ${response.statusText}`.trim();
|
||||
try {
|
||||
const body = await response.json();
|
||||
if (typeof body?.message === "string") detail = body.message;
|
||||
else if (typeof body?.error === "string") detail = body.error;
|
||||
const body: unknown = await response.json();
|
||||
if (typeof body === "object" && body !== null && !Array.isArray(body)) {
|
||||
const record = body as Record<string, unknown>;
|
||||
if (typeof record.message === "string") detail = record.message.slice(0, 512);
|
||||
else if (typeof record.error === "string") detail = record.error.slice(0, 512);
|
||||
}
|
||||
} catch {
|
||||
// Preserve the bounded status text when the Backend did not return JSON.
|
||||
}
|
||||
throw new WorkspaceCatalogError(response.status, detail);
|
||||
}
|
||||
return await response.json() as unknown;
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { ApiResult } from "$lib/workspace/api/http";
|
||||
import type {
|
||||
GitCommitSummary,
|
||||
RepositoryLogResponse,
|
||||
RepositorySshConnectionProbeResponse,
|
||||
RepositorySshConnectionTrustState,
|
||||
RepositorySshHostKeyCandidate,
|
||||
WorkspaceAuthConfig,
|
||||
WorkspaceCatalogListResponse,
|
||||
WorkspaceCreateResponse,
|
||||
@@ -24,22 +19,25 @@ import type {
|
||||
import type {
|
||||
CreateWorkspaceRepositoryResponse,
|
||||
Diagnostic,
|
||||
GitCommitSummary,
|
||||
GitRemoteSummary,
|
||||
GitRepositorySummary,
|
||||
HostListResponse,
|
||||
HostSummary,
|
||||
RepositoryApiError,
|
||||
RepositoryDetailResponse,
|
||||
RepositoryDiagnostic,
|
||||
RepositoryListResponse,
|
||||
RepositoryLogResponse,
|
||||
RepositorySource,
|
||||
RepositorySourceKind,
|
||||
RepositorySshConnectionProbeResponse,
|
||||
RepositorySshConnectionTrustState,
|
||||
RepositorySshHostKeyCandidate,
|
||||
RepositorySummary,
|
||||
} from "$lib/generated/repository-api.ts";
|
||||
|
||||
export type {
|
||||
GitCommitSummary,
|
||||
RepositoryLogResponse,
|
||||
RepositorySshConnectionProbeResponse,
|
||||
RepositorySshHostKeyCandidate,
|
||||
WorkspaceCatalogListResponse,
|
||||
WorkspaceCreateResponse,
|
||||
WorkspaceDeletionOperationResponse,
|
||||
@@ -51,13 +49,19 @@ export type {
|
||||
export type {
|
||||
CreateWorkspaceRepositoryRequest,
|
||||
CreateWorkspaceRepositoryResponse,
|
||||
GitCommitSummary,
|
||||
GitRemoteSummary,
|
||||
GitRepositorySummary,
|
||||
HostListResponse,
|
||||
HostSummary,
|
||||
RepositoryApiError,
|
||||
RepositoryDetailResponse,
|
||||
RepositoryListResponse,
|
||||
RepositoryLogResponse,
|
||||
RepositorySource,
|
||||
RepositorySourceKind,
|
||||
RepositorySshConnectionProbeResponse,
|
||||
RepositorySshHostKeyCandidate,
|
||||
RepositorySummary,
|
||||
} from "$lib/generated/repository-api.ts";
|
||||
|
||||
@@ -395,6 +399,27 @@ function workspaceRepositoryRecord(
|
||||
};
|
||||
}
|
||||
|
||||
function workspaceString(value: unknown, path: string): string {
|
||||
const parsed = string(value, path);
|
||||
if (parsed.length > REPOSITORY_API_LIMITS.maxStringCodeUnits) {
|
||||
throw new Error(`${path} exceeds the Workspace API string limit`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function workspaceDiagnostics(value: unknown, path: string): Diagnostic[] {
|
||||
const entries = array(value, path);
|
||||
if (entries.length > 100) {
|
||||
throw new Error(`${path} exceeds the Workspace API collection limit`);
|
||||
}
|
||||
return entries.map((entry, index) => {
|
||||
const parsed = diagnostic(entry, `${path}[${index}]`);
|
||||
workspaceString(parsed.code, `${path}[${index}].code`);
|
||||
workspaceString(parsed.message, `${path}[${index}].message`);
|
||||
return parsed;
|
||||
});
|
||||
}
|
||||
|
||||
function extensionPoint(
|
||||
value: unknown,
|
||||
path: string,
|
||||
@@ -402,12 +427,9 @@ function extensionPoint(
|
||||
const item = object(value, path);
|
||||
exactKeys(item, ["status", "note", "diagnostics"], path);
|
||||
return {
|
||||
status: string(item.status, `${path}.status`),
|
||||
note: string(item.note, `${path}.note`),
|
||||
diagnostics: array(item.diagnostics, `${path}.diagnostics`).map((
|
||||
entry,
|
||||
index,
|
||||
) => diagnostic(entry, `${path}.diagnostics[${index}]`)),
|
||||
status: workspaceString(item.status, `${path}.status`),
|
||||
note: workspaceString(item.note, `${path}.note`),
|
||||
diagnostics: workspaceDiagnostics(item.diagnostics, `${path}.diagnostics`),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -423,7 +445,7 @@ function extensionPoints(
|
||||
"companion_console",
|
||||
], path);
|
||||
return {
|
||||
store: string(item.store, `${path}.store`),
|
||||
store: workspaceString(item.store, `${path}.store`),
|
||||
event_stream: extensionPoint(item.event_stream, `${path}.event_stream`),
|
||||
host_worker_bridge: extensionPoint(
|
||||
item.host_worker_bridge,
|
||||
@@ -447,13 +469,16 @@ function authConfig(value: unknown, path: string): WorkspaceAuthConfig {
|
||||
);
|
||||
return {
|
||||
Passkey: {
|
||||
rp_id: string(passkey.rp_id, `${path}.Passkey.rp_id`),
|
||||
origin: string(passkey.origin, `${path}.Passkey.origin`),
|
||||
public_base_url: string(
|
||||
rp_id: workspaceString(passkey.rp_id, `${path}.Passkey.rp_id`),
|
||||
origin: workspaceString(passkey.origin, `${path}.Passkey.origin`),
|
||||
public_base_url: workspaceString(
|
||||
passkey.public_base_url,
|
||||
`${path}.Passkey.public_base_url`,
|
||||
),
|
||||
cookie_name: string(passkey.cookie_name, `${path}.Passkey.cookie_name`),
|
||||
cookie_name: workspaceString(
|
||||
passkey.cookie_name,
|
||||
`${path}.Passkey.cookie_name`,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -575,23 +600,27 @@ export function parseWorkspaceResponse(value: unknown): WorkspaceResponse {
|
||||
],
|
||||
"workspace response",
|
||||
);
|
||||
const schemaVersion = integer(
|
||||
response.schema_version,
|
||||
"workspace response.schema_version",
|
||||
);
|
||||
if (schemaVersion < 0) {
|
||||
throw new Error("workspace response.schema_version must be non-negative");
|
||||
}
|
||||
return {
|
||||
workspace_id: string(
|
||||
workspace_id: workspaceString(
|
||||
response.workspace_id,
|
||||
"workspace response.workspace_id",
|
||||
),
|
||||
display_name: string(
|
||||
display_name: workspaceString(
|
||||
response.display_name,
|
||||
"workspace response.display_name",
|
||||
),
|
||||
record_authority: string(
|
||||
record_authority: workspaceString(
|
||||
response.record_authority,
|
||||
"workspace response.record_authority",
|
||||
),
|
||||
schema_version: integer(
|
||||
response.schema_version,
|
||||
"workspace response.schema_version",
|
||||
),
|
||||
schema_version: schemaVersion,
|
||||
auth: authConfig(response.auth, "workspace response.auth"),
|
||||
permissions: permissions(
|
||||
response.permissions,
|
||||
@@ -1069,6 +1098,77 @@ export function parseWorkspaceDeletionOperationResponse(
|
||||
};
|
||||
}
|
||||
|
||||
export function parseHostListResponse(value: unknown): HostListResponse {
|
||||
const response = object(value, "host list response");
|
||||
exactKeys(
|
||||
response,
|
||||
["workspace_id", "limit", "items", "source", "diagnostics"],
|
||||
"host list response",
|
||||
);
|
||||
const limit = repositorySourceRevision(
|
||||
response.limit,
|
||||
"host list response.limit",
|
||||
);
|
||||
return {
|
||||
workspace_id: repositoryString(
|
||||
response.workspace_id,
|
||||
"host list response.workspace_id",
|
||||
),
|
||||
limit,
|
||||
items: repositoryArray(response.items, "host list response.items").map(
|
||||
(item, index) => parseHostSummary(item, `host list response.items[${index}]`),
|
||||
),
|
||||
source: repositoryString(response.source, "host list response.source"),
|
||||
diagnostics: repositoryArray(
|
||||
response.diagnostics,
|
||||
"host list response.diagnostics",
|
||||
).map((item, index) =>
|
||||
repositoryApiDiagnostic(
|
||||
item,
|
||||
`host list response.diagnostics[${index}]`,
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseHostSummary(value: unknown, path: string): HostSummary {
|
||||
const host = object(value, path);
|
||||
exactKeys(
|
||||
host,
|
||||
[
|
||||
"runtime_id",
|
||||
"host_id",
|
||||
"label",
|
||||
"kind",
|
||||
"status",
|
||||
"observed_at",
|
||||
"last_seen_at",
|
||||
"os",
|
||||
"arch",
|
||||
"diagnostics",
|
||||
],
|
||||
path,
|
||||
);
|
||||
return {
|
||||
runtime_id: repositoryString(host.runtime_id, `${path}.runtime_id`),
|
||||
host_id: repositoryString(host.host_id, `${path}.host_id`),
|
||||
label: repositoryString(host.label, `${path}.label`),
|
||||
kind: repositoryString(host.kind, `${path}.kind`),
|
||||
status: repositoryString(host.status, `${path}.status`),
|
||||
observed_at: repositoryString(host.observed_at, `${path}.observed_at`),
|
||||
last_seen_at: optionalNullableRepositoryString(
|
||||
host.last_seen_at,
|
||||
`${path}.last_seen_at`,
|
||||
),
|
||||
os: repositoryString(host.os, `${path}.os`),
|
||||
arch: repositoryString(host.arch, `${path}.arch`),
|
||||
diagnostics: repositoryArray(host.diagnostics, `${path}.diagnostics`).map(
|
||||
(item, index) =>
|
||||
repositoryApiDiagnostic(item, `${path}.diagnostics[${index}]`),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseRepositoryLogResponse(
|
||||
value: unknown,
|
||||
): RepositoryLogResponse {
|
||||
|
||||
@@ -1,20 +1,267 @@
|
||||
import type {
|
||||
ConfigCommitRequest,
|
||||
ConfigContentType,
|
||||
ConfigEntry,
|
||||
ConfigProjectionValidator,
|
||||
ConfigSchemaContribution,
|
||||
ConfigTreeSnapshot,
|
||||
ToolchainContract,
|
||||
WorkspaceConfigSchemaBundle,
|
||||
WorkspaceConfigTreeResponse,
|
||||
} from "./types.ts";
|
||||
|
||||
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
||||
const MAX_ERROR_BYTES = 4 * 1024;
|
||||
const MAX_ENTRY_COUNT = 256;
|
||||
const MAX_ENTRY_BYTES = 256 * 1024;
|
||||
const MAX_PATH_BYTES = 512;
|
||||
const MAX_STRING_BYTES = 4 * 1024;
|
||||
const MAX_DIGEST_BYTES = 256;
|
||||
const MAX_COLLECTION_ITEMS = 256;
|
||||
|
||||
export class ConfigSourceApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ConfigSourceApiError";
|
||||
}
|
||||
}
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
type Parser<T> = (value: unknown) => T;
|
||||
|
||||
function sourceTreeUrl(workspaceId: string): string {
|
||||
return `/api/w/${encodeURIComponent(workspaceId)}/config/source-tree`;
|
||||
}
|
||||
|
||||
async function readJson<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(body || `${response.status} ${response.statusText}`);
|
||||
async function readJson<T>(
|
||||
response: Response,
|
||||
parser: Parser<T>,
|
||||
): Promise<T> {
|
||||
const limit = response.ok ? MAX_RESPONSE_BYTES : MAX_ERROR_BYTES;
|
||||
const declared = response.headers.get("content-length");
|
||||
if (declared !== null) {
|
||||
const bytes = Number(declared);
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > limit) {
|
||||
throw new ConfigSourceApiError(
|
||||
response.ok
|
||||
? "Workspace configuration response is too large."
|
||||
: "Workspace configuration request failed.",
|
||||
response.ok ? 502 : response.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
return await response.json() as T;
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
if (bytes.byteLength > limit) {
|
||||
throw new ConfigSourceApiError(
|
||||
response.ok
|
||||
? "Workspace configuration response is too large."
|
||||
: "Workspace configuration request failed.",
|
||||
response.ok ? 502 : response.status,
|
||||
);
|
||||
}
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
if (!response.ok) {
|
||||
throw new ConfigSourceApiError(
|
||||
text || `${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(text) as unknown;
|
||||
} catch {
|
||||
throw schemaError();
|
||||
}
|
||||
return parser(value);
|
||||
}
|
||||
|
||||
function schemaError(): ConfigSourceApiError {
|
||||
return new ConfigSourceApiError(
|
||||
"Workspace configuration returned an invalid response.",
|
||||
502,
|
||||
);
|
||||
}
|
||||
|
||||
function objectValue(value: unknown): JsonRecord {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw schemaError();
|
||||
}
|
||||
return value as JsonRecord;
|
||||
}
|
||||
|
||||
function record(
|
||||
value: unknown,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = [],
|
||||
): JsonRecord {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw schemaError();
|
||||
}
|
||||
const result = value as JsonRecord;
|
||||
const allowed = new Set([...required, ...optional]);
|
||||
if (
|
||||
required.some((key) => !(key in result)) ||
|
||||
Object.keys(result).some((key) => !allowed.has(key))
|
||||
) {
|
||||
throw schemaError();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function boundedString(value: unknown, maxBytes = MAX_STRING_BYTES): string {
|
||||
if (
|
||||
typeof value !== "string" ||
|
||||
new TextEncoder().encode(value).byteLength > maxBytes
|
||||
) {
|
||||
throw schemaError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function safeInteger(value: unknown): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||
throw schemaError();
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function boundedArray(value: unknown): unknown[] {
|
||||
if (!Array.isArray(value) || value.length > MAX_COLLECTION_ITEMS) {
|
||||
throw schemaError();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseContentType(value: unknown): ConfigContentType {
|
||||
if (value !== "decodal" && value !== "text") throw schemaError();
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseConfigEntry(value: unknown): ConfigEntry {
|
||||
const item = record(value, [
|
||||
"path",
|
||||
"content_type",
|
||||
"content",
|
||||
"content_digest",
|
||||
]);
|
||||
return {
|
||||
path: boundedString(item.path, MAX_PATH_BYTES),
|
||||
content_type: parseContentType(item.content_type),
|
||||
content: boundedString(item.content, MAX_ENTRY_BYTES),
|
||||
content_digest: boundedString(item.content_digest, MAX_DIGEST_BYTES),
|
||||
};
|
||||
}
|
||||
|
||||
function parseStringMap(value: unknown): Record<string, string> {
|
||||
const item = objectValue(value);
|
||||
const entries = Object.entries(item);
|
||||
if (entries.length > MAX_COLLECTION_ITEMS) throw schemaError();
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, entry] of entries) {
|
||||
const boundedKey = boundedString(key, MAX_PATH_BYTES);
|
||||
result[boundedKey] = boundedString(entry, MAX_PATH_BYTES);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseProjectionValidator(value: unknown): ConfigProjectionValidator {
|
||||
const item = record(value, ["kind", "namespace"], ["key_aliases"]);
|
||||
if (item.kind !== "static_template_catalog") throw schemaError();
|
||||
return {
|
||||
kind: "static_template_catalog",
|
||||
namespace: boundedString(item.namespace, MAX_PATH_BYTES),
|
||||
key_aliases: item.key_aliases === undefined
|
||||
? {}
|
||||
: parseStringMap(item.key_aliases),
|
||||
};
|
||||
}
|
||||
|
||||
function parseContribution(value: unknown): ConfigSchemaContribution {
|
||||
const item = record(
|
||||
value,
|
||||
["provider_id", "namespace", "version", "source", "source_digest"],
|
||||
["projection_validator"],
|
||||
);
|
||||
const projection = item.projection_validator === undefined
|
||||
? undefined
|
||||
: parseProjectionValidator(item.projection_validator);
|
||||
return {
|
||||
provider_id: boundedString(item.provider_id),
|
||||
namespace: boundedString(item.namespace),
|
||||
version: boundedString(item.version),
|
||||
source: boundedString(item.source),
|
||||
...(projection === undefined ? {} : { projection_validator: projection }),
|
||||
source_digest: boundedString(item.source_digest, MAX_DIGEST_BYTES),
|
||||
};
|
||||
}
|
||||
|
||||
function parseSchemaBundle(value: unknown): WorkspaceConfigSchemaBundle {
|
||||
const item = record(value, ["contributions", "source", "fingerprint"]);
|
||||
return {
|
||||
contributions: boundedArray(item.contributions).map(parseContribution),
|
||||
source: boundedString(item.source),
|
||||
fingerprint: boundedString(item.fingerprint, MAX_DIGEST_BYTES),
|
||||
};
|
||||
}
|
||||
|
||||
function parseToolchainContract(value: unknown): ToolchainContract {
|
||||
const item = record(value, [
|
||||
"contract_version",
|
||||
"decodal_version",
|
||||
"schema_version",
|
||||
"entrypoints",
|
||||
"import_policy_version",
|
||||
"schema_bundle",
|
||||
"fingerprint",
|
||||
]);
|
||||
return {
|
||||
contract_version: safeInteger(item.contract_version),
|
||||
decodal_version: boundedString(item.decodal_version),
|
||||
schema_version: safeInteger(item.schema_version),
|
||||
entrypoints: boundedArray(item.entrypoints).map((entry) =>
|
||||
boundedString(entry, MAX_PATH_BYTES)
|
||||
),
|
||||
import_policy_version: safeInteger(item.import_policy_version),
|
||||
schema_bundle: parseSchemaBundle(item.schema_bundle),
|
||||
fingerprint: boundedString(item.fingerprint, MAX_DIGEST_BYTES),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseConfigTreeSnapshot(value: unknown): ConfigTreeSnapshot {
|
||||
const item = record(value, ["revision", "digest", "entries"]);
|
||||
const entriesRecord = objectValue(item.entries);
|
||||
const entries = Object.entries(entriesRecord);
|
||||
if (entries.length > MAX_ENTRY_COUNT) throw schemaError();
|
||||
const parsedEntries: Record<string, ConfigEntry> = {};
|
||||
for (const [path, entry] of entries) {
|
||||
const boundedPath = boundedString(path, MAX_PATH_BYTES);
|
||||
const parsed = parseConfigEntry(entry);
|
||||
if (parsed.path !== boundedPath) throw schemaError();
|
||||
parsedEntries[boundedPath] = parsed;
|
||||
}
|
||||
return {
|
||||
revision: safeInteger(item.revision),
|
||||
digest: boundedString(item.digest, MAX_DIGEST_BYTES),
|
||||
entries: parsedEntries,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseWorkspaceConfigTreeResponse(
|
||||
value: unknown,
|
||||
): WorkspaceConfigTreeResponse {
|
||||
const item = record(value, ["snapshot", "contract", "projection_digest"]);
|
||||
return {
|
||||
snapshot: parseConfigTreeSnapshot(item.snapshot),
|
||||
contract: parseToolchainContract(item.contract),
|
||||
projection_digest: boundedString(
|
||||
item.projection_digest,
|
||||
MAX_DIGEST_BYTES,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchConfigTree(
|
||||
@@ -25,6 +272,7 @@ export async function fetchConfigTree(
|
||||
await fetcher(sourceTreeUrl(workspaceId), {
|
||||
headers: { accept: "application/json" },
|
||||
}),
|
||||
parseWorkspaceConfigTreeResponse,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +286,7 @@ export async function fetchConfigEntry(
|
||||
`${sourceTreeUrl(workspaceId)}/entries/${encodeURIComponent(path)}`,
|
||||
{ headers: { accept: "application/json" } },
|
||||
),
|
||||
parseConfigEntry,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +299,7 @@ export async function fetchConfigRevision(
|
||||
await fetcher(`${sourceTreeUrl(workspaceId)}/revisions/${revision}`, {
|
||||
headers: { accept: "application/json" },
|
||||
}),
|
||||
parseConfigTreeSnapshot,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,5 +314,6 @@ export async function commitConfigTree(
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
}),
|
||||
parseWorkspaceConfigTreeResponse,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
// Core and HTTP DTOs are generated from Rust with ts-rs.
|
||||
export type { ConfigContentType } from "./generated/types/ConfigContentType.ts";
|
||||
// Workspace REST DTOs are generated from the server-api Rust authority.
|
||||
export type {
|
||||
ConfigCommitRequest,
|
||||
ConfigContentType,
|
||||
ConfigEntry,
|
||||
ConfigProjectionValidator,
|
||||
ConfigSchemaContribution,
|
||||
ConfigTreeChange,
|
||||
ConfigTreeSnapshot,
|
||||
ToolchainContract,
|
||||
WorkspaceConfigSchemaBundle,
|
||||
WorkspaceConfigTreeResponse,
|
||||
} from "$lib/generated/legacy-server-api.ts";
|
||||
|
||||
// Browser-local Decodal/WASM projections remain owned by config-source.
|
||||
export type { ConfigDiagnostic } from "./generated/types/ConfigDiagnostic.ts";
|
||||
export type { ConfigDiagnosticLabel } from "./generated/types/ConfigDiagnosticLabel.ts";
|
||||
export type { ConfigEntry } from "./generated/types/ConfigEntry.ts";
|
||||
export type { ConfigSpan } from "./generated/types/ConfigSpan.ts";
|
||||
export type { ConfigTreeChange } from "./generated/types/ConfigTreeChange.ts";
|
||||
export type { ConfigSchemaContribution } from "./generated/types/ConfigSchemaContribution.ts";
|
||||
export type { WorkspaceConfigSchemaBundle } from "./generated/types/WorkspaceConfigSchemaBundle.ts";
|
||||
export type { ConfigTreeSnapshot } from "./generated/types/ConfigTreeSnapshot.ts";
|
||||
export type { EvaluatedProjection } from "./generated/types/EvaluatedProjection.ts";
|
||||
export type { EvaluationResult } from "./generated/types/EvaluationResult.ts";
|
||||
export type { ToolchainContract } from "./generated/types/ToolchainContract.ts";
|
||||
export type { VirtualPath } from "./generated/types/VirtualPath.ts";
|
||||
export type { ConfigCommitRequest } from "./generated/types/ConfigCommitRequest.ts";
|
||||
export type { WorkspaceConfigState as WorkspaceConfigTreeResponse } from "./generated/types/WorkspaceConfigState.ts";
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
AttachmentUploadGrantResponse,
|
||||
WorkerFileUploadResponse,
|
||||
} from "$lib/generated/runtime-api.ts";
|
||||
import type { UploadedFileRef } from "$lib/generated/protocol.ts";
|
||||
|
||||
export const MAX_UPLOADED_FILE_BYTES = 10 * 1024 * 1024;
|
||||
@@ -133,15 +137,24 @@ 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(
|
||||
value: unknown,
|
||||
): value is { file: UploadedFileRef } {
|
||||
): value is WorkerFileUploadResponse {
|
||||
if (!value || typeof value !== "object" || !("file" in value)) return false;
|
||||
const file = value.file;
|
||||
return !!file && typeof file === "object" &&
|
||||
|
||||
@@ -397,7 +397,7 @@ Deno.test("workspace Tickets surface provides Kanban and lifecycle controls", as
|
||||
"Tickets, Objectives, and Merge Requests should each be a single primary sidebar link",
|
||||
);
|
||||
assert(
|
||||
ticketsLoad.includes("Object.entries(LANE_STATES)") &&
|
||||
ticketsLoad.includes("loadLane(fetch, workspaceId") &&
|
||||
ticketsLoad.includes('limit: "30"') &&
|
||||
ticketsLoad.includes('states: states.join(",")') &&
|
||||
ticketsLoad.includes("/tickets?${search}") &&
|
||||
@@ -417,9 +417,12 @@ Deno.test("workspace Tickets surface provides Kanban and lifecycle controls", as
|
||||
"Ticket Kanban should combine related states into independent 30-item display windows",
|
||||
);
|
||||
assert(
|
||||
generatedTicketApi.includes("Generated from yoi-workspace-server") &&
|
||||
generatedTicketApi.includes(
|
||||
"@generated by yoi-ticket-browser-openapi-typescript@1",
|
||||
) &&
|
||||
generatedTicketApi.includes("Input contract: 0.1.0 (sha256:") &&
|
||||
generatedTicketApi.includes("export type TicketListResponse") &&
|
||||
generatedTicketApi.includes("items: Array<TicketSummary>"),
|
||||
generatedTicketApi.includes("items: Array<TicketListItemSummary>"),
|
||||
"Ticket response types should come from the generated Server contract",
|
||||
);
|
||||
assert(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
|
||||
import type {
|
||||
Diagnostic,
|
||||
DiagnosticSeverity,
|
||||
@@ -13,10 +14,10 @@ import type {
|
||||
MemoryStagingRecord,
|
||||
} from "$lib/generated/memory-api";
|
||||
|
||||
const MAX_STAGING_ITEMS = 500;
|
||||
const MAX_EVIDENCE_PER_RECORD = 500;
|
||||
const MAX_SOURCE_REFS_PER_RECORD = 500;
|
||||
const MAX_ORIGIN_VALUE_LENGTH = 512;
|
||||
const MAX_STAGING_ITEMS = MEMORY_API_LIMITS.maxCollectionItems;
|
||||
const MAX_EVIDENCE_PER_RECORD = MEMORY_API_LIMITS.maxCollectionItems;
|
||||
const MAX_SOURCE_REFS_PER_RECORD = MEMORY_API_LIMITS.maxCollectionItems;
|
||||
const MAX_ORIGIN_VALUE_LENGTH = MEMORY_API_LIMITS.maxIdentifierBytes;
|
||||
|
||||
const candidateKinds = new Set<MemoryCandidateKind>([
|
||||
"preference",
|
||||
@@ -51,7 +52,11 @@ export function parseMemoryDocumentResponse(
|
||||
"Memory document response",
|
||||
);
|
||||
return {
|
||||
body_md: requiredString(record, "body_md"),
|
||||
body_md: requiredString(
|
||||
record,
|
||||
"body_md",
|
||||
MEMORY_API_LIMITS.maxDocumentBytes,
|
||||
),
|
||||
created_at: requiredString(record, "created_at"),
|
||||
updated_at: requiredString(record, "updated_at"),
|
||||
bytes: requiredNonNegativeInteger(record, "bytes"),
|
||||
@@ -258,7 +263,10 @@ function parseEvidenceOrigin(value: unknown): MemoryEvidenceOrigin {
|
||||
) {
|
||||
if (key in record) {
|
||||
const text = nullableString(record, key);
|
||||
if (text !== null && text.length > MAX_ORIGIN_VALUE_LENGTH) {
|
||||
if (
|
||||
text !== null &&
|
||||
new TextEncoder().encode(text).byteLength > MAX_ORIGIN_VALUE_LENGTH
|
||||
) {
|
||||
invalid(`${key} exceeds the Memory origin limit`);
|
||||
}
|
||||
result[key] = text;
|
||||
@@ -328,11 +336,19 @@ function boundedArray(
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredString(record: Record<string, unknown>, key: string): string {
|
||||
if (typeof record[key] !== "string") {
|
||||
invalid(`${key} must be a string`);
|
||||
function requiredString(
|
||||
record: Record<string, unknown>,
|
||||
key: string,
|
||||
maxBytes: number = MEMORY_API_LIMITS.maxStringBytes,
|
||||
): string {
|
||||
const value = record[key];
|
||||
if (
|
||||
typeof value !== "string" ||
|
||||
new TextEncoder().encode(value).byteLength > maxBytes
|
||||
) {
|
||||
invalid(`${key} must be a bounded string`);
|
||||
}
|
||||
return record[key];
|
||||
return value;
|
||||
}
|
||||
|
||||
function nullableString(
|
||||
@@ -340,8 +356,13 @@ function nullableString(
|
||||
key: string,
|
||||
): string | null {
|
||||
const value = record[key];
|
||||
if (value !== null && typeof value !== "string") {
|
||||
invalid(`${key} must be a string or null`);
|
||||
if (
|
||||
value !== null &&
|
||||
(typeof value !== "string" ||
|
||||
new TextEncoder().encode(value).byteLength >
|
||||
MEMORY_API_LIMITS.maxStringBytes)
|
||||
) {
|
||||
invalid(`${key} must be a bounded string or null`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,17 @@ import type {
|
||||
MergeRequestThreadEvent,
|
||||
} from "./api/merge-requests.ts";
|
||||
|
||||
type ReviewEvent = Extract<MergeRequestThreadEvent, { kind: "review" }>;
|
||||
type SourceEvidenceEvent = Extract<
|
||||
MergeRequestThreadEvent,
|
||||
{ kind: "review" | "review_requested" }
|
||||
>;
|
||||
|
||||
function isCurrentSourceEvent(
|
||||
event: MergeRequestThreadEvent,
|
||||
source: string,
|
||||
): boolean {
|
||||
return event.subject_ref === source;
|
||||
return "subject_ref" in event && event.subject_ref === source;
|
||||
}
|
||||
|
||||
function requestHasTerminalOutcome(
|
||||
@@ -26,16 +32,18 @@ export function sourceReviewFreshness(
|
||||
const source = mergeRequest.source.ref;
|
||||
if (!source) return "Source review unavailable: selector_from is unresolved.";
|
||||
|
||||
const effectiveReview = [...mergeRequest.thread].reverse().find((event) => {
|
||||
if (event.kind !== "review" || !isCurrentSourceEvent(event, source)) {
|
||||
return false;
|
||||
}
|
||||
return !mergeRequest.thread.some(
|
||||
(candidate) =>
|
||||
candidate.kind === "review_revoked" &&
|
||||
candidate.review_event_id === event.event_id,
|
||||
);
|
||||
});
|
||||
const effectiveReview = [...mergeRequest.thread].reverse().find(
|
||||
(event): event is ReviewEvent => {
|
||||
if (event.kind !== "review" || !isCurrentSourceEvent(event, source)) {
|
||||
return false;
|
||||
}
|
||||
return !mergeRequest.thread.some(
|
||||
(candidate) =>
|
||||
candidate.kind === "review_revoked" &&
|
||||
candidate.review_event_id === event.event_id,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (effectiveReview) {
|
||||
return effectiveReview.decision === "approve"
|
||||
? `Current source approved at exact ref ${source}.`
|
||||
@@ -43,9 +51,8 @@ export function sourceReviewFreshness(
|
||||
}
|
||||
|
||||
const latestEvidence = [...mergeRequest.thread].reverse().find(
|
||||
(event) =>
|
||||
(event.kind === "review" || event.kind === "review_requested") &&
|
||||
typeof event.subject_ref === "string",
|
||||
(event): event is SourceEvidenceEvent =>
|
||||
event.kind === "review" || event.kind === "review_requested",
|
||||
);
|
||||
if (latestEvidence?.subject_ref && latestEvidence.subject_ref !== source) {
|
||||
return `Fresh source review required: selector_from moved from ${latestEvidence.subject_ref} to ${source}.`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,12 @@ import type {
|
||||
WorkspaceSigningIdentityResponse,
|
||||
WorkspaceSigningIdentityState,
|
||||
} from "$lib/generated/legacy-server-api";
|
||||
import { readBoundedJson } from "$lib/workspace/api/http.ts";
|
||||
|
||||
const MAX_PROFILE_RESPONSE_BYTES = 4 * 1024 * 1024;
|
||||
const MAX_PROFILE_ERROR_BYTES = 4 * 1024;
|
||||
const MAX_PROFILE_STRING_BYTES = 1024 * 1024;
|
||||
const MAX_PROFILE_COLLECTION_ITEMS = 1000;
|
||||
|
||||
export class ProfileApiError extends Error {
|
||||
constructor(
|
||||
@@ -49,10 +55,7 @@ function exactKeys(
|
||||
}
|
||||
|
||||
function stringValue(value: unknown, context: string): string {
|
||||
if (typeof value !== "string") {
|
||||
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
|
||||
}
|
||||
return value;
|
||||
return boundedStringValue(value, context, MAX_PROFILE_STRING_BYTES);
|
||||
}
|
||||
|
||||
function boundedStringValue(
|
||||
@@ -60,7 +63,10 @@ function boundedStringValue(
|
||||
context: string,
|
||||
maxBytes: number,
|
||||
): string {
|
||||
const text = stringValue(value, context);
|
||||
if (typeof value !== "string") {
|
||||
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
|
||||
}
|
||||
const text = value;
|
||||
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
||||
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
|
||||
}
|
||||
@@ -107,7 +113,7 @@ function arrayValue<T>(
|
||||
parser: (item: unknown) => T,
|
||||
context: string,
|
||||
): T[] {
|
||||
if (!Array.isArray(value)) {
|
||||
if (!Array.isArray(value) || value.length > MAX_PROFILE_COLLECTION_ITEMS) {
|
||||
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
|
||||
}
|
||||
return value.map(parser);
|
||||
@@ -502,17 +508,23 @@ export function parseWorkspaceSigningIdentityResponse(
|
||||
};
|
||||
}
|
||||
|
||||
async function boundedErrorText(response: Response): Promise<string> {
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
if (bytes.byteLength > MAX_PROFILE_ERROR_BYTES) return "";
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
async function parseResponse<T>(
|
||||
response: Response,
|
||||
parser: (value: unknown) => T,
|
||||
): Promise<T> {
|
||||
if (!response.ok) {
|
||||
throw new ProfileApiError(
|
||||
(await response.text()) || response.statusText,
|
||||
(await boundedErrorText(response)) || response.statusText,
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
return parser(await response.json() as unknown);
|
||||
return parser(await readBoundedJson(response, MAX_PROFILE_RESPONSE_BYTES));
|
||||
}
|
||||
|
||||
export async function fetchWorkspaceMetadata(
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import type {
|
||||
CleanupTargetKind,
|
||||
CleanupWorkdirCandidate,
|
||||
CleanupWorkerCandidate,
|
||||
RuntimeCleanupExecutionResponse,
|
||||
RuntimeCleanupPlanResponse,
|
||||
} from "$lib/generated/runtime-api";
|
||||
import type {
|
||||
BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse,
|
||||
BrowserWorkerWorkingDirectorySelection
|
||||
as SharedBrowserWorkerWorkingDirectorySelection,
|
||||
Diagnostic as SharedDiagnostic,
|
||||
WorkerCapabilitySummary as SharedWorkerCapabilitySummary,
|
||||
WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse,
|
||||
WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate,
|
||||
WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption,
|
||||
WorkerLaunchWorkerSummary as WorkerSummary,
|
||||
WorkerLaunchWorkerSummary,
|
||||
WorkerSummary as SharedWorkerSummary,
|
||||
WorkerWorkdirAttachmentSummary as SharedWorkerWorkdirAttachmentSummary,
|
||||
WorkingDirectoryRepositoryOption as SharedWorkingDirectoryRepositoryOption,
|
||||
} from "$lib/generated/worker-launch-api";
|
||||
import type {
|
||||
@@ -46,11 +57,7 @@ export type {
|
||||
};
|
||||
export type WorkspaceResponse = SharedWorkspaceResponse;
|
||||
|
||||
export type Diagnostic = {
|
||||
code: string;
|
||||
severity: string;
|
||||
message: string;
|
||||
};
|
||||
export type Diagnostic = SharedDiagnostic;
|
||||
|
||||
export type Runtime = {
|
||||
runtime_id: string;
|
||||
@@ -71,50 +78,21 @@ 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 = SharedWorkerCapabilitySummary;
|
||||
|
||||
export type WorkerCapabilities = {
|
||||
can_stop: boolean;
|
||||
can_spawn_followup: boolean;
|
||||
};
|
||||
export type WorkerWorkdirAttachment = SharedWorkerWorkdirAttachmentSummary;
|
||||
|
||||
export type WorkerWorkdirAttachment = {
|
||||
alias: string;
|
||||
working_directory: WorkingDirectorySummary;
|
||||
};
|
||||
|
||||
export type Worker = {
|
||||
runtime_id: string;
|
||||
worker_id: string;
|
||||
resource_key: string;
|
||||
host_id: string;
|
||||
display_name: string;
|
||||
label: string;
|
||||
profile?: string | null;
|
||||
singleton_key?: string | null;
|
||||
tags: string[];
|
||||
workspace: { visibility: string; identity: string };
|
||||
state: string;
|
||||
worker_state?: WorkerStateSnapshot | null;
|
||||
pinned?: boolean;
|
||||
retention_state?: string;
|
||||
last_seen_at?: string | null;
|
||||
implementation: { kind: string; display_hint: string };
|
||||
capabilities: WorkerCapabilities;
|
||||
workdir_attachments?: WorkerWorkdirAttachment[];
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
export type Worker =
|
||||
& Omit<
|
||||
SharedWorkerSummary,
|
||||
"display_name" | "tags" | "worker_state" | "diagnostics"
|
||||
>
|
||||
& {
|
||||
display_name: string;
|
||||
tags: string[];
|
||||
worker_state?: WorkerStateSnapshot | null;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type WorkerOperationState = "accepted" | "unsupported" | "rejected";
|
||||
|
||||
@@ -127,7 +105,7 @@ export type WorkerRestoreState =
|
||||
|
||||
export type WorkerRestoreResult = {
|
||||
state: WorkerRestoreState;
|
||||
worker?: WorkerSummary | null;
|
||||
worker?: WorkerLaunchWorkerSummary | null;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
@@ -136,67 +114,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 =
|
||||
@@ -228,61 +151,22 @@ export type RepositoryDetailResponse = SharedRepositoryDetailResponse;
|
||||
export type RepositoryLogResponse = SharedRepositoryLogResponse;
|
||||
|
||||
export type {
|
||||
DerivedTicketRelation,
|
||||
ObjectiveDetail,
|
||||
ObjectiveLinkedTicketSummary,
|
||||
ObjectiveListResponse,
|
||||
ObjectiveSummary,
|
||||
TicketDetail,
|
||||
TicketEventDetail,
|
||||
TicketListResponse,
|
||||
TicketRelation,
|
||||
TicketRelationBlocker,
|
||||
TicketRelationNotice,
|
||||
TicketRelationView,
|
||||
TicketSummary,
|
||||
} from "$lib/generated/ticket-api";
|
||||
|
||||
export type ObjectiveSummary = {
|
||||
id: string;
|
||||
resource_key: string;
|
||||
title: string;
|
||||
state: string;
|
||||
updated_at?: string | null;
|
||||
summary: string;
|
||||
linked_tickets?: string[];
|
||||
record_source?: string;
|
||||
};
|
||||
|
||||
export type ObjectiveLinkedTicketSummary = {
|
||||
id: string;
|
||||
resource_key: string;
|
||||
title: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
export type ObjectiveDetail = {
|
||||
id: string;
|
||||
resource_key: string;
|
||||
title: string;
|
||||
state: string;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
linked_tickets: string[];
|
||||
linked_ticket_summaries: ObjectiveLinkedTicketSummary[];
|
||||
body: string;
|
||||
body_truncated: boolean;
|
||||
record_source: string;
|
||||
};
|
||||
|
||||
export type InvalidProjectRecord = {
|
||||
label: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type ObjectiveListResponse = {
|
||||
workspace_id: string;
|
||||
limit: number;
|
||||
items: ObjectiveSummary[];
|
||||
invalid_records: InvalidProjectRecord[];
|
||||
record_authority: string;
|
||||
};
|
||||
export type {
|
||||
TicketDetailDerivedRelation as DerivedTicketRelation,
|
||||
TicketDetailRelation as TicketRelation,
|
||||
TicketDetailRelationBlocker as TicketRelationBlocker,
|
||||
TicketDetailRelationNotice as TicketRelationNotice,
|
||||
TicketDetailRelationView as TicketRelationView,
|
||||
TicketListItemSummary as TicketSummary,
|
||||
} from "$lib/generated/ticket-api";
|
||||
|
||||
export type {
|
||||
CompanionCancelRequest,
|
||||
|
||||
@@ -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,
|
||||
@@ -26,11 +24,18 @@ function workerPath(workspaceId: string, worker: WorkerActionTarget): string {
|
||||
async function responseError(response: Response): Promise<string> {
|
||||
const fallback = `${response.status} ${response.statusText}`.trim();
|
||||
try {
|
||||
const payload = await response.json() as {
|
||||
message?: string;
|
||||
error?: { message?: string };
|
||||
};
|
||||
return payload.error?.message ?? payload.message ?? fallback;
|
||||
const payload: unknown = await response.json();
|
||||
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
||||
return fallback;
|
||||
}
|
||||
const record = payload as Record<string, unknown>;
|
||||
if (typeof record.message === "string") return record.message.slice(0, 512);
|
||||
const nested = record.error;
|
||||
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
|
||||
const message = (nested as Record<string, unknown>).message;
|
||||
if (typeof message === "string") return message.slice(0, 512);
|
||||
}
|
||||
return fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
@@ -62,7 +67,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 +86,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 +112,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
|
||||
);
|
||||
|
||||
@@ -342,6 +342,27 @@
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.ticket-target-list, .ticket-target-row {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.ticket-target-row {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
padding: var(--space-3) 0 0;
|
||||
}
|
||||
.ticket-target-row:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
.ticket-target-row legend {
|
||||
color: var(--text-strong);
|
||||
font-size: var(--font-size-compact);
|
||||
font-weight: 700;
|
||||
padding: 0;
|
||||
}
|
||||
.ticket-editor label, .ticket-control-form label {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
@@ -349,6 +370,7 @@
|
||||
font-size: var(--font-size-body);
|
||||
}
|
||||
.ticket-editor input,
|
||||
.ticket-editor select,
|
||||
.ticket-editor textarea,
|
||||
.ticket-control-form input,
|
||||
.ticket-control-form select,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "./ticket-panel.ts";
|
||||
import type {
|
||||
TicketDetail,
|
||||
TicketSummary,
|
||||
TicketListItemSummary as TicketSummary,
|
||||
} from "../../generated/ticket-api.ts";
|
||||
|
||||
declare const Deno: {
|
||||
@@ -88,8 +88,18 @@ Deno.test("ticket worker launch uses the common Worker route and bounded Ticket
|
||||
const ticket = {
|
||||
id: "00001KYRRDVH9",
|
||||
title: "Ticket panel API",
|
||||
repository_key: "main repo",
|
||||
ref_selector: "work/ticket",
|
||||
targets: [
|
||||
{
|
||||
repository_key: "docs",
|
||||
ref_selector: "main",
|
||||
access: "read_only",
|
||||
},
|
||||
{
|
||||
repository_key: "main repo",
|
||||
ref_selector: "work/ticket",
|
||||
access: "read_write",
|
||||
},
|
||||
],
|
||||
} as TicketDetail;
|
||||
|
||||
assertEquals(
|
||||
@@ -110,6 +120,38 @@ Deno.test("ticket worker launch uses the common Worker route and bounded Ticket
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("ticket worker launch never selects a read_only or ambiguous write target", () => {
|
||||
const readOnly = {
|
||||
id: "ticket-read-only",
|
||||
title: "Read only",
|
||||
targets: [{
|
||||
repository_key: "docs",
|
||||
ref_selector: "main",
|
||||
access: "read_only",
|
||||
}],
|
||||
} as TicketDetail;
|
||||
const readOnlyUrl = new URL(
|
||||
ticketWorkerLaunchHref("workspace", readOnly, "coder"),
|
||||
"https://example.test",
|
||||
);
|
||||
assertEquals(readOnlyUrl.searchParams.has("repositoryKey"), false);
|
||||
assertEquals(readOnlyUrl.searchParams.has("refSelector"), false);
|
||||
|
||||
const ambiguous = {
|
||||
...readOnly,
|
||||
targets: [
|
||||
{ repository_key: "main", ref_selector: "develop", access: "read_write" },
|
||||
{ repository_key: "docs", ref_selector: "main", access: "read_write" },
|
||||
],
|
||||
} as TicketDetail;
|
||||
const ambiguousUrl = new URL(
|
||||
ticketWorkerLaunchHref("workspace", ambiguous, "coder"),
|
||||
"https://example.test",
|
||||
);
|
||||
assertEquals(ambiguousUrl.searchParams.has("repositoryKey"), false);
|
||||
assertEquals(ambiguousUrl.searchParams.has("refSelector"), false);
|
||||
});
|
||||
|
||||
Deno.test("ticket detail uses server-derived role assignment actions", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL(
|
||||
@@ -120,7 +162,10 @@ Deno.test("ticket detail uses server-derived role assignment actions", async ()
|
||||
|
||||
assertEquals(source.includes("ticket.action_eligibility.can_queue"), true);
|
||||
assertEquals(source.includes("ticket.relations.blockers.length > 0"), true);
|
||||
assertEquals(source.includes("ticket.action_eligibility.queue_tickets"), true);
|
||||
assertEquals(
|
||||
source.includes("ticket.action_eligibility.queue_tickets"),
|
||||
true,
|
||||
);
|
||||
assertEquals(source.includes("This operation queues:"), true);
|
||||
assertEquals(source.includes("outcome.queued_tickets.join"), true);
|
||||
assertEquals(
|
||||
@@ -135,6 +180,26 @@ Deno.test("ticket detail uses server-derived role assignment actions", async ()
|
||||
assertEquals(source.includes('kind: "workspace_agent"'), true);
|
||||
assertEquals(source.includes('kind: "worker"'), true);
|
||||
assertEquals(source.includes("ticket.assignee"), false);
|
||||
assertEquals(source.includes('action: "set", targets'), true);
|
||||
assertEquals(source.includes("updatedTicket.targets"), true);
|
||||
assertEquals(source.includes("ticket.repository_key"), false);
|
||||
assertEquals(source.includes("ticket.ref_selector"), false);
|
||||
});
|
||||
|
||||
Deno.test("Ticket authoring creates the target collection without singular authority", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../../../routes/w/[workspaceId]/tickets/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
assertEquals(source.includes("const request: NewTicket"), true);
|
||||
assertEquals(source.includes("targets: normalizedCreateTargets()"), true);
|
||||
assertEquals(source.includes('value="read_write"'), true);
|
||||
assertEquals(source.includes('value="read_only"'), true);
|
||||
assertEquals(source.includes("repository_key: create"), false);
|
||||
assertEquals(source.includes("ref_selector: create"), false);
|
||||
});
|
||||
|
||||
Deno.test("ticket detail keeps the operation rail outside main content", async () => {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { BrowserWorkspaceOrchestratorResponse } from "$lib/generated/worker-launch-api";
|
||||
import type { TicketDetail, TicketSummary } from "$lib/generated/ticket-api";
|
||||
import type {
|
||||
TicketDetail,
|
||||
TicketListItemSummary,
|
||||
} from "$lib/generated/ticket-api";
|
||||
|
||||
export const TICKET_STATES = [
|
||||
"planning",
|
||||
@@ -38,7 +41,7 @@ export const TICKET_LANE_PAGE_SIZE = 30;
|
||||
export type TicketLaneDefinition = (typeof LANE_DEFINITIONS)[number];
|
||||
export type TicketLaneId = TicketLaneDefinition["id"];
|
||||
export type TicketCardSummary = Pick<
|
||||
TicketSummary,
|
||||
TicketListItemSummary,
|
||||
"id" | "resource_key" | "title" | "state" | "priority" | "updated_at"
|
||||
>;
|
||||
|
||||
@@ -104,23 +107,26 @@ export function ticketWorkerMessage(
|
||||
|
||||
export function ticketWorkerLaunchHref(
|
||||
workspaceId: string,
|
||||
ticket: Pick<
|
||||
TicketDetail,
|
||||
"id" | "title" | "repository_key" | "ref_selector"
|
||||
>,
|
||||
ticket: Pick<TicketDetail, "id" | "title" | "targets">,
|
||||
role: TicketWorkerRole,
|
||||
): string {
|
||||
const implementationTargets = ticket.targets.filter((target) =>
|
||||
target.access === "read_write"
|
||||
);
|
||||
const implementationTarget = implementationTargets.length === 1
|
||||
? implementationTargets[0]
|
||||
: null;
|
||||
const params = new URLSearchParams({
|
||||
ticketId: ticket.id,
|
||||
ticketTitle: ticket.title,
|
||||
ticketRole: role,
|
||||
initialInput: ticketWorkerMessage(ticket.id, role),
|
||||
});
|
||||
if (ticket.repository_key) {
|
||||
params.set("repositoryKey", ticket.repository_key);
|
||||
}
|
||||
if (ticket.ref_selector) {
|
||||
params.set("refSelector", ticket.ref_selector);
|
||||
if (implementationTarget) {
|
||||
params.set("repositoryKey", implementationTarget.repository_key);
|
||||
if (implementationTarget.ref_selector) {
|
||||
params.set("refSelector", implementationTarget.ref_selector);
|
||||
}
|
||||
}
|
||||
return `/w/${
|
||||
encodeURIComponent(workspaceId)
|
||||
|
||||
@@ -7,21 +7,18 @@ import type { LayoutLoad } from "./$types";
|
||||
export const load: LayoutLoad = async ({ fetch, params }) => {
|
||||
const workspaceId = params.workspaceId;
|
||||
const [workspaceResult, repositoryResult] = await Promise.all([
|
||||
loadJson<unknown>(fetch, workspaceApiPath(workspaceId, "/workspace")),
|
||||
loadJson(
|
||||
fetch,
|
||||
workspaceApiPath(workspaceId, "/workspace"),
|
||||
undefined,
|
||||
parseWorkspaceResponse,
|
||||
{ diagnosticLabel: "Workspace API", maxResponseBytes: 1024 * 1024 },
|
||||
),
|
||||
loadWorkspaceRepositoryList(fetch, workspaceId),
|
||||
]);
|
||||
|
||||
let workspace = null;
|
||||
let workspaceError = workspaceResult.error;
|
||||
if (workspaceResult.data !== null) {
|
||||
try {
|
||||
workspace = parseWorkspaceResponse(workspaceResult.data);
|
||||
} catch (cause) {
|
||||
workspaceError = cause instanceof Error
|
||||
? cause.message
|
||||
: "invalid workspace response";
|
||||
}
|
||||
}
|
||||
const workspace = workspaceResult.data;
|
||||
const workspaceError = workspaceResult.error;
|
||||
if (!workspace) {
|
||||
error(404, {
|
||||
message: workspaceError ?? `Workspace ${workspaceId} is unavailable`,
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { Host, ListResponse } from "$lib/workspace/sidebar/types";
|
||||
import {
|
||||
parseHostListResponse,
|
||||
type HostListResponse,
|
||||
} from "$lib/workspace/api/workspace-model";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
const HOST_API_LOAD_POLICY = {
|
||||
diagnosticLabel: "Host API",
|
||||
maxResponseBytes: 4 * 1024 * 1024,
|
||||
} as const;
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
||||
const hosts = await loadJson<ListResponse<Host>>(fetch, apiPath("/hosts"));
|
||||
const hosts = await loadJson<HostListResponse>(
|
||||
fetch,
|
||||
apiPath("/hosts"),
|
||||
undefined,
|
||||
parseHostListResponse,
|
||||
HOST_API_LOAD_POLICY,
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseMemoryDocumentResponse } from "$lib/workspace/memory/api";
|
||||
import type { PageLoad } from "./$types";
|
||||
@@ -10,6 +11,10 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
||||
workspaceApiPath(params.workspaceId, "/memory"),
|
||||
undefined,
|
||||
parseMemoryDocumentResponse,
|
||||
{
|
||||
diagnosticLabel: "Memory API",
|
||||
maxResponseBytes: MEMORY_API_LIMITS.maxResponseBytes,
|
||||
},
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseMemoryStagingListResponse } from "$lib/workspace/memory/api";
|
||||
import type { PageLoad } from "./$types";
|
||||
@@ -10,6 +11,10 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
||||
`${workspaceApiPath(params.workspaceId, "/memory/staging")}?limit=200`,
|
||||
undefined,
|
||||
parseMemoryStagingListResponse,
|
||||
{
|
||||
diagnosticLabel: "Memory API",
|
||||
maxResponseBytes: MEMORY_API_LIMITS.maxResponseBytes,
|
||||
},
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import { loadJson } from "$lib/workspace/api/http";
|
||||
import { mergeRequestCollectionPath } from "$lib/workspace/api/merge-requests";
|
||||
import {
|
||||
mergeRequestCollectionPath,
|
||||
type MergeRequestListPage,
|
||||
} from "$lib/workspace/api/merge-requests";
|
||||
parseMergeRequestListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${mergeRequestCollectionPath(params.workspaceId)}?limit=100`,
|
||||
);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequests: null,
|
||||
error: body?.error ?? body?.message ??
|
||||
`Request failed (${response.status})`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequests: body as MergeRequestListPage,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequests: null,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
`${mergeRequestCollectionPath(params.workspaceId)}?limit=100`,
|
||||
undefined,
|
||||
parseMergeRequestListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequests: result.data,
|
||||
error: result.error,
|
||||
};
|
||||
};
|
||||
|
||||
+11
-3
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { MergeRequestThreadEvent } from "$lib/generated/ticket-api";
|
||||
import { mergeRequestPagePath } from "$lib/workspace/api/merge-requests";
|
||||
import {
|
||||
sourceReviewFreshness,
|
||||
@@ -13,8 +14,15 @@
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||
}
|
||||
|
||||
function textField(event: Record<string, unknown>, key: string): string | null {
|
||||
const value = event[key];
|
||||
function textField(
|
||||
event: MergeRequestThreadEvent,
|
||||
key: "subject_ref" | "decision" | "reason" | "body",
|
||||
): string | null {
|
||||
let value: unknown;
|
||||
if (key === "subject_ref" && "subject_ref" in event) value = event.subject_ref;
|
||||
if (key === "decision" && "decision" in event) value = event.decision;
|
||||
if (key === "reason" && "reason" in event) value = event.reason;
|
||||
if (key === "body" && "body" in event) value = event.body;
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
@@ -77,7 +85,7 @@
|
||||
<div>
|
||||
<header>
|
||||
<strong>{event.kind}</strong>
|
||||
<time>{prettyDate(event.at)}</time>
|
||||
<time>{prettyDate(event.created_at)}</time>
|
||||
</header>
|
||||
{#if textField(event, "subject_ref")}
|
||||
<p><code>{textField(event, "subject_ref")}</code></p>
|
||||
|
||||
@@ -1,38 +1,25 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import { loadJson } from "$lib/workspace/api/http";
|
||||
import { mergeRequestDetailPath } from "$lib/workspace/api/merge-requests";
|
||||
import {
|
||||
type MergeRequestDetail,
|
||||
mergeRequestDetailPath,
|
||||
} from "$lib/workspace/api/merge-requests";
|
||||
parseMergeRequestDetailResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${
|
||||
mergeRequestDetailPath(params.workspaceId, params.mergeRequestId)
|
||||
}?limit=100`,
|
||||
);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequestId: params.mergeRequestId,
|
||||
mergeRequest: null,
|
||||
error: body?.error ?? body?.message ??
|
||||
`Request failed (${response.status})`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequestId: params.mergeRequestId,
|
||||
mergeRequest: body as MergeRequestDetail,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequestId: params.mergeRequestId,
|
||||
mergeRequest: null,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
`${
|
||||
mergeRequestDetailPath(params.workspaceId, params.mergeRequestId)
|
||||
}?limit=100`,
|
||||
undefined,
|
||||
parseMergeRequestDetailResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
mergeRequestId: params.mergeRequestId,
|
||||
mergeRequest: result.data,
|
||||
error: result.error,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { ObjectiveListResponse } from "$lib/workspace/sidebar/types";
|
||||
import {
|
||||
parseObjectiveListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
||||
const objectives = await loadJson<ObjectiveListResponse>(
|
||||
const objectives = await loadJson(
|
||||
fetch,
|
||||
apiPath("/objectives"),
|
||||
undefined,
|
||||
parseObjectiveListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import {
|
||||
parseObjectiveDetail,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { ObjectiveDetail } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
||||
const objectiveId = resourceKey(params.objectiveId);
|
||||
const objective = await loadJson<ObjectiveDetail>(
|
||||
const objective = await loadJson(
|
||||
fetch,
|
||||
apiPath(`/objectives/${encodeURIComponent(objectiveId)}`),
|
||||
undefined,
|
||||
parseObjectiveDetail,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
|
||||
if (objective.data) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
ConfirmRepositorySshHostTrustRequest,
|
||||
RepositorySshConnectionProbeRequest,
|
||||
RepositorySshConnectionProbeResponse
|
||||
} from '$lib/generated/legacy-server-api';
|
||||
} from '$lib/generated/repository-api';
|
||||
import { parseRepositorySshHostTrust } from '$lib/workspace/api/repository-access';
|
||||
import { formatDate, workspaceApiPath } from '$lib/workspace/api/http';
|
||||
import { parseRepositorySshConnectionProbeResponse } from '$lib/workspace/api/workspace-model';
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
RotateRepositorySshCredentialRequest,
|
||||
} from '$lib/generated/repository-access-api';
|
||||
import {
|
||||
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
|
||||
parseRepositorySshCredential,
|
||||
parseRepositorySshHostTrust,
|
||||
parseRepositorySshPublicKey,
|
||||
} from '$lib/workspace/api/repository-access';
|
||||
import { readBoundedJson } from '$lib/workspace/api/http';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
@@ -71,13 +73,21 @@
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel();
|
||||
throw new Error(`Repository Access request failed with status ${response.status}.`);
|
||||
}
|
||||
if (response.status === 204) return undefined;
|
||||
const payload: unknown = await response.json();
|
||||
if (parse === null) {
|
||||
throw new Error('Repository Access returned an unexpected response body.');
|
||||
if (response.status !== 204) {
|
||||
await response.body?.cancel();
|
||||
throw new Error('Repository Access returned an unexpected response body.');
|
||||
}
|
||||
await response.body?.cancel();
|
||||
return undefined;
|
||||
}
|
||||
if (response.status === 204) {
|
||||
throw new Error('Repository Access returned an unexpected empty response.');
|
||||
}
|
||||
const payload = await readBoundedJson(response, REPOSITORY_ACCESS_MAX_RESPONSE_BYTES);
|
||||
return parse(payload);
|
||||
}
|
||||
|
||||
@@ -86,10 +96,11 @@
|
||||
`${base}/credentials/${encodeURIComponent(credentialId)}/public-key`,
|
||||
{ headers: { accept: 'application/json' } }
|
||||
);
|
||||
const body = await response.json();
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel();
|
||||
throw new Error(`Repository Access request failed with status ${response.status}.`);
|
||||
}
|
||||
const body = await readBoundedJson(response, REPOSITORY_ACCESS_MAX_RESPONSE_BYTES);
|
||||
return parseRepositorySshPublicKey(body);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
+5
-8
@@ -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');
|
||||
}
|
||||
|
||||
+8
-7
@@ -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,12 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { untrack } from "svelte";
|
||||
import type { ApiResult } from "$lib/workspace/api/http";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import {
|
||||
loadJson,
|
||||
workspaceApiJsonWithBody,
|
||||
workspaceApiPath,
|
||||
} from "$lib/workspace/api/http";
|
||||
import { parseBrowserWorkspaceOrchestratorResponse } from "$lib/workspace/api/workers";
|
||||
import {
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
parseTicketListResponse,
|
||||
parseTicketRecordRef,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import type {
|
||||
NewTicket,
|
||||
QueryPage,
|
||||
TicketListResponse,
|
||||
TicketSummary,
|
||||
TicketListItemSummary as TicketSummary,
|
||||
TicketTarget,
|
||||
TicketTargetAccess,
|
||||
} from "$lib/generated/ticket-api";
|
||||
import { ticketHref } from "$lib/workspace/resource-links";
|
||||
import {
|
||||
@@ -23,6 +36,12 @@
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type EditableTicketTarget = {
|
||||
repository_key: string;
|
||||
ref_selector: string;
|
||||
access: TicketTargetAccess;
|
||||
};
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
// svelte-ignore state_referenced_locally
|
||||
let laneState = $state<Record<string, LaneState>>(
|
||||
@@ -43,6 +62,25 @@
|
||||
untrack(() => data.orchestrator),
|
||||
);
|
||||
let orchestratorStarting = $state(false);
|
||||
let creatingTicket = $state(false);
|
||||
let createBusy = $state(false);
|
||||
let createError = $state<string | null>(null);
|
||||
let createTitle = $state("");
|
||||
let createBody = $state("");
|
||||
let createTargets = $state<EditableTicketTarget[]>([]);
|
||||
const repositories = $derived(data.repositories.data?.items ?? []);
|
||||
const createTargetsSavable = $derived.by(() => {
|
||||
const keys = new Set<string>();
|
||||
for (const target of createTargets) {
|
||||
if (
|
||||
!repositories.some((repository) =>
|
||||
repository.repository_key === target.repository_key
|
||||
) || keys.has(target.repository_key)
|
||||
) return false;
|
||||
keys.add(target.repository_key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const tickets = $derived(
|
||||
Object.values(laneState).flatMap((lane) => lane.tickets),
|
||||
);
|
||||
@@ -70,15 +108,18 @@
|
||||
states: lane.states.join(","),
|
||||
cursor: lane.page.next_cursor,
|
||||
});
|
||||
const response = await fetch(
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
`/api/w/${encodeURIComponent(data.workspaceId)}/tickets?${search}`,
|
||||
undefined,
|
||||
parseTicketListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`追加読み込みに失敗しました (${response.status})`);
|
||||
if (!result.data) {
|
||||
throw new Error(result.error ?? "追加読み込みに失敗しました");
|
||||
}
|
||||
const page = (await response.json()) as TicketListResponse;
|
||||
lane.tickets = mergeTickets(lane.tickets, page.items);
|
||||
lane.page = page.page;
|
||||
lane.tickets = mergeTickets(lane.tickets, result.data.items);
|
||||
lane.page = result.data.page;
|
||||
} catch (error) {
|
||||
lane.error = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
@@ -93,6 +134,61 @@
|
||||
if (remaining <= 96) void loadMore(laneId);
|
||||
}
|
||||
|
||||
function addCreateTarget(): void {
|
||||
createTargets.push({
|
||||
repository_key: "",
|
||||
ref_selector: "",
|
||||
access: createTargets.some((target) => target.access === "read_write")
|
||||
? "read_only"
|
||||
: "read_write",
|
||||
});
|
||||
}
|
||||
|
||||
function removeCreateTarget(index: number): void {
|
||||
createTargets.splice(index, 1);
|
||||
}
|
||||
|
||||
function normalizedCreateTargets(): TicketTarget[] {
|
||||
return createTargets.map((target) => ({
|
||||
repository_key: target.repository_key.trim(),
|
||||
ref_selector: target.ref_selector.trim() || null,
|
||||
access: target.access,
|
||||
}));
|
||||
}
|
||||
|
||||
async function createTicket(event: SubmitEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
if (createBusy || !createTitle.trim() || !createTargetsSavable) return;
|
||||
createBusy = true;
|
||||
createError = null;
|
||||
const request: NewTicket = {
|
||||
title: createTitle.trim(),
|
||||
kind: "task",
|
||||
priority: "P2",
|
||||
labels: [],
|
||||
body: createBody,
|
||||
risk_flags: [],
|
||||
workflow_state: "planning",
|
||||
targets: normalizedCreateTargets(),
|
||||
};
|
||||
try {
|
||||
const created = await workspaceApiJsonWithBody(
|
||||
workspaceApiPath(data.workspaceId, "/tickets"),
|
||||
{ method: "POST", body: JSON.stringify(request) },
|
||||
parseTicketRecordRef,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
const reference = created.resource_key ?? created.id;
|
||||
await goto(
|
||||
`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(reference)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
createError = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
createBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startOrchestrator() {
|
||||
if (orchestratorStarting || orchestrator.data?.online) return;
|
||||
orchestratorStarting = true;
|
||||
@@ -126,6 +222,11 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="ticket-panel-controls">
|
||||
<button
|
||||
class="workspace-primary-button"
|
||||
type="button"
|
||||
onclick={() => creatingTicket = !creatingTicket}
|
||||
>{creatingTicket ? "Cancel" : "Add Ticket"}</button>
|
||||
<div class="orchestrator-status" data-online={orchestrator.data?.online ?? false}>
|
||||
<span class="orchestrator-status-dot"></span>
|
||||
<div>
|
||||
@@ -150,6 +251,50 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if creatingTicket}
|
||||
<form class="ticket-editor ticket-create-form" onsubmit={createTicket}>
|
||||
<h2>New Ticket</h2>
|
||||
{#if createError}<p class="workspace-callout is-error" role="alert">{createError}</p>{/if}
|
||||
{#if data.repositories.error}
|
||||
<p class="workspace-callout is-error" role="alert">Repositories: {data.repositories.error}</p>
|
||||
{/if}
|
||||
<label>Title<input bind:value={createTitle} required /></label>
|
||||
<label>Body<textarea bind:value={createBody} rows="8"></textarea></label>
|
||||
<div class="ticket-target-list">
|
||||
<strong>Repository targets</strong>
|
||||
{#each createTargets as target, index}
|
||||
{@const selectedRepository = repositories.find((repository) => repository.repository_key === target.repository_key)}
|
||||
<fieldset class="ticket-target-row">
|
||||
<legend>Target {index + 1}</legend>
|
||||
<label>Repository
|
||||
<select bind:value={target.repository_key} required>
|
||||
<option value="">Choose repository</option>
|
||||
{#each repositories as repository}
|
||||
<option value={repository.repository_key}>{repository.repository_key}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>Ref selector<input bind:value={target.ref_selector} placeholder={selectedRepository?.default_selector ?? "branch, tag, or revision"} /></label>
|
||||
<label>Access
|
||||
<select bind:value={target.access}>
|
||||
<option value="read_write">Read and write</option>
|
||||
<option value="read_only">Read only</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="workspace-secondary-button" type="button" onclick={() => removeCreateTarget(index)}>Remove target</button>
|
||||
</fieldset>
|
||||
{/each}
|
||||
<button class="workspace-secondary-button" type="button" onclick={addCreateTarget}>Add target</button>
|
||||
</div>
|
||||
{#if !createTargetsSavable}
|
||||
<p class="workspace-empty-copy">Every target row must select a different repository. The exact one read-write target and selector requirements are enforced when the Ticket is marked ready.</p>
|
||||
{/if}
|
||||
<button class="workspace-primary-button" type="submit" disabled={createBusy || !createTitle.trim() || !createTargetsSavable}>
|
||||
{createBusy ? "Creating…" : "Create Ticket"}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if orchestrator.error}
|
||||
<p class="workspace-callout is-error">
|
||||
Orchestrator status: {orchestrator.error}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { parseBrowserWorkspaceOrchestratorResponse } from "$lib/workspace/api/workers";
|
||||
import { loadWorkspaceRepositoryList } from "$lib/workspace/api/repositories";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import {
|
||||
parseTicketListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import type { TicketListResponse } from "$lib/generated/ticket-api";
|
||||
import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel";
|
||||
import type { PageLoad } from "./$types";
|
||||
@@ -17,43 +22,58 @@ export type TicketLanePage = {
|
||||
response: TicketListResponse;
|
||||
};
|
||||
|
||||
async function loadLane(
|
||||
fetchFn: typeof fetch,
|
||||
workspaceId: string,
|
||||
laneId: TicketLaneId,
|
||||
): Promise<TicketLanePage> {
|
||||
const states = LANE_STATES[laneId];
|
||||
const search = new URLSearchParams({
|
||||
limit: "30",
|
||||
states: states.join(","),
|
||||
});
|
||||
const result = await loadJson(
|
||||
fetchFn,
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/tickets?${search}`,
|
||||
undefined,
|
||||
parseTicketListResponse,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
);
|
||||
if (!result.data) {
|
||||
throw new Error(result.error ?? `failed to load ${laneId} Ticket lane`);
|
||||
}
|
||||
return { states: [...states], response: result.data };
|
||||
}
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const workspaceId = params.workspaceId;
|
||||
const [entries, orchestrator] = await Promise.all([
|
||||
Promise.all(
|
||||
Object.entries(LANE_STATES).map(async ([laneId, states]) => {
|
||||
const search = new URLSearchParams({
|
||||
limit: "30",
|
||||
states: states.join(","),
|
||||
});
|
||||
const response = await fetch(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/tickets?${search}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`failed to load ${laneId} Ticket lane (${response.status})`,
|
||||
);
|
||||
}
|
||||
return [
|
||||
laneId,
|
||||
{ states: [...states], response: await response.json() },
|
||||
] as const;
|
||||
}),
|
||||
),
|
||||
const [
|
||||
readyPlanning,
|
||||
inprogressQueued,
|
||||
doneClosed,
|
||||
orchestrator,
|
||||
repositories,
|
||||
] = await Promise.all([
|
||||
loadLane(fetch, workspaceId, "ready-planning"),
|
||||
loadLane(fetch, workspaceId, "inprogress-queued"),
|
||||
loadLane(fetch, workspaceId, "done-closed"),
|
||||
loadJson<WorkspaceOrchestratorStatus>(
|
||||
fetch,
|
||||
workspaceApiPath(workspaceId, "/orchestrator"),
|
||||
undefined,
|
||||
parseBrowserWorkspaceOrchestratorResponse,
|
||||
),
|
||||
loadWorkspaceRepositoryList(fetch, workspaceId),
|
||||
]);
|
||||
|
||||
return {
|
||||
workspaceId,
|
||||
ticketLanes: Object.fromEntries(entries) as unknown as Record<
|
||||
TicketLaneId,
|
||||
TicketLanePage
|
||||
>,
|
||||
ticketLanes: {
|
||||
"ready-planning": readyPlanning,
|
||||
"inprogress-queued": inprogressQueued,
|
||||
"done-closed": doneClosed,
|
||||
},
|
||||
orchestrator,
|
||||
repositories,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
workspaceApiJsonWithBody,
|
||||
workspaceApiPath,
|
||||
} from "$lib/workspace/api/http";
|
||||
import {
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
parseTicketDetail,
|
||||
parseTicketQueueOutcome,
|
||||
parseTicketRoleAssignmentMutationResponse,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import { mergeRequestPagePath } from "$lib/workspace/api/merge-requests";
|
||||
import {
|
||||
relationLabel,
|
||||
@@ -13,16 +19,42 @@
|
||||
type WorkspaceOrchestratorStatus,
|
||||
} from "$lib/workspace/tickets/ticket-panel";
|
||||
import type { ApiResult } from "$lib/workspace/api/http";
|
||||
import type {
|
||||
TicketTarget,
|
||||
TicketTargetAccess,
|
||||
} from "$lib/generated/ticket-api";
|
||||
import type {
|
||||
RepositoryListResponse,
|
||||
RepositorySummary,
|
||||
TicketDetail,
|
||||
} from "$lib/workspace/sidebar/types";
|
||||
|
||||
type EditableTicketTarget = {
|
||||
repository_key: string;
|
||||
ref_selector: string;
|
||||
access: TicketTargetAccess;
|
||||
};
|
||||
|
||||
const MUTABLE_TICKET_STATES = TICKET_STATES.filter((state) =>
|
||||
state !== "done" && state !== "ready" && state !== "queued"
|
||||
);
|
||||
|
||||
function editableTargets(targets: TicketTarget[]): EditableTicketTarget[] {
|
||||
return targets.map((target) => ({
|
||||
repository_key: target.repository_key,
|
||||
ref_selector: target.ref_selector ?? "",
|
||||
access: target.access,
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizedTargets(targets: EditableTicketTarget[]): TicketTarget[] {
|
||||
return targets.map((target) => ({
|
||||
repository_key: target.repository_key.trim(),
|
||||
ref_selector: target.ref_selector.trim() || null,
|
||||
access: target.access,
|
||||
}));
|
||||
}
|
||||
|
||||
const { data } = $props<{
|
||||
data: {
|
||||
workspaceId: string;
|
||||
@@ -38,18 +70,14 @@
|
||||
if (!loadedTicket) throw new Error(initialData.ticket.error ?? "ticket load failed");
|
||||
const loadedRepositories = $derived(data.repositories.data);
|
||||
|
||||
type QueueOutcome = {
|
||||
requested_ticket: string;
|
||||
queued_tickets: string[];
|
||||
};
|
||||
|
||||
let ticket = $state<TicketDetail>(loadedTicket);
|
||||
const mergeRequest = $derived(ticket.merge_request);
|
||||
let editing = $state(false);
|
||||
let editTitle = $state(loadedTicket.title);
|
||||
let editBody = $state(loadedTicket.body);
|
||||
let repositoryKey = $state(loadedTicket.repository_key ?? "");
|
||||
let refSelector = $state(loadedTicket.ref_selector ?? "");
|
||||
let targetDrafts = $state<EditableTicketTarget[]>(
|
||||
editableTargets(loadedTicket.targets),
|
||||
);
|
||||
let nextState = $state(loadedTicket.state);
|
||||
let transitionReason = $state("");
|
||||
let threadRole = $state("comment");
|
||||
@@ -67,23 +95,34 @@
|
||||
const coderAssignment = $derived(
|
||||
ticket.assignments.find((assignment) => assignment.role === "coder") ?? null,
|
||||
);
|
||||
const selectedRepository = $derived(
|
||||
(loadedRepositories?.items ?? []).find((repository: RepositorySummary) => repository.repository_key === repositoryKey) ?? null,
|
||||
);
|
||||
const effectiveRefSelector = $derived(refSelector.trim() || selectedRepository?.default_ref || "");
|
||||
const targetCandidateValid = $derived(
|
||||
ticket.state === "planning" &&
|
||||
selectedRepository !== null &&
|
||||
(selectedRepository.diagnostics ?? []).length === 0 &&
|
||||
effectiveRefSelector.length > 0,
|
||||
);
|
||||
const persistedTargetValid = $derived(
|
||||
ticket.repository_key !== null &&
|
||||
ticket.ref_selector !== null &&
|
||||
(loadedRepositories?.items ?? []).some((repository: RepositorySummary) =>
|
||||
repository.repository_key === ticket.repository_key && (repository.diagnostics ?? []).length === 0
|
||||
),
|
||||
);
|
||||
function repositoryFor(repositoryKey: string): RepositorySummary | null {
|
||||
return (loadedRepositories?.items ?? []).find((repository: RepositorySummary) =>
|
||||
repository.repository_key === repositoryKey
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
function effectiveRefSelector(target: EditableTicketTarget): string {
|
||||
return target.ref_selector.trim() ||
|
||||
repositoryFor(target.repository_key)?.default_selector || "";
|
||||
}
|
||||
|
||||
const targetCandidateValid = $derived.by(() => {
|
||||
if (ticket.state !== "planning" || targetDrafts.length === 0) return false;
|
||||
const repositoryKeys = new Set<string>();
|
||||
let readWriteCount = 0;
|
||||
for (const target of targetDrafts) {
|
||||
const repository = repositoryFor(target.repository_key);
|
||||
if (
|
||||
!target.repository_key || repository === null ||
|
||||
(repository.diagnostics ?? []).length > 0 ||
|
||||
!effectiveRefSelector(target) ||
|
||||
repositoryKeys.has(target.repository_key)
|
||||
) return false;
|
||||
repositoryKeys.add(target.repository_key);
|
||||
if (target.access === "read_write") readWriteCount += 1;
|
||||
}
|
||||
return readWriteCount === 1;
|
||||
});
|
||||
const implementationStartEligible = $derived(
|
||||
ticket.action_eligibility.can_start_manual_coder,
|
||||
);
|
||||
@@ -99,8 +138,7 @@
|
||||
ticket = updatedTicket;
|
||||
editTitle = updatedTicket.title;
|
||||
editBody = updatedTicket.body;
|
||||
repositoryKey = updatedTicket.repository_key ?? "";
|
||||
refSelector = updatedTicket.ref_selector ?? "";
|
||||
targetDrafts = editableTargets(updatedTicket.targets);
|
||||
nextState = updatedTicket.state;
|
||||
}
|
||||
|
||||
@@ -146,10 +184,10 @@
|
||||
busy = action;
|
||||
errorMessage = null;
|
||||
try {
|
||||
const response = await workspaceApiJsonWithBody<TicketDetail>(path, {
|
||||
const response = await workspaceApiJsonWithBody(path, {
|
||||
method,
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
}, parseTicketDetail, TICKET_BROWSER_API_MAX_RESPONSE_BYTES);
|
||||
if (generation !== routeGeneration) return false;
|
||||
applyTicket(response);
|
||||
return true;
|
||||
@@ -171,12 +209,18 @@
|
||||
errorMessage = null;
|
||||
queueMessage = null;
|
||||
try {
|
||||
const outcome = await workspaceApiJsonWithBody<QueueOutcome>(
|
||||
const outcome = await workspaceApiJsonWithBody(
|
||||
`${path}/queue`,
|
||||
{ method: "POST", body: JSON.stringify({}) },
|
||||
parseTicketQueueOutcome,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
if (generation !== routeGeneration) return;
|
||||
const updatedTicket = await workspaceApiJson<TicketDetail>(path);
|
||||
const updatedTicket = await workspaceApiJson(
|
||||
path,
|
||||
parseTicketDetail,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
if (generation !== routeGeneration) return;
|
||||
queueMessage = `Queued ${outcome.queued_tickets.length} Ticket(s): ${outcome.queued_tickets.join(", ")}`;
|
||||
applyTicket(updatedTicket);
|
||||
@@ -210,9 +254,15 @@
|
||||
expected_assignment_id: null,
|
||||
}),
|
||||
},
|
||||
parseTicketRoleAssignmentMutationResponse,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
if (generation !== routeGeneration) return;
|
||||
const updatedTicket = await workspaceApiJson<TicketDetail>(path);
|
||||
const updatedTicket = await workspaceApiJson(
|
||||
path,
|
||||
parseTicketDetail,
|
||||
TICKET_BROWSER_API_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
if (generation !== routeGeneration) return;
|
||||
applyTicket(updatedTicket);
|
||||
} catch (error) {
|
||||
@@ -263,32 +313,35 @@
|
||||
) editing = false;
|
||||
}
|
||||
|
||||
function addTarget(access: TicketTargetAccess = "read_only"): void {
|
||||
if (ticket.state !== "planning") return;
|
||||
targetDrafts.push({ repository_key: "", ref_selector: "", access });
|
||||
}
|
||||
|
||||
function removeTarget(index: number): void {
|
||||
if (ticket.state !== "planning") return;
|
||||
targetDrafts.splice(index, 1);
|
||||
}
|
||||
|
||||
function targetEditBody(): Record<string, unknown> {
|
||||
const targets = normalizedTargets(targetDrafts);
|
||||
return {
|
||||
target: targets.length > 0
|
||||
? { action: "set", targets }
|
||||
: { action: "clear" },
|
||||
};
|
||||
}
|
||||
|
||||
async function saveTarget(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
await mutate("target", "", {
|
||||
target: repositoryKey
|
||||
? {
|
||||
action: "set",
|
||||
repository_key: repositoryKey,
|
||||
ref_selector: refSelector.trim() || null,
|
||||
}
|
||||
: { action: "clear" },
|
||||
}, "PATCH");
|
||||
await mutate("target", "", targetEditBody(), "PATCH");
|
||||
}
|
||||
|
||||
async function markReady() {
|
||||
if (!targetCandidateValid || busy) return;
|
||||
if (
|
||||
ticket.repository_key !== repositoryKey ||
|
||||
(ticket.ref_selector ?? "") !== refSelector.trim()
|
||||
) {
|
||||
const saved = await mutate("target", "", {
|
||||
target: {
|
||||
action: "set",
|
||||
repository_key: repositoryKey,
|
||||
ref_selector: refSelector.trim() || null,
|
||||
},
|
||||
}, "PATCH");
|
||||
const targets = normalizedTargets(targetDrafts);
|
||||
if (JSON.stringify(ticket.targets) !== JSON.stringify(targets)) {
|
||||
const saved = await mutate("target", "", targetEditBody(), "PATCH");
|
||||
if (!saved) return;
|
||||
}
|
||||
readyOperationKey ??= crypto.randomUUID();
|
||||
@@ -317,7 +370,7 @@
|
||||
event.preventDefault();
|
||||
if (!threadBody.trim()) return;
|
||||
if (
|
||||
await mutate("thread", "/thread", {
|
||||
await mutate("thread", "/events", {
|
||||
role: threadRole,
|
||||
body: threadBody.trim(),
|
||||
})
|
||||
@@ -537,19 +590,45 @@
|
||||
</section>
|
||||
|
||||
<section class="ticket-control-card">
|
||||
<header><h2>Repository target</h2></header>
|
||||
<header><h2>Repository targets</h2><span>{targetDrafts.length}</span></header>
|
||||
<form class="ticket-control-form" onsubmit={saveTarget}>
|
||||
<label>Repository
|
||||
<select bind:value={repositoryKey} disabled={ticket.state !== "planning"}>
|
||||
<option value="">Not assigned</option>
|
||||
{#each loadedRepositories?.items ?? [] as repository}
|
||||
<option value={repository.repository_key}>{repository.repository_key}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>Ref selector<input bind:value={refSelector} placeholder={selectedRepository?.default_ref ?? "branch, tag, or revision"} disabled={ticket.state !== "planning"} /></label>
|
||||
<div class="ticket-target-list">
|
||||
{#each targetDrafts as target, index}
|
||||
{@const selectedRepository = repositoryFor(target.repository_key)}
|
||||
<fieldset class="ticket-target-row">
|
||||
<legend>Target {index + 1}</legend>
|
||||
<label>Repository
|
||||
<select bind:value={target.repository_key} disabled={ticket.state !== "planning"} required>
|
||||
<option value="">Choose repository</option>
|
||||
{#each loadedRepositories?.items ?? [] as repository}
|
||||
<option value={repository.repository_key}>{repository.repository_key}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>Ref selector<input bind:value={target.ref_selector} placeholder={selectedRepository?.default_selector ?? "branch, tag, or revision"} disabled={ticket.state !== "planning"} /></label>
|
||||
<label>Access
|
||||
<select bind:value={target.access} disabled={ticket.state !== "planning"}>
|
||||
<option value="read_write">Read and write</option>
|
||||
<option value="read_only">Read only</option>
|
||||
</select>
|
||||
</label>
|
||||
{#if ticket.state === "planning"}
|
||||
<button class="workspace-secondary-button" type="button" onclick={() => removeTarget(index)}>Remove target</button>
|
||||
{/if}
|
||||
</fieldset>
|
||||
{:else}
|
||||
<p class="workspace-empty-copy">No repository targets.</p>
|
||||
{/each}
|
||||
</div>
|
||||
{#if ticket.state === "planning"}
|
||||
<button
|
||||
class="workspace-secondary-button"
|
||||
type="button"
|
||||
onclick={() => addTarget(targetDrafts.some((target) => target.access === "read_write") ? "read_only" : "read_write")}
|
||||
>Add target</button>
|
||||
{/if}
|
||||
<button class="workspace-secondary-button" type="submit" disabled={busy === "target" || ticket.state !== "planning"}>
|
||||
{busy === "target" ? "Saving…" : "Save target"}
|
||||
{busy === "target" ? "Saving…" : "Save targets"}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -572,7 +651,7 @@
|
||||
{busy === "ready" ? "Marking ready…" : "Mark ready"}
|
||||
</button>
|
||||
{#if !targetCandidateValid}
|
||||
<p class="workspace-empty-copy">Choose a healthy repository and an effective ref selector before marking ready.</p>
|
||||
<p class="workspace-empty-copy">Add unique healthy repository targets with effective ref selectors and exactly one read-write target before marking ready.</p>
|
||||
{/if}
|
||||
{:else if ticket.state === "ready"}
|
||||
<button class="workspace-primary-button ticket-queue-button" type="button" disabled={busy === "queue" || !ticket.action_eligibility.can_queue} onclick={() => void queueTicket()}>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { loadWorkspaceRepositoryList } from "$lib/workspace/api/repositories";
|
||||
import { parseBrowserWorkspaceOrchestratorResponse } from "$lib/workspace/api/workers";
|
||||
import {
|
||||
parseTicketDetail,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
} from "$lib/workspace/api/ticket-browser";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel";
|
||||
import type { TicketDetail } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
@@ -16,11 +20,19 @@ export const load = (async ({ fetch, params }) => {
|
||||
`/tickets/${encodeURIComponent(reference)}`,
|
||||
);
|
||||
const [ticket, repositoriesRaw, orchestrator] = await Promise.all([
|
||||
loadJson<TicketDetail>(fetch, ticketPath),
|
||||
loadJson(
|
||||
fetch,
|
||||
ticketPath,
|
||||
undefined,
|
||||
parseTicketDetail,
|
||||
TICKET_BROWSER_API_LOAD_POLICY,
|
||||
),
|
||||
loadWorkspaceRepositoryList(fetch, params.workspaceId),
|
||||
loadJson<WorkspaceOrchestratorStatus>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/orchestrator"),
|
||||
undefined,
|
||||
parseBrowserWorkspaceOrchestratorResponse,
|
||||
),
|
||||
]);
|
||||
if (ticket.data) {
|
||||
|
||||
@@ -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,27 +1,32 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseWorkerListResponse } from "$lib/workspace/api/workers";
|
||||
import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers";
|
||||
import type {
|
||||
ListResponse,
|
||||
RuntimeCleanupPlanResponse,
|
||||
Worker,
|
||||
} from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const workers = await loadJson<ListResponse<Worker>>(
|
||||
const workers = await loadJson(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/workers"),
|
||||
undefined,
|
||||
parseWorkerListResponse,
|
||||
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
|
||||
);
|
||||
const runtimeIds = Array.from(
|
||||
new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []),
|
||||
);
|
||||
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,20 +1,23 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseWorkerSummary } from "$lib/workspace/api/workers";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const reference = resourceKey(params.workerRef);
|
||||
const result = await loadJson<Worker>(
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/workers/${encodeURIComponent(reference)}`,
|
||||
),
|
||||
undefined,
|
||||
parseWorkerSummary,
|
||||
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
|
||||
);
|
||||
if (result.data?.resource_key) {
|
||||
const canonical = canonicalResourceReference(
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseWorkerSummary } from "$lib/workspace/api/workers";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const reference = resourceKey(params.workerRef);
|
||||
const result = await loadJson<Worker>(
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/workers/${encodeURIComponent(reference)}`,
|
||||
),
|
||||
undefined,
|
||||
parseWorkerSummary,
|
||||
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
|
||||
);
|
||||
if (result.data?.resource_key) {
|
||||
const canonical = canonicalResourceReference(
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
import { workspaceApiPath } from '$lib/workspace/api/http';
|
||||
import { readBoundedJson, workspaceApiPath } from '$lib/workspace/api/http';
|
||||
import {
|
||||
parseWorkingDirectoryCreateResponse,
|
||||
validateWorkingDirectoryCreateRequest,
|
||||
} from '$lib/workspace/api/workdirs';
|
||||
import {
|
||||
parseBrowserCreateWorkerResponse,
|
||||
parseWorkerApiError,
|
||||
parseWorkerLaunchOptionsResponse,
|
||||
} from '$lib/workspace/api/workers';
|
||||
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||
@@ -29,12 +30,6 @@
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
type ErrorPayload = {
|
||||
error?: { message?: string; code?: string } | string;
|
||||
message?: string;
|
||||
diagnostics?: Diagnostic[];
|
||||
};
|
||||
|
||||
function workdirOptionLabel(directory: WorkingDirectorySummary): string {
|
||||
const repositoryKey = directory.source.kind === 'repository'
|
||||
? directory.source.repository_key
|
||||
@@ -171,7 +166,9 @@
|
||||
if (!response.ok) {
|
||||
throw new Error(`worker launch options failed (${response.status})`);
|
||||
}
|
||||
const payload = parseWorkerLaunchOptionsResponse(await response.json());
|
||||
const payload = parseWorkerLaunchOptionsResponse(
|
||||
await readBoundedJson(response, 8 * 1024 * 1024),
|
||||
);
|
||||
options = payload;
|
||||
const form = defaultWorkerLaunchForm(payload, {
|
||||
runtime_id: runtimeId,
|
||||
@@ -313,7 +310,9 @@
|
||||
submitError = await responseDisplayError(response, 'worker create failed');
|
||||
return;
|
||||
}
|
||||
const payload = parseBrowserCreateWorkerResponse(await response.json());
|
||||
const payload = parseBrowserCreateWorkerResponse(
|
||||
await readBoundedJson(response, 8 * 1024 * 1024),
|
||||
);
|
||||
await goto(payload.console_href);
|
||||
} catch (err) {
|
||||
submitError = exceptionDisplayError(err, 'worker create failed');
|
||||
@@ -324,18 +323,13 @@
|
||||
|
||||
async function responseDisplayError(response: Response, fallback: string): Promise<DisplayError> {
|
||||
try {
|
||||
const payload = (await response.json()) as ErrorPayload;
|
||||
const diagnostics = Array.isArray(payload.diagnostics) ? payload.diagnostics : [];
|
||||
if (typeof payload.error === 'object' && payload.error?.message) {
|
||||
return {
|
||||
message: `${payload.error.code ?? 'request_failed'}: ${payload.error.message}`,
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
if (payload.message) {
|
||||
const code = typeof payload.error === 'string' ? payload.error : 'request_failed';
|
||||
return { message: `${code}: ${payload.message}`, diagnostics };
|
||||
}
|
||||
const payload = parseWorkerApiError(
|
||||
await readBoundedJson(response, 1024 * 1024),
|
||||
);
|
||||
return {
|
||||
message: `${payload.error}: ${payload.message}`,
|
||||
diagnostics: payload.diagnostics ?? [],
|
||||
};
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
/// <reference lib="deno.ns" />
|
||||
|
||||
import { assert, assertEquals } from "jsr:@std/assert";
|
||||
import {
|
||||
assert,
|
||||
assertEquals,
|
||||
assertRejects,
|
||||
assertThrows,
|
||||
} from "jsr:@std/assert";
|
||||
import {
|
||||
commitConfigTree,
|
||||
ConfigSourceApiError,
|
||||
fetchConfigEntry,
|
||||
fetchConfigRevision,
|
||||
fetchConfigTree,
|
||||
parseWorkspaceConfigTreeResponse,
|
||||
} from "../../src/lib/workspace/config-source/api.ts";
|
||||
|
||||
function response(body: unknown, status = 200): Response {
|
||||
@@ -15,11 +22,43 @@ function response(body: unknown, status = 200): Response {
|
||||
});
|
||||
}
|
||||
|
||||
const entry = {
|
||||
path: "profiles/main.dcdl",
|
||||
content_type: "decodal",
|
||||
content: "profile = {}",
|
||||
content_digest: "sha256:entry",
|
||||
};
|
||||
const snapshot = {
|
||||
revision: 7,
|
||||
digest: "sha256:tree",
|
||||
entries: { "profiles/main.dcdl": entry },
|
||||
};
|
||||
const tree = {
|
||||
snapshot,
|
||||
contract: {
|
||||
contract_version: 1,
|
||||
decodal_version: "0.4.0",
|
||||
schema_version: 1,
|
||||
entrypoints: ["main.dcdl"],
|
||||
import_policy_version: 1,
|
||||
schema_bundle: {
|
||||
contributions: [],
|
||||
source: "builtin",
|
||||
fingerprint: "sha256:schema",
|
||||
},
|
||||
fingerprint: "sha256:toolchain",
|
||||
},
|
||||
projection_digest: "sha256:projection",
|
||||
};
|
||||
|
||||
Deno.test("config source API commits directly through the workspace scope", async () => {
|
||||
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
||||
const fetcher = ((input: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({ url: String(input), init });
|
||||
return Promise.resolve(response({ ok: true }));
|
||||
const url = String(input);
|
||||
calls.push({ url, init });
|
||||
if (url.includes("/entries/")) return Promise.resolve(response(entry));
|
||||
if (url.includes("/revisions/")) return Promise.resolve(response(snapshot));
|
||||
return Promise.resolve(response(tree));
|
||||
}) as typeof fetch;
|
||||
|
||||
await fetchConfigTree("w/one", fetcher);
|
||||
@@ -44,7 +83,63 @@ Deno.test("config source API commits directly through the workspace scope", asyn
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config source API surfaces failed evaluation instead of treating it as a successful commit", async () => {
|
||||
Deno.test("config source API rejects unknown response fields", async () => {
|
||||
const fetcher =
|
||||
(() =>
|
||||
Promise.resolve(response({ ...tree, unexpected: true }))) as typeof fetch;
|
||||
await assertRejects(
|
||||
() => fetchConfigTree("w", fetcher),
|
||||
ConfigSourceApiError,
|
||||
"invalid response",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config source API rejects mismatched entry map paths", async () => {
|
||||
const invalid = {
|
||||
...tree,
|
||||
snapshot: {
|
||||
...snapshot,
|
||||
entries: { "profiles/other.dcdl": entry },
|
||||
},
|
||||
};
|
||||
const fetcher = (() => Promise.resolve(response(invalid))) as typeof fetch;
|
||||
await assertRejects(
|
||||
() => fetchConfigTree("w", fetcher),
|
||||
ConfigSourceApiError,
|
||||
"invalid response",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config source API rejects unsafe revisions and oversized entry content", () => {
|
||||
assertThrows(
|
||||
() =>
|
||||
parseWorkspaceConfigTreeResponse({
|
||||
...tree,
|
||||
snapshot: { ...snapshot, revision: Number.MAX_SAFE_INTEGER + 1 },
|
||||
}),
|
||||
ConfigSourceApiError,
|
||||
"invalid response",
|
||||
);
|
||||
assertThrows(
|
||||
() =>
|
||||
parseWorkspaceConfigTreeResponse({
|
||||
...tree,
|
||||
snapshot: {
|
||||
...snapshot,
|
||||
entries: {
|
||||
"profiles/main.dcdl": {
|
||||
...entry,
|
||||
content: "x".repeat(256 * 1024 + 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ConfigSourceApiError,
|
||||
"invalid response",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config source API surfaces bounded failed evaluation details", async () => {
|
||||
const fetcher = (() =>
|
||||
Promise.resolve(
|
||||
new Response("structured diagnostics", { status: 422 }),
|
||||
|
||||
@@ -14,11 +14,88 @@ function assertEquals(actual: unknown, expected: unknown): void {
|
||||
}
|
||||
}
|
||||
|
||||
function event(
|
||||
kind: string,
|
||||
fields: Record<string, unknown>,
|
||||
): MergeRequestThreadEvent {
|
||||
return { kind, sequence: 1, at: "2026-09-01T00:00:00Z", ...fields };
|
||||
const worker = { runtime_id: "runtime", worker_id: "worker" };
|
||||
const createdAt = "2026-09-01T00:00:00Z";
|
||||
|
||||
type ReviewRequested = Extract<
|
||||
MergeRequestThreadEvent,
|
||||
{ kind: "review_requested" }
|
||||
>;
|
||||
type Review = Extract<MergeRequestThreadEvent, { kind: "review" }>;
|
||||
type ReviewRevoked = Extract<
|
||||
MergeRequestThreadEvent,
|
||||
{ kind: "review_revoked" }
|
||||
>;
|
||||
type ReviewCancelled = Extract<
|
||||
MergeRequestThreadEvent,
|
||||
{ kind: "review_cancelled" }
|
||||
>;
|
||||
|
||||
function reviewRequested(
|
||||
eventId: string,
|
||||
subjectRef: string,
|
||||
): ReviewRequested {
|
||||
return {
|
||||
kind: "review_requested",
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
subject_ref: subjectRef,
|
||||
requested_by: worker,
|
||||
reviewer: worker,
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function review(
|
||||
eventId: string,
|
||||
requestEventId: string,
|
||||
subjectRef: string,
|
||||
): Review {
|
||||
return {
|
||||
kind: "review",
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
request_event_id: requestEventId,
|
||||
subject_ref: subjectRef,
|
||||
decision: "approve",
|
||||
body: "approved",
|
||||
findings: [],
|
||||
reviewer: worker,
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function reviewRevoked(
|
||||
eventId: string,
|
||||
reviewEventId: string,
|
||||
subjectRef: string,
|
||||
): ReviewRevoked {
|
||||
return {
|
||||
kind: "review_revoked",
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
review_event_id: reviewEventId,
|
||||
subject_ref: subjectRef,
|
||||
reason: "superseded",
|
||||
revoked_by: worker,
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function reviewCancelled(
|
||||
eventId: string,
|
||||
requestEventId: string,
|
||||
subjectRef: string,
|
||||
): ReviewCancelled {
|
||||
return {
|
||||
kind: "review_cancelled",
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
request_event_id: requestEventId,
|
||||
subject_ref: subjectRef,
|
||||
reason: "cancelled",
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function detail(thread: MergeRequestThreadEvent[]): MergeRequestDetail {
|
||||
@@ -30,22 +107,17 @@ function detail(thread: MergeRequestThreadEvent[]): MergeRequestDetail {
|
||||
selector_from: "work/ticket",
|
||||
selector_to: "develop",
|
||||
state: "open",
|
||||
opened_by: {
|
||||
runtime_id: "runtime",
|
||||
worker_id: "worker",
|
||||
assignment_id: "assignment",
|
||||
},
|
||||
created_at: "2026-09-01T00:00:00Z",
|
||||
updated_at: "2026-09-01T00:00:00Z",
|
||||
created_at: createdAt,
|
||||
updated_at: createdAt,
|
||||
source: {
|
||||
status: "known",
|
||||
ref: "source-2",
|
||||
observed_at: "2026-09-01T00:00:00Z",
|
||||
observed_at: createdAt,
|
||||
},
|
||||
target: {
|
||||
status: "known",
|
||||
ref: "target-2",
|
||||
observed_at: "2026-09-01T00:00:00Z",
|
||||
observed_at: createdAt,
|
||||
},
|
||||
linked_tickets: [{ ticket_id: "T-1", key: "T-1" }],
|
||||
thread,
|
||||
@@ -54,20 +126,9 @@ function detail(thread: MergeRequestThreadEvent[]): MergeRequestDetail {
|
||||
|
||||
Deno.test("revoked review requires a fresh review instead of appearing pending", () => {
|
||||
const mergeRequest = detail([
|
||||
event("review_requested", {
|
||||
event_id: "request-1",
|
||||
subject_ref: "source-2",
|
||||
}),
|
||||
event("review", {
|
||||
event_id: "review-1",
|
||||
request_event_id: "request-1",
|
||||
subject_ref: "source-2",
|
||||
decision: "approve",
|
||||
}),
|
||||
event("review_revoked", {
|
||||
event_id: "revoke-1",
|
||||
review_event_id: "review-1",
|
||||
}),
|
||||
reviewRequested("request-1", "source-2"),
|
||||
review("review-1", "request-1", "source-2"),
|
||||
reviewRevoked("revoke-1", "review-1", "source-2"),
|
||||
]);
|
||||
|
||||
assertEquals(
|
||||
@@ -78,10 +139,7 @@ Deno.test("revoked review requires a fresh review instead of appearing pending",
|
||||
|
||||
Deno.test("unresolved review request for the current source is pending", () => {
|
||||
const mergeRequest = detail([
|
||||
event("review_requested", {
|
||||
event_id: "request-2",
|
||||
subject_ref: "source-2",
|
||||
}),
|
||||
reviewRequested("request-2", "source-2"),
|
||||
]);
|
||||
|
||||
assertEquals(
|
||||
@@ -92,16 +150,8 @@ Deno.test("unresolved review request for the current source is pending", () => {
|
||||
|
||||
Deno.test("completed or cancelled request is not projected as pending", () => {
|
||||
const approved = detail([
|
||||
event("review_requested", {
|
||||
event_id: "request-3",
|
||||
subject_ref: "source-2",
|
||||
}),
|
||||
event("review", {
|
||||
event_id: "review-3",
|
||||
request_event_id: "request-3",
|
||||
subject_ref: "source-2",
|
||||
decision: "approve",
|
||||
}),
|
||||
reviewRequested("request-3", "source-2"),
|
||||
review("review-3", "request-3", "source-2"),
|
||||
]);
|
||||
assertEquals(
|
||||
sourceReviewFreshness(approved),
|
||||
@@ -109,15 +159,8 @@ Deno.test("completed or cancelled request is not projected as pending", () => {
|
||||
);
|
||||
|
||||
const cancelled = detail([
|
||||
event("review_requested", {
|
||||
event_id: "request-4",
|
||||
subject_ref: "source-2",
|
||||
}),
|
||||
event("review_cancelled", {
|
||||
event_id: "cancel-4",
|
||||
request_event_id: "request-4",
|
||||
subject_ref: "source-2",
|
||||
}),
|
||||
reviewRequested("request-4", "source-2"),
|
||||
reviewCancelled("cancel-4", "request-4", "source-2"),
|
||||
]);
|
||||
assertEquals(
|
||||
sourceReviewFreshness(cancelled),
|
||||
@@ -127,12 +170,7 @@ Deno.test("completed or cancelled request is not projected as pending", () => {
|
||||
|
||||
Deno.test("source movement explains the exact stale and current refs", () => {
|
||||
const mergeRequest = detail([
|
||||
event("review", {
|
||||
event_id: "review-old",
|
||||
request_event_id: "request-old",
|
||||
subject_ref: "source-1",
|
||||
decision: "approve",
|
||||
}),
|
||||
review("review-old", "request-old", "source-1"),
|
||||
]);
|
||||
|
||||
assertEquals(
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user