Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type { DesktopRuntimeHostProfileAddInput } from '../../preload/bridge-con
import type { DesktopRuntimeHostManagedService } from '../runtime-host-managed-services.js';
import { createDesktopRuntimeHostOnboarding } from '../runtime-host-onboarding.js';

test('persists a verified SSH profile without projecting its credential', async () => {
test('persists a verified on-demand SSH profile without endpoint or credential projection', async () => {
let setupInput: unknown;
let saved:
| (DesktopRuntimeHostProfileAddInput & {
Expand Down Expand Up @@ -62,19 +62,18 @@ test('persists a verified SSH profile without projecting its credential', async
assert.deepEqual(saved?.profile.transport, {
kind: 'ssh',
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
});
assert.deepEqual(saved?.managedService, {
id: 'b'.repeat(64),
rootPath: '/home/operator/.config/Maka/workspaces/default',
operatorPath: '/home/operator/.local/share/maka/operator',
activation: {
kind: 'ssh_operator',
operatorPath: '/home/operator/.local/share/maka/operator',
},
});
assert.equal(saved?.managedService, undefined);
assert.equal(saved?.credential, 'secret-access-token');
assert.deepEqual(
(setupInput as { projectDirectoryRoots?: unknown }).projectDirectoryRoots,
[{ label: 'Work', path: '/srv/work' }],
);
assert.equal((setupInput as { lifecycle?: unknown }).lifecycle, 'on_demand');
assert.doesNotMatch(JSON.stringify(harness.events), /secret-access-token/u);
await harness.onboarding.close();
assert.equal(harness.handlers.size, 0);
Expand Down
38 changes: 35 additions & 3 deletions apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test } from 'node:test';
import type { IPty } from 'node-pty';
import { type RuntimeHostSshProcessFactory } from '@maka/runtime-host/client';
import {
type RuntimeHostSshProcessFactory,
} from '@maka/runtime-host/client';
import {
encodeRuntimeHostActivationFrame,
encodeRuntimeHostAccessManagementFrame,
encodeRuntimeHostServiceManagementFrame,
encodeRuntimeHostSetupFrame,
Expand Down Expand Up @@ -689,6 +688,39 @@ test('does not launch a management process after the terminal owner closes', asy
assert.equal(launches.length, 0);
});

test('runs interactive operator activation as one strict framed SSH command', async () => {
const harness = createHarness('pending');
const rootId = 'a'.repeat(64);
const activation = harness.terminal.activateSshOperator({
destination: 'operator@example.com',
operatorPath: '/home/operator/.local/share/maka/operator',
rootId,
interaction: 'terminal',
});
await waitFor(() => harness.pty.hasDataListener());
harness.pty.emitData(
encodeRuntimeHostActivationFrame({
schemaVersion: 1,
kind: 'result',
deploymentId: '00000000-0000-4000-8000-000000000001',
configRevision: 1,
rootId,
hostEpoch: 'host-epoch',
pid: 1234,
protocolVersion: 1,
endpoint: { host: '127.0.0.1', port: 43_210, websocketPath: '/runtime-host' },
}),
);
harness.pty.exit(0);

assert.equal((await activation).pid, 1234);
const remoteCommand = harness.launchArgs[0]?.at(-1) ?? '';
assert.match(remoteCommand, /'activate' '--framed' '--root-id'/u);
assert.match(remoteCommand, new RegExp(rootId, 'u'));
assert.doesNotMatch(remoteCommand, /credential|token/u);
await harness.terminal.close();
});

test('uploads a development release archive before running the same remote setup', async (t) => {
const directory = await mkdtemp(join(tmpdir(), 'maka-runtime-host-development-package-'));
t.after(() => rm(directory, { recursive: true, force: true }));
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,7 @@ runtimeHostManager = await startRuntimeHostDesktopManager(
console.error("[runtime-host] projection refresh failed:", error),
registerClientIpc: registerHostClientIpc,
openSshTunnel: runtimeHostSshTerminal.openSshTunnel,
activateSshOperator: runtimeHostSshTerminal.activateSshOperator,
},
{
upgradePrompts: createRuntimeHostUpgradePrompts(
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/main/runtime-host-desktop-candidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { CreateSessionRequestInput } from '@maka/core/runtime-inputs';
import type { SessionChangedEvent, SessionChangedReason } from '@maka/core/session';
import type { BotRegistry } from '@maka/runtime/bots';
import {
type RuntimeHostSshOperatorActivationInput,
connectOrSpawnRuntimeHost,
connectRemoteRuntimeHostProfile,
type RuntimeHostSshInteraction,
Expand All @@ -38,6 +39,7 @@ import {
type RemoteRuntimeHostProfile,
type CandidateExitDetails,
} from "@maka/runtime-host/client";
import type { RuntimeHostActivationResult } from "@maka/runtime-host/operator";
import {
INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
RUNTIME_HOST_PROTOCOL_VERSION,
Expand Down Expand Up @@ -125,6 +127,9 @@ export interface DesktopRuntimeHostCandidateDeps {
readonly openSshTunnel?: (
input: RuntimeHostSshTunnelInput,
) => Promise<RuntimeHostSshTunnel>;
readonly activateSshOperator?: (
input: RuntimeHostSshOperatorActivationInput,
) => Promise<RuntimeHostActivationResult>;
readonly createSessionCopyCleanup: (input: {
removeSession: (sessionId: string) => Promise<SessionCopyCleanupDisposition>;
resumeSessionCopy: (input: {
Expand Down Expand Up @@ -389,6 +394,9 @@ async function startRemoteDesktopRuntimeHostCandidate(
},
{
...(input.openSshTunnel ? { openSshTunnel: input.openSshTunnel } : {}),
...(input.activateSshOperator
? { activateSshOperator: input.activateSshOperator }
: {}),
});
try {
return {
Expand Down
29 changes: 22 additions & 7 deletions apps/desktop/src/main/runtime-host-onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export function createDesktopRuntimeHostOnboarding(input: {
): Promise<DesktopRuntimeHostOnboardingSnapshot> => {
try {
const setupPackage = await input.resolveSetupPackage(signal);
const lifecycle = setupPackage.kind === 'npm' ? 'on_demand' : 'supervised';
signal.throwIfAborted();
publish({ kind: 'running', phase: 'connecting_ssh' });
let commitStarted = false;
Expand All @@ -123,6 +124,7 @@ export function createDesktopRuntimeHostOnboarding(input: {
destination: request.destination,
...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
setupPackage,
lifecycle,
principalId: `desktop:${input.clientInstanceId}`,
...(request.projectDirectoryRoots
? { projectDirectoryRoots: request.projectDirectoryRoots }
Expand Down Expand Up @@ -153,16 +155,29 @@ export function createDesktopRuntimeHostOnboarding(input: {
kind: 'ssh',
destination: request.destination,
...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
remotePort: endpoint.port,
websocketPath: endpoint.websocketPath,
...(lifecycle === 'on_demand'
? {
activation: {
kind: 'ssh_operator' as const,
operatorPath: complete.operatorPath,
},
}
: {
remotePort: endpoint.port,
websocketPath: endpoint.websocketPath,
}),
},
},
credential: complete.credential,
managedService: {
id: complete.serviceId,
rootPath: complete.rootPath,
operatorPath: complete.operatorPath,
},
...(lifecycle === 'supervised'
? {
managedService: {
id: complete.serviceId,
rootPath: complete.rootPath,
operatorPath: complete.operatorPath,
},
}
: {}),
});
return publish({
kind: 'complete',
Expand Down
54 changes: 53 additions & 1 deletion apps/desktop/src/main/runtime-host-ssh-terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,23 @@ import type { IPty } from 'node-pty';
import { spawn as spawnPty } from 'node-pty';
import { terminateProcessTree } from '@maka/runtime/process-tree-terminator';
import {
activateRuntimeHostSshOperator,
normalizeRuntimeHostSshDestination,
openRuntimeHostSshTunnel,
type RuntimeHostSshOperatorActivationInput,
type RuntimeHostSshProcess,
type RuntimeHostSshProcessFactory,
type RuntimeHostSshTunnel,
type RuntimeHostSshTunnelInput,
} from '@maka/runtime-host/client';
import {
decodeRuntimeHostActivationFrame,
decodeRuntimeHostAccessManagementFrame,
decodeRuntimeHostPeerManagementFrame,
decodeRuntimeHostServiceManagementFrame,
decodeRuntimeHostSetupFrame,
RUNTIME_HOST_ACTIVATION_FRAME_MAX_BYTES,
RUNTIME_HOST_ACTIVATION_FRAME_PREFIX,
RUNTIME_HOST_ACCESS_MANAGEMENT_FRAME_PREFIX,
RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY,
RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV,
Expand All @@ -47,6 +52,7 @@ import {
RUNTIME_HOST_SERVICE_MANAGEMENT_FRAME_PREFIX,
RUNTIME_HOST_SETUP_FRAME_PREFIX,
type RuntimeHostAccessManagementFrame,
type RuntimeHostActivationResult,
type RuntimeHostManagedUpdatePolicy,
type RuntimeHostPeerManagementAction,
type RuntimeHostPeerManagementFrame,
Expand Down Expand Up @@ -94,6 +100,7 @@ export interface DesktopRuntimeHostSshSetupInput {
readonly sshPort?: number;
readonly setupPackage: DesktopRuntimeHostSetupPackage;
readonly principalId: string;
readonly lifecycle?: 'supervised' | 'on_demand';
readonly projectDirectoryRoots?: readonly { readonly label: string; readonly path: string }[];
readonly signal?: AbortSignal;
}
Expand Down Expand Up @@ -217,11 +224,15 @@ export function createDesktopRuntimeHostSshTerminal(input: {
readonly send: (channel: string, event: DesktopRuntimeHostSshTerminalEvent) => void;
readonly spawnPty?: typeof spawnPty;
readonly openSshTunnel?: typeof openRuntimeHostSshTunnel;
readonly activateSshOperator?: typeof activateRuntimeHostSshOperator;
readonly revealDelayMs?: number;
readonly managementTimeoutMs?: number;
readonly processStopGraceMs?: number;
readonly terminateProcessTree?: typeof terminateProcessTree;
}): {
activateSshOperator(
input: RuntimeHostSshOperatorActivationInput,
): Promise<RuntimeHostActivationResult>;
openSshTunnel(input: RuntimeHostSshTunnelInput): Promise<RuntimeHostSshTunnel>;
runSetup(
input: DesktopRuntimeHostSshSetupInput,
Expand Down Expand Up @@ -557,6 +568,27 @@ export function createDesktopRuntimeHostSshTerminal(input: {
};

return {
activateSshOperator: async (activationInput) => {
if (activationInput.interaction !== 'terminal') {
return (input.activateSshOperator ?? activateRuntimeHostSshOperator)(activationInput);
}
const frame = await runFramedManagement({
...activationInput,
remoteCommand: runtimeHostActivationRemoteCommand(activationInput),
prefix: RUNTIME_HOST_ACTIVATION_FRAME_PREFIX,
pendingMaxBytes: RUNTIME_HOST_ACTIVATION_FRAME_MAX_BYTES,
decode: decodeRuntimeHostActivationFrame,
action: 'activate',
frameAction: () => 'activate',
label: 'Remote Runtime Host activation',
timeoutMs: activationInput.timeoutMs,
});
if (frame.kind === 'error') throw new Error(frame.error.message);
if (frame.rootId !== activationInput.rootId) {
throw new Error('Remote Runtime Host activation returned an inconsistent root');
}
return frame;
},
openSshTunnel: async (tunnelInput) => {
if (closed) throw new Error('Runtime Host SSH terminal is closed');
const openSshTunnel = input.openSshTunnel ?? openRuntimeHostSshTunnel;
Expand Down Expand Up @@ -997,7 +1029,10 @@ async function settlesWithin(promise: Promise<unknown>, timeoutMs: number): Prom

function runtimeHostSetupRemoteCommand(
setupPackage: PreparedSetupPackage,
input: Pick<DesktopRuntimeHostSshSetupInput, 'principalId' | 'projectDirectoryRoots'>,
input: Pick<
DesktopRuntimeHostSshSetupInput,
'principalId' | 'projectDirectoryRoots' | 'lifecycle'
>,
): string {
if (!/^[A-Za-z0-9_.:-]{1,128}$/u.test(input.principalId)) {
throw new Error('Runtime Host setup principal is invalid');
Expand All @@ -1009,6 +1044,8 @@ function runtimeHostSetupRemoteCommand(
input.principalId,
'--preset',
'desktop-client',
'--lifecycle',
input.lifecycle === 'on_demand' ? 'on-demand' : 'supervised',
'--defer-pairing-commit',
...(input.projectDirectoryRoots === undefined
? []
Expand All @@ -1022,6 +1059,21 @@ function runtimeHostSetupRemoteCommand(
]);
}

function runtimeHostActivationRemoteCommand(
input: RuntimeHostSshOperatorActivationInput,
): string {
if (!pathPosix.isAbsolute(input.operatorPath)) {
throw new Error('Runtime Host operator path must be absolute');
}
return [
input.operatorPath,
'activate',
'--framed',
'--root-id',
input.rootId,
].map(quotePosix).join(' ');
}

function runtimeHostServiceManagementRemoteCommand(
input: DesktopRuntimeHostSshManagementInput,
): string {
Expand Down
9 changes: 6 additions & 3 deletions docs/windows-test-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ Locations intentionally omit line numbers so unrelated edits do not invalidate t

| Classification | Count |
|---|---:|
| windows-backend-gap | 23 |
| portable-candidate | 8 |
| windows-backend-gap | 24 |
| portable-candidate | 10 |
| platform-contract | 35 |

Total Windows-excluded declarations: **66**
Total Windows-excluded declarations: **69**

## Inventory

Expand All @@ -45,6 +45,7 @@ Total Windows-excluded declarations: **66**
| windows-backend-gap | `packages/runtime-host/src/__tests__/host-kernel.test.ts` a non-reading Client overload is isolated to its connection | `process.platform === 'win32'` |
| windows-backend-gap | `packages/runtime-host/src/__tests__/host-kernel.test.ts` reports one shutdown failure through close and closed while releasing ownership | `process.platform === 'win32'` |
| platform-contract | `packages/runtime-host/src/__tests__/host-kernel.test.ts` publishes private POSIX endpoint and registration permissions | `process.platform === 'win32'` |
| windows-backend-gap | `packages/runtime-host/src/__tests__/managed-activation.test.ts` two real managed activations converge on one Host and exit at true idle | `process.platform === 'win32' ? 'requires a POSIX package-entrypoint symlink' : false` |
| windows-backend-gap | `packages/runtime-host/src/__tests__/memory-two-client-uds.test.ts` two UDS clients share one recoverable Memory authority across Host death | `process.platform === 'win32' ? 'POSIX process death gate' : false` |
| windows-backend-gap | `packages/runtime-host/src/__tests__/project-catalog-two-client-uds.test.ts` two UDS clients converge on one Host-owned Project Catalog | `process.platform === 'win32'` |
| windows-backend-gap | `packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts` invalidates when a real published mutation loses its commit reply | `process.platform === 'win32'` |
Expand Down Expand Up @@ -81,13 +82,15 @@ Total Windows-excluded declarations: **66**
| platform-contract | `packages/storage/src/__tests__/root-authority.test.ts` preserves unexpected marker I/O failures at the public authority boundary | `process.platform === 'win32' ? 'POSIX permissions are required to make the marker unreadable' : typeof process.getuid === 'function' && process.getuid() === 0` |
| platform-contract | `packages/storage/src/__tests__/root-authority.test.ts` rejects FIFO marker paths without blocking root resolution | `process.platform === 'win32'` |
| platform-contract | `packages/storage/src/__tests__/root-authority.test.ts` rejects a lock path that aliases another filesystem object | `process.platform === 'win32' ? 'Windows file-symlink permissions are not guaranteed in CI' : false` |
| portable-candidate | `packages/storage/src/__tests__/root-authority.test.ts` cache deletion cannot create a second State Root owner | `process.platform === 'win32' ? 'Windows does not unlink an open native lock file' : false` |
| platform-contract | `packages/storage/src/__tests__/root-authority.test.ts` validates an existing control directory without repairing its permissions | `process.platform === 'win32'` |
| platform-contract | `packages/storage/src/__tests__/runtime-policy-stores.test.ts` reports unknown outcome when credential persistence fails after clearing verified state | `process.platform === 'win32' ? 'POSIX permissions are required to inject a persistence failure' : false` |
| platform-contract | `packages/storage/src/__tests__/runtime-policy-stores.test.ts` validates proxy policy mutations before clearing and reports failed follow-up commits as unknown | `process.platform === 'win32' ? 'POSIX permissions are required to inject a persistence failure' : false` |
| platform-contract | `packages/storage/src/__tests__/runtime-policy-stores.test.ts` reports unknown outcome when active proxy password persistence fails after clearing | `process.platform === 'win32' ? 'POSIX permissions are required to inject a persistence failure' : false` |
| platform-contract | `packages/storage/src/__tests__/runtime-policy-stores.test.ts` preserves unknown commit semantics and consumes the completion ticket | `process.platform === 'win32' ? 'POSIX permissions are required to inject a persistence failure' : false` |
| platform-contract | `packages/storage/src/__tests__/runtime-policy-stores.test.ts` successor recovery removes credentials orphaned by an interrupted connection removal | `process.platform === 'win32' ? 'POSIX permissions are required to inject a persistence failure' : false` |
| platform-contract | `packages/storage/src/__tests__/runtime-policy-stores.test.ts` fails closed on final symlinks, FIFOs, and oversized documents without changing bytes | `process.platform === 'win32'` |
| portable-candidate | `packages/storage/src/__tests__/stable-storage.test.ts` rejects a symlink instead of following it | `process.platform === 'win32' ? 'POSIX no-follow semantics are required' : false` |
| platform-contract | `packages/storage/src/__tests__/usage-stores.test.ts` classifies a renamed or replaced live root as a draining persistence failure | `process.platform === 'win32' ? 'Windows does not permit renaming a directory with an open SQLite database' : false` |
| platform-contract | `packages/storage/src/__tests__/workspace-identity.test.ts` an unmarked read-only workspace fails without leaving marker state | `process.platform === 'win32' ? 'POSIX permissions are required to create a read-only workspace fixture' : false` |
| portable-candidate | `scripts/release-cli-eval-support.test.mjs` preserves the primary process failure when diagnostics cannot be read | `process.platform === 'win32'` |
4 changes: 2 additions & 2 deletions packages/cli/src/__tests__/runtime-host-cli-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ test('remote CLI profiles pin root identity and resolve credential outside the p
},
profileCatalog: {
read: async () => ({
schemaVersion: 1,
schemaVersion: 2,
profiles: [
{
id: 'office',
Expand Down Expand Up @@ -545,7 +545,7 @@ function incompatibleRemoteHandshake(overrides: Partial<HostIncompatible> = {}):

function singleRemoteProfileCatalog(profile: RemoteRuntimeHostProfile): RuntimeHostProfileCatalog {
return {
read: async () => ({ schemaVersion: 1, profiles: [profile] }),
read: async () => ({ schemaVersion: 2, profiles: [profile] }),
resolve: async (profileId) => {
assert.equal(profileId, profile.id);
return { profile, credential: 'opaque-token' };
Expand Down
Loading
Loading