feat(workers): run device sessions from Gateway bundles (#124037)

* feat(workers): run device sessions from Gateway bundles

Install the current Gateway bundle before a device environment becomes ready, verify it at attach and tunnel boundaries, launch only from the immutable namespaced bundle directory, and retire stale environments for idempotent reprovisioning. Remove the local execution mode and preserve the node-local build claim only as temporary inventory metadata for the final projection/cleanup slice.

* docs(runners): record Gateway bundle cutover

* test(ci): repair runner validation fixtures

# Conflicts:
#	src/scripts/test-projects.test.ts

* fix(workers): surface outdated node recovery

Keep legacy runner inventory diagnostic-only while exposing the update-and-reconnect action through node, environment, provider, placement, and Control UI surfaces.

* fix(workers): reject legacy inventory with recovery

* fix(workers): bundle worker deploy closure

* test(workers): close bundle cutover gates

* fix(workers): compose browser runtime at build

* fix(workers): satisfy bundle cutover gates

* fix(workers): route temp runtime through infra

* docs(workers): align bundle host guidance

* fix(ui): fence outdated session destinations
This commit is contained in:
Peter Steinberger
2026-08-15 17:46:44 -07:00
committed by GitHub
parent eb13f5719f
commit 78502eda6d
105 changed files with 1950 additions and 1533 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ type NodeWorkerBuildOptions = {
protocolFeatures?: readonly string[];
};
export type NodeWorkerInstallation = {
type NodeWorkerInstallation = {
packageRoot: string;
build: WorkerAdmissionHandshake;
revalidateBuild(): Promise<boolean>;
@@ -5,7 +5,6 @@ import os from "node:os";
import path from "node:path";
import * as tar from "tar";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runCommandWithTimeout } from "../process/exec.js";
import {
DEFAULT_WORKER_BUNDLE_ARCHIVE_LIMITS,
readWorkerBundleDirectoryManifest,
@@ -33,29 +32,33 @@ describe("node worker bundle installer", () => {
await fs.rm(root, { recursive: true, force: true });
});
async function bundleFixture(): Promise<{
async function bundleFixture(options: { packageShell?: boolean } = {}): Promise<{
archive: Buffer;
input: NodeWorkerBundleInstallInput;
}> {
const source = path.join(root, "source");
const archivePath = path.join(root, "bundle.tgz");
await fs.mkdir(path.join(source, "dist"), { recursive: true });
await fs.writeFile(path.join(source, "openclaw.mjs"), "#!/usr/bin/env node\n");
await fs.chmod(path.join(source, "openclaw.mjs"), 0o700);
await fs.writeFile(path.join(source, "package.json"), '{"name":"openclaw"}\n');
await fs.chmod(path.join(source, "package.json"), 0o600);
await fs.writeFile(path.join(source, "dist", "worker.js"), "export {};\n");
await fs.chmod(path.join(source, "dist", "worker.js"), 0o600);
await fs.mkdir(source, { recursive: true });
await fs.writeFile(path.join(source, "worker.mjs"), "export {};\n", { mode: 0o700 });
const archiveEntries = ["worker.mjs"];
if (options.packageShell) {
await fs.mkdir(path.join(source, "dist"));
await fs.writeFile(path.join(source, "openclaw.mjs"), "#!/usr/bin/env node\n", {
mode: 0o700,
});
await fs.writeFile(path.join(source, "package.json"), '{"name":"openclaw"}\n');
await fs.writeFile(path.join(source, "dist", "worker.js"), "export {};\n");
archiveEntries.push("dist/worker.js", "openclaw.mjs", "package.json");
}
const manifest = await readWorkerBundleDirectoryManifest({
root: source,
limits: DEFAULT_WORKER_BUNDLE_ARCHIVE_LIMITS,
});
const bundleHash = hashWorkerBundleManifest(manifest);
await tar.create({ cwd: source, file: archivePath, gzip: true, noDirRecurse: true }, [
"dist/worker.js",
"openclaw.mjs",
"package.json",
]);
await tar.create(
{ cwd: source, file: archivePath, gzip: true, noDirRecurse: true },
archiveEntries,
);
const archive = await fs.readFile(archivePath);
return {
archive,
@@ -106,15 +109,7 @@ describe("node worker bundle installer", () => {
);
await fs.mkdir(staleStaging, { recursive: true });
const served = await serve(fixture.archive, fixture.input.archive.token);
const runCommand = vi.fn<typeof runCommandWithTimeout>(async () => ({
stdout: "",
stderr: "",
code: 0,
signal: null,
killed: false,
termination: "exit" as const,
}));
const installer = new NodeWorkerBundleInstaller({ root, runCommand });
const installer = new NodeWorkerBundleInstaller({ root });
await expect(
installer.ensure({ input: fixture.input, gatewayUrl: served.gatewayUrl }),
@@ -124,9 +119,7 @@ describe("node worker bundle installer", () => {
).resolves.toEqual(fixture.input.build);
expect(served.requests).toHaveBeenCalledOnce();
expect(runCommand).toHaveBeenCalledOnce();
await expect(fs.access(staleStaging)).rejects.toThrow();
expect(runCommand.mock.calls[0]?.[0]).toContain("--ignore-scripts");
await expect(
fs.readFile(
path.join(
@@ -141,18 +134,36 @@ describe("node worker bundle installer", () => {
).resolves.toContain(fixture.input.build.bundleHash);
});
it("reinstalls when executable dependency material appears outside the bundle hash", async () => {
const fixture = await bundleFixture({ packageShell: true });
const served = await serve(fixture.archive, fixture.input.archive.token);
const installer = new NodeWorkerBundleInstaller({ root });
const bundleDir = path.join(
root,
fixture.input.gatewayNamespace,
"bundles",
fixture.input.build.bundleHash,
);
const tamperedDependency = path.join(bundleDir, "node_modules", "tampered", "index.js");
await installer.ensure({ input: fixture.input, gatewayUrl: served.gatewayUrl });
await fs.mkdir(path.dirname(tamperedDependency), { recursive: true });
await fs.writeFile(tamperedDependency, "export const trusted = false;\n");
await installer.ensure({ input: fixture.input, gatewayUrl: served.gatewayUrl });
expect(served.requests).toHaveBeenCalledTimes(2);
await expect(fs.access(tamperedDependency)).rejects.toThrow();
});
it("rejects archive digest mismatch without publishing a bundle", async () => {
const fixture = await bundleFixture();
fixture.input.archive.sha256 = "f".repeat(64);
const served = await serve(fixture.archive, fixture.input.archive.token);
const installer = new NodeWorkerBundleInstaller({
root,
runCommand: vi.fn(),
});
const installer = new NodeWorkerBundleInstaller({ root });
await expect(
installer.ensure({ input: fixture.input, gatewayUrl: served.gatewayUrl }),
).rejects.toThrow("bundle installation did not complete");
).rejects.toThrow("worker bundle download failed integrity validation");
await expect(
fs.access(
path.join(root, fixture.input.gatewayNamespace, "bundles", fixture.input.build.bundleHash),
@@ -167,10 +178,10 @@ describe("node worker bundle installer", () => {
fixture.input.archive.token,
fixture.archive.byteLength + 1,
);
const installer = new NodeWorkerBundleInstaller({ root, runCommand: vi.fn() });
const installer = new NodeWorkerBundleInstaller({ root });
await expect(
installer.ensure({ input: fixture.input, gatewayUrl: served.gatewayUrl }),
).rejects.toThrow("bundle installation did not complete");
).rejects.toThrow("gateway returned an unexpected worker bundle length");
});
});
+16 -75
View File
@@ -1,22 +1,27 @@
import { createHash, randomUUID } from "node:crypto";
import { once } from "node:events";
import fs from "node:fs";
import fsp from "node:fs/promises";
import type { IncomingMessage } from "node:http";
import path from "node:path";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import {
validateWorkerAdmissionHandshake,
type WorkerAdmissionHandshake,
} from "../../packages/gateway-protocol/src/index.js";
import { resolveStateDir } from "../config/paths.js";
import { isPathInside } from "../infra/path-guards.js";
import { redactSensitiveText } from "../logging/redact.js";
import { KeyedAsyncQueue } from "../plugin-sdk/keyed-async-queue.js";
import { runCommandWithTimeout } from "../process/exec.js";
import {
DEFAULT_WORKER_BUNDLE_ARCHIVE_LIMITS,
extractWorkerBundleArchive,
readWorkerBundleDirectoryManifest,
} from "../shared/worker-bundle-archive.js";
import { hashWorkerBundleManifest } from "../shared/worker-bundle-hash.js";
import {
hashWorkerBundleManifest,
WORKER_BUNDLE_ENTRY_PATH,
} from "../shared/worker-bundle-hash.js";
import { MAX_WORKER_BUNDLE_ARCHIVE_BYTES } from "../shared/worker-bundle-limits.js";
import {
nodeWorkerBundleTransferPath,
@@ -30,28 +35,7 @@ import {
} from "./node-worker-transfer-http.js";
const INSTALL_RECEIPT = "bootstrap-receipt.json";
const INSTALL_TIMEOUT_MS = 35 * 60_000;
const INSTALL_IGNORED_TOP_LEVEL = new Set(["node_modules", INSTALL_RECEIPT]);
type BundleInstallCommandRunner = typeof runCommandWithTimeout;
function commandEnv(homeDir: string, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
return {
...env,
HOME: homeDir,
...(process.platform === "win32" ? { USERPROFILE: homeDir } : {}),
CI: "1",
GIT_ASKPASS: "",
GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
GIT_CONFIG_NOSYSTEM: "1",
GIT_TERMINAL_PROMPT: "0",
NPM_CONFIG_AUDIT: "false",
NPM_CONFIG_FUND: "false",
NPM_CONFIG_IGNORE_SCRIPTS: "true",
NPM_CONFIG_UPDATE_NOTIFIER: "false",
SSH_ASKPASS: "",
};
}
const INSTALL_IGNORED_TOP_LEVEL = new Set([INSTALL_RECEIPT]);
async function responseBody(response: IncomingMessage, maxBytes = 64 * 1024): Promise<string> {
const chunks: Buffer[] = [];
@@ -104,10 +88,7 @@ async function downloadBundle(params: {
}
hash.update(chunk);
if (!output.write(chunk)) {
await new Promise<void>((resolve, reject) => {
output.once("drain", resolve);
output.once("error", reject);
});
await once(output, "drain");
}
}
await new Promise<void>((resolve, reject) => {
@@ -158,7 +139,7 @@ async function validateInstalledBundle(
return false;
}
const root = await fsp.realpath(bundleDir);
const entry = await fsp.realpath(path.join(root, "openclaw.mjs"));
const entry = await fsp.realpath(path.join(root, WORKER_BUNDLE_ENTRY_PATH));
return isPathInside(root, entry) && (await fsp.stat(entry)).isFile();
} catch {
return false;
@@ -202,21 +183,11 @@ async function publishBundle(destination: string, staging: string): Promise<void
export class NodeWorkerBundleInstaller {
readonly #root: string;
readonly #env: NodeJS.ProcessEnv;
readonly #runCommand: BundleInstallCommandRunner;
readonly #operations = new KeyedAsyncQueue();
constructor(
options: {
root?: string;
env?: NodeJS.ProcessEnv;
runCommand?: BundleInstallCommandRunner;
} = {},
) {
constructor(options: { root?: string; env?: NodeJS.ProcessEnv } = {}) {
const env = options.env ?? process.env;
this.#root = path.resolve(options.root ?? path.join(resolveStateDir(env), "node-host"));
this.#env = { ...env };
this.#runCommand = options.runCommand ?? runCommandWithTimeout;
}
async ensure(params: {
@@ -244,8 +215,6 @@ export class NodeWorkerBundleInstaller {
try {
const archivePath = path.join(operationRoot, "bundle.tgz");
const staging = path.join(operationRoot, "root");
const homeDir = path.join(operationRoot, "home");
await fsp.mkdir(homeDir, { mode: 0o700 });
await downloadBundle({
gatewayUrl: params.gatewayUrl,
gatewayTlsFingerprint: params.gatewayTlsFingerprint,
@@ -259,38 +228,6 @@ export class NodeWorkerBundleInstaller {
expectedBundleHash: input.build.bundleHash,
limits: DEFAULT_WORKER_BUNDLE_ARCHIVE_LIMITS,
});
const install = await this.#runCommand(
[
"npm",
"install",
"--prefix",
staging,
"--ignore-scripts",
"--omit=dev",
"--no-audit",
"--no-fund",
"--package-lock=false",
],
{
cwd: staging,
baseEnv: commandEnv(homeDir, this.#env),
timeoutMs: INSTALL_TIMEOUT_MS,
signal: params.signal,
maxOutputBytes: 256 * 1024,
maxCombinedOutputBytes: 512 * 1024,
},
);
if (install.termination !== "exit" || install.code !== 0) {
throw new Error("worker bundle dependency installation failed");
}
const installedManifest = await readWorkerBundleDirectoryManifest({
root: staging,
limits: DEFAULT_WORKER_BUNDLE_ARCHIVE_LIMITS,
ignoreTopLevel: new Set(["node_modules"]),
});
if (hashWorkerBundleManifest(installedManifest) !== input.build.bundleHash) {
throw new Error("worker bundle changed during dependency installation");
}
const receipt = await fsp.open(path.join(staging, INSTALL_RECEIPT), "wx", 0o600);
try {
await receipt.writeFile(`${JSON.stringify(input.build)}\n`);
@@ -318,8 +255,12 @@ export class NodeWorkerBundleInstaller {
{ cause: error },
);
}
const detail = truncateUtf16Safe(
redactSensitiveText(error instanceof Error ? error.message : String(error)),
512,
);
throw new NodeWorkerBundleInstallError(
"worker-bundle-install-failed: bundle installation did not complete",
`worker-bundle-install-failed: ${detail || "bundle installation did not complete"}`,
{ cause: error },
);
}
+5 -23
View File
@@ -1,32 +1,14 @@
import fs from "node:fs";
import path from "node:path";
import { isPathInside } from "../infra/path-guards.js";
import type { NodeWorkerLaunchInput } from "../worker/node-supervisor-protocol.js";
import type { NodeWorkerInstallation } from "./node-worker-build.js";
import { WORKER_BUNDLE_ENTRY_PATH } from "../shared/worker-bundle-hash.js";
/** Resolves an explicitly selected worker install without crossing local/bundle trust modes. */
export async function resolveNodeWorkerEntry(params: {
/** Resolves one exact Gateway-managed worker bundle from its isolated namespace. */
export function resolveNodeWorkerEntry(params: {
bundleRoot: string;
installKind: NodeWorkerLaunchInput["installKind"];
expectedBundleHash: string;
gatewayNamespace: string;
localInstallation?: NodeWorkerInstallation;
}): Promise<string> {
if (params.installKind === "local") {
const installation = params.localInstallation;
if (!installation || installation.build.bundleHash !== params.expectedBundleHash) {
throw new Error("node worker local install does not match its advertised build");
}
if (!(await installation.revalidateBuild())) {
throw new Error("node worker local install changed after its build was advertised");
}
const root = fs.realpathSync.native(installation.packageRoot);
const entry = fs.realpathSync.native(path.join(root, "openclaw.mjs"));
if (!isPathInside(root, entry) || !fs.statSync(entry).isFile()) {
throw new Error("node worker local entry must be a regular file inside its install");
}
return entry;
}
}): string {
const root = fs.realpathSync.native(params.bundleRoot);
const bundle = fs.realpathSync.native(
path.join(root, params.gatewayNamespace, "bundles", params.expectedBundleHash),
@@ -34,7 +16,7 @@ export async function resolveNodeWorkerEntry(params: {
if (!isPathInside(root, bundle)) {
throw new Error("node worker bundle resolves outside its configured root");
}
const entry = fs.realpathSync.native(path.join(bundle, "openclaw.mjs"));
const entry = fs.realpathSync.native(path.join(bundle, WORKER_BUNDLE_ENTRY_PATH));
if (!isPathInside(bundle, entry) || !fs.statSync(entry).isFile()) {
throw new Error("node worker entry must be a regular file inside its bundle");
}
@@ -61,7 +61,6 @@ function planHash(input: ReturnType<typeof testWorkerLaunchInput>): string {
return createHash("sha256")
.update(
stableStringify({
installKind: input.installKind,
expectedBundleHash: input.expectedBundleHash,
descriptor: input.descriptor,
gatewayNamespace: input.gatewayNamespace,
@@ -202,7 +202,7 @@ export function writeNodeWorkerFixture(root: string) {
const bundleDir = path.join(bundleRoot, "gateway-1", "bundles", TEST_BUNDLE_HASH);
fs.mkdirSync(bundleDir, { recursive: true });
fs.mkdirSync(workspaceDir, { recursive: true });
fs.writeFileSync(path.join(bundleDir, "openclaw.mjs"), TEST_WORKER_SOURCE);
fs.writeFileSync(path.join(bundleDir, "worker.mjs"), TEST_WORKER_SOURCE);
return { bundleRoot, env: { OPENCLAW_STATE_DIR: stateDir }, root, stateDir, workspaceDir };
}
@@ -214,7 +214,6 @@ export function testWorkerLaunchInput(
return {
launchId,
gatewayNamespace: "gateway-1",
installKind: "bundle",
expectedBundleHash: TEST_BUNDLE_HASH,
placementGeneration: 4,
descriptor: testWorkerDescriptor(workspaceDir, prompt),
+2 -54
View File
@@ -1,6 +1,5 @@
import childProcess from "node:child_process";
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
@@ -12,7 +11,6 @@ import {
openOpenClawStateDatabase,
} from "../state/openclaw-state-db.js";
import { withEnvAsync } from "../test-utils/env.js";
import { resolveNodeWorkerInstallation } from "./node-worker-build.js";
import { NodeWorkerLaunchStore } from "./node-worker-launch-store.js";
import {
inspectNodeWorkerProcessIdentity,
@@ -193,7 +191,7 @@ describe("node worker supervisor", () => {
const completed = await waitForTerminal(supervisor, input.launchId);
expect(completed).toMatchObject({ state: "completed", errorText: null });
expect(JSON.parse(completed.resultJson ?? "null")).toEqual({
argv: ["worker", "--internal-worker-ipc"],
argv: ["--internal-worker-ipc"],
status: "completed",
});
expect(await supervisor.launch(input, TEST_WORKER_ENDPOINT)).toEqual(completed);
@@ -307,56 +305,6 @@ describe("node worker supervisor", () => {
expect(new NodeWorkerLaunchStore({ env }).get(waiting.launchId)).toBeUndefined();
});
it("reuses the advertised local install and refuses it after its worker bytes change", async () => {
const root = tempDirs.make("node-worker-local-install-");
const packageRoot = path.join(root, "package");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
fs.mkdirSync(path.join(packageRoot, "dist"), { recursive: true });
fs.mkdirSync(workspaceDir, { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "2026.8.1", type: "module", dependencies: {} }),
);
fs.writeFileSync(path.join(packageRoot, "openclaw.mjs"), TEST_WORKER_SOURCE, { mode: 0o755 });
const distPath = path.join(packageRoot, "dist", "entry.js");
fs.writeFileSync(distPath, "export const workerBuild = 1;\n");
const installation = await resolveNodeWorkerInstallation({
packageRoot,
openclawVersion: "2026.8.1",
protocolFeatures: [],
});
const supervisor = createNodeWorkerSupervisor({
env: { OPENCLAW_STATE_DIR: stateDir },
localInstallation: installation,
});
const localInput = (launchId: string) => {
const input = launchInput(workspaceDir, launchId);
input.installKind = "local";
input.expectedBundleHash = installation.build.bundleHash;
input.descriptor.admission.handshake = structuredClone(installation.build);
return input;
};
const stagingRoot = vi.spyOn(fsp, "mkdtemp");
const first = localInput("local-success");
expect(await supervisor.launch(first, TEST_WORKER_ENDPOINT)).toMatchObject({
state: "running",
});
expect(await waitForTerminal(supervisor, first.launchId)).toMatchObject({ state: "completed" });
expect(stagingRoot).not.toHaveBeenCalled();
fs.writeFileSync(distPath, "export const workerBuild = 2;\n");
expect(
await supervisor.launch(localInput("local-mutated"), TEST_WORKER_ENDPOINT),
).toMatchObject({
state: "failed",
errorText: expect.stringContaining("changed after its build was advertised"),
});
expect(stagingRoot).toHaveBeenCalledTimes(1);
await supervisor.close();
});
it.each(["status", "launch", "cancel", "close"] as const)(
"retains an observed terminal outcome when %s reconciliation keeps failing",
async (operation) => {
@@ -744,7 +692,7 @@ describe("node worker supervisor", () => {
const outsideEntry = path.join(root, "outside.mjs");
fs.mkdirSync(escapedBundle, { recursive: true });
fs.writeFileSync(outsideEntry, TEST_WORKER_SOURCE);
fs.symlinkSync(outsideEntry, path.join(escapedBundle, "openclaw.mjs"));
fs.symlinkSync(outsideEntry, path.join(escapedBundle, "worker.mjs"));
const input = launchInput(workspaceDir, "escaped-entry");
input.expectedBundleHash = escapedHash;
input.descriptor.admission.handshake.bundleHash = escapedHash;
+2 -8
View File
@@ -19,7 +19,6 @@ import type {
} from "../worker/node-workspace-retain-protocol.js";
import { formatWorkerConnectionFailure } from "../worker/worker-connection-contract.js";
import type { WorkerConnectionEndpoint } from "../worker/worker-connection-endpoint.js";
import type { NodeWorkerInstallation } from "./node-worker-build.js";
import { NodeWorkerCapacity } from "./node-worker-capacity.js";
import { resolveNodeWorkerEntry } from "./node-worker-entry.js";
import { snapshotNodeWorkerEnv } from "./node-worker-environment.js";
@@ -90,7 +89,6 @@ type ActiveOwnership = RunningChild | ObservedTerminal;
type NodeWorkerSupervisorOptions = {
bundleRoot?: string;
env?: NodeJS.ProcessEnv;
localInstallation?: NodeWorkerInstallation;
capacity?: number;
capacityWaitMs?: number;
onAvailabilityChanged?: (available: boolean) => void;
@@ -126,7 +124,6 @@ class NodeWorkerSupervisor {
private readonly bundleRoot: string;
private readonly store: NodeWorkerLaunchStore;
private readonly workerEnv: NodeJS.ProcessEnv;
private readonly localInstallation?: NodeWorkerInstallation;
private readonly capacity: NodeWorkerCapacity;
private readonly workspace: NodeWorkerWorkspaceRuntime;
private supervisorIdentity?: NodeWorkerProcessIdentity;
@@ -141,7 +138,6 @@ class NodeWorkerSupervisor {
);
this.store = new NodeWorkerLaunchStore({ env });
this.workerEnv = snapshotNodeWorkerEnv(env);
this.localInstallation = options.localInstallation;
this.workspace =
options.workspace ??
new NodeWorkerWorkspaceRuntime({ root: this.bundleRoot, env: this.workerEnv });
@@ -481,15 +477,13 @@ class NodeWorkerSupervisor {
registerSecretValueForRedaction(credential);
let adapter: ChildAdapter;
try {
const entry = await resolveNodeWorkerEntry({
const entry = resolveNodeWorkerEntry({
bundleRoot: this.bundleRoot,
installKind: params.input.installKind,
expectedBundleHash: params.input.expectedBundleHash,
gatewayNamespace: params.input.gatewayNamespace,
...(this.localInstallation ? { localInstallation: this.localInstallation } : {}),
});
adapter = await createChildAdapter({
argv: [process.execPath, entry, "worker", "--internal-worker-ipc"],
argv: [process.execPath, entry, "--internal-worker-ipc"],
env: this.workerEnv,
exactEnv: true,
ownedWorker: true,
+6 -8
View File
@@ -283,10 +283,9 @@ export async function prepareNodeHostRuntime(params?: {
params?.enableAgentRuns === true && config.nodeHost?.agentRuns?.claude?.enabled === true
? resolveExecutableTrustPathFromEnv("claude", pathEnv)
: null;
const workerInstallation =
params?.enableWorkerRuns === true && config.nodeHost?.workerRuns?.enabled === true
? await resolveNodeWorkerInstallation()
: undefined;
const workerRunsEnabled =
params?.enableWorkerRuns === true && config.nodeHost?.workerRuns?.enabled === true;
const workerInstallation = workerRunsEnabled ? await resolveNodeWorkerInstallation() : undefined;
const workerRuns = workerInstallation?.build;
const skills = config.nodeHost?.skills?.enabled === false ? null : scanNodeHostedSkills();
const buildManifest = (pluginManifest: typeof pluginNodeHost): NodeHostManifest => ({
@@ -326,16 +325,15 @@ export async function prepareNodeHostRuntime(params?: {
initialInventory,
start({ client, onInventoryChanged, onManifestChanged, onRunnerAvailabilityChanged }) {
const mcpAbort = new AbortController();
const workerWorkspace = workerInstallation
const workerWorkspace = workerRunsEnabled
? new NodeWorkerWorkspaceRuntime({ env })
: undefined;
const workerBundleInstaller = workerInstallation
const workerBundleInstaller = workerRunsEnabled
? new NodeWorkerBundleInstaller({ env })
: undefined;
const workerSupervisor = workerInstallation
const workerSupervisor = workerRunsEnabled
? createNodeWorkerSupervisor({
env,
localInstallation: workerInstallation,
onAvailabilityChanged: onRunnerAvailabilityChanged,
workspace: workerWorkspace,
})