You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/10/19 00:12:40 UTC

(maka) branch main updated: fix(runtime-host): validate project roots for the target platform (#6122)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new c8f0dae03c fix(runtime-host): validate project roots for the target platform (#6122)
c8f0dae03c is described below

commit c8f0dae03cf687ada0e0e916d090770f29a6d0f7
Author: Dan Martin <93...@users.noreply.github.com>
AuthorDate: Mon Oct 19 08:12:36 2026 +0800

    fix(runtime-host): validate project roots for the target platform (#6122)
    
    Fix valid Windows project roots such as `C:\Projects` and UNC paths being rejected by Desktop and managed-service CLI commands even though `serve` accepts them.
    
    Desktop now validates against the target Host platform, keeping WSL/POSIX rules intact. SSH onboarding validates again after its Node platform probe. Managed-service parsing uses native Host path rules. Existing input, identity, CAS and interruption checks remain in place.
    
    Generated-by: Codex
---
 .github/workflows/windows-recovery.yml             | 23 ++++++++-
 .../main/__tests__/runtime-host-management.test.ts | 22 ++++++--
 .../main/__tests__/runtime-host-onboarding.test.ts | 45 +++++++++++++++++
 .../runtime-host-project-directory-policy.test.ts  | 23 ++++++++-
 .../src/main/runtime-host-local-management.ts      |  1 +
 .../src/main/runtime-host-management-provider.ts   |  1 +
 apps/desktop/src/main/runtime-host-management.ts   | 12 ++---
 apps/desktop/src/main/runtime-host-onboarding.ts   | 12 +++--
 .../settings/runtime-host-management-dialog.tsx    |  2 +-
 .../settings/runtime-host-onboarding-dialog.tsx    |  2 +-
 .../runtime-host-project-directory-policy.ts       | 18 +++++--
 .../__tests__/runtime-host-service-manager.test.ts | 59 ++++++++++++++++++++++
 packages/cli/src/runtime-host-cli.ts               | 36 ++++---------
 scripts/ci-workflow-policy.test.mjs                |  2 +-
 scripts/windows-package-source-closure.test.mjs    | 12 +++--
 15 files changed, 215 insertions(+), 55 deletions(-)

diff --git a/.github/workflows/windows-recovery.yml b/.github/workflows/windows-recovery.yml
index 1b490060db..8bdacde643 100644
--- a/.github/workflows/windows-recovery.yml
+++ b/.github/workflows/windows-recovery.yml
@@ -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
@@ -68,7 +70,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'
@@ -135,6 +147,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
@@ -206,6 +219,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: |
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 a72a25fd0c..1a3b353698 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
@@ -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';
@@ -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',
@@ -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[] = [];
@@ -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,
@@ -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>();
@@ -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 {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
index 7f21b797eb..dbb74ec469 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
@@ -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:
diff --git a/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts b/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts
index 58597427bc..c1081c9c4e 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts
@@ -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);
   }
 });
diff --git a/apps/desktop/src/main/runtime-host-local-management.ts b/apps/desktop/src/main/runtime-host-local-management.ts
index c6a1f90090..904fef8766 100644
--- a/apps/desktop/src/main/runtime-host-local-management.ts
+++ b/apps/desktop/src/main/runtime-host-local-management.ts
@@ -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) =>
diff --git a/apps/desktop/src/main/runtime-host-management-provider.ts b/apps/desktop/src/main/runtime-host-management-provider.ts
index 67bf941bc5..95344efb4c 100644
--- a/apps/desktop/src/main/runtime-host-management-provider.ts
+++ b/apps/desktop/src/main/runtime-host-management-provider.ts
@@ -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'>,
diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts
index a7fa881c88..295958bfd4 100644
--- a/apps/desktop/src/main/runtime-host-management.ts
+++ b/apps/desktop/src/main/runtime-host-management.ts
@@ -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);
@@ -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 } : {}),
         });
@@ -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 } : {}),
         });
diff --git a/apps/desktop/src/main/runtime-host-onboarding.ts b/apps/desktop/src/main/runtime-host-onboarding.ts
index 14dac44db2..80d39cf1c2 100644
--- a/apps/desktop/src/main/runtime-host-onboarding.ts
+++ b/apps/desktop/src/main/runtime-host-onboarding.ts
@@ -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';
@@ -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,
         },
@@ -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' ||
diff --git a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
index 564bf0e3a4..a472d7469b 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
@@ -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);
diff --git a/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx
index f989d6dac5..ef6edfab2f 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx
@@ -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
diff --git a/apps/desktop/src/shared/runtime-host-project-directory-policy.ts b/apps/desktop/src/shared/runtime-host-project-directory-policy.ts
index aedb4df26a..4289c17c22 100644
--- a/apps/desktop/src/shared/runtime-host-project-directory-policy.ts
+++ b/apps/desktop/src/shared/runtime-host-project-directory-policy.ts
@@ -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[] {
@@ -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');
@@ -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;
diff --git a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
index 5ad9084e66..39aa10a1ef 100644
--- a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
@@ -87,6 +87,65 @@ import {
 } from '../runtime-host-systemd-service.js';
 
 describe('managed Runtime Host service', () => {
+  it('uses native Host paths consistently for serve and managed service roots', () => {
+    const paths =
+      process.platform === 'win32'
+        ? ['C:\\projects', 'C:/projects', '\\\\server\\share\\projects']
+        : ['/srv/projects'];
+    for (const path of paths) {
+      for (const [option, value] of [
+        ['--project-root', `Work=${path}`],
+        ['--project-root-json', JSON.stringify({ label: 'Work', path })],
+      ]) {
+        assert.ok(option && value);
+        assert.deepEqual(parseRuntimeHostCommand(['serve', option, value]), {
+          kind: 'runtime-host-serve',
+          json: false,
+          projectDirectoryRoots: [{ label: 'Work', path }],
+        });
+        for (const action of ['install', 'configure']) {
+          const target =
+            action === 'configure'
+              ? [
+                  '--expected-service-id',
+                  'b'.repeat(64),
+                  '--expected-root-path',
+                  path,
+                  '--expected-root-id',
+                  'a'.repeat(64),
+                  '--expected-config-fingerprint',
+                  `sha256:${'c'.repeat(64)}`,
+                ]
+              : [];
+          const result = parseRuntimeHostCommand(['service', action, option, value, ...target]);
+          assert.equal(result.kind, 'runtime-host-service-manage', JSON.stringify(result));
+          if (result.kind !== 'runtime-host-service-manage')
+            assert.fail('expected managed command');
+          assert.deepEqual(result.projectDirectoryRoots, [{ label: 'Work', path }]);
+        }
+      }
+    }
+    for (const path of [
+      'relative',
+      'C:relative',
+      ...(process.platform === 'win32' ? [] : ['C:\\projects']),
+    ]) {
+      assert.equal(
+        parseRuntimeHostCommand(['service', 'install', '--project-root', `Work=${path}`]).kind,
+        'error',
+      );
+      assert.equal(
+        parseRuntimeHostCommand([
+          'service',
+          'install',
+          '--project-root-json',
+          JSON.stringify({ label: 'Work', path }),
+        ]).kind,
+        'error',
+      );
+    }
+  });
+
   it('parses the bounded managed service command surface', () => {
     const nativeProjectRoot = process.platform === 'win32' ? 'C:\\projects' : '/srv/projects';
     assert.deepEqual(
diff --git a/packages/cli/src/runtime-host-cli.ts b/packages/cli/src/runtime-host-cli.ts
index 3348bb9bc6..e9fb5aecfe 100644
--- a/packages/cli/src/runtime-host-cli.ts
+++ b/packages/cli/src/runtime-host-cli.ts
@@ -34,7 +34,6 @@ import {
   canonicalProjectDirectoryRootSpec,
   isCanonicalRuntimeHostWebSocketPath,
   PROJECT_DIRECTORY_MAX_ROOTS,
-  projectDirectoryPosixRootSpecValid,
   projectDirectoryRootSpecValid,
 } from '@maka/runtime-host/protocol';
 import type { RuntimeHostManagedServiceTarget } from './runtime-host-service-manager.js';
@@ -1552,9 +1551,7 @@ function parseManagedServiceOptions(
         }
         projectDirectoryPolicySpecified = true;
         const root =
-          argument === '--project-root'
-            ? parseProjectRoot(parsed, 'posix')
-            : parseProjectRootJson(parsed, 'posix');
+          argument === '--project-root' ? parseProjectRoot(parsed) : parseProjectRootJson(parsed);
         if ('kind' in root) return root;
         if (projectDirectoryRoots.length >= PROJECT_DIRECTORY_MAX_ROOTS) {
           return error(
@@ -2086,9 +2083,7 @@ function parseServeCommand(argv: string[]): RuntimeHostCliCommand {
       const parsed = optionValue(argv, index, argument);
       if (typeof parsed !== 'string') return parsed;
       const root =
-        argument === '--project-root'
-          ? parseProjectRoot(parsed, 'native')
-          : parseProjectRootJson(parsed, 'native');
+        argument === '--project-root' ? parseProjectRoot(parsed) : parseProjectRootJson(parsed);
       if ('kind' in root) return root;
       if (projectDirectoryRoots.length >= PROJECT_DIRECTORY_MAX_ROOTS) {
         return error(`--project-root may be provided at most ${PROJECT_DIRECTORY_MAX_ROOTS} times`);
@@ -2277,10 +2272,7 @@ function parseServeCommand(argv: string[]): RuntimeHostCliCommand {
   };
 }
 
-function parseProjectRoot(
-  value: string,
-  pathKind: 'native' | 'posix',
-): { label: string; path: string } | RuntimeHostCliError {
+function parseProjectRoot(value: string): { label: string; path: string } | RuntimeHostCliError {
   const separator = value.indexOf('=');
   if (separator <= 0 || separator === value.length - 1) {
     return error('--project-root must use <label>=<absolute-path>');
@@ -2289,17 +2281,14 @@ function parseProjectRoot(
     label: value.slice(0, separator),
     path: value.slice(separator + 1),
   });
-  if (!projectRootValid(root, pathKind)) {
-    return error(
-      `--project-root must use a valid label and absolute ${pathKind === 'posix' ? 'POSIX' : 'Host'} path`,
-    );
+  if (!projectRootValid(root)) {
+    return error(`--project-root must use a valid label and absolute Host path`);
   }
   return root;
 }
 
 function parseProjectRootJson(
   value: string,
-  pathKind: 'native' | 'posix',
 ): { label: string; path: string } | RuntimeHostCliError {
   let parsed: unknown;
   try {
@@ -2322,21 +2311,14 @@ function parseProjectRootJson(
     label: root.label,
     path: root.path,
   });
-  if (!projectRootValid(canonical, pathKind)) {
-    return error(
-      `--project-root-json must use a valid label and absolute ${pathKind === 'posix' ? 'POSIX' : 'Host'} path`,
-    );
+  if (!projectRootValid(canonical)) {
+    return error(`--project-root-json must use a valid label and absolute Host path`);
   }
   return canonical;
 }
 
-function projectRootValid(
-  root: { readonly label: string; readonly path: string },
-  pathKind: 'native' | 'posix',
-): boolean {
-  return pathKind === 'posix'
-    ? projectDirectoryPosixRootSpecValid(root)
-    : projectDirectoryRootSpecValid(root) && isAbsolute(root.path);
+function projectRootValid(root: { readonly label: string; readonly path: string }): boolean {
+  return projectDirectoryRootSpecValid(root) && isAbsolute(root.path);
 }
 
 function parseAccessCommand(argv: string[]): RuntimeHostCliCommand {
diff --git a/scripts/ci-workflow-policy.test.mjs b/scripts/ci-workflow-policy.test.mjs
index 3595ef3980..293ecad845 100644
--- a/scripts/ci-workflow-policy.test.mjs
+++ b/scripts/ci-workflow-policy.test.mjs
@@ -715,7 +715,7 @@ test('the recovery lane leaves the suites it executes to the required test lane'
       [...workflow.matchAll(/packages[/\\]([^/\\]+)[/\\]dist[/\\]/gu)].map((match) => match[1]),
     ),
   ].sort();
-  assert.deepEqual(executed, ['runtime', 'runtime-host', 'storage']);
+  assert.deepEqual(executed, ['cli', 'runtime', 'runtime-host', 'storage']);
 
   // None of that closure belongs in the filter. These suites are ordinary
   // TypeScript, so `test` runs them on Linux on every pull request and fails
diff --git a/scripts/windows-package-source-closure.test.mjs b/scripts/windows-package-source-closure.test.mjs
index 8af252ca14..f24c5e40de 100644
--- a/scripts/windows-package-source-closure.test.mjs
+++ b/scripts/windows-package-source-closure.test.mjs
@@ -93,7 +93,7 @@ test('the Windows package workflow path list has no duplicate entries', () => {
  * `RELEASE_CONTRACT_FILES` so that editing the filter selects the gate that
  * checks it.
  */
-test('the recovery filter is exactly the Windows-branching closure of its tests', async () => {
+test('the recovery filter is exactly the Windows-specific closure of its tests', async () => {
   const workflow = readFileSync(
     new URL('../.github/workflows/windows-recovery.yml', import.meta.url),
     'utf8',
@@ -133,11 +133,15 @@ test('the recovery filter is exactly the Windows-branching closure of its tests'
   // because it names every Windows-only path: `git-worktree-child-executor.ts`
   // carries a genuine Windows workaround with no such literal. A branch written
   // without it stays outside this filter, and the lane's schedule is what
-  // observes it.
+  // observes it. The CLI project-root parser is an explicit exception: its
+  // node:path.isAbsolute call dispatches to Windows semantics without a win32
+  // literal, and the native Host path regression above exercises that behavior.
   const closure = await collectWorkspaceSourceClosure(entrypoints);
   const windowsBranching = closure
-    .filter((path) =>
-      readFileSync(new URL(`../${path}`, import.meta.url), 'utf8').includes('win32'),
+    .filter(
+      (path) =>
+        path === 'packages/cli/src/runtime-host-cli.ts' ||
+        readFileSync(new URL(`../${path}`, import.meta.url), 'utf8').includes('win32'),
     )
     .sort();
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org