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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { BotRegistry } from '@maka/runtime/bots';
import type { ComputerUseToolSet } from '@maka/runtime/computer-use-tools';
import type { MakaTool } from '@maka/runtime/tool-runtime';
import { connectRuntimeHost } from '@maka/runtime-host/client';
import { acquireOperationalStateDatabase } from '@maka/storage/operational-state-store';
import {
RUNTIME_HOST_PROTOCOL_VERSION,
type SessionCatalogProjection,
Expand Down Expand Up @@ -250,6 +251,9 @@ test('drives the renderer Session catalog facade through real UDS framing', asyn
});
const ipc = ipcHarness();
const changes: Array<{ reason: string; sessionId?: string }> = [];
// This fixture replaces the real execution composition; initialize its
// owned storage before Desktop admits the local candidate.
acquireOperationalStateDatabase(base).close();
const started = await startDesktopRuntimeHostCandidate({
rootPath: base,
candidateEntrypoint: new URL('file:///unused-runtime-host-candidate.js'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@

import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import test from 'node:test';
import { acquireOperationalStateDatabase } from '@maka/storage/operational-state-store';
import type { IpcMain } from 'electron';
import type { BotIncomingMessage, BotRegistry } from '@maka/runtime/bots';
import type { ComputerUseToolSet } from '@maka/runtime/computer-use-tools';
Expand Down Expand Up @@ -55,6 +60,7 @@ import { RuntimeHostSessionObservationRegistry } from '../runtime-host-session-o
import { RuntimeHostReconnectingIpcMain } from '../runtime-host-reconnecting-ipc-main.js';
import { desktopSessionResourceKey } from '../../shared/runtime-host-identity.js';
import { waitFor as pollFor } from '@maka/core/test-only/async-primitives';
import { startDesktopRuntimeHostWithRecovery } from '../runtime-host-startup-recovery.js';

const TEST_HOST_ID = 'a'.repeat(64);
const TEST_TARGET_EPOCH = 'test-target-epoch';
Expand Down Expand Up @@ -84,6 +90,80 @@ test('uses the manager-owned launch barrier for local candidate startup', async
assert.equal(connectedRoot, 'C:\\workspace');
});

test('updates a protocol-compatible managed Host before exposing a candidate over its old storage', async (t) => {
const root = await mkdtemp(join(tmpdir(), 'maka-desktop-managed-schema-'));
t.after(() => rm(root, { recursive: true, force: true }));
acquireOperationalStateDatabase(root).close();
const databasePath = join(root, 'runtime.sqlite');
const legacy = new DatabaseSync(databasePath);
legacy.exec(`
DROP TABLE usage_model_call_attempts;
CREATE TABLE usage_model_call_attempts (
attempt_id TEXT PRIMARY KEY,
completed_at INTEGER NOT NULL,
record_json TEXT NOT NULL,
session_id TEXT
);
INSERT INTO usage_model_call_attempts VALUES ('retained', 1, '{}', 'deleted-session');
UPDATE operational_schema_migrations SET version = 6 WHERE scope = 'usage';
`);
legacy.close();
const ipc = ipcHarness();
const old = connectionHarness('old');
const updated = connectionHarness('updated');
let starts = 0;
let repairs = 0;
const candidate = await startDesktopRuntimeHostWithRecovery({
start: async () => {
const host = starts++ === 0 ? old : updated;
const result = await startDesktopRuntimeHostCandidate({
...deps(ipc),
workspaceRoot: root,
rootPath: root,
candidateEntrypoint: 'unused.js',
candidateLaunchBarrier: {
connect: async () => ({
kind: 'connected',
connection: host.connection,
registration: { lifecycleMode: 'supervised', pid: 123 },
}),
},
} as unknown as DesktopRuntimeHostCandidateStartInput);
assert.equal(result.kind, 'ready');
if (result.kind !== 'ready') throw new Error('Expected a ready candidate');
return result.candidate;
},
repair: async (authority) => {
repairs += 1;
assert.deepEqual(authority, { allowManualUpdate: false, allowInterruptActiveTasks: false });
assert.equal(old.closeCalls, 1, 'release the old connection before managed update');
assert.equal(old.capabilityRegistrations, 0, 'do not expose capabilities before storage admission');
assert.equal(ipc.size, 0);
const preserved = new DatabaseSync(databasePath, { readOnly: true });
try {
assert.equal(preserved.prepare("SELECT version FROM operational_schema_migrations WHERE scope = 'usage'").get()?.version, 6);
assert.equal(preserved.prepare("SELECT record_json FROM usage_model_call_attempts WHERE attempt_id = 'retained'").get()?.record_json, '{}');
} finally {
preserved.close();
}
// Simulate the updated owning Host, not Desktop, performing migration.
acquireOperationalStateDatabase(root).close();
return { kind: 'repaired' };
},
prompt: async () => { throw new Error('No prompt needed for an idle automatically updatable Host'); },
});
t.after(() => candidate.close());
assert.equal(starts, 2);
assert.equal(repairs, 1);
assert.equal(updated.capabilityRegistrations, 1);
const current = acquireOperationalStateDatabase(root, { schemaMigration: 'require_current' });
try {
assert.equal(current.database.prepare("SELECT session_id FROM usage_model_call_attempts WHERE attempt_id = 'retained'").get()?.session_id, 'deleted-session');
} finally {
current.close();
}
});

test('formats bounded local Host exit evidence without leaking stderr secrets', () => {
const diagnostic = formatLocalRuntimeHostProcessExitDiagnostic(42, {
code: 23,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { runtimeHostStartupError } from "@maka/runtime-host/client";
import { OperationalStateMigrationBlockedError } from '@maka/storage/operational-state-store';
import { startDesktopRuntimeHostWithRecovery } from "../runtime-host-startup-recovery.js";

test("repairs a managed Host once and resumes startup without asking the user", async () => {
Expand Down Expand Up @@ -55,7 +56,10 @@ test("repairs a managed Host once and resumes startup without asking the user",
assert.equal(prompts, 0);
});

test("separates manual update consent from active-work interruption", async () => {
for (const failure of [
runtimeHostStartupError('managed_root_requires_operator'),
new OperationalStateMigrationBlockedError(new Error('Host migration required'), 'requires_host_migration'),
]) test(`separates manual update consent from active-work interruption: ${failure.name}`, async () => {
let starts = 0;
const repairModes: Array<{
readonly allowManualUpdate: boolean;
Expand All @@ -67,7 +71,7 @@ test("separates manual update consent from active-work interruption", async () =
start: async () => {
starts += 1;
if (starts === 1)
throw runtimeHostStartupError("managed_root_requires_operator");
throw failure;
return "ready";
},
repair: async (authority) => {
Expand All @@ -92,8 +96,10 @@ test("separates manual update consent from active-work interruption", async () =
assert.deepEqual(prompts, [false, true]);
});

test("does not offer managed repair for an unrelated startup failure", async () => {
const failure = new Error("renderer prerequisites failed");
for (const failure of [
new Error('renderer prerequisites failed'),
new OperationalStateMigrationBlockedError(new Error('unsupported newer schema')),
]) test(`does not offer managed repair for an unrelated startup failure: ${failure.name}`, async () => {
let repairs = 0;
let prompts = 0;

Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/runtime-host-desktop-candidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import { randomUUID } from "node:crypto";
import { acquireOperationalStateDatabase } from '@maka/storage/operational-state-store';
import type { IpcMain } from "electron";
import type { ActiveInteractionRequestEvent } from '@maka/core/events';
import { redactSecrets } from '@maka/core/redaction';
Expand Down Expand Up @@ -344,6 +345,10 @@ export async function startDesktopRuntimeHostCandidate(
if (connection.kind !== "connected") return connection;
observeLocalRuntimeHostProcess(connection.spawnedProcess);
try {
// A resident managed Host can speak this protocol while still using an
// older storage schema. Validate the shared local database before exposing
// any candidate services, so startup recovery can update its owning Host.
acquireOperationalStateDatabase(input.rootPath, { schemaMigration: 'require_current' }).close();
return {
kind: "ready",
candidate: await createDesktopRuntimeHostCandidate(
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/runtime-host-startup-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import { RuntimeHostStartupError } from "@maka/runtime-host/client";
import { OperationalStateMigrationBlockedError } from '@maka/storage/operational-state-store';

export type DesktopRuntimeHostStartupRepairResult =
| { readonly kind: "repaired" }
Expand Down Expand Up @@ -121,6 +122,9 @@ export async function startDesktopRuntimeHostWithRecovery<T>(input: {
}

export function canRepairManagedRuntimeHostStartup(error: Error): boolean {
if (error instanceof OperationalStateMigrationBlockedError) {
return error.reason === 'requires_host_migration';
}
return (
error instanceof RuntimeHostStartupError &&
(error.reason === "managed_root_requires_operator" ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ test('a non-owner rejects an older schema without migrating it behind the Runtim
}),
(error: unknown) =>
error instanceof OperationalStateMigrationBlockedError &&
error.reason === 'requires_host_migration' &&
/requires migration by its Runtime Host/u.test(error.message),
);

Expand Down Expand Up @@ -1295,6 +1296,13 @@ test('rejects a newer scope before migrating an older scope', async () => {
() => acquireOperationalStateDatabase(root),
/Operational schema usage is newer than supported/,
);
assert.throws(
() => acquireOperationalStateDatabase(root, { schemaMigration: 'require_current' }),
(error: unknown) =>
error instanceof OperationalStateMigrationBlockedError &&
error.reason === 'blocked' &&
/Operational schema usage is newer than supported/u.test(error.message),
);

const preserved = new DatabaseSync(databasePath, { readOnly: true });
try {
Expand Down
1 change: 1 addition & 0 deletions packages/storage/src/operational-state-store-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
*/
export {
acquireOperationalStateDatabase,
OperationalStateMigrationBlockedError,
OPERATIONAL_STATE_DATABASE_NAME,
OPERATIONAL_STATE_SCHEMA_VERSION,
resolveOperationalStateDatabasePath,
Expand Down
11 changes: 9 additions & 2 deletions packages/storage/src/operational-state-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ export interface OperationalStateDatabaseOptions {
export class OperationalStateMigrationBlockedError extends Error {
readonly code = 'operational_state_migration_blocked';

constructor(cause: unknown) {
constructor(
cause: unknown,
readonly reason: 'requires_host_migration' | 'blocked' = 'blocked',
) {
super(cause instanceof Error ? cause.message : 'Operational state migration is blocked', {
cause,
});
Expand Down Expand Up @@ -202,6 +205,7 @@ class OperationalStateDatabaseOwner {
if (options.schemaMigration === 'require_current' && !existsSync(databasePath)) {
throw new OperationalStateMigrationBlockedError(
new Error('Operational state has not been initialized by its Runtime Host'),
'requires_host_migration',
);
}
mkdirSync(dirname(databasePath), { recursive: true });
Expand Down Expand Up @@ -291,7 +295,10 @@ function requireCurrentOperationalState(database: DatabaseSync): void {
try {
const inspection = inspectOperationalStateSchema(database);
if (inspection.status === 'current' && isCurrentOperationalTargetSchema(database)) return;
throw new Error('Operational state requires migration by its Runtime Host');
throw new OperationalStateMigrationBlockedError(
new Error('Operational state requires migration by its Runtime Host'),
'requires_host_migration',
);
} catch (error) {
if (isSqliteEnvironmentError(error)) throw error;
if (error instanceof OperationalStateMigrationBlockedError) throw error;
Expand Down