fix(plugins): register static node-host commands without activation (#127043)

The node host resolves its plugin registry via loadPluginRegistryHandle
(activate:false). Since #117587 static definition.nodeHostCommands only
registered under runFullActivationOnlyRegistrations, so headless nodes
silently lost browser.proxy (and the browser/file caps), breaking the
meeting-bot chain with 'No connected Google Meet-capable node with
browser proxy'. Register node-host commands in every load mode; each
command keeps its own isAvailable gate. reload and security audit
collectors stay activation-only.
This commit is contained in:
Peter Steinberger
2026-08-21 09:52:20 -07:00
committed by GitHub
parent 4343b38ce7
commit 781ded80d6
2 changed files with 56 additions and 3 deletions
+7 -3
View File
@@ -507,13 +507,17 @@ export function loadRuntimePluginCandidate(params: {
}
return;
}
// Node-host commands register in every load mode: the node host resolves its
// registry without activation (loadPluginRegistryHandle), and each command is
// already availability-gated per invocation. Gating them on full activation
// silently strips static registrations like browser.proxy from headless nodes.
for (const nodeHostCommand of definition?.nodeHostCommands ?? []) {
params.registryBuilder.registerNodeHostCommand(record, nodeHostCommand);
}
if (registrationPlan.runFullActivationOnlyRegistrations) {
if (definition?.reload) {
params.registryBuilder.registerReload(record, definition.reload);
}
for (const nodeHostCommand of definition?.nodeHostCommands ?? []) {
params.registryBuilder.registerNodeHostCommand(record, nodeHostCommand);
}
for (const collector of definition?.securityAuditCollectors ?? []) {
params.registryBuilder.registerSecurityAuditCollector(record, collector);
}
@@ -0,0 +1,49 @@
/** Verifies static plugin nodeHostCommands survive non-activating registry loads (node-host path). */
import { afterAll, afterEach, expect, it } from "vitest";
import {
cleanupPluginLoaderFixturesForTest,
loadOpenClawPlugins,
resetPluginLoaderTestStateForTest,
useNoBundledPlugins,
writePlugin,
} from "./loader.test-fixtures.js";
afterEach(resetPluginLoaderTestStateForTest);
afterAll(cleanupPluginLoaderFixturesForTest);
// The node host resolves its registry via loadPluginRegistryHandle (activate:false).
// Static nodeHostCommands (e.g. the browser plugin's browser.proxy) must register
// there too, or headless meeting/browser nodes silently lose their surface.
it("registers static nodeHostCommands without activation", () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "node-surface",
body: `module.exports = {
id: "node-surface",
nodeHostCommands: [{
command: "nodesurface.proxy",
cap: "node-surface",
handle: async () => "ok",
}],
register() {},
};`,
});
const registry = loadOpenClawPlugins({
cache: false,
activate: false,
workspaceDir: plugin.dir,
config: {
plugins: {
load: { paths: [plugin.file] },
allow: [plugin.id],
},
},
onlyPluginIds: [plugin.id],
});
expect(registry.plugins.find((entry) => entry.id === plugin.id)?.status).toBe("loaded");
expect(registry.nodeHostCommands.map((entry) => entry.command.command)).toContain(
"nodesurface.proxy",
);
});