You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/09/07 06:26:56 UTC
(maka) branch main updated: fix(cli): activate managed local hosts through installed operator (#4962)
This is an automated email from the ASF dual-hosted git repository.
me2seeks 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 08ab3a51e1 fix(cli): activate managed local hosts through installed operator (#4962)
08ab3a51e1 is described below
commit 08ab3a51e15e8bb73de004d034758ece78ced1ea
Author: Zhang <96...@users.noreply.github.com>
AuthorDate: Mon Sep 7 14:26:52 2026 +0800
fix(cli): activate managed local hosts through installed operator (#4962)
* fix(cli): activate managed local hosts through installed operator
Generated-by: OpenAI Codex
* chore(ci): refresh Windows test skip inventory
Generated-by: OpenAI Codex
---
docs/windows-test-inventory.md | 5 +-
.../src/__tests__/runtime-host-cli-context.test.ts | 101 ++++++++++++++
.../runtime-host-local-managed-activation.test.ts | 150 +++++++++++++++++++++
packages/cli/src/runtime-host-cli-context.ts | 32 ++++-
.../src/runtime-host-local-managed-activation.ts | 97 +++++++++++++
5 files changed, 382 insertions(+), 3 deletions(-)
diff --git a/docs/windows-test-inventory.md b/docs/windows-test-inventory.md
index d13667bfd7..50f0f66928 100644
--- a/docs/windows-test-inventory.md
+++ b/docs/windows-test-inventory.md
@@ -16,10 +16,10 @@ Locations intentionally omit line numbers so unrelated edits do not invalidate t
| Classification | Count |
|---|---:|
| windows-backend-gap | 27 |
-| portable-candidate | 18 |
+| portable-candidate | 19 |
| platform-contract | 31 |
-Total Windows-excluded declarations: **76**
+Total Windows-excluded declarations: **77**
## Inventory
@@ -32,6 +32,7 @@ Total Windows-excluded declarations: **76**
| platform-contract | `apps/desktop/src/main/__tests__/shell-env.test.ts` bounds shell output instead of buffering until the global timeout | `process.platform === 'win32'` |
| portable-candidate | `packages/cli/src/__tests__/pi-transcript.test.ts` shortens POSIX paths under the home directory | `process.platform === 'win32'` |
| portable-candidate | `packages/cli/src/__tests__/pi-transcript.test.ts` keeps POSIX paths outside the home directory absolute | `process.platform === 'win32'` |
+| portable-candidate | `packages/cli/src/__tests__/runtime-host-local-managed-activation.test.ts` local CLI cold-starts through the installed ${legacy ? 'legacy' : 'Node'} operator | `process.platform === 'win32'` |
| portable-candidate | `packages/cli/src/__tests__/runtime-host-setup.test.ts` managed operator binds its Client Data Root and routes deployment cleanup | `process.platform === 'win32'` |
| portable-candidate | `packages/eval/src/__tests__/install-preflight.test.ts` rejects an unusable trials root before invoking external prerequisites | `process.platform === 'win32' \|\| process.geteuid?.() === 0` |
| windows-backend-gap | `packages/runtime-host/src/__tests__/connection-effect-coordinator.test.ts` leaves canonical onboarding state unchanged when the durable intent cannot be published | `process.platform === 'win32'` |
diff --git a/packages/cli/src/__tests__/runtime-host-cli-context.test.ts b/packages/cli/src/__tests__/runtime-host-cli-context.test.ts
index 8d39fbf148..02d61da94c 100644
--- a/packages/cli/src/__tests__/runtime-host-cli-context.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-cli-context.test.ts
@@ -714,3 +714,104 @@ async function waitFor(predicate: () => boolean): Promise<void> {
}
assert.fail('condition was not reached');
}
+
+test('local CLI delegates a managed cold start once and reconnects without a launch claim', async () => {
+ const calls: string[] = [];
+ const connection = {
+ rootId: 'root-id',
+ hostEpoch: 'host-epoch',
+ connectionId: 'connection-id',
+ closed: new Promise<void>(() => {}),
+ close: async () => {},
+ subscribeConfigurationChanges: () => () => {},
+ subscribeConnectionCatalogChanges: () => () => {},
+ subscribeProjectCatalogChanges: () => () => {},
+ subscribeSessionCatalogChanges: () => () => {},
+ subscribeScheduledTaskChanges: () => () => {},
+ } as unknown as RuntimeHostConnection;
+ const context = await connectRuntimeHostCliConnection(
+ { rootPath: '/managed-root' },
+ {
+ connectOrSpawn: async (input) => {
+ assert.equal(input.managedLaunchClaim, undefined);
+ calls.push('connect');
+ return calls.length === 1
+ ? { kind: 'failed', reason: 'managed_root_requires_operator' }
+ : connectedHostResult(connection);
+ },
+ connectActivatedHost: async () => {
+ calls.push('connect');
+ return connectedHostResult(connection);
+ },
+ activateLocalManagedHost: async (input) => {
+ assert.equal(input.rootPath, '/managed-root');
+ calls.push('operator');
+ },
+ },
+ );
+ assert.deepEqual(calls, ['connect', 'operator', 'connect']);
+ await context.close();
+});
+
+test('local CLI does not loop if operator activation fails to make the Host available', async () => {
+ let activations = 0;
+ await assert.rejects(
+ connectRuntimeHostCliConnection(
+ { rootPath: '/managed-root' },
+ {
+ connectOrSpawn: async () => ({ kind: 'failed', reason: 'managed_root_requires_operator' }),
+ connectActivatedHost: async () => ({ kind: 'unavailable', reason: 'not_registered' }),
+ activateLocalManagedHost: async () => {
+ activations += 1;
+ },
+ },
+ ),
+ /could not join it \(not_registered\)/,
+ );
+ assert.equal(activations, 1);
+});
+
+test('local CLI propagates operator failure without attempting unmanaged recovery', async () => {
+ let connections = 0;
+ const failure = new Error('operator failed');
+ await assert.rejects(
+ connectRuntimeHostCliConnection(
+ { rootPath: '/managed-root' },
+ {
+ connectOrSpawn: async () => {
+ connections += 1;
+ return { kind: 'failed', reason: 'managed_root_requires_operator' };
+ },
+ activateLocalManagedHost: async () => {
+ throw failure;
+ },
+ },
+ ),
+ (error) => error === failure,
+ );
+ assert.equal(connections, 1);
+});
+
+test('activated managed Host incompatibility stays operator-owned', async () => {
+ await assert.rejects(
+ connectRuntimeHostCliConnection(
+ { rootPath: '/managed-root' },
+ {
+ connectOrSpawn: async () => ({ kind: 'failed', reason: 'managed_root_requires_operator' }),
+ activateLocalManagedHost: async () => {},
+ connectActivatedHost: async () => ({
+ kind: 'incompatible',
+ registration: hostRegistration(),
+ handshake: incompatibleRemoteHandshake(),
+ }),
+ },
+ ),
+ (error: unknown) => {
+ assert.ok(error instanceof Error);
+ assert.ok(!(error instanceof RuntimeHostCliConflictError));
+ assert.match(error.message, /incompatible/);
+ assert.match(error.message, /configured operator/);
+ return true;
+ },
+ );
+});
diff --git a/packages/cli/src/__tests__/runtime-host-local-managed-activation.test.ts b/packages/cli/src/__tests__/runtime-host-local-managed-activation.test.ts
new file mode 100644
index 0000000000..6dfdd82dad
--- /dev/null
+++ b/packages/cli/src/__tests__/runtime-host-local-managed-activation.test.ts
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { randomUUID } from 'node:crypto';
+import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import test from 'node:test';
+import { resolveStorageRoot } from '@maka/storage/root-authority';
+import {
+ claimRuntimeHostManagedDeployment,
+ resolveRuntimeHostManagedDeploymentConfigPath,
+ resolveRuntimeHostNpmDeploymentLayout,
+ type RuntimeHostManagedDeploymentConfig,
+} from '@maka/runtime-host/operator';
+import { activateLocalManagedRuntimeHost } from '../runtime-host-local-managed-activation.js';
+import { connectRuntimeHostCliConnection } from '../runtime-host-cli-context.js';
+
+for (const legacy of [false, true]) {
+ test(`local CLI cold-starts through the installed ${legacy ? 'legacy' : 'Node'} operator`, {
+ skip: process.platform === 'win32',
+ timeout: 30_000,
+ }, async (t) => {
+ const base = await mkdtemp(join(tmpdir(), 'maka-local-managed-'));
+ const capability = await resolveStorageRoot({ path: join(base, 'state'), kind: 'interactive' });
+ const config: RuntimeHostManagedDeploymentConfig = {
+ schemaVersion: 1,
+ state: 'active',
+ deploymentId: randomUUID(),
+ configRevision: 1,
+ deploymentRoot: join(base, 'deployment'),
+ root: { path: capability.canonicalPath, id: capability.rootId },
+ projectDirectoryRoots: [],
+ launch: {
+ kind: 'exact_package',
+ nodePath: process.execPath,
+ package: {
+ kind: 'npm_registry',
+ version: '1.2.3',
+ integrity: `sha512-${Buffer.alloc(64, 1).toString('base64')}`,
+ },
+ },
+ listeners: {
+ localIpc: true,
+ websocket: { host: '127.0.0.1', port: 0, path: '/runtime-host' },
+ },
+ lifecycle: { mode: 'on_demand', availability: 'activation' },
+ reconciliation: { trigger: 'manual' },
+ };
+ const layout = resolveRuntimeHostNpmDeploymentLayout(
+ config.deploymentRoot,
+ config.launch.package.integrity,
+ );
+ await mkdir(dirname(layout.candidateEntrypoint), { recursive: true });
+ await symlink(
+ fileURLToPath(import.meta.resolve('@maka/runtime-host/execution-candidate-main')),
+ layout.candidateEntrypoint,
+ );
+ await claimRuntimeHostManagedDeployment(capability, config);
+ const modulePath = join(config.deploymentRoot, legacy ? 'legacy-entry.mjs' : 'operator.mjs');
+ await writeFile(
+ modulePath,
+ `
+ import { activateRuntimeHostManagedDeployment } from ${JSON.stringify(import.meta.resolve('@maka/runtime-host/client'))};
+ import { encodeRuntimeHostActivationFrame } from ${JSON.stringify(import.meta.resolve('@maka/runtime-host/operator'))};
+ const result = await activateRuntimeHostManagedDeployment({ rootId: process.argv.at(-1) });
+ process.stdout.write(encodeRuntimeHostActivationFrame(result));
+ `,
+ );
+ if (legacy) {
+ const launcher = join(config.deploymentRoot, 'operator');
+ await writeFile(
+ launcher,
+ '#!/bin/sh\nexec ' + "'" + process.execPath + "' '" + modulePath + '\' "$@"\n',
+ );
+ await chmod(launcher, 0o700);
+ }
+ let first: Awaited<ReturnType<typeof connectRuntimeHostCliConnection>> | undefined;
+ let second: typeof first;
+ t.after(async () => {
+ await second?.close();
+ if (first) {
+ const diagnostics = await first.connection.request('host.diagnostics.query', {});
+ await first.close();
+ try {
+ process.kill(diagnostics.pid, 'SIGTERM');
+ } catch {}
+ }
+ await rm(base, { recursive: true, force: true });
+ await rm(dirname(resolveRuntimeHostManagedDeploymentConfigPath(capability.rootId)), {
+ recursive: true,
+ force: true,
+ });
+ });
+ first = await connectRuntimeHostCliConnection({ rootPath: capability.canonicalPath });
+ second = await connectRuntimeHostCliConnection(
+ { rootPath: capability.canonicalPath },
+ {
+ activateLocalManagedHost: async () =>
+ assert.fail('a running managed Host must be joined directly'),
+ },
+ );
+ assert.equal(first.connection.rootId, capability.rootId);
+ assert.equal(second.connection.hostEpoch, first.connection.hostEpoch);
+ // Exercise the real operator subprocess failure contract, including nonzero exit.
+ await writeFile(
+ modulePath,
+ `
+ import { encodeRuntimeHostActivationFrame } from ${JSON.stringify(import.meta.resolve('@maka/runtime-host/operator'))};
+ process.stdout.write(encodeRuntimeHostActivationFrame({schemaVersion: 1, kind: 'error', error: {code: 'activation_failed', message: 'Operator recovery is required'}}));
+ process.exitCode = 1;
+ `,
+ );
+ await assert.rejects(activateLocalManagedRuntimeHost({ rootPath: capability.canonicalPath }), {
+ message: 'Operator recovery is required',
+ });
+ await writeFile(modulePath, `process.stdout.write('not an activation frame');`);
+ await assert.rejects(
+ activateLocalManagedRuntimeHost({ rootPath: capability.canonicalPath }),
+ /invalid activation result/,
+ );
+ const controller = new AbortController();
+ controller.abort(new Error('activation cancelled'));
+ await assert.rejects(
+ activateLocalManagedRuntimeHost({
+ rootPath: capability.canonicalPath,
+ signal: controller.signal,
+ }),
+ /activation cancelled/,
+ );
+ });
+}
diff --git a/packages/cli/src/runtime-host-cli-context.ts b/packages/cli/src/runtime-host-cli-context.ts
index b148320530..e4901f5f7b 100644
--- a/packages/cli/src/runtime-host-cli-context.ts
+++ b/packages/cli/src/runtime-host-cli-context.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { activateLocalManagedRuntimeHost } from './runtime-host-local-managed-activation.js';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { NO_REAL_CONNECTION_CODE } from '@maka/core/connection-error-copy';
@@ -27,6 +28,7 @@ import type {
import type { ChatDefaultPermissionMode } from '@maka/core/settings';
import {
connectOrSpawnRuntimeHost,
+ connectRuntimeHost,
connectRuntimeHostProfile,
createClientRuntimeHostProfileCatalog,
createRuntimeHostPeerClientFromEnvironment,
@@ -121,6 +123,8 @@ export interface RuntimeHostCliTarget {
interface RuntimeHostCliContextDeps {
readonly connectOrSpawn: typeof connectOrSpawnRuntimeHost;
+ readonly connectActivatedHost: typeof connectRuntimeHost;
+ readonly activateLocalManagedHost: typeof activateLocalManagedRuntimeHost;
readonly connectProfile: typeof connectRuntimeHostProfile;
readonly readConnectionCatalog: typeof readRuntimeHostConnectionCatalog;
readonly loadClientInstanceId: typeof loadOrCreateRuntimeHostClientInstanceId;
@@ -154,6 +158,8 @@ export async function connectRuntimeHostCliConnection(
): Promise<RuntimeHostCliConnectionOnlyContextWithIdentity> {
const deps: RuntimeHostCliContextDeps = {
connectOrSpawn: connectOrSpawnRuntimeHost,
+ activateLocalManagedHost: activateLocalManagedRuntimeHost,
+ connectActivatedHost: connectRuntimeHost,
connectProfile: connectRuntimeHostProfile,
readConnectionCatalog: readRuntimeHostConnectionCatalog,
loadClientInstanceId: loadOrCreateRuntimeHostClientInstanceId,
@@ -197,10 +203,34 @@ export async function connectRuntimeHostCliConnection(
...(signal ? { signal } : {}),
});
}
- const connected = await deps.connectOrSpawn({
+ let connected = await deps.connectOrSpawn({
...connectInput,
...(signal ? { signal } : {}),
});
+ if (connected.kind === 'failed' && connected.reason === 'managed_root_requires_operator') {
+ await deps.activateLocalManagedHost({
+ rootPath: input.rootPath,
+ ...(signal ? { signal } : {}),
+ });
+ signal?.throwIfAborted();
+ // Rejoin over Local IPC. The operator retains launch and update authority.
+ const activated = await deps.connectActivatedHost(connectInput);
+ if (activated.kind === 'connected') {
+ if (signal?.aborted) {
+ await activated.connection.close();
+ signal.throwIfAborted();
+ }
+ connected = activated;
+ } else {
+ const ConnectionError =
+ activated.kind === 'incompatible' || activated.kind === 'upgrade_required'
+ ? RuntimeHostPermanentReconnectError
+ : Error;
+ throw new ConnectionError(
+ `The installed Runtime Host was activated but the local CLI could not join it (${activated.kind === 'unavailable' ? activated.reason : activated.kind}). Use a compatible CLI or update the managed Host through its configured operator.`,
+ );
+ }
+ }
if (connected.kind === 'incompatible') {
throw new RuntimeHostCliConflictError(connected.handshake, connected.registration);
}
diff --git a/packages/cli/src/runtime-host-local-managed-activation.ts b/packages/cli/src/runtime-host-local-managed-activation.ts
new file mode 100644
index 0000000000..6a195ebd8c
--- /dev/null
+++ b/packages/cli/src/runtime-host-local-managed-activation.ts
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { execFile } from 'node:child_process';
+import { access } from 'node:fs/promises';
+import { join } from 'node:path';
+import { promisify } from 'node:util';
+import { resolveStorageRoot } from '@maka/storage/root-authority';
+import { runtimeHostStartupError } from '@maka/runtime-host/client';
+import {
+ decodeRuntimeHostActivationFrame,
+ readRuntimeHostManagedDeploymentConfig,
+ RUNTIME_HOST_ACTIVATION_FRAME_MAX_BYTES,
+ runtimeHostManagedOperatorCommand,
+ runtimeHostOperatorInvocation,
+} from '@maka/runtime-host/operator';
+
+const run = promisify(execFile);
+
+/** Invoke the installed owner; never launch the calling CLI's candidate as the owner. */
+export async function activateLocalManagedRuntimeHost(input: {
+ readonly rootPath: string;
+ readonly signal?: AbortSignal;
+}): Promise<void> {
+ input.signal?.throwIfAborted();
+ const capability = await resolveStorageRoot({ path: input.rootPath, kind: 'interactive' });
+ const config = await readRuntimeHostManagedDeploymentConfig(capability);
+ if (!config || config.lifecycle.mode !== 'on_demand') {
+ throw runtimeHostStartupError('managed_root_requires_operator');
+ }
+ const operator = runtimeHostManagedOperatorCommand(
+ config,
+ process.platform === 'win32' ? 'win32' : 'posix',
+ );
+ let invocation = runtimeHostOperatorInvocation(operator, [
+ 'activate',
+ '--framed',
+ '--root-id',
+ capability.rootId,
+ ]);
+ // Older installations have only the stable POSIX operator executable.
+ try {
+ await access(operator.modulePath);
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT' || process.platform === 'win32') {
+ throw error;
+ }
+ invocation = {
+ executable: join(config.deploymentRoot, 'operator'),
+ args: invocation.args.slice(1),
+ };
+ }
+ const result = await run(invocation.executable, [...invocation.args], {
+ encoding: 'utf8',
+ timeout: 120_000,
+ killSignal: 'SIGKILL',
+ maxBuffer: RUNTIME_HOST_ACTIVATION_FRAME_MAX_BYTES + 256,
+ ...(input.signal ? { signal: input.signal } : {}),
+ }).catch((error: unknown) => {
+ input.signal?.throwIfAborted();
+ // Operators report a framed diagnostic and exit nonzero on expected failures.
+ if (error instanceof Error && 'stdout' in error && typeof error.stdout === 'string') {
+ const frame = decodeSingleActivationFrame(error.stdout);
+ if (frame?.kind === 'error') throw new Error(frame.error.message, { cause: error });
+ }
+ throw error;
+ });
+ input.signal?.throwIfAborted();
+ const frame = decodeSingleActivationFrame(result.stdout);
+ if (!frame)
+ throw new Error('The installed Runtime Host operator returned an invalid activation result');
+ if (frame.kind === 'error') throw new Error(frame.error.message);
+ if (frame.rootId !== capability.rootId || frame.deploymentId !== config.deploymentId) {
+ throw new Error('The installed Runtime Host operator activated a different deployment');
+ }
+}
+
+function decodeSingleActivationFrame(stdout: string) {
+ const line = stdout.replace(/\r?\n$/u, '');
+ return /[\r\n]/u.test(line) ? undefined : decodeRuntimeHostActivationFrame(line);
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org