You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/08/28 13:44:48 UTC

(maka) branch main updated: fix(runtime-host): recover WSL roots after remount (#4084)

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

M4n5ter 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 9baa499b0 fix(runtime-host): recover WSL roots after remount (#4084)
9baa499b0 is described below

commit 9baa499b037f5788e198238551610046dc185926
Author: Wang <m4...@gmail.com>
AuthorDate: Fri Aug 28 21:44:44 2026 +0800

    fix(runtime-host): recover WSL roots after remount (#4084)
---
 .../src/main/runtime-host-wsl-controller.ts        |  1 +
 .../runtime-host-operator-command.test.ts          | 15 ++++++
 packages/cli/src/cli-core.ts                       | 11 ++++-
 .../cli/src/runtime-host-activation-command.ts     |  8 +++-
 packages/cli/src/runtime-host-cli.ts               | 16 +++++++
 packages/cli/src/runtime-host-connect-command.ts   |  9 +++-
 packages/cli/src/runtime-host-setup-command.ts     | 24 +++++-----
 .../src/__tests__/wsl-environment.test.ts          |  1 +
 .../runtime-host/src/client/wsl-environment.ts     |  1 +
 .../src/operator/managed-deployment.ts             | 10 ++++
 .../storage/src/__tests__/root-authority.test.ts   | 38 ++++++++++++++++
 packages/storage/src/root-authority.ts             | 53 ++++++++++++++++++++++
 12 files changed, 170 insertions(+), 17 deletions(-)

diff --git a/apps/desktop/src/main/runtime-host-wsl-controller.ts b/apps/desktop/src/main/runtime-host-wsl-controller.ts
index 63bd2e074..a7b92d44d 100644
--- a/apps/desktop/src/main/runtime-host-wsl-controller.ts
+++ b/apps/desktop/src/main/runtime-host-wsl-controller.ts
@@ -185,6 +185,7 @@ function runtimeHostWslSetupCommand(
     'desktop-client',
     '--lifecycle',
     'on-demand',
+    '--repair-root-after-remount',
     ...(input.projectDirectoryRoots === undefined
       ? []
       : input.projectDirectoryRoots.length === 0
diff --git a/packages/cli/src/__tests__/runtime-host-operator-command.test.ts b/packages/cli/src/__tests__/runtime-host-operator-command.test.ts
index b11796ec6..e2236ddad 100644
--- a/packages/cli/src/__tests__/runtime-host-operator-command.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-operator-command.test.ts
@@ -49,6 +49,21 @@ describe('Runtime Host operator commands', () => {
       framed: true,
     });
     assert.equal(parseRuntimeHostCommand(['activate', '--root-id', rootId]).kind, 'error');
+    assert.deepEqual(
+      parseRuntimeHostCommand([
+        'connect',
+        '--framed',
+        '--root-id',
+        rootId,
+        '--repair-root-after-remount',
+      ]),
+      {
+        kind: 'runtime-host-managed-connect',
+        rootId,
+        framed: true,
+        repairRootAfterRemount: true,
+      },
+    );
 
     let output = '';
     assert.equal(
diff --git a/packages/cli/src/cli-core.ts b/packages/cli/src/cli-core.ts
index 95046512e..8ee356dd7 100644
--- a/packages/cli/src/cli-core.ts
+++ b/packages/cli/src/cli-core.ts
@@ -248,11 +248,17 @@ export async function runMakaCli(
       const { runRuntimeHostManagedActivationCli } = await import(
         './runtime-host-activation-command.js'
       );
-      return runRuntimeHostManagedActivationCli({ rootId: command.rootId });
+      return runRuntimeHostManagedActivationCli({
+        rootId: command.rootId,
+        ...(command.repairRootAfterRemount ? { repairRootAfterRemount: true } : {}),
+      });
     }
     case 'runtime-host-managed-connect': {
       const { runRuntimeHostManagedConnectCli } = await import('./runtime-host-connect-command.js');
-      return runRuntimeHostManagedConnectCli({ rootId: command.rootId });
+      return runRuntimeHostManagedConnectCli({
+        rootId: command.rootId,
+        ...(command.repairRootAfterRemount ? { repairRootAfterRemount: true } : {}),
+      });
     }
     case 'run': {
       const { runRuntimeHostTextCli } = await import('./runtime-host-run-command.js');
@@ -403,6 +409,7 @@ export async function runMakaCli(
         lifecycle: command.lifecycle,
         deferPairingCommit: command.deferPairingCommit,
         bindPairingToClient: command.bindPairingToClient,
+        ...(command.repairRootAfterRemount ? { repairRootAfterRemount: true } : {}),
         ...(command.rootPath ? { rootPath: command.rootPath } : {}),
         ...(command.projectDirectoryRoots
           ? { projectDirectoryRoots: command.projectDirectoryRoots }
diff --git a/packages/cli/src/runtime-host-activation-command.ts b/packages/cli/src/runtime-host-activation-command.ts
index a10c85555..cb47a48ec 100644
--- a/packages/cli/src/runtime-host-activation-command.ts
+++ b/packages/cli/src/runtime-host-activation-command.ts
@@ -32,6 +32,7 @@ import { reconcileRuntimeHostUpdateOnActivation } from './runtime-host-update-re
 
 export interface RuntimeHostManagedActivationCliOptions {
   readonly rootId: string;
+  readonly repairRootAfterRemount?: true;
 }
 
 export function activateRuntimeHostManagedDeploymentWithReconciliation(
@@ -54,7 +55,12 @@ export async function runRuntimeHostManagedActivationCli(
   try {
     const result = await (
       overrides.activate ?? activateRuntimeHostManagedDeploymentWithReconciliation
-    )({ rootId: options.rootId });
+    )({
+      rootId: options.rootId,
+      ...(options.repairRootAfterRemount
+        ? { authority: { repairRootAfterRemount: true as const } }
+        : {}),
+    });
     writeOutput(encodeRuntimeHostActivationFrame(result));
     return 0;
   } catch (error) {
diff --git a/packages/cli/src/runtime-host-cli.ts b/packages/cli/src/runtime-host-cli.ts
index d48ef2272..cf2f884e1 100644
--- a/packages/cli/src/runtime-host-cli.ts
+++ b/packages/cli/src/runtime-host-cli.ts
@@ -44,11 +44,13 @@ export type RuntimeHostCliCommand =
       kind: 'runtime-host-managed-activate';
       rootId: string;
       framed: true;
+      repairRootAfterRemount?: true;
     }
   | {
       kind: 'runtime-host-managed-connect';
       rootId: string;
       framed: true;
+      repairRootAfterRemount?: true;
     }
   | {
       kind: 'runtime-host-installed-update';
@@ -115,6 +117,7 @@ export type RuntimeHostCliCommand =
       lifecycle: 'supervised' | 'on_demand';
       deferPairingCommit: boolean;
       bindPairingToClient?: true;
+      repairRootAfterRemount?: true;
       clientDataRoot?: string;
       rootPath?: string;
       projectDirectoryRoots?: { label: string; path: string }[];
@@ -325,6 +328,7 @@ function parseManagedRootFramedCommand(
 ): RuntimeHostCliCommand {
   let rootId: string | undefined;
   let framed = false;
+  let repairRootAfterRemount = false;
   for (let index = 0; index < argv.length; index += 1) {
     const argument = argv[index];
     if (argument === '--framed') {
@@ -339,6 +343,11 @@ function parseManagedRootFramedCommand(
       if (rootId === undefined) return error('--root-id requires a value');
       continue;
     }
+    if (argument === '--repair-root-after-remount') {
+      if (repairRootAfterRemount) return error('Duplicate --repair-root-after-remount');
+      repairRootAfterRemount = true;
+      continue;
+    }
     return error(`Unexpected runtime-host ${action} option: ${String(argument)}`);
   }
   if (!framed) return error(`runtime-host ${action} requires --framed`);
@@ -349,6 +358,7 @@ function parseManagedRootFramedCommand(
     kind: action === 'activate' ? 'runtime-host-managed-activate' : 'runtime-host-managed-connect',
     rootId,
     framed: true,
+    ...(repairRootAfterRemount ? { repairRootAfterRemount: true } : {}),
   };
 }
 
@@ -544,6 +554,7 @@ function parseSetupCommand(argv: string[]): RuntimeHostCliCommand {
   let lifecycleProvided = false;
   let deferPairingCommit = false;
   let bindPairingToClient = false;
+  let repairRootAfterRemount = false;
   let clientDataRoot: string | undefined;
   let enableDirectPeer = false;
   const coordinationRelays: string[] = [];
@@ -588,6 +599,10 @@ function parseSetupCommand(argv: string[]): RuntimeHostCliCommand {
         if (bindPairingToClient) return error('Duplicate --bind-pairing-to-client');
         bindPairingToClient = true;
       },
+      '--repair-root-after-remount': () => {
+        if (repairRootAfterRemount) return error('Duplicate --repair-root-after-remount');
+        repairRootAfterRemount = true;
+      },
     },
   });
   if ('kind' in options) return options;
@@ -609,6 +624,7 @@ function parseSetupCommand(argv: string[]): RuntimeHostCliCommand {
     lifecycle,
     deferPairingCommit,
     ...(bindPairingToClient ? { bindPairingToClient: true } : {}),
+    ...(repairRootAfterRemount ? { repairRootAfterRemount: true } : {}),
     ...(clientDataRoot ? { clientDataRoot } : {}),
     ...(enableDirectPeer ? { directPeer: { coordinationRelays } } : {}),
   };
diff --git a/packages/cli/src/runtime-host-connect-command.ts b/packages/cli/src/runtime-host-connect-command.ts
index e49070da4..c7cb12fda 100644
--- a/packages/cli/src/runtime-host-connect-command.ts
+++ b/packages/cli/src/runtime-host-connect-command.ts
@@ -23,7 +23,7 @@ import { openRuntimeHostManagedStdioBridge } from '@maka/runtime-host/client';
 import { activateRuntimeHostManagedDeploymentWithReconciliation } from './runtime-host-activation-command.js';
 
 export async function runRuntimeHostManagedConnectCli(
-  input: { readonly rootId: string },
+  input: { readonly rootId: string; readonly repairRootAfterRemount?: true },
   overrides: {
     readonly openBridge?: typeof openRuntimeHostManagedStdioBridge;
     readonly stdin?: Readable;
@@ -36,7 +36,12 @@ export async function runRuntimeHostManagedConnectCli(
   let socket: Awaited<ReturnType<typeof openRuntimeHostManagedStdioBridge>> | undefined;
   try {
     socket = await (overrides.openBridge ?? openRuntimeHostManagedStdioBridge)(
-      { rootId: input.rootId },
+      {
+        rootId: input.rootId,
+        ...(input.repairRootAfterRemount
+          ? { authority: { repairRootAfterRemount: true as const } }
+          : {}),
+      },
       { activate: activateRuntimeHostManagedDeploymentWithReconciliation },
     );
     stdin.pipe(socket);
diff --git a/packages/cli/src/runtime-host-setup-command.ts b/packages/cli/src/runtime-host-setup-command.ts
index dfd556a1d..d578e1370 100644
--- a/packages/cli/src/runtime-host-setup-command.ts
+++ b/packages/cli/src/runtime-host-setup-command.ts
@@ -76,7 +76,7 @@ import {
   RuntimeHostUpdateDiscoveryError,
   type RuntimeHostUpdateCandidate,
 } from './runtime-host-update-discovery.js';
-import { resolveStorageRoot } from '@maka/storage/root-authority';
+import { repairStorageRootAfterRemount, resolveStorageRoot } from '@maka/storage/root-authority';
 import {
   createPlatformRuntimeHostServiceBackend,
   discoverRuntimeHostLifecycleProvider,
@@ -131,6 +131,7 @@ export interface RuntimeHostSetupCliOptions {
   readonly lifecycle?: 'supervised' | 'on_demand';
   readonly deferPairingCommit?: boolean;
   readonly bindPairingToClient?: boolean;
+  readonly repairRootAfterRemount?: true;
   readonly rootPath?: string;
   readonly projectDirectoryRoots?: readonly {
     readonly label: string;
@@ -305,17 +306,16 @@ async function resolveRuntimeHostSetupRootId(options: RuntimeHostSetupCliOptions
       throw error;
     }
   }
-  return (
-    await resolveStorageRoot({
-      path: resolve(
-        options.rootPath ??
-          legacyRootPath ??
-          options.expectedTarget?.rootPath ??
-          options.defaultRootPath,
-      ),
-      kind: 'interactive',
-    })
-  ).rootId;
+  const path = resolve(
+    options.rootPath ??
+      legacyRootPath ??
+      options.expectedTarget?.rootPath ??
+      options.defaultRootPath,
+  );
+  if (options.repairRootAfterRemount) {
+    await repairStorageRootAfterRemount({ path, kind: 'interactive' });
+  }
+  return (await resolveStorageRoot({ path, kind: 'interactive' })).rootId;
 }
 
 async function runRuntimeHostSetupLocked(
diff --git a/packages/runtime-host/src/__tests__/wsl-environment.test.ts b/packages/runtime-host/src/__tests__/wsl-environment.test.ts
index 6a36d804a..722be8d46 100644
--- a/packages/runtime-host/src/__tests__/wsl-environment.test.ts
+++ b/packages/runtime-host/src/__tests__/wsl-environment.test.ts
@@ -54,6 +54,7 @@ test('passes WSL target values as literal argv to the absolute operator', async
       '--framed',
       '--root-id',
       'a'.repeat(64),
+      '--repair-root-after-remount',
     ],
   });
 });
diff --git a/packages/runtime-host/src/client/wsl-environment.ts b/packages/runtime-host/src/client/wsl-environment.ts
index b93c41ef0..56b9a01f0 100644
--- a/packages/runtime-host/src/client/wsl-environment.ts
+++ b/packages/runtime-host/src/client/wsl-environment.ts
@@ -93,6 +93,7 @@ export async function connectRuntimeHostWslEnvironment(
     '--framed',
     '--root-id',
     rootId,
+    '--repair-root-after-remount',
   ]);
   const resource = new WslProcessByteStream(child);
   const transport = new FramedByteStreamTransport(resource);
diff --git a/packages/runtime-host/src/operator/managed-deployment.ts b/packages/runtime-host/src/operator/managed-deployment.ts
index 777de4ea7..9aaa6d297 100644
--- a/packages/runtime-host/src/operator/managed-deployment.ts
+++ b/packages/runtime-host/src/operator/managed-deployment.ts
@@ -26,6 +26,7 @@ import {
   type StateRootOwner,
   type StorageRootCapability,
   assertStorageRootLease,
+  repairStorageRootAfterRemount,
   resolveExistingStorageRoot,
   tryAcquireStateRootOwner,
 } from '@maka/storage/root-authority';
@@ -321,6 +322,8 @@ export interface RuntimeHostManagedDeploymentTransitionInput {
 }
 
 export interface RuntimeHostManagedDeploymentAuthorityOptions {
+  /** Explicitly accept a device-only root identity change at a known remount boundary. */
+  readonly repairRootAfterRemount?: true;
   /** Test-only or embedding override. Production uses the account-local durable default. */
   readonly authorityRoot?: string;
   readonly homeDir?: string;
@@ -565,6 +568,13 @@ export async function resolveRuntimeHostManagedDeploymentAuthority(
       'The Runtime Host managed deployment record has an invalid Root identity',
     );
   }
+  if (options.repairRootAfterRemount) {
+    await repairStorageRootAfterRemount({
+      path: initial.root.path,
+      kind: 'interactive',
+      expectedRootId: rootId,
+    });
+  }
   const capability = await resolveExistingStorageRoot({
     path: initial.root.path,
     kind: 'interactive',
diff --git a/packages/storage/src/__tests__/root-authority.test.ts b/packages/storage/src/__tests__/root-authority.test.ts
index 450b5e813..cc6c70454 100644
--- a/packages/storage/src/__tests__/root-authority.test.ts
+++ b/packages/storage/src/__tests__/root-authority.test.ts
@@ -45,6 +45,7 @@ import {
   prepareArtifactWriterBootstrapAuthority,
   prepareStorageRootControlDirectory,
   prepareStorageRootIdentityRepair,
+  repairStorageRootAfterRemount,
   repairStorageRootIdentity,
   resolveExistingStorageRoot,
   resolveExistingStorageRootControlDirectory,
@@ -215,6 +216,43 @@ describe('storage root authority', () => {
     });
   });
 
+  test('repairs a remounted device but refuses a different directory inode', async () => {
+    await withRoots(async ({ base, root }) => {
+      const initialized = await resolveStorageRoot({ path: root, kind: 'interactive' });
+      const markerPath = join(root, STORAGE_ROOT_MARKER_FILE);
+      const marker = JSON.parse(await readFile(markerPath, 'utf8')) as {
+        rootIdentity: { dev: string; ino: string };
+      };
+      marker.rootIdentity.dev = (BigInt(marker.rootIdentity.dev) + 1n).toString();
+      await writeFile(markerPath, `${JSON.stringify(marker)}\n`);
+
+      await repairStorageRootAfterRemount({
+        path: root,
+        kind: 'interactive',
+        expectedRootId: initialized.rootId,
+      });
+      assert.equal(
+        (await resolveStorageRoot({ path: root, kind: 'interactive' })).rootId,
+        initialized.rootId,
+      );
+
+      const replacement = join(base, 'replacement');
+      await mkdir(replacement);
+      const replacementMarkerPath = join(replacement, STORAGE_ROOT_MARKER_FILE);
+      const repairedMarker = JSON.parse(await readFile(markerPath, 'utf8')) as {
+        rootIdentity: { dev: string; ino: string };
+      };
+      const replacementStat = await lstat(replacement, { bigint: true });
+      repairedMarker.rootIdentity.ino = (replacementStat.ino + 1n).toString();
+      await writeFile(replacementMarkerPath, `${JSON.stringify(repairedMarker)}\n`);
+      await assert.rejects(
+        () => repairStorageRootAfterRemount({ path: replacement, kind: 'interactive' }),
+        (error: unknown) =>
+          error instanceof StorageRootAuthorityError && error.code === 'root_identity_changed',
+      );
+    });
+  });
+
   test('rejects a prepared repair when its marker changes before commit', async () => {
     await withRoots(async ({ root }) => {
       await resolveStorageRoot({ path: root, kind: 'interactive' });
diff --git a/packages/storage/src/root-authority.ts b/packages/storage/src/root-authority.ts
index 0b6211944..b7ed6773b 100644
--- a/packages/storage/src/root-authority.ts
+++ b/packages/storage/src/root-authority.ts
@@ -95,6 +95,11 @@ export interface ResolveExistingStorageRootInput<K extends StorageRootKind>
 export type AdoptStorageRootOnImportInput<K extends StorageRootKind> =
   ResolveExistingStorageRootInput<K>;
 
+export interface RepairStorageRootAfterRemountInput<K extends StorageRootKind>
+  extends ResolveStorageRootInput<K> {
+  expectedRootId?: string;
+}
+
 export interface StorageRootIdentityRepairCandidate<K extends StorageRootKind = StorageRootKind> {
   readonly kind: K;
   readonly canonicalPath: string;
@@ -360,6 +365,54 @@ export async function prepareStorageRootIdentityRepair<K extends StorageRootKind
   );
 }
 
+/**
+ * Repairs only the mount-local portion of a root identity. This is for callers
+ * that already know their execution environment remounted the same filesystem:
+ * the inode must stay unchanged, and an expected durable root id may still pin
+ * the repair to an existing Client binding.
+ */
+export async function repairStorageRootAfterRemount<K extends StorageRootKind>(
+  input: RepairStorageRootAfterRemountInput<K>,
+): Promise<StorageRootCapability<K> | undefined> {
+  let candidate: StorageRootIdentityRepairCandidate<K> | undefined;
+  try {
+    candidate = await prepareStorageRootIdentityRepair(input);
+  } catch (error) {
+    if (
+      error instanceof StorageRootAuthorityError &&
+      (error.code === 'root_not_found' || error.code === 'root_unmarked')
+    ) {
+      return undefined;
+    }
+    throw error;
+  }
+  if (!candidate) return undefined;
+  const record = storageRootIdentityRepairs.get(candidate) as
+    | StorageRootIdentityRepairRecord<K>
+    | undefined;
+  if (!record) {
+    throw new StorageRootAuthorityError(
+      'invalid_repair',
+      'Expected a prepared storage root identity repair',
+    );
+  }
+  if (input.expectedRootId !== undefined && record.rootId !== input.expectedRootId) {
+    storageRootIdentityRepairs.delete(candidate);
+    throw new StorageRootAuthorityError(
+      'root_identity_changed',
+      `Remounted storage root does not match the expected root: ${record.canonicalPath}`,
+    );
+  }
+  if (record.marker.rootIdentity.ino !== record.identity.ino.toString()) {
+    storageRootIdentityRepairs.delete(candidate);
+    throw new StorageRootAuthorityError(
+      'root_identity_changed',
+      `Storage root directory changed across remount: ${record.canonicalPath}`,
+    );
+  }
+  return repairStorageRootIdentity(candidate);
+}
+
 /**
  * Explicit recovery boundary for a root whose host-local filesystem identity
  * is stale. Callers must obtain user intent for this exact candidate first.


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