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
23 changes: 22 additions & 1 deletion .github/workflows/windows-recovery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ name: Windows recovery
# regression this lane has caught that no other lane could have. That second
# group is generated rather than curated, because a hand-picked list cannot be
# shown to be complete and this one has to be.
# The CLI project-root parser is also included: node:path.isAbsolute has native
# Windows behavior without a win32 literal in the caller.
#
# What is deliberately absent is the portable rest of those workspaces. Every
# source directory in the closure used to be here, which put this lane on 118 of
Expand Down Expand Up @@ -65,7 +67,17 @@ on:
# `process.platform === 'win32'`. Generated, not curated:
# `scripts/windows-package-source-closure.test.mjs` recomputes this set
# from the dist tests the steps run and fails on any difference in either
# direction.
# direction, including the CLI's native path parser described above.
- 'packages/cli/src/__tests__/runtime-host-service-manager.test.ts'
- 'packages/cli/src/runtime-host-cli-installation.ts'
- 'packages/cli/src/runtime-host-cli.ts'
- 'packages/cli/src/runtime-host-lifecycle-transaction.ts'
- 'packages/cli/src/runtime-host-managed-deployment.ts'
- 'packages/cli/src/runtime-host-package-deployment.ts'
- 'packages/cli/src/runtime-host-peer-artifact.ts'
- 'packages/cli/src/runtime-host-service-management-command.ts'
- 'packages/cli/src/runtime-host-update-command.ts'
- 'packages/cli/src/runtime-host-windows-task-launcher-artifact.ts'
- 'packages/core/src/diagnostic-log.ts'
- 'packages/runtime-host/src/__tests__/control-endpoint.test.ts'
- 'packages/runtime-host/src/__tests__/fixtures/endpoint-hygiene.ts'
Expand Down Expand Up @@ -132,6 +144,7 @@ on:
- 'packages/storage/src/session-bundle-file-service.ts'
- 'packages/storage/src/sqlite-long-term-memory-store.ts'
- 'packages/storage/src/stable-storage.ts'
- 'packages/storage/src/workspace-root.ts'
- '.github/workflows/windows-recovery.yml'
# Unfiltered on purpose: required_status_checks is `strict: false`, so a pull
# request goes green against a stale base and only the merged result proves
Expand Down Expand Up @@ -199,6 +212,14 @@ jobs:
exit 1
}

# The remaining service-manager tests exercise POSIX service backends.
- name: Verify native Runtime Host project-root parsing
shell: pwsh
run: >-
node.exe --test
--test-name-pattern="uses native Host paths consistently|parses the bounded managed service"
packages/cli/dist/__tests__/runtime-host-service-manager.test.js

- name: Verify root initialization replacement race
shell: pwsh
run: |
Expand Down
22 changes: 17 additions & 5 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
RUNTIME_HOST_OPERATOR_PEER_WEBRTC_STUN_CAPABILITY,
runtimeHostAccessCredentialFingerprint,
type RuntimeHostServiceManagementFrame,
type RuntimeHostOperatorCommand,
} from '@maka/runtime-host/operator';
import { createDesktopRuntimeHostManagement } from '../runtime-host-management.js';
import { createDesktopRuntimeHostPeerMeshManagement } from '../runtime-host-peer-mesh-management.js';
Expand Down Expand Up @@ -793,7 +794,12 @@ test('publishes update progress and waits for the managed profile to reconnect',
);
});

test('configures Project roots with CAS and reconnects only after a committed cutover', async () => {
for (const platform of ['posix', 'win32'] as const) {
test(`configures ${platform} Project roots with CAS and reconnects only after a committed cutover`, () =>
assertConfiguresProjectRoots(platform));
}

async function assertConfiguresProjectRoots(platform: 'posix' | 'win32'): Promise<void> {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
id: 'office',
Expand All @@ -810,7 +816,9 @@ test('configures Project roots with CAS and reconnects only after a committed cu
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operator: OPERATOR,
operator: platform === 'posix' ? OPERATOR : {
kind: 'node' as const, platform, nodePath: 'C:\\node.exe', modulePath: 'C:\\Maka\\operator.mjs',
},
};
const fingerprint = `sha256:${'c'.repeat(64)}`;
const inputs: DesktopRuntimeHostSshManagementInput[] = [];
Expand Down Expand Up @@ -859,7 +867,11 @@ test('configures Project roots with CAS and reconnects only after a committed cu
});
const configure = handlers.get('runtime-host-management:configure-project-directories');
assert.ok(configure);
const roots = [{ label: 'Work', path: '/srv/work ' }];
const roots = [{ label: 'Work', path: platform === 'win32' ? 'C:\\work ' : '/srv/work ' }];
if (platform === 'posix') {
await assert.rejects(async () => configure({}, profile.id, [{ label: 'Work', path: 'C:\\work' }], fingerprint, false), /Project directory is invalid/u);
assert.equal(inputs.length, 0);
}
const blocked = await configure({}, profile.id, roots, fingerprint, false);
assert.equal(
(blocked as { configuration: { kind: string } }).configuration.kind,
Expand Down Expand Up @@ -899,7 +911,7 @@ test('configures Project roots with CAS and reconnects only after a committed cu
'The Runtime Host change was applied, but Desktop could not reconnect: authentication required',
},
);
});
}

test('manages one Host update policy and reconciles it through the bound operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
Expand Down Expand Up @@ -1431,7 +1443,7 @@ function managedSshBinding() {

function managedBinding<
Profile,
Service extends { readonly id: string; readonly rootPath: string; readonly operator: typeof OPERATOR },
Service extends { readonly id: string; readonly rootPath: string; readonly operator: RuntimeHostOperatorCommand },
State extends 'active' | 'uninstalling' | 'cleanup_pending',
>(profile: Profile, service: Service, state: State) {
return {
Expand Down
45 changes: 45 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,51 @@ const OPERATOR = {
modulePath: '/home/operator/.local/share/maka/operator.mjs',
};

test('checks SSH project directories against the probed Host platform before setup', async () => {
const roots = [{ label: 'Work', path: 'C:\\projects' }];
for (const platform of ['win32', 'linux'] as const) {
let setupCalls = 0;
const harness = createHarness({
resolveSshNodeIdentity: async () => ({ platform, architecture: 'x64' }),
profiles: { addAndEnableVerified: async (input) => ({ profileId: input.profile.id }) },
runSetup: async (input) => {
setupCalls++;
assert.equal(input.remotePlatform, 'win32');
assert.deepEqual(input.projectDirectoryRoots, roots);
return {
serviceId: 'b'.repeat(64), deploymentId: '00000000-0000-4000-8000-000000000001',
rootPath: 'C:\\Maka', rootId: 'a'.repeat(64),
operator: { kind: 'node', platform: 'win32', nodePath: 'C:\\node.exe', modulePath: 'C:\\Maka\\operator.mjs' },
endpoint: 'ws://127.0.0.1:7443/runtime-host', credential: 'secret-access-token',
};
},
});
try {
const result = await harness.invoke('runtime-host-onboarding:start', {
kind: 'ssh', name: 'Lab', destination: 'operator@example.com', projectDirectoryRoots: roots,
});
assert.equal((result as { kind: string }).kind, platform === 'win32' ? 'complete' : 'failed');
assert.equal(setupCalls, platform === 'win32' ? 1 : 0);
if (platform === 'linux') assert.match(JSON.stringify(result), /Project directory is invalid/u);
} finally {
await harness.onboarding.close();
}
}
});

test('rejects Windows directories for WSL before starting setup', async () => {
const harness = createHarness();
try {
const result = await harness.invoke('runtime-host-onboarding:start', {
kind: 'wsl', distribution: 'Ubuntu', projectDirectoryRoots: [{ label: 'Work', path: 'C:\\work' }],
});
assert.equal((result as { kind: string }).kind, 'failed');
assert.match(JSON.stringify(result), /Project directory is invalid/u);
} finally {
await harness.onboarding.close();
}
});

test('persists a verified on-demand SSH profile without endpoint or credential projection', async () => {
let setupInput: unknown;
let saved:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,26 @@ test('preserves opaque remote paths while normalizing labels', () => {
test('rejects paths that are not absolute on the remote POSIX Host', () => {
for (const path of ['srv/work', 'C:\\work']) {
const roots = [{ label: 'Work', path }];
assert.equal(projectDirectoryRootsValid(roots), false);
assert.throws(() => requireProjectDirectoryRoots(roots), /Project directory is invalid/u);
assert.equal(projectDirectoryRootsValid(roots, 'posix'), false);
assert.throws(() => requireProjectDirectoryRoots(roots, 'posix'), /Project directory is invalid/u);
}
});

test('validates absolute paths for the target Host rather than the Desktop platform', () => {
for (const path of ['C:\\work', 'C:/work', '\\\\server\\share\\work', '/work']) {
const roots = [{ label: 'Work', path }];
assert.equal(projectDirectoryRootsValid(roots, 'win32'), true, path);
assert.deepEqual(requireProjectDirectoryRoots(roots, 'win32'), roots);
assert.equal(projectDirectoryRootsValid(roots), true, path);
}
for (const platform of ['posix', 'win32', undefined] as const) {
for (const path of ['work', 'C:work', '', '/work\nother']) {
const roots = [{ label: 'Work', path }];
assert.equal(projectDirectoryRootsValid(roots, platform), false, path);
assert.throws(() => requireProjectDirectoryRoots(roots, platform), /invalid/u);
}
const duplicates = [{ label: 'Work', path: '/one' }, { label: ' Work ', path: '/two' }];
assert.equal(projectDirectoryRootsValid(duplicates, platform), false);
assert.throws(() => requireProjectDirectoryRoots(duplicates, platform), /unique/u);
}
});
1 change: 1 addition & 0 deletions apps/desktop/src/main/runtime-host-local-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function createDesktopRuntimeHostLocalManagement(input: {
}): DesktopRuntimeHostManagementProvider {
return {
profileId: LOCAL_RUNTIME_HOST_PROFILE.id,
platform: process.platform === 'win32' ? 'win32' : 'posix',
accessManagementAvailable: false,
run: (action, allowInterruptActiveTasks) => {
const execute = (target: DesktopRuntimeHostLocalManagementTarget) =>
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/runtime-host-management-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type DesktopRuntimeHostManagementTerminalFrame = Exclude<

export interface DesktopRuntimeHostManagementProvider {
readonly profileId: string;
readonly platform: 'posix' | 'win32';
readonly accessManagementAvailable: boolean;
run(
action: Exclude<DesktopRuntimeHostManagementAction, 'uninstall'>,
Expand Down
12 changes: 6 additions & 6 deletions apps/desktop/src/main/runtime-host-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,10 +721,10 @@ export function createDesktopRuntimeHostManagement(input: {
if (provider) {
const previousHostEpoch = provider.currentHostEpoch();
execute = () => provider.configureProjectDirectories(
roots,
expectedConfigFingerprintValue,
allowInterruptActiveTasksValue,
);
requireProjectDirectoryRoots(roots, provider.platform),
expectedConfigFingerprintValue,
allowInterruptActiveTasksValue,
);
reconnect = () => provider.awaitUpdatedConnection(previousHostEpoch, true);
} else {
const { managed, expectedTarget } = await activeManagedTarget(profileId);
Expand All @@ -735,7 +735,7 @@ export function createDesktopRuntimeHostManagement(input: {
operator: managed.profile.operator,
action: 'configure',
expectedTarget,
projectDirectoryRoots: roots,
projectDirectoryRoots: requireProjectDirectoryRoots(roots, 'posix'),
expectedConfigFingerprint: expectedConfigFingerprintValue,
...(allowInterruptActiveTasksValue ? { allowInterruptActiveTasks: true } : {}),
});
Expand All @@ -747,7 +747,7 @@ export function createDesktopRuntimeHostManagement(input: {
operator: managed.control.operator,
action: 'configure',
expectedTarget,
projectDirectoryRoots: roots,
projectDirectoryRoots: requireProjectDirectoryRoots(roots, managed.control.operator.kind === 'node' ? managed.control.operator.platform : 'posix'),
expectedConfigFingerprint: expectedConfigFingerprintValue,
...(allowInterruptActiveTasksValue ? { allowInterruptActiveTasks: true } : {}),
});
Expand Down
12 changes: 8 additions & 4 deletions apps/desktop/src/main/runtime-host-onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export function createDesktopRuntimeHostOnboarding(input: {
return await runWsl(request, setupPackage, signal);
}
const nodeIdentity = await resolveSshNodeIdentity(request, signal);
const remotePlatform = nodeIdentity.platform === 'win32' ? 'win32' : 'posix';
const projectDirectoryRoots = request.projectDirectoryRoots === undefined
? undefined
: requireProjectDirectoryRoots(request.projectDirectoryRoots, remotePlatform);
const peerTarget = input.setupPackageMode === 'development'
? runtimeHostPeerTargetFromNode(nodeIdentity.platform, nodeIdentity.architecture)
: 'none';
Expand All @@ -166,11 +170,11 @@ export function createDesktopRuntimeHostOnboarding(input: {
destination: request.destination,
...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
setupPackage,
remotePlatform: nodeIdentity.platform === 'win32' ? 'win32' : 'posix',
remotePlatform,
lifecycle,
principalId: `desktop:${input.clientInstanceId}`,
...(request.projectDirectoryRoots
? { projectDirectoryRoots: request.projectDirectoryRoots }
...(projectDirectoryRoots
? { projectDirectoryRoots }
: {}),
signal,
},
Expand Down Expand Up @@ -348,7 +352,7 @@ function requireOnboardingInput(value: unknown): DesktopRuntimeHostOnboardingInp
}
const roots = input.projectDirectoryRoots === undefined
? undefined
: requireProjectDirectoryRoots(input.projectDirectoryRoots);
: requireProjectDirectoryRoots(input.projectDirectoryRoots, input.kind === 'wsl' ? 'posix' : undefined);
if (input.kind === 'wsl') {
if (
typeof input.distribution !== 'string' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ export function RuntimeHostManagementDialog(props: {
const automaticUpdatesAvailable = updatePolicy?.schedulingState === 'ready';
const directoryRoots = directoryPolicyEdit?.draft ?? [];
const normalizedDirectoryRoots = canonicalProjectDirectoryRoots(directoryRoots);
const directoryRootsAreValid = projectDirectoryRootsValid(directoryRoots);
const directoryRootsAreValid = projectDirectoryRootsValid(directoryRoots, service?.platform === 'win32' ? 'win32' : 'posix');
const directoryRootsDirty =
directoryPolicyEdit !== undefined &&
JSON.stringify(normalizedDirectoryRoots) !== JSON.stringify(directoryPolicyEdit.baseline.roots);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function RuntimeHostOnboardingDialog(props: {
(targetKind === 'wsl'
? distribution.trim().length > 0
: destination.trim().length > 0 && validOptionalPort(sshPort)) &&
projectDirectoryRootsValid(projectDirectoryRoots);
projectDirectoryRootsValid(projectDirectoryRoots, targetKind === 'wsl' ? 'posix' : undefined);

async function start(): Promise<void> {
const roots = projectDirectoryRoots.length === 0
Expand Down
18 changes: 15 additions & 3 deletions apps/desktop/src/shared/runtime-host-project-directory-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@
import {
canonicalProjectDirectoryRootSpec,
PROJECT_DIRECTORY_MAX_ROOTS,
projectDirectoryPosixRootSpecValid,
projectDirectoryRootSpecValid,
type ProjectDirectoryRootSpec,
} from '@maka/runtime-host/protocol';

export type RuntimeHostProjectDirectoryRootInput = ProjectDirectoryRootSpec;

// Undefined is only a preliminary check while SSH onboarding has not probed
// the target yet. Main validates again with the discovered operator platform.
type DirectoryPlatform = 'posix' | 'win32';

function directoryRootValid(root: ProjectDirectoryRootSpec, platform?: DirectoryPlatform): boolean {
if (!projectDirectoryRootSpecValid(root)) return false;
if (root.path.startsWith('/')) return true;
return platform !== 'posix' && (/^[A-Za-z]:[\\/]/u.test(root.path) || root.path.startsWith('\\'));
}

export function canonicalProjectDirectoryRoots(
roots: readonly RuntimeHostProjectDirectoryRootInput[],
): readonly RuntimeHostProjectDirectoryRootInput[] {
Expand All @@ -34,17 +44,19 @@ export function canonicalProjectDirectoryRoots(

export function projectDirectoryRootsValid(
roots: readonly RuntimeHostProjectDirectoryRootInput[],
platform?: DirectoryPlatform,
): boolean {
if (roots.length > PROJECT_DIRECTORY_MAX_ROOTS) return false;
const canonical = canonicalProjectDirectoryRoots(roots);
return (
canonical.every(projectDirectoryPosixRootSpecValid) &&
canonical.every((root) => directoryRootValid(root, platform)) &&
new Set(canonical.map(({ label }) => label)).size === canonical.length
);
}

export function requireProjectDirectoryRoots(
value: unknown,
platform?: DirectoryPlatform,
): readonly RuntimeHostProjectDirectoryRootInput[] {
if (!Array.isArray(value) || value.length > PROJECT_DIRECTORY_MAX_ROOTS) {
throw new Error('Runtime Host Project directory policy is invalid');
Expand All @@ -62,7 +74,7 @@ export function requireProjectDirectoryRoots(
throw new Error('Runtime Host Project directory is invalid');
}
const canonical = canonicalProjectDirectoryRootSpec({ label: root.label, path: root.path });
if (!projectDirectoryPosixRootSpecValid(canonical)) {
if (!directoryRootValid(canonical, platform)) {
throw new Error('Runtime Host Project directory is invalid');
}
return canonical;
Expand Down
Loading
Loading