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