From e6c872d451e95b8e5a52ba6558bcf7c89f3c20ce Mon Sep 17 00:00:00 2001 From: Wang Date: Wed, 2 Sep 2026 13:19:02 +0800 Subject: [PATCH 1/6] feat(desktop): unify Runtime Host connection-code onboarding Generated-by: OpenAI Codex --- apps/desktop/renderer-architecture.json | 31 +----- .../__tests__/runtime-host-management.test.ts | 16 ++- .../runtime-host-ssh-terminal.test.ts | 35 +++++++ .../main/runtime-host-local-remote-access.ts | 29 ++---- .../src/main/runtime-host-management.ts | 17 ++++ .../src/main/runtime-host-ssh-terminal.ts | 5 +- apps/desktop/src/preload/bridge-contract.d.ts | 1 + apps/desktop/src/preload/preload.ts | 3 + .../features/runtime-host-management/index.ts | 3 + .../features/runtime-host-management/ports.ts | 21 ++++ .../ui/runtime-host-add-computer-menu.tsx | 86 ++++++++++++++++ .../runtime-host-connection-code-button.tsx | 62 ++++++++++++ .../runtime-host-connection-code-dialog.tsx | 97 +++++++++++++++---- .../locales/settings-projects-copy.ts | 41 ++++++-- ...create-runtime-host-management-services.ts | 7 ++ .../runtime-host-management-dialog.tsx | 21 +++- .../runtime-host-onboarding-dialog.tsx | 16 ++- .../runtime-host-profiles-section.tsx | 71 +++++++------- docs/astryx-surface-file-inventory.md | 6 +- docs/astryx-surface-file-inventory.paths | 4 +- docs/runtime-host-remote-access.md | 7 +- docs/runtime-host-remote-access.zh-CN.md | 6 +- .../runtime-host-operator-command.test.ts | 24 +++++ packages/cli/src/cli-core.ts | 14 +++ .../cli/src/runtime-host-access-command.ts | 40 ++++++++ packages/cli/src/runtime-host-cli.ts | 53 ++++++++-- packages/runtime-host/src/client/index.ts | 2 + .../src/client/owner-connection-code.ts | 46 ++++++++- .../src/operator/access-management-frame.ts | 11 ++- 29 files changed, 643 insertions(+), 132 deletions(-) create mode 100644 apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-add-computer-menu.tsx create mode 100644 apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-connection-code-button.tsx rename apps/desktop/src/renderer/{settings => features/runtime-host-management/ui}/runtime-host-connection-code-dialog.tsx (53%) diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 6d7402ca9d..9efd7016c9 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -171,7 +171,6 @@ "src/renderer/settings/relay-profile-draft.ts", "src/renderer/settings/relay-thinking-bulk.ts", "src/renderer/settings/request-customization-editor.tsx", - "src/renderer/settings/runtime-host-connection-code-dialog.tsx", "src/renderer/settings/runtime-host-interaction-boundary.tsx", "src/renderer/settings/runtime-host-management-dialog.tsx", "src/renderer/settings/runtime-host-onboarding-dialog.tsx", @@ -3723,30 +3722,6 @@ "@maka/ui/icons": 1 } }, - "src/renderer/settings/runtime-host-connection-code-dialog.tsx": { - "bridgePaths": { - "window.maka.runtimeHostProfiles.importConnectionCode": 1 - }, - "environmentCapabilities": { - "navigator.clipboard.writeText": 1 - }, - "hookCalls": { - "useState": 2, - "useToast": 1, - "useUiLocale": 1 - }, - "lifecycleMethods": {}, - "unresolvedDependencies": 0, - "actionFactories": [], - "dependencyPaths": { - "../locales/settings-projects-copy.js": 1, - "./settings-error-copy.js": 1, - "@astryxdesign/core/Dialog": 1, - "@astryxdesign/core/Layout": 1, - "@maka/ui": 1, - "react": 1 - } - }, "src/renderer/settings/runtime-host-interaction-boundary.tsx": { "bridgePaths": {}, "environmentCapabilities": {}, @@ -3788,13 +3763,12 @@ "dependencyPaths": { "../../preload/bridge-contract.js": 1, "../../shared/runtime-host-project-directory-policy.js": 1, + "../features/runtime-host-management": 1, "../locales/settings-projects-copy.js": 1, "./runtime-host-project-directory-editor.js": 1, "./settings-error-copy.js": 1, "@astryxdesign/core/Dialog": 1, "@astryxdesign/core/Layout": 1, - "@astryxdesign/core/Switch": 1, - "@astryxdesign/core/Text": 1, "@astryxdesign/core/Tooltip": 1, "@maka/core/ui-locale": 1, "@maka/ui": 1, @@ -3812,7 +3786,7 @@ "window.maka.runtimeHostOnboarding.subscribe": 1 }, "environmentCapabilities": { - "navigator.userAgent.includes": 2 + "navigator.userAgent.includes": 1 }, "hookCalls": { "useEffect": 2, @@ -3870,7 +3844,6 @@ "../locales/session-collaboration-copy.js": 1, "../locales/settings-projects-copy.js": 1, "./password-input.js": 1, - "./runtime-host-connection-code-dialog.js": 1, "./runtime-host-management-dialog.js": 1, "./runtime-host-onboarding-dialog.js": 1, "./settings-error-copy.js": 1, diff --git a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts index 7b1725a66b..585a81ef11 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts @@ -346,6 +346,15 @@ test('identifies, rotates, and revokes managed credentials without exposing secr if (input.action === 'list') { return { schemaVersion: 1, kind: 'result', action: 'list', credentials }; } + if (input.action === 'connection-code') { + assert.equal(input.name, profile.name); + return { + schemaVersion: 1, + kind: 'result', + action: 'connection-code', + connectionCode: 'maka-runtime-host:connect:v1:example', + }; + } if (input.action === 'prepare') { prepareCalls += 1; assert.equal(input.currentCredentialFingerprint, currentFingerprint); @@ -391,9 +400,14 @@ test('identifies, rotates, and revokes managed credentials without exposing secr }); const list = handlers.get('runtime-host-management:list-credentials'); + const connectionCode = handlers.get('runtime-host-management:create-connection-code'); const rotate = handlers.get('runtime-host-management:rotate-credential'); const revoke = handlers.get('runtime-host-management:revoke-credential'); - assert.ok(list && rotate && revoke); + assert.ok(list && connectionCode && rotate && revoke); + assert.equal( + await connectionCode({}, profile.id), + 'maka-runtime-host:connect:v1:example', + ); const initial = await list({}, profile.id); assert.equal((initial as { canRotate: boolean }).canRotate, true); assert.deepEqual( diff --git a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts index 30c750dec0..88413a47a4 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts @@ -641,6 +641,41 @@ test('keeps a prepared access credential out of the SSH terminal projection', as await harness.terminal.close(); }); +test('creates an owner connection code through the framed SSH operator channel', async () => { + const harness = createHarness('pending'); + const connectionCode = 'maka-runtime-host:connect:v1:secret-code'; + const management = harness.terminal.runAccessManagement({ + destination: 'operator@example.com', + operatorPath: '/home/operator/.local/share/maka/operator', + rootPath: '/srv/maka', + expectedRootId: 'a'.repeat(64), + action: 'connection-code', + name: "Owner's Linux", + }); + await waitFor(() => harness.pty.hasDataListener()); + harness.pty.emitData(encodeRuntimeHostAccessManagementFrame({ + schemaVersion: 1, + kind: 'result', + action: 'connection-code', + connectionCode, + })); + harness.pty.exit(0); + + const result = await management; + assert.equal( + result.kind === 'result' && result.action === 'connection-code' + ? result.connectionCode + : undefined, + connectionCode, + ); + const command = harness.launchArgs.at(-1)?.at(-1) ?? ''; + assert.match(command, /access.*connection-code/u); + assert.match(command, /--name/u); + assert.match(command, /Owner/u); + assert.doesNotMatch(JSON.stringify(harness.events), /secret-code|MAKA_RUNTIME/u); + await harness.terminal.close(); +}); + test('requests adaptive-connectivity status only on the peer-management frame', async () => { const harness = createHarness('pending'); const management = harness.terminal.runPeerManagement({ diff --git a/apps/desktop/src/main/runtime-host-local-remote-access.ts b/apps/desktop/src/main/runtime-host-local-remote-access.ts index 942e245c82..34e859d923 100644 --- a/apps/desktop/src/main/runtime-host-local-remote-access.ts +++ b/apps/desktop/src/main/runtime-host-local-remote-access.ts @@ -23,11 +23,11 @@ import { hostname } from 'node:os'; import { dirname, isAbsolute, join } from 'node:path'; import type { IpcMain } from 'electron'; import { - consumeAccessCredentialDelivery, encodeRuntimeHostOwnerConnectionCode, + issueRuntimeHostOwnerConnectionCode, } from '@maka/runtime-host/client'; import { resolveRuntimeHostManagedDeploymentAuthority } from '@maka/runtime-host/operator'; -import { REMOTE_OWNER_OPERATION_GRANTS, type HostRegistration } from '@maka/runtime-host/protocol'; +import type { HostRegistration } from '@maka/runtime-host/protocol'; import type { DesktopLocalRuntimeHostRemoteAccessEnableResult, DesktopLocalRuntimeHostRemoteAccessSnapshot, @@ -982,25 +982,13 @@ async function issueConnectionCode( peer: LocalPeerDescriptor, client: DesktopRuntimeHostClient, ): Promise { - const livePeer = await readLivePeer(client, peer); - const prepared = await client.request('access.credential.prepare', { - principalKind: 'remote_owner', - principalId: LOCAL_REMOTE_ACCESS_PRINCIPAL_ID, - operationGrants: REMOTE_OWNER_OPERATION_GRANTS, - canPublishClientCapabilities: true, - canUseHostPaths: false, - bindClientInstance: true, - }); - const credential = await consumeAccessCredentialDelivery( + return issueRuntimeHostOwnerConnectionCode({ rootPath, - prepared.deliveryId, - prepared.credentialId, - ); - return encodeRuntimeHostOwnerConnectionCode({ - name: hostName(), rootId, - transport: { kind: 'libp2p-direct', ...livePeer }, - credential, + name: hostName(), + principalId: LOCAL_REMOTE_ACCESS_PRINCIPAL_ID, + expectedPeerId: peer.peerId, + client, }); } @@ -1032,6 +1020,9 @@ async function hasSharedAccess( target, }); if (response.kind === 'error') throw new Error(response.error.message); + if (response.action !== 'list') { + throw new Error('Runtime Host operator returned an unrelated access result'); + } return response.credentials.some( (credential) => credential.principalKind === 'remote_owner' && diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts index f97b0253e7..59ee26bcf3 100644 --- a/apps/desktop/src/main/runtime-host-management.ts +++ b/apps/desktop/src/main/runtime-host-management.ts @@ -900,6 +900,20 @@ export function createDesktopRuntimeHostManagement(input: { ); }; + const createConnectionCode = async (profileId: unknown): Promise => { + const access = await resolveAccess(profileId); + const response = await input.runAccessManagement({ + ...access.target, + action: 'connection-code', + name: access.managed.profile.name, + }); + if (response.kind === 'error') throw new Error(response.error.message); + if (response.action !== 'connection-code') { + throw new Error('Remote Runtime Host did not return a connection code'); + } + return response.connectionCode; + }; + const rotateCredential = async ( profileId: unknown, ): Promise => { @@ -980,6 +994,7 @@ export function createDesktopRuntimeHostManagement(input: { run: 'runtime-host-management:run', update: 'runtime-host-management:update', configureProjectDirectories: 'runtime-host-management:configure-project-directories', + createConnectionCode: 'runtime-host-management:create-connection-code', listCredentials: 'runtime-host-management:list-credentials', rotateCredential: 'runtime-host-management:rotate-credential', revokeCredential: 'runtime-host-management:revoke-credential', @@ -1019,6 +1034,8 @@ export function createDesktopRuntimeHostManagement(input: { allowInterruptActiveTasks, ), ); + input.ipcMain.handle(channels.createConnectionCode, (_event, profileId: unknown) => + createConnectionCode(profileId)); input.ipcMain.handle(channels.listCredentials, (_event, profileId: unknown) => listCredentials(profileId)); input.ipcMain.handle(channels.rotateCredential, (_event, profileId: unknown) => diff --git a/apps/desktop/src/main/runtime-host-ssh-terminal.ts b/apps/desktop/src/main/runtime-host-ssh-terminal.ts index 535379e60a..bf8fe7ae9f 100644 --- a/apps/desktop/src/main/runtime-host-ssh-terminal.ts +++ b/apps/desktop/src/main/runtime-host-ssh-terminal.ts @@ -220,6 +220,7 @@ interface DesktopRuntimeHostSshAccessTarget { export type DesktopRuntimeHostSshAccessInput = DesktopRuntimeHostSshAccessTarget & ( | { readonly action: 'list' } + | { readonly action: 'connection-code'; readonly name: string } | { readonly action: 'prepare'; readonly currentCredentialFingerprint: string } | { readonly action: 'revoke'; @@ -1326,7 +1327,9 @@ function runtimeHostAccessManagementRemoteCommand( '--credential', input.credentialId, '--current-fingerprint', input.currentCredentialFingerprint, ] - : []; + : input.action === 'connection-code' + ? ['--name', input.name] + : []; const command = [ input.operatorPath, 'access', diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index 1b0183f1c4..f825aaf1cb 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -868,6 +868,7 @@ export interface MakaBridge { automaticRelayDiscovery: boolean, webRtcStunPolicy?: import('@maka/runtime-host/operator').RuntimeHostWebRtcStunPolicy, ): Promise; + createConnectionCode(profileId: string): Promise; listCredentials(profileId: string): Promise; rotateCredential(profileId: string): Promise; revokeCredential( diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 4c59db252b..10949441a2 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -1587,6 +1587,9 @@ const makaBridge = { webRtcStunPolicy, ); }, + createConnectionCode(profileId: string): Promise { + return ipcRenderer.invoke('runtime-host-management:create-connection-code', profileId); + }, listCredentials(profileId: string): Promise { return ipcRenderer.invoke('runtime-host-management:list-credentials', profileId); }, diff --git a/apps/desktop/src/renderer/features/runtime-host-management/index.ts b/apps/desktop/src/renderer/features/runtime-host-management/index.ts index f8a658bc7a..005423d585 100644 --- a/apps/desktop/src/renderer/features/runtime-host-management/index.ts +++ b/apps/desktop/src/renderer/features/runtime-host-management/index.ts @@ -19,6 +19,9 @@ export { RuntimeHostPeerMeshDialog } from './ui/runtime-host-peer-mesh-dialog'; export { PeerMeshPeerIdButton } from './ui/peer-mesh-peer-id-button'; +export { RuntimeHostAddComputerMenu } from './ui/runtime-host-add-computer-menu'; +export { RuntimeHostConnectionCodeButton } from './ui/runtime-host-connection-code-button'; +export { RuntimeHostConnectionCodeDialog } from './ui/runtime-host-connection-code-dialog'; export { RuntimeHostPairingRecoveryButton, RuntimeHostProfileMoreMenu, diff --git a/apps/desktop/src/renderer/features/runtime-host-management/ports.ts b/apps/desktop/src/renderer/features/runtime-host-management/ports.ts index b09ab17d15..606d21dd91 100644 --- a/apps/desktop/src/renderer/features/runtime-host-management/ports.ts +++ b/apps/desktop/src/renderer/features/runtime-host-management/ports.ts @@ -84,7 +84,28 @@ export interface RuntimeHostProfilePairingServices { discard(profileId: string): Promise; } +export type RuntimeHostConnectionCodeImportResult = + | { readonly kind: 'connected'; readonly profileId: string } + | { + readonly kind: 'error'; + readonly reason: + | 'invalid_code' + | 'code_unavailable' + | 'host_unreachable' + | 'host_mismatch' + | 'unknown'; + }; + +export interface RuntimeHostConnectionCodeServices { + create(profileId: string): Promise; + importCode(code: string): Promise; + readClipboardText(): Promise; + writeClipboardText(value: string): Promise; +} + export interface RuntimeHostManagementServices { readonly peerMesh: PeerMeshServices; readonly profilePairing: RuntimeHostProfilePairingServices; + readonly connectionCodes: RuntimeHostConnectionCodeServices; + readonly supportsWsl: boolean; } diff --git a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-add-computer-menu.tsx b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-add-computer-menu.tsx new file mode 100644 index 0000000000..1d48424651 --- /dev/null +++ b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-add-computer-menu.tsx @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DropdownMenu, DropdownMenuItem } from '@astryxdesign/core'; +import { useRuntimeHostManagementServices } from '../services-context.js'; + +export interface RuntimeHostAddComputerCopy { + readonly addComputer: string; + readonly useConnectionCode: string; + readonly useConnectionCodeDescription: string; + readonly addSshComputer: string; + readonly addSshComputerDescription: string; + readonly addWslEnvironment: string; + readonly addWslEnvironmentDescription: string; + readonly configureManually: string; + readonly configureManuallyDescription: string; + readonly cancel: string; +} + +export function RuntimeHostAddComputerMenu(props: { + readonly copy: RuntimeHostAddComputerCopy; + readonly isDisabled: boolean; + readonly isManualConfigurationOpen: boolean; + readonly onUseConnectionCode: () => void; + readonly onSetupSsh: () => void; + readonly onSetupWsl: () => void; + readonly onConfigureManually: () => void; +}) { + const services = useRuntimeHostManagementServices(); + return ( + + + + {services.supportsWsl ? ( + + ) : null} + + + ); +} diff --git a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-connection-code-button.tsx b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-connection-code-button.tsx new file mode 100644 index 0000000000..c239cb6824 --- /dev/null +++ b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-connection-code-button.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState } from 'react'; +import { Button, useToast } from '@maka/ui'; +import { useRuntimeHostManagementServices } from '../services-context.js'; + +export function RuntimeHostConnectionCodeButton(props: { + readonly profileId: string; + readonly label: string; + readonly failureTitle: string; + readonly isDisabled: boolean; + readonly errorMessage: (error: unknown) => string; + readonly onCreated: (connectionCode: string) => void; + readonly onWorkingChange: (working: boolean) => void; +}) { + const services = useRuntimeHostManagementServices(); + const toast = useToast(); + const [working, setWorking] = useState(false); + + async function create(): Promise { + setWorking(true); + props.onWorkingChange(true); + let connectionCode: string | undefined; + try { + connectionCode = await services.connectionCodes.create(props.profileId); + } catch (error) { + toast.error(props.failureTitle, props.errorMessage(error)); + } finally { + setWorking(false); + props.onWorkingChange(false); + } + if (connectionCode) props.onCreated(connectionCode); + } + + return ( +