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