chore: merge develop into hare/develop

This commit is contained in:
2026-09-24 01:01:35 +09:00
178 changed files with 61787 additions and 7023 deletions
+4 -2
View File
@@ -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>;
};
+93 -11
View File
@@ -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; };
+61 -7
View File
@@ -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;
};
+112 -28
View File
@@ -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";
+34 -26
View File
@@ -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; };
+25 -5
View File
@@ -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
+204 -3
View File
@@ -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(
+33 -12
View File
@@ -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(
+45 -161
View File
@@ -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,
};
};
@@ -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>
@@ -1,10 +1,10 @@
<script lang="ts">
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
import { workspaceApiPath } from '$lib/workspace/api/http';
import { parseRuntimeCleanupExecution } from '$lib/workspace/api/runtime-workers';
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
import type {
CleanupWorkdirCandidate,
RuntimeCleanupExecutionResponse,
RuntimeCleanupPlanResponse,
WorkingDirectorySummary,
} from '$lib/workspace/sidebar/types';
@@ -83,14 +83,11 @@
}),
},
);
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | unknown;
const payload = await response.json().catch(() => null);
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
if (payload && typeof payload === 'object' && 'plan_after' in payload) {
cleanupPlan = (payload as RuntimeCleanupExecutionResponse).plan_after;
}
const result = payload && typeof payload === 'object' && 'results' in payload
? (payload as RuntimeCleanupExecutionResponse).results.find((entry) => entry.target_id === candidate.target_id)
: undefined;
const execution = parseRuntimeCleanupExecution(payload);
cleanupPlan = execution.plan_after;
const result = execution.results.find((entry) => entry.target_id === candidate.target_id);
if (!result || result.status !== 'deleted') {
throw new Error(result?.message ?? 'Runtime did not delete the selected Workdir');
}
@@ -1,18 +1,17 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseWorkspaceRuntimeList } from "$lib/workspace/api/runtime-management";
import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers";
import { parseWorkingDirectoryListResponse } from "$lib/workspace/api/workdirs";
import type {
ListResponse,
Runtime,
RuntimeCleanupPlanResponse,
} from "$lib/workspace/sidebar/types";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const runtimeId = params.runtimeId;
const [runtimes, workdirs, cleanupPlan] = await Promise.all([
loadJson<ListResponse<Runtime>>(
loadJson(
fetch,
workspaceApiPath(params.workspaceId, "/runtimes"),
undefined,
parseWorkspaceRuntimeList,
),
loadJson(
fetch,
@@ -23,12 +22,14 @@ export const load: PageLoad = async ({ fetch, params }) => {
undefined,
parseWorkingDirectoryListResponse,
),
loadJson<RuntimeCleanupPlanResponse>(
loadJson(
fetch,
workspaceApiPath(
params.workspaceId,
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
),
undefined,
parseRuntimeCleanupPlan,
),
]);
@@ -1,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
}
+99 -4
View File
@@ -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 }),
+95 -57
View File
@@ -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: [],
+122
View File
@@ -0,0 +1,122 @@
declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
};
import {
parseRuntimeCleanupExecution,
parseRuntimeCleanupPlan,
parseRuntimeWorkerLifecycleResult,
parseWorkerRetentionResponse,
} from "../src/lib/workspace/api/runtime-workers.ts";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function assertThrows(operation: () => unknown, expected: string): void {
try {
operation();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes(expected)) return;
throw new Error(`expected error containing ${expected}, received ${message}`);
}
throw new Error("expected operation to throw");
}
function plan() {
return {
workspace_id: "workspace-a",
runtime_id: "runtime-a",
generated_at: "2026-09-01T00:00:00Z",
revision: "revision-a",
digest: "digest-a",
workers: [{
target_id: "worker-target",
action: "worker_delete",
worker_id: "W-1",
runtime_worker_id: "worker-1",
runtime_id: "runtime-a",
reason: "stopped",
blocking_reason: null,
pinned: false,
retention_state: "active",
linked_workdir_ids: [],
running_linked: false,
estimated_reclaim_bytes: 10,
}],
workdirs: [{
target_id: "workdir-target",
action: "workdir_clean_cleanup",
workdir_id: "workdir-1",
runtime_id: "runtime-a",
repository_key: "main",
reason: "unused",
blocking_reason: null,
linked_worker_ids: [],
linked_running_worker_ids: [],
running_linked: false,
pinned_linked: false,
file_status: "active",
cleanliness: "clean",
estimated_reclaim_bytes: null,
}],
diagnostics: [],
};
}
Deno.test("Runtime cleanup parsers accept the generated exact response shapes", () => {
const parsedPlan = parseRuntimeCleanupPlan(plan());
assert(parsedPlan.workers[0]?.runtime_worker_id === "worker-1", "worker candidate missing");
const execution = parseRuntimeCleanupExecution({
workspace_id: "workspace-a",
runtime_id: "runtime-a",
executed_at: "2026-09-01T00:01:00Z",
results: [{
target_id: "worker-target",
action: "worker_delete",
status: "deleted",
message: "deleted",
}],
plan_after: plan(),
diagnostics: [],
});
assert(execution.results[0]?.status === "deleted", "execution result missing");
const lifecycle = parseRuntimeWorkerLifecycleResult({
state: "accepted",
runtime_id: "runtime-a",
worker_id: "worker-1",
diagnostics: [],
});
assert(lifecycle.state === "accepted", "lifecycle state missing");
const retention = parseWorkerRetentionResponse({
workspace_id: "workspace-a",
runtime_id: "runtime-a",
worker_id: "worker-1",
pinned: true,
retention_state: "pinned",
});
assert(retention.pinned, "retention state missing");
});
Deno.test("Runtime cleanup parsers reject unknown fields and unsafe values", () => {
assertThrows(
() => parseRuntimeCleanupPlan({ ...plan(), unexpected: true }),
"not part of the wire contract",
);
assertThrows(
() => parseRuntimeCleanupPlan({ ...plan(), workers: new Array(1_001).fill(plan().workers[0]) }),
"at most 1000",
);
assertThrows(
() => parseRuntimeCleanupPlan({ ...plan(), workers: [{ ...plan().workers[0], estimated_reclaim_bytes: Number.MAX_SAFE_INTEGER + 1 }] }),
"safe integer",
);
assertThrows(
() => parseRuntimeWorkerLifecycleResult({ state: "future", runtime_id: "runtime-a", worker_id: "worker-1", diagnostics: [] }),
"unknown value",
);
});