feat: project Runtime verification state

This commit is contained in:
2026-09-08 09:22:44 +09:00
parent f29c343879
commit 7b6a84a550
10 changed files with 596 additions and 57 deletions
@@ -349,16 +349,42 @@ export type WorkspaceRuntimeBindingState =
| "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 WorkspaceRuntimeAuthenticationMode =
| "legacy_server_issuer"
| "workspace_identity";
export type WorkspaceRuntimeBindingSummary = {
state: WorkspaceRuntimeBindingState;
connection_state: RuntimeConnectionDisplayState;
authentication_mode: WorkspaceRuntimeAuthenticationMode;
revision: number;
workspace_key_id?: string | null;
workspace_key_generation?: number | null;
verification?: RuntimeVerificationEvidenceSummary | null;
};
export type RuntimeManagementSummary = {
@@ -464,6 +490,9 @@ export type RuntimeConnectionTestFailureKind =
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;
@@ -2,11 +2,15 @@ import type {
Diagnostic,
RuntimeConnectionTestFailureKind,
RuntimeConnectionTestResponse,
RuntimeVerificationEvidenceSummary,
} from "$lib/generated/workspace-api";
const RESPONSE_KEYS = [
"workspace_id",
"runtime_id",
"binding_revision",
"connection_state",
"verification",
"checked_at",
"status",
"failure_kind",
@@ -103,9 +107,28 @@ export function parseRuntimeConnectionTestResponse(
) {
return null;
}
const bindingRevision = value.binding_revision;
const connectionState = parseConnectionState(value.connection_state);
const verification = parseVerificationEvidence(value.verification);
if (
!isSafeRevision(bindingRevision) ||
connectionState === null ||
(value.verification !== null && verification === null) ||
(verification !== null &&
verification.binding_revision !== bindingRevision) ||
(connectionState === "verified" && value.status !== "compatible") ||
(connectionState === "verified" && verification !== null &&
(verification.last_outcome !== "verified" ||
verification.verified_at === null))
) {
return null;
}
return {
workspace_id: value.workspace_id,
runtime_id: value.runtime_id,
binding_revision: bindingRevision,
connection_state: connectionState,
verification,
checked_at: value.checked_at,
status: value.status,
failure_kind: failureKind as RuntimeConnectionTestFailureKind | null,
@@ -115,6 +138,67 @@ export function parseRuntimeConnectionTestResponse(
};
}
function parseConnectionState(
value: unknown,
): "configured" | "verified" | "unavailable" | "revoked" | null {
return value === "configured" || value === "verified" ||
value === "unavailable" || value === "revoked"
? value
: null;
}
function isSafeRevision(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function parseVerificationEvidence(
value: unknown,
): RuntimeVerificationEvidenceSummary | null {
if (value === null) return null;
const keys = [
"verified_at",
"last_checked_at",
"last_outcome",
"binding_revision",
"workspace_key_id",
"workspace_identity_revision",
"workspace_trust_generation",
"runtime_public_key_fingerprint",
"runtime_identity_revision",
] as const;
if (!isRecord(value) || !hasExactKeys(value, keys)) return null;
if (
(value.verified_at !== null &&
(!isBoundedString(value.verified_at, 128) ||
Number.isNaN(Date.parse(value.verified_at)))) ||
!isBoundedString(value.last_checked_at, 128) ||
Number.isNaN(Date.parse(value.last_checked_at)) ||
(value.last_outcome !== "verified" &&
value.last_outcome !== "challenge_issued" &&
value.last_outcome !== "verification_failed" &&
value.last_outcome !== "connectivity_failed") ||
!isSafeRevision(value.binding_revision) ||
!isBoundedString(value.workspace_key_id, 128) ||
!isSafeRevision(value.workspace_identity_revision) ||
!isSafeRevision(value.workspace_trust_generation) ||
!isBoundedString(value.runtime_public_key_fingerprint, 128) ||
!isSafeRevision(value.runtime_identity_revision)
) {
return null;
}
return {
verified_at: value.verified_at,
last_checked_at: value.last_checked_at,
last_outcome: value.last_outcome,
binding_revision: value.binding_revision,
workspace_key_id: value.workspace_key_id,
workspace_identity_revision: value.workspace_identity_revision,
workspace_trust_generation: value.workspace_trust_generation,
runtime_public_key_fingerprint: value.runtime_public_key_fingerprint,
runtime_identity_revision: value.runtime_identity_revision,
};
}
export async function testRuntimeConnection(
workspaceId: string,
runtimeId: string,
@@ -3,6 +3,7 @@ import type {
Diagnostic,
PutRuntimeTrustKeyRequest,
RevokeRuntimeTrustKeyRequest,
RuntimeConnectionDisplayState,
RuntimeIdentityAuthority,
RuntimeManagementSummary,
RuntimeSourceKind,
@@ -15,6 +16,7 @@ import type {
RuntimeTrustKeyRevealResponse,
RuntimeTrustKeyState,
RuntimeTrustKeyStatus,
RuntimeVerificationEvidenceSummary,
WorkspaceRuntimeAuthenticationMode,
WorkspaceRuntimeBindingState,
WorkspaceRuntimeBindingSummary,
@@ -76,6 +78,13 @@ const BINDING_STATES = new Set<WorkspaceRuntimeBindingState>([
"verified",
"revoked",
]);
const CONNECTION_STATES = new Set<RuntimeConnectionDisplayState>([
"configured",
"verified",
"unavailable",
"revoked",
]);
const AUTHENTICATION_MODES = new Set<WorkspaceRuntimeAuthenticationMode>([
"legacy_server_issuer",
"workspace_identity",
@@ -299,6 +308,79 @@ function runtimeSource(value: unknown, path: string): RuntimeSourceSummary {
};
}
function runtimeVerification(
value: unknown,
path: string,
): RuntimeVerificationEvidenceSummary {
const item = object(value, path);
exactKeys(
item,
[
"verified_at",
"last_checked_at",
"last_outcome",
"binding_revision",
"workspace_key_id",
"workspace_identity_revision",
"workspace_trust_generation",
"runtime_public_key_fingerprint",
"runtime_identity_revision",
],
[],
path,
);
const verifiedAt = item.verified_at === null
? null
: boundedString(item.verified_at, `${path}.verified_at`, 128);
const lastOutcome = enumValue(
item.last_outcome,
`${path}.last_outcome`,
new Set(
[
"verified",
"challenge_issued",
"verification_failed",
"connectivity_failed",
] as const,
),
);
return {
verified_at: verifiedAt,
last_checked_at: boundedString(
item.last_checked_at,
`${path}.last_checked_at`,
128,
),
last_outcome: lastOutcome,
binding_revision: safeRevision(
item.binding_revision,
`${path}.binding_revision`,
),
workspace_key_id: boundedString(
item.workspace_key_id,
`${path}.workspace_key_id`,
LIMITS.idBytes,
),
workspace_identity_revision: safeRevision(
item.workspace_identity_revision,
`${path}.workspace_identity_revision`,
),
workspace_trust_generation: safeRevision(
item.workspace_trust_generation,
`${path}.workspace_trust_generation`,
),
runtime_public_key_fingerprint: boundedString(
item.runtime_public_key_fingerprint,
`${path}.runtime_public_key_fingerprint`,
LIMITS.fingerprintBytes,
),
runtime_identity_revision: safeRevision(
item.runtime_identity_revision,
`${path}.runtime_identity_revision`,
),
};
}
function runtimeBinding(
value: unknown,
path: string,
@@ -306,8 +388,8 @@ function runtimeBinding(
const item = object(value, path);
exactKeys(
item,
["state", "authentication_mode", "revision"],
["workspace_key_id", "workspace_key_generation"],
["state", "connection_state", "authentication_mode", "revision"],
["workspace_key_id", "workspace_key_generation", "verification"],
path,
);
const authenticationMode = enumValue(
@@ -339,16 +421,45 @@ function runtimeBinding(
"must not attach Workspace key metadata to legacy authority",
);
}
const state = enumValue(item.state, `${path}.state`, BINDING_STATES);
const connectionState = enumValue(
item.connection_state,
`${path}.connection_state`,
CONNECTION_STATES,
);
const revision = safeRevision(item.revision, `${path}.revision`);
const verification = item.verification === undefined
? undefined
: runtimeVerification(item.verification, `${path}.verification`);
if (
verification !== undefined && verification.binding_revision !== revision
) {
return fail(path, "verification must match the current binding revision");
}
if (
authenticationMode === "workspace_identity" &&
connectionState === "verified" &&
(verification === undefined ||
verification.verified_at === null ||
verification.last_outcome !== "verified")
) {
return fail(
path,
"verified Workspace identity binding requires verification evidence",
);
}
return {
state: enumValue(item.state, `${path}.state`, BINDING_STATES),
state,
connection_state: connectionState,
authentication_mode: authenticationMode,
revision: safeRevision(item.revision, `${path}.revision`),
revision,
...(workspaceKeyId === undefined
? {}
: { workspace_key_id: workspaceKeyId }),
...(workspaceKeyGeneration === undefined
? {}
: { workspace_key_generation: workspaceKeyGeneration }),
...(verification === undefined ? {} : { verification }),
};
}
@@ -26,6 +26,7 @@
let busyRuntimeId = $state<string | null>(null);
let requestError = $state<string | null>(null);
let testResults = $state<Record<string, RuntimeConnectionTestResponse>>({});
let connectionTestGeneration = 0;
function runtimePlatform(runtime: WorkspaceRuntimeResource): string {
return runtime.os && runtime.arch ? `${runtime.os} / ${runtime.arch}` : 'Unknown';
@@ -33,7 +34,9 @@
function connectionTestSummary(result: RuntimeConnectionTestResponse): string {
if (result.status === 'compatible') {
return `Compatible · protocol v${result.actual_protocol_version}`;
return result.connection_state === 'verified'
? `Verified · protocol v${result.actual_protocol_version}`
: `Compatible · ${result.connection_state} · protocol v${result.actual_protocol_version}`;
}
switch (result.failure_kind) {
case 'authentication': return 'Authentication failed';
@@ -144,16 +147,40 @@
}
}
function currentTestResult(
runtime: WorkspaceRuntimeResource,
): RuntimeConnectionTestResponse | undefined {
const result = testResults[runtime.runtime_id];
return result?.binding_revision === runtime.management?.binding?.revision
? result
: undefined;
}
async function testRuntime(runtime: WorkspaceRuntimeResource): Promise<void> {
const bindingRevision = runtime.management?.binding?.revision;
if (typeof bindingRevision !== 'number') return;
const generation = ++connectionTestGeneration;
requestError = null;
busyRuntimeId = runtime.runtime_id;
try {
const result = await testRuntimeConnection(data.workspaceId, runtime.runtime_id);
if (
generation !== connectionTestGeneration ||
result.binding_revision !== bindingRevision
) {
await invalidateAll();
return;
}
testResults = { ...testResults, [runtime.runtime_id]: result };
await invalidateAll();
} catch (error) {
requestError = error instanceof Error ? error.message : String(error);
if (generation === connectionTestGeneration) {
requestError = error instanceof Error ? error.message : String(error);
}
} finally {
busyRuntimeId = null;
if (generation === connectionTestGeneration) {
busyRuntimeId = null;
}
}
}
</script>
@@ -287,7 +314,7 @@
</td>
<td>{runtime.kind}</td>
<td>
{runtime.management?.binding?.state ?? runtime.status}
{runtime.management?.binding?.connection_state ?? runtime.status}
</td>
<td>{runtimePlatform(runtime)}</td>
<td>{managementLabel(runtime)}</td>
@@ -298,7 +325,7 @@
</td>
<td>
<div class="settings-action-row">
{#if runtime.management?.config_managed && runtime.management.binding?.state === 'verified'}
{#if runtime.management?.config_managed && runtime.management.binding?.connection_state !== 'revoked'}
<button
type="button"
disabled={busyRuntimeId !== null}
@@ -313,7 +340,8 @@
</div>
</td>
</tr>
{#if runtime.diagnostics.length > 0 || testResults[runtime.runtime_id]}
{@const currentResult = currentTestResult(runtime)}
{#if runtime.diagnostics.length > 0 || currentResult}
<tr class="settings-runtime-detail-row">
<td colspan="7">
{#if runtime.diagnostics.length > 0}
@@ -326,14 +354,13 @@
{/each}
</ul>
{/if}
{#if testResults[runtime.runtime_id]}
{@const result = testResults[runtime.runtime_id]}
<div class:failed={result.status === 'failed'} class="settings-test-result">
<strong>Connection test: {connectionTestSummary(result)}</strong>
{#if result.diagnostics[0]}
<span>{result.diagnostics[0].message}</span>
{#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}
<small>Checked {new Date(result.checked_at).toLocaleString()}</small>
<small>Checked {new Date(currentResult.checked_at).toLocaleString()}</small>
</div>
{/if}
</td>
@@ -332,9 +332,12 @@
<div><dt>Kind</dt><dd>{runtime.kind}</dd></div>
<div><dt>Endpoint</dt><dd>{detail.endpoint ?? 'Not configured'}</dd></div>
<div><dt>Status</dt><dd>{runtime.status}</dd></div>
<div><dt>Relationship state</dt><dd>{runtime.management.binding?.state ?? 'Not configured'}</dd></div>
<div><dt>Connection state</dt><dd>{runtime.management.binding?.connection_state ?? 'Not configured'}</dd></div>
<div><dt>Authentication mode</dt><dd>{runtime.management.binding?.authentication_mode ?? '—'}</dd></div>
<div><dt>Workspace signing key</dt><dd><code>{runtime.management.binding?.workspace_key_id ?? '—'}</code></dd></div>
<div><dt>Verified</dt><dd>{formatTimestamp(runtime.management.binding?.verification?.verified_at)}</dd></div>
<div><dt>Verified binding revision</dt><dd>{runtime.management.binding?.verification?.binding_revision?.toString() ?? '—'}</dd></div>
<div><dt>Last verification check</dt><dd>{runtime.management.binding?.verification?.last_outcome ?? '—'} · {formatTimestamp(runtime.management.binding?.verification?.last_checked_at)}</dd></div>
<div><dt>Runtime key status</dt><dd>{trust.status}</dd></div>
<div><dt>Fingerprint</dt><dd><code>{trust.fingerprint ?? '—'}</code></dd></div>
<div><dt>Revision</dt><dd>{trust.revision?.toString() ?? '—'}</dd></div>
@@ -19,6 +19,9 @@ function compatibleResponse(): Record<string, unknown> {
return {
workspace_id: "workspace-a",
runtime_id: "runtime-a",
binding_revision: 3,
connection_state: "verified",
verification: null,
checked_at: "2026-09-01T12:00:00Z",
status: "compatible",
failure_kind: null,
@@ -57,6 +60,23 @@ Deno.test("runtime connection response rejects unknown fields and incoherent com
}),
null,
);
assertEquals(
parseRuntimeConnectionTestResponse({
...compatibleResponse(),
verification: {
verified_at: "2026-09-01T12:00:00Z",
last_checked_at: "2026-09-01T12:00:01Z",
last_outcome: "verified",
binding_revision: 2,
workspace_key_id: "WK-a",
workspace_identity_revision: 1,
workspace_trust_generation: 1,
runtime_public_key_fingerprint: "sha256:runtime",
runtime_identity_revision: 1,
},
}),
null,
);
});
Deno.test("runtime connection response rejects unknown failure kinds and unbounded diagnostics", () => {
@@ -42,10 +42,22 @@ function runtime() {
token_ref_configured: false,
binding: {
state: "verified",
connection_state: "verified",
authentication_mode: "workspace_identity",
revision: 3,
workspace_key_id: "WK-1",
workspace_key_generation: 1,
verification: {
verified_at: "2026-09-01T13:00:00Z",
last_checked_at: "2026-09-01T13:00:00Z",
last_outcome: "verified",
binding_revision: 3,
workspace_key_id: "WK-1",
workspace_identity_revision: 1,
workspace_trust_generation: 1,
runtime_public_key_fingerprint: "SHA256:current",
runtime_identity_revision: 1,
},
},
},
runtime_id: "arcadia",