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/09/03 08:52:22 UTC
(maka) branch main updated: fix(runtime-host): recover approved guest regeneration (#4636)
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 afe06f2b1c fix(runtime-host): recover approved guest regeneration (#4636)
afe06f2b1c is described below
commit afe06f2b1cb007c00cfad06c2d90ce203de82500
Author: Wang <m4...@gmail.com>
AuthorDate: Thu Sep 3 16:52:16 2026 +0800
fix(runtime-host): recover approved guest regeneration (#4636)
Admit durable Guest authorization on regenerate roots, and let explicit managed-Host repair safely replace a Host that dies while preparing retirement.
Generated-by: OpenAI Codex
---
.../__tests__/runtime-host-local-operator.test.ts | 37 ++++++++++
.../src/main/runtime-host-local-operator.ts | 2 +-
.../runtime-host-lifecycle-transaction.test.ts | 79 +++++++++++++++++++++-
.../cli/src/runtime-host-lifecycle-transaction.ts | 56 ++++++++++-----
.../src/__tests__/root-turn-coordinator.test.ts | 58 ++++++++++++++++
.../__tests__/regenerate-root-admission.test.ts | 9 +++
packages/storage/src/agent-run-store.ts | 8 ++-
7 files changed, 229 insertions(+), 20 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts b/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts
index f32962bf26..89ef5a27bc 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts
@@ -185,6 +185,7 @@ test('local update runs the selected package against the exact managed deploymen
deploymentId,
},
expectedHost: { hostEpoch: 'older-host', pid: 42 },
+ allowManualUpdate: true,
},
(phase) => phases.push(phase),
);
@@ -200,12 +201,48 @@ test('local update runs the selected package against the exact managed deploymen
'--expected-root-path', '/tmp/maka/root',
'--expected-root-id', 'a'.repeat(64),
'--expected-deployment-id', deploymentId,
+ '--allow-manual-update',
]);
assert.equal(
environment?.[RUNTIME_HOST_OPERATOR_PROJECT_DIRECTORY_CONFIGURATION_REQUEST_ENV],
'1',
);
assert.deepEqual(phases, ['staging']);
+
+ const developmentIntegrity = `sha512-${Buffer.alloc(64, 9).toString('base64')}`;
+ await operator.runUpdate(
+ {
+ setupPackage: {
+ kind: 'development_archive',
+ path: '/tmp/maka-agent-development.tgz',
+ integrity: developmentIntegrity,
+ },
+ target: {
+ serviceId: 'a'.repeat(64),
+ rootPath: '/tmp/maka/root',
+ rootId: 'a'.repeat(64),
+ deploymentId,
+ },
+ allowManualUpdate: true,
+ allowInterruptActiveTasks: true,
+ },
+ () => undefined,
+ );
+
+ assert.deepEqual(args, [
+ 'exec', '--yes', '--package', '/tmp/maka-agent-development.tgz', '--',
+ 'maka', 'runtime-host', 'service', 'update', '--framed',
+ '--managed-root-id', 'a'.repeat(64),
+ '--expected-service-id', 'a'.repeat(64),
+ '--expected-root-path', '/tmp/maka/root',
+ '--expected-root-id', 'a'.repeat(64),
+ '--expected-deployment-id', deploymentId,
+ '--allow-interrupt-active-tasks',
+ ]);
+ assert.equal(
+ environment?.[RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV],
+ developmentIntegrity,
+ );
});
test('local Peer Mesh join keeps invitations off argv and accepts bounded large results', async (t) => {
diff --git a/apps/desktop/src/main/runtime-host-local-operator.ts b/apps/desktop/src/main/runtime-host-local-operator.ts
index e8179f9195..6e0613ab60 100644
--- a/apps/desktop/src/main/runtime-host-local-operator.ts
+++ b/apps/desktop/src/main/runtime-host-local-operator.ts
@@ -425,7 +425,7 @@ export function createDesktopRuntimeHostLocalOperator(input: {
? ['--expected-host-json', JSON.stringify(command.expectedHost)]
: []),
...managedTargetArgs(command.target),
- ...(command.allowManualUpdate ? ['--allow-manual-update'] : []),
+ ...(command.allowManualUpdate && targetVersion ? ['--allow-manual-update'] : []),
...(command.allowInterruptActiveTasks ? ['--allow-interrupt-active-tasks'] : []),
],
},
diff --git a/packages/cli/src/__tests__/runtime-host-lifecycle-transaction.test.ts b/packages/cli/src/__tests__/runtime-host-lifecycle-transaction.test.ts
index 195ae429ff..ce68751979 100644
--- a/packages/cli/src/__tests__/runtime-host-lifecycle-transaction.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-lifecycle-transaction.test.ts
@@ -31,7 +31,10 @@ import {
type RuntimeHostManagedDeploymentConfig,
type RuntimeHostSupervisorProvider,
} from '@maka/runtime-host/operator';
-import type { connectExistingRuntimeHost } from '@maka/runtime-host/client';
+import {
+ RuntimeHostOperationError,
+ type connectExistingRuntimeHost,
+} from '@maka/runtime-host/client';
import { resolveStorageRoot, tryAcquireStateRootOwner } from '@maka/storage/root-authority';
import type {
RuntimeHostLifecycleProvider,
@@ -533,6 +536,80 @@ test('does not consume replacement consent after the supervised Host exits', asy
assert.equal(retired, false);
});
+test('requires explicit interruption authority when a supervised Host drains before diagnostics', async (t) => {
+ const stateRoot = await mkdtemp(join(tmpdir(), 'maka-lifecycle-diagnostics-drain-'));
+ t.after(() => rm(stateRoot, { recursive: true, force: true }));
+ const capability = await resolveStorageRoot({ path: stateRoot, kind: 'interactive' });
+ let owner: Awaited<ReturnType<typeof tryAcquireStateRootOwner>> =
+ await tryAcquireStateRootOwner(capability);
+ assert.ok(owner);
+ t.after(async () => owner?.close());
+ const events: string[] = [];
+ const connectExisting = (async () => {
+ events.push('connect');
+ return {
+ kind: 'connected',
+ registration: { hostEpoch: 'host-a', pid: 42 },
+ connection: {
+ request: async (operation: string) => {
+ events.push(operation);
+ throw new RuntimeHostOperationError(
+ 'host.diagnostics.query',
+ 'host_draining',
+ 'Runtime Host is draining',
+ );
+ },
+ close: async () => {
+ events.push('close');
+ },
+ },
+ } as unknown as Awaited<ReturnType<typeof connectExistingRuntimeHost>>;
+ }) as typeof connectExistingRuntimeHost;
+ const supervisor = {
+ status: async () => {
+ events.push('status');
+ return { active: true, pid: 42 };
+ },
+ retire: async () => {
+ events.push('retire');
+ await owner?.close();
+ owner = undefined;
+ },
+ };
+
+ assert.deepEqual(
+ await retireRuntimeHostLifecycleOwner({
+ rootPath: capability.canonicalPath,
+ rootId: capability.rootId,
+ connectExisting,
+ expectedOwner: { hostEpoch: 'host-a', pid: 42 },
+ supervisor,
+ }),
+ { kind: 'active_tasks' },
+ );
+ const retired = await retireRuntimeHostLifecycleOwner({
+ rootPath: capability.canonicalPath,
+ rootId: capability.rootId,
+ connectExisting,
+ expectedOwner: { hostEpoch: 'host-a', pid: 42 },
+ supervisor,
+ allowInterruptActiveTasks: true,
+ });
+ assert.equal(retired.kind, 'retired');
+ if (retired.kind === 'retired') await retired.owner.close();
+ assert.deepEqual(events, [
+ 'connect',
+ 'status',
+ 'host.diagnostics.query',
+ 'close',
+ 'connect',
+ 'status',
+ 'host.diagnostics.query',
+ 'retire',
+ 'close',
+ ]);
+});
+
test('requires explicit interruption authority to recover an unreachable supervised transition', async (t) => {
const stateRoot = await mkdtemp(join(tmpdir(), 'maka-lifecycle-recovery-consent-'));
const capability = await resolveStorageRoot({ path: stateRoot, kind: 'interactive' });
diff --git a/packages/cli/src/runtime-host-lifecycle-transaction.ts b/packages/cli/src/runtime-host-lifecycle-transaction.ts
index 3f541919c9..60e24492a6 100644
--- a/packages/cli/src/runtime-host-lifecycle-transaction.ts
+++ b/packages/cli/src/runtime-host-lifecycle-transaction.ts
@@ -28,6 +28,8 @@ import {
import {
connectExistingRuntimeHost,
prepareConnectedRuntimeHostRetirement,
+ RuntimeHostOperationError,
+ RuntimeHostRequestInterruptedError,
waitForRuntimeHostReady,
} from '@maka/runtime-host/client';
import { RUNTIME_HOST_PROTOCOL_VERSION } from '@maka/runtime-host/protocol';
@@ -316,6 +318,7 @@ function assertRecoveryTarget(
export async function retireRuntimeHostLifecycleOwner(input: {
readonly rootPath: string;
readonly rootId: string;
+ readonly connectExisting?: typeof connectExistingRuntimeHost;
readonly allowInterruptActiveTasks?: boolean;
/**
* Freshness fence evaluated before a canonical supervised-deployment retirement is admitted.
@@ -357,7 +360,7 @@ export async function retireRuntimeHostLifecycleOwner(input: {
throw error;
}
}
- const connected = await connectExistingRuntimeHost({
+ const connected = await (input.connectExisting ?? connectExistingRuntimeHost)({
rootPath: capability.canonicalPath,
protocol: {
min: RUNTIME_HOST_PROTOCOL_VERSION,
@@ -384,31 +387,44 @@ export async function retireRuntimeHostLifecycleOwner(input: {
);
}
try {
- const diagnostics = await connected.connection.request('host.diagnostics.query', {});
const supervisorStatus = await input.supervisor?.status();
if (supervisorStatus) assertExpectedSupervisorOwner(input.expectedOwner, supervisorStatus);
if (
supervisorStatus &&
- (!supervisorStatus.active || supervisorStatus.pid !== diagnostics.pid)
+ (!supervisorStatus.active || supervisorStatus.pid !== connected.registration.pid)
) {
throw new RuntimeHostLifecycleTransactionError(
'transition_failed',
- 'The supervisor and State Root report different Runtime Host processes',
+ 'The supervisor and Runtime Host registration report different processes',
);
}
- // The exact Root owner and canonical supervisor now agree while the deployment lock is held.
- // This admits retirement of that deployment; a later same-deployment restart is not a new
- // authority, but it must not acquire the Root before the supervisor is retired.
- const prepared = await prepareConnectedRuntimeHostRetirement(
- connected.connection,
- input.allowInterruptActiveTasks ? 'interrupt_active_work' : 'refuse_active_work',
- );
- if (prepared.kind === 'active_tasks') return prepared;
- if (prepared.pid !== diagnostics.pid) {
- throw new RuntimeHostLifecycleTransactionError(
- 'transition_failed',
- 'The Runtime Host process changed while retirement was prepared',
+ try {
+ const diagnostics = await connected.connection.request('host.diagnostics.query', {});
+ if (diagnostics.pid !== connected.registration.pid) {
+ throw new RuntimeHostLifecycleTransactionError(
+ 'transition_failed',
+ 'The Runtime Host registration and diagnostics report different processes',
+ );
+ }
+ // The exact Root owner and canonical supervisor now agree while the deployment lock is held.
+ // This admits retirement of that deployment; a later same-deployment restart is not a new
+ // authority, but it must not acquire the Root before the supervisor is retired.
+ const prepared = await prepareConnectedRuntimeHostRetirement(
+ connected.connection,
+ input.allowInterruptActiveTasks ? 'interrupt_active_work' : 'refuse_active_work',
);
+ if (prepared.kind === 'active_tasks') return prepared;
+ if (prepared.pid !== diagnostics.pid) {
+ throw new RuntimeHostLifecycleTransactionError(
+ 'transition_failed',
+ 'The Runtime Host process changed while retirement was prepared',
+ );
+ }
+ } catch (error) {
+ if (!isRuntimeHostRetirementUnavailable(error) || !input.supervisor) throw error;
+ if (!input.allowInterruptActiveTasks) return { kind: 'active_tasks' };
+ await input.supervisor.retire();
+ return waitForRuntimeHostLifecycleOwner(capability, input.timeoutMs ?? 45_000);
}
const retirement = await waitForRuntimeHostLifecycleOwner(
capability,
@@ -426,6 +442,14 @@ export async function retireRuntimeHostLifecycleOwner(input: {
}
}
+function isRuntimeHostRetirementUnavailable(error: unknown): boolean {
+ return (
+ error instanceof RuntimeHostRequestInterruptedError ||
+ (error instanceof RuntimeHostOperationError &&
+ (error.code === 'host_not_ready' || error.code === 'host_draining'))
+ );
+}
+
function assertExpectedRuntimeHostOwner(
expected: { readonly hostEpoch: string; readonly pid: number } | undefined,
observed: { readonly hostEpoch: string; readonly pid: number } | undefined,
diff --git a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
index 8ba15639e0..810184f4bc 100644
--- a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
@@ -875,6 +875,64 @@ test('turn.start durably binds a Guest request approval to the admitted Turn', a
}
});
+test('turn.regenerate durably binds a Guest request approval to the admitted Turn', async () => {
+ const fixture = await createFailureFixture({
+ registerBackend: (backends) =>
+ backends.register('ai-sdk', (context) => new FakeBackend(context)),
+ });
+ const authorization = {
+ kind: 'session_turn_access_request' as const,
+ requestId: 'request-regenerate-1',
+ principalId: 'session_guest:guest-1',
+ grantId: 'grant-1',
+ approvedAt: 1_788_000_000_000,
+ approvedBy: 'local_owner',
+ };
+ const input = {
+ sessionId: fixture.sessionId,
+ sourceTurnId: 'turn-regenerate-source',
+ turnId: 'turn-regenerate-approved',
+ };
+ try {
+ assertStartedTurn(
+ await fixture.interactiveTurns.handlers['turn.start'](
+ {
+ sessionId: fixture.sessionId,
+ turnId: input.sourceTurnId,
+ content: { text: 'Regenerate this approved request.' },
+ },
+ operationContext(fixture.hostEpoch, fixture.acquireResidency),
+ ),
+ );
+ await fixture.coordinator.whenIdle(fixture.sessionId);
+
+ const regenerated = await fixture.interactiveTurns.handlers['turn.regenerate'](input, {
+ ...operationContext(fixture.hostEpoch, fixture.acquireResidency),
+ principal: authorization.principalId,
+ turnAdmissionAuthorization: authorization,
+ });
+ assert.equal(regenerated.ok, true, JSON.stringify(regenerated));
+ const admission = await fixture.stores.agentRunStore.readRootTurnAdmission(
+ fixture.sessionId,
+ input.turnId,
+ );
+ assert.deepEqual(admission?.execution, {
+ kind: 'regenerate',
+ sourceTurnId: input.sourceTurnId,
+ });
+ assert.deepEqual(admission?.authorization, authorization);
+
+ const conflictingRetry = await fixture.interactiveTurns.handlers['turn.regenerate'](
+ input,
+ operationContext(fixture.hostEpoch, fixture.acquireResidency),
+ );
+ assert.equal(conflictingRetry.ok, false);
+ if (!conflictingRetry.ok) assert.equal(conflictingRetry.error.code, 'operation_conflict');
+ } finally {
+ await fixture.dispose();
+ }
+});
+
test('turn.start resolves explicit Skills once before durable admission and replays the result', async () => {
let preparationCount = 0;
let blocked = false;
diff --git a/packages/storage/src/__tests__/regenerate-root-admission.test.ts b/packages/storage/src/__tests__/regenerate-root-admission.test.ts
index f1c2d0c501..4429d04de6 100644
--- a/packages/storage/src/__tests__/regenerate-root-admission.test.ts
+++ b/packages/storage/src/__tests__/regenerate-root-admission.test.ts
@@ -36,6 +36,7 @@ test('regenerate admission durably binds the immutable source Turn', async () =>
kind: 'regenerate',
sourceTurnId: 'source-turn',
});
+ assert.deepEqual(admitted.admission.authorization, input.authorization);
store.close?.();
const reopened = createSqliteAgentRunStore(root);
@@ -116,6 +117,14 @@ function admissionInput(overrides: Partial<AdmitRootTurnInput> = {}): AdmitRootT
previousRootTurnId: null,
normalizedInput: { text: 'Original request' },
sourceMessages: [],
+ authorization: {
+ kind: 'session_turn_access_request',
+ requestId: 'request-regenerate',
+ principalId: 'session_guest:guest-1',
+ grantId: 'grant-1',
+ approvedAt: 45,
+ approvedBy: 'local_owner',
+ },
admittedAt: 50,
...overrides,
};
diff --git a/packages/storage/src/agent-run-store.ts b/packages/storage/src/agent-run-store.ts
index b10e782776..b227cc2d75 100644
--- a/packages/storage/src/agent-run-store.ts
+++ b/packages/storage/src/agent-run-store.ts
@@ -1897,9 +1897,13 @@ function assertRootTurnAdmissionContract(admission: RootTurnAdmission): void {
'Invalid root turn admission contract: Skill invocation requires external message execution',
);
}
- if (admission.authorization && execution.kind !== 'external_message') {
+ if (
+ admission.authorization &&
+ execution.kind !== 'external_message' &&
+ execution.kind !== 'regenerate'
+ ) {
throw new Error(
- 'Invalid root turn admission contract: authorization proof requires external message execution',
+ 'Invalid root turn admission contract: authorization proof requires external message or regenerate execution',
);
}
if (execution.kind === 'claimed_agent_graph_intent') {
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org