From 3df611636b80ff573654dc6b5e4b771f1572bff9 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 9 Sep 2026 09:38:26 +0900 Subject: [PATCH] feat: complete Workspace Runtime management flow --- .../lib/workspace/api/runtime-management.ts | 24 +++ .../console/worker-console.ui.test.ts | 2 +- .../settings/runtimes/+page.svelte | 176 ++++++++++++------ .../runtimes/[runtimeId]/+page.svelte | 147 +++++++++------ .../tests/runtime-management-source.test.ts | 63 ++++++- .../tests/runtime-management.test.ts | 19 ++ 6 files changed, 312 insertions(+), 119 deletions(-) diff --git a/web/workspace/src/lib/workspace/api/runtime-management.ts b/web/workspace/src/lib/workspace/api/runtime-management.ts index 90773aa6..1ef10295 100644 --- a/web/workspace/src/lib/workspace/api/runtime-management.ts +++ b/web/workspace/src/lib/workspace/api/runtime-management.ts @@ -919,6 +919,30 @@ export async function createRemoteRuntime( return runtime; } +export async function deleteRemoteRuntime( + workspaceId: string, + runtimeId: string, + fetchImpl: typeof fetch = fetch, +): Promise { + const response = await fetchImpl( + workspaceApiPath( + workspaceId, + `/runtimes/${encodeURIComponent(runtimeId)}`, + ), + { method: "DELETE" }, + ); + if (response.ok) return; + let payload: unknown; + try { + payload = await readBoundedJson(response); + } catch { + throw new RuntimeTrustRequestError( + `Runtime registration delete failed (${response.status})`, + ); + } + throw requestErrorFrom(payload, response.status); +} + export async function revealRuntimeTrustKey( workspaceId: string, runtimeId: string, diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index ae35bd0f..a321d623 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -678,7 +678,7 @@ Deno.test("workspace Runtime inventory lives under Settings admin routes", async "Runtimes should be admin Settings navigation, not primary workspace sidebar navigation", ); assert( - runtimesPage.includes("Add remote Runtime") && + runtimesPage.includes("Connect a remote Runtime") && runtimesPage.includes("Open workdirs") && runtimesPage.includes("settings-runtime-table") && runtimesPage.includes("testRuntimeConnection") && diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.svelte index 49929bfd..398e26f8 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.svelte @@ -11,6 +11,7 @@ RuntimeTrustRequestError, } from '$lib/workspace/api/runtime-management'; import { testRuntimeConnection } from '$lib/workspace/api/runtime-connection'; + import { provisionWorkspaceSigningIdentity } from '$lib/workspace/settings/profile-api'; import type { PageProps } from './$types'; const runtimeBundlePlaceholder = @@ -21,10 +22,10 @@ let displayName = $state(''); let endpoint = $state(''); let runtimeFingerprint = $state(null); - let fingerprintConfirmation = $state(''); let showAddRuntime = $state(false); let busyRuntimeId = $state(null); let requestError = $state(null); + let requestNotice = $state(null); let testResults = $state>({}); let connectionTestGeneration = 0; @@ -88,6 +89,25 @@ : ''; } + function workspaceBundleFilename(): string { + return `workspace-${data.workspaceId}-public-bundle.json`; + } + + async function provisionSigningIdentity(): Promise { + requestError = null; + requestNotice = null; + busyRuntimeId = 'provision-workspace-identity'; + try { + await provisionWorkspaceSigningIdentity(data.workspaceId); + await invalidateAll(); + requestNotice = 'Workspace identity provisioned. Copy its public bundle to the Runtime host.'; + } catch (error) { + requestError = error instanceof Error ? error.message : String(error); + } finally { + busyRuntimeId = null; + } + } + async function copyWorkspaceBundle(): Promise { requestError = null; try { @@ -100,7 +120,6 @@ async function previewRuntimeFingerprint(): Promise { requestError = null; runtimeFingerprint = null; - fingerprintConfirmation = ''; busyRuntimeId = 'preview'; try { const bundle = parseRuntimePublicBundle(runtimePublicBundle); @@ -115,15 +134,13 @@ async function addRuntime(event: SubmitEvent): Promise { event.preventDefault(); requestError = null; + requestNotice = null; busyRuntimeId = 'create'; try { const publicBundle = parseRuntimePublicBundle(runtimePublicBundle); const currentFingerprint = await previewRuntimePublicKeyFingerprint(publicBundle.public_key); - if ( - runtimeFingerprint !== currentFingerprint || - fingerprintConfirmation.trim() !== currentFingerprint - ) { - throw new Error('Preview and confirm the exact Runtime public key fingerprint before registration'); + if (runtimeFingerprint !== currentFingerprint) { + throw new Error('Preview the Runtime public key fingerprint before registration'); } await createRemoteRuntime(data.workspaceId, { public_bundle: publicBundle, @@ -133,10 +150,10 @@ }); runtimePublicBundle = ''; runtimeFingerprint = null; - fingerprintConfirmation = ''; displayName = ''; endpoint = ''; showAddRuntime = false; + requestNotice = 'Runtime registered for this Workspace. Run Test to complete authenticated verification.'; await invalidateAll(); } catch (error) { requestError = error instanceof RuntimeTrustRequestError || error instanceof Error @@ -205,71 +222,107 @@ {#if showAddRuntime && data.workspace.permissions.manage_runtimes}
-

Add remote Runtime

-
- - {#if runtimeFingerprint} - - {/if} - - -
-
-

Trust this Workspace on the Runtime

+
+

Connect a remote Runtime

+

+ This creates a binding for this Workspace. The Runtime can remain connected to other Workspaces; + their trust entries are not replaced. +

+
+ +
+

1. Trust this Workspace on the Runtime

+

+ Each Workspace has its own signing identity. Add this Workspace public bundle to the same store used + when starting the Runtime. +

{#if data.signingIdentityError}

{data.signingIdentityError}

+ {:else if data.signingIdentity?.identity.state === 'pending_provisioning'} +

This Workspace does not have an active signing identity yet.

+ {:else if data.signingIdentity?.public_bundle}

- Save this public bundle as workspace-public-bundle.json on the Runtime host. - It contains no private key material. + Save the bundle as {workspaceBundleFilename()} on the Runtime host. It contains no + private key material.

{workspacePublicBundle()}
- -
yoi-runtime trust-workspace add --bundle workspace-public-bundle.json
-

- Runtime registration remains configured until authenticated verification is completed. -

+ +
yoi-runtime trust-workspace add --bundle {workspaceBundleFilename()}
+ + Pass the same --fs-root and --fs-runtime-dir options used by the Runtime + service. Existing Workspace trust entries are preserved. + {:else}

Loading Workspace public identity…

{/if}
+ +
+

2. Verify the Runtime identity

+

+ On the Runtime host, run yoi-runtime identity show --json with the same Runtime storage + options, then paste the public bundle below. +

+
+ + {#if runtimeFingerprint} +
+
+
Runtime fingerprint
+
{runtimeFingerprint}
+
+
+ {/if} +
+
+ +
+

3. Register the connection

+
+ + +
+

+ Registration stores this Workspace-scoped binding. After it appears in the list, run + Test to complete authenticated verification. +

+
+
+ disabled={busyRuntimeId !== null || !data.signingIdentity?.public_bundle || !runtimeFingerprint} + >Register Runtime @@ -280,6 +333,9 @@ {#if requestError}

{requestError}

{/if} + {#if requestNotice} +

{requestNotice}

+ {/if} {#if data.runtimesError}

{data.runtimesError}

diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte index 27c73576..7888d3ff 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte @@ -1,11 +1,12 @@