Skip to content

Commit e1934d9

Browse files
authored
fix(nodes): stop advertising a disabled browser proxy (#103894)
* fix(nodes): hide disabled browser proxy capability * chore: defer node fix changelog to release * chore: ratchet plugin SDK surface budget
1 parent 69f8198 commit e1934d9

9 files changed

Lines changed: 83 additions & 4 deletions

File tree

docs/plugins/sdk-entrypoints.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ export default definePluginEntry({
144144
- `configSchema` can be a function for lazy evaluation. OpenClaw resolves and
145145
memoizes the schema on first access, so expensive schema builders only run
146146
once.
147+
- A `nodeHostCommands` descriptor can define `isAvailable({ config, env })`.
148+
Returning `false` omits that command and its capability from the headless
149+
node's Gateway declaration. OpenClaw evaluates it against the node-local
150+
startup config; command handlers should still validate availability when
151+
invoked.
147152

148153
## `defineChannelPluginEntry`
149154

extensions/browser/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@ describe("browser plugin", () => {
115115
expect(browserPluginNodeHostCommands).toHaveLength(1);
116116
expect(browserPluginNodeHostCommands[0]?.command).toBe("browser.proxy");
117117
expect(browserPluginNodeHostCommands[0]?.cap).toBe("browser");
118+
expect(browserPluginNodeHostCommands[0]?.isAvailable?.({ config: {}, env: {} })).toBe(true);
119+
expect(
120+
browserPluginNodeHostCommands[0]?.isAvailable?.({
121+
config: { browser: { enabled: false } },
122+
env: {},
123+
}),
124+
).toBe(false);
125+
expect(
126+
browserPluginNodeHostCommands[0]?.isAvailable?.({
127+
config: { nodeHost: { browserProxy: { enabled: false } } },
128+
env: {},
129+
}),
130+
).toBe(false);
118131
expect(typeof browserPluginNodeHostCommands[0]?.handle).toBe("function");
119132
expect(browserSecurityAuditCollectors).toHaveLength(1);
120133
});

extensions/browser/plugin-registration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
142142
{
143143
command: "browser.proxy",
144144
cap: "browser",
145+
isAvailable: ({ config }) =>
146+
config.browser?.enabled !== false && config.nodeHost?.browserProxy?.enabled !== false,
145147
handle: async (paramsJSON) => {
146148
const { runBrowserProxyCommand } = await loadBrowserRegistrationRuntimeModule();
147149
return await runBrowserProxyCommand(paramsJSON);

scripts/plugin-sdk-surface-report.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
195195
),
196196
publicExports: readPluginSdkSurfaceBudgetEnv(
197197
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
198-
10490,
198+
10492,
199199
env,
200200
),
201201
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(

src/node-host/plugin-node-host.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
listRegisteredNodeHostCapsAndCommands,
88
} from "./plugin-node-host.js";
99

10+
const availabilityContext = { config: {}, env: {} };
11+
1012
afterEach(() => {
1113
resetPluginRuntimeStateForTest();
1214
});
@@ -48,12 +50,50 @@ describe("plugin node-host registry", () => {
4850
];
4951
setActivePluginRegistry(registry);
5052

51-
expect(listRegisteredNodeHostCapsAndCommands()).toEqual({
53+
expect(listRegisteredNodeHostCapsAndCommands(availabilityContext)).toEqual({
5254
caps: ["browser", "photos"],
5355
commands: ["browser.inspect", "browser.proxy", "photos.proxy"],
5456
});
5557
});
5658

59+
it("omits commands and capabilities unavailable in the node-local config", () => {
60+
const registry = createEmptyPluginRegistry();
61+
registry.nodeHostCommands = [
62+
{
63+
pluginId: "browser",
64+
pluginName: "Browser",
65+
command: {
66+
command: "browser.proxy",
67+
cap: "browser",
68+
isAvailable: ({ config }) => config.browser?.enabled !== false,
69+
handle: vi.fn(async () => "{}"),
70+
},
71+
source: "test",
72+
},
73+
{
74+
pluginId: "photos",
75+
pluginName: "Photos",
76+
command: {
77+
command: "photos.proxy",
78+
cap: "photos",
79+
handle: vi.fn(async () => "{}"),
80+
},
81+
source: "test",
82+
},
83+
];
84+
setActivePluginRegistry(registry);
85+
86+
expect(
87+
listRegisteredNodeHostCapsAndCommands({
88+
config: { browser: { enabled: false } },
89+
env: {},
90+
}),
91+
).toEqual({
92+
caps: ["photos"],
93+
commands: ["photos.proxy"],
94+
});
95+
});
96+
5797
it("dispatches plugin-declared node-host commands", async () => {
5898
const handle = vi.fn(async (paramsJSON?: string | null) => paramsJSON ?? "");
5999
const registry = createEmptyPluginRegistry();

src/node-host/plugin-node-host.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
import type { OpenClawConfig } from "../config/types.openclaw.js";
88
import { getActivePluginRegistry } from "../plugins/runtime.js";
9+
import type { OpenClawPluginNodeHostCommandAvailabilityContext } from "../plugins/types.js";
910
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
1011

1112
const loadPluginRegistryLoaderModule = createLazyRuntimeModule(
@@ -26,14 +27,21 @@ export async function ensureNodeHostPluginRegistry(params: {
2627
}
2728

2829
/** List registered node-host capabilities and command ids in deterministic order. */
29-
export function listRegisteredNodeHostCapsAndCommands(): {
30+
export function listRegisteredNodeHostCapsAndCommands(
31+
context: OpenClawPluginNodeHostCommandAvailabilityContext,
32+
): {
3033
caps: string[];
3134
commands: string[];
3235
} {
3336
const registry = getActivePluginRegistry();
3437
const caps = new Set<string>();
3538
const commands = new Set<string>();
3639
for (const entry of registry?.nodeHostCommands ?? []) {
40+
// Availability belongs to the node-local plugin. Gateway policy still keeps
41+
// the command registered so a differently configured remote node can expose it.
42+
if (entry.command.isAvailable?.(context) === false) {
43+
continue;
44+
}
3745
if (entry.command.cap) {
3846
caps.add(entry.command.cap);
3947
}

src/node-host/runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise<void> {
255255

256256
const cfg = getRuntimeConfig();
257257
await ensureNodeHostPluginRegistry({ config: cfg, env: process.env });
258-
const pluginNodeHost = listRegisteredNodeHostCapsAndCommands();
258+
const pluginNodeHost = listRegisteredNodeHostCapsAndCommands({ config: cfg, env: process.env });
259259
const { token, password } = await resolveNodeHostGatewayCredentials({
260260
config: cfg,
261261
env: process.env,

src/plugin-sdk/plugin-entry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export type OpenClawPluginHttpRouteHandler =
2727
import("../plugins/types.js").OpenClawPluginHttpRouteHandler;
2828
export type OpenClawPluginNodeHostCommand =
2929
import("../plugins/types.js").OpenClawPluginNodeHostCommand;
30+
export type OpenClawPluginNodeHostCommandAvailabilityContext =
31+
import("../plugins/types.js").OpenClawPluginNodeHostCommandAvailabilityContext;
3032
export type OpenClawPluginNodeInvokePolicy =
3133
import("../plugins/types.js").OpenClawPluginNodeInvokePolicy;
3234
export type OpenClawPluginNodeInvokePolicyContext =

src/plugins/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2223,10 +2223,19 @@ export type OpenClawPluginReloadRegistration = {
22232223
noopPrefixes?: string[];
22242224
};
22252225

2226+
export type OpenClawPluginNodeHostCommandAvailabilityContext = {
2227+
/** Node-local configuration used to build this host's Gateway declaration. */
2228+
config: OpenClawConfig;
2229+
/** Node-host process environment. */
2230+
env: NodeJS.ProcessEnv;
2231+
};
2232+
22262233
export type OpenClawPluginNodeHostCommand = {
22272234
command: string;
22282235
cap?: string;
22292236
dangerous?: boolean;
2237+
/** Return false to omit this command and capability from the node declaration. */
2238+
isAvailable?: (context: OpenClawPluginNodeHostCommandAvailabilityContext) => boolean;
22302239
handle: (paramsJSON?: string | null) => Promise<string>;
22312240
};
22322241

0 commit comments

Comments
 (0)