mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:40:42 +00:00
feat(plugins): expose nodes runtime to cli commands
This commit is contained in:
@@ -104,6 +104,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/TTS: suppress successful spoken transcripts from verbose chat tool output when structured voice media is already queued, while preserving text output for non-builtin tool-name collisions. Fixes #71282. Thanks @neeravmakwana.
|
||||
- Plugins/Google Meet: reuse existing Meet tabs and active sessions across harmless URL query differences, avoiding duplicate Chrome windows when agents retry a join. Thanks @steipete.
|
||||
- Plugins/Google Meet: tell agents to recover already-open Meet tabs after browser timeouts, and make the dev CLI release its build lock if compiler spawning fails. Thanks @steipete.
|
||||
- Plugins/CLI: provide Gateway-backed node inspection to plugin commands, so `googlemeet recover-tab` can inspect paired browser nodes from the terminal. Thanks @steipete.
|
||||
- Gateway/sessions: recover main-agent turns interrupted by a gateway restart from stale transcript-lock evidence, avoiding stuck `status: "running"` sessions without broad post-boot transcript scans. Fixes #70555. Thanks @bitloi.
|
||||
- Codex approvals: keep command approval responses within Codex app-server `availableDecisions`, including deny/cancel fallbacks for prompts that do not offer `decline`. (#71338) Thanks @Lucenx9.
|
||||
- Codex harness: reject same-thread app-server notifications without `turnId` or `turn.id` after a bound turn starts, preventing unscoped events from mutating or completing the active reply. (#71317) Thanks @Lucenx9.
|
||||
|
||||
@@ -931,7 +931,8 @@ openclaw googlemeet recover-tab https://meet.google.com/abc-defg-hij
|
||||
The equivalent tool action is `recover_current_tab`. It focuses and inspects an
|
||||
existing Meet tab on the configured Chrome node. It does not open a new tab or
|
||||
create a new session; it reports the current blocker, such as login, admission,
|
||||
permissions, or audio-choice state.
|
||||
permissions, or audio-choice state. The CLI command talks to the configured
|
||||
Gateway, so the Gateway must be running and the Chrome node must be connected.
|
||||
|
||||
### Twilio setup checks fail
|
||||
|
||||
|
||||
@@ -122,8 +122,8 @@ await api.runtime.subagent.deleteSession({
|
||||
### `api.runtime.nodes`
|
||||
|
||||
List connected nodes and invoke a node-host command from Gateway-loaded plugin
|
||||
code. Use this when a plugin owns local work on a paired device, for example a
|
||||
browser or audio bridge on another Mac.
|
||||
code or from plugin CLI commands. Use this when a plugin owns local work on a
|
||||
paired device, for example a browser or audio bridge on another Mac.
|
||||
|
||||
```typescript
|
||||
const { nodes } = await api.runtime.nodes.list({ connected: true });
|
||||
@@ -136,7 +136,9 @@ const result = await api.runtime.nodes.invoke({
|
||||
});
|
||||
```
|
||||
|
||||
This runtime is only available inside the Gateway. Node commands still go
|
||||
Inside the Gateway this runtime is in-process. In plugin CLI commands it calls
|
||||
the configured Gateway over RPC, so commands such as `openclaw googlemeet
|
||||
recover-tab` can inspect paired nodes from the terminal. Node commands still go
|
||||
through normal Gateway node pairing, command allowlists, and node-local command
|
||||
handling.
|
||||
|
||||
|
||||
45
src/plugins/cli-gateway-nodes-runtime.ts
Normal file
45
src/plugins/cli-gateway-nodes-runtime.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
|
||||
export function createPluginCliGatewayNodesRuntime(): PluginRuntime["nodes"] {
|
||||
return {
|
||||
async list(params) {
|
||||
const payload = await callGateway({
|
||||
method: "node.list",
|
||||
params: {},
|
||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
||||
mode: GATEWAY_CLIENT_MODES.CLI,
|
||||
});
|
||||
const nodes = Array.isArray(payload?.nodes) ? payload.nodes : [];
|
||||
const filteredNodes =
|
||||
params?.connected === true
|
||||
? nodes.filter(
|
||||
(node) =>
|
||||
node !== null &&
|
||||
typeof node === "object" &&
|
||||
(node as { connected?: unknown }).connected === true,
|
||||
)
|
||||
: nodes;
|
||||
return {
|
||||
nodes: filteredNodes as Awaited<ReturnType<PluginRuntime["nodes"]["list"]>>["nodes"],
|
||||
};
|
||||
},
|
||||
async invoke(params) {
|
||||
return await callGateway({
|
||||
method: "node.invoke",
|
||||
params: {
|
||||
nodeId: params.nodeId,
|
||||
command: params.command,
|
||||
...(params.params !== undefined && { params: params.params }),
|
||||
timeoutMs: params.timeoutMs,
|
||||
idempotencyKey: params.idempotencyKey || randomUUID(),
|
||||
},
|
||||
timeoutMs: params.timeoutMs ? params.timeoutMs + 5_000 : undefined,
|
||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
||||
mode: GATEWAY_CLIENT_MODES.CLI,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { collectUniqueCommandDescriptors } from "../cli/program/command-descriptor-utils.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveManifestActivationPluginIds } from "./activation-planner.js";
|
||||
import { createPluginCliGatewayNodesRuntime } from "./cli-gateway-nodes-runtime.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadOpenClawPluginCliRegistry, loadOpenClawPlugins } from "./loader.js";
|
||||
import type { PluginRegistry } from "./registry.js";
|
||||
@@ -117,6 +118,9 @@ export async function loadPluginCliCommandRegistryWithContext(params: {
|
||||
...(onlyPluginIds.length > 0 ? { onlyPluginIds } : {}),
|
||||
activate: false,
|
||||
cache: false,
|
||||
runtimeOptions: {
|
||||
nodes: createPluginCliGatewayNodesRuntime(),
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -181,6 +181,21 @@ describe("registerPluginCliCommands", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("injects gateway-backed node runtime into plugin CLI commands", async () => {
|
||||
await registerPluginCliCommands(createProgram(), {} as OpenClawConfig);
|
||||
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runtimeOptions: {
|
||||
nodes: {
|
||||
list: expect.any(Function),
|
||||
invoke: expect.any(Function),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads plugin CLI commands from the auto-enabled config snapshot", async () => {
|
||||
const { rawConfig, autoEnabledConfig } = createAutoEnabledCliFixture();
|
||||
mocks.applyPluginAutoEnable.mockReturnValue({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { VERSION } from "../../version.js";
|
||||
import {
|
||||
clearGatewaySubagentRuntime,
|
||||
createPluginRuntime,
|
||||
setGatewayNodesRuntime,
|
||||
setGatewaySubagentRuntime,
|
||||
} from "./index.js";
|
||||
|
||||
@@ -267,4 +268,33 @@ describe("plugin runtime command execution", () => {
|
||||
});
|
||||
expect(run).toHaveBeenCalledWith({ sessionKey: "s-2", message: "hello" });
|
||||
});
|
||||
|
||||
it("uses explicit nodes runtime when provided", async () => {
|
||||
const nodes = {
|
||||
list: vi.fn().mockResolvedValue({ nodes: [] }),
|
||||
invoke: vi.fn().mockResolvedValue({ ok: true }),
|
||||
};
|
||||
const runtime = createPluginRuntime({ nodes });
|
||||
|
||||
await expect(runtime.nodes.list({ connected: true })).resolves.toEqual({ nodes: [] });
|
||||
await expect(
|
||||
runtime.nodes.invoke({ nodeId: "node-1", command: "browser.proxy" }),
|
||||
).resolves.toEqual({ ok: true });
|
||||
expect(nodes.list).toHaveBeenCalledWith({ connected: true });
|
||||
expect(nodes.invoke).toHaveBeenCalledWith({ nodeId: "node-1", command: "browser.proxy" });
|
||||
});
|
||||
|
||||
it("late-binds to gateway nodes when explicitly enabled", async () => {
|
||||
const nodes = {
|
||||
list: vi.fn().mockResolvedValue({ nodes: [{ nodeId: "node-1" }] }),
|
||||
invoke: vi.fn().mockResolvedValue({ ok: true }),
|
||||
};
|
||||
const runtime = createPluginRuntime({ allowGatewaySubagentBinding: true });
|
||||
setGatewayNodesRuntime(nodes);
|
||||
|
||||
await expect(runtime.nodes.list({ connected: true })).resolves.toEqual({
|
||||
nodes: [{ nodeId: "node-1" }],
|
||||
});
|
||||
expect(nodes.list).toHaveBeenCalledWith({ connected: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
|
||||
_options.subagent,
|
||||
_options.allowGatewaySubagentBinding === true,
|
||||
),
|
||||
nodes: createLateBindingNodes(_options.allowGatewaySubagentBinding === true),
|
||||
nodes: _options.nodes ?? createLateBindingNodes(_options.allowGatewaySubagentBinding === true),
|
||||
system: createRuntimeSystem(),
|
||||
media: createRuntimeMedia(),
|
||||
webSearch: {
|
||||
|
||||
@@ -95,5 +95,6 @@ export type PluginRuntime = PluginRuntimeCore & {
|
||||
|
||||
export type CreatePluginRuntimeOptions = {
|
||||
subagent?: PluginRuntime["subagent"];
|
||||
nodes?: PluginRuntime["nodes"];
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user