File size: 2,861 Bytes
f778c12 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | // Generic node.invoke command with shell-exec commands intentionally blocked.
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import type { Command } from "commander";
import { randomIdempotencyKey } from "../../gateway/call.js";
import { defaultRuntime } from "../../runtime.js";
import { runNodesCommand } from "./cli-utils.js";
import {
callNodesGatewayCli,
nodesCallOpts,
parseOptionalNodePositiveInteger,
resolveCliNodeId,
} from "./rpc.js";
import type { NodesRpcOpts } from "./types.js";
const BLOCKED_NODE_INVOKE_COMMANDS = new Set(["system.run", "system.run.prepare"]);
function parseNodeInvokeParams(value = "{}"): unknown {
try {
return JSON.parse(value) as unknown;
} catch {
throw new Error("--params must be valid JSON.");
}
}
/** Register direct node command invocation. */
export function registerNodesInvokeCommands(nodes: Command) {
nodesCallOpts(
nodes
.command("invoke")
.description("Invoke a command on a paired node")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.requiredOption("--command <command>", "Command (e.g. canvas.navigate)")
.option("--params <json>", "JSON object string for params", "{}")
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 15000)", "15000")
.option("--idempotency-key <key>", "Idempotency key (optional)")
.action(async (opts: NodesRpcOpts) => {
await runNodesCommand("invoke", async () => {
const nodeQuery = normalizeOptionalString(opts.node) ?? "";
const command = normalizeOptionalString(opts.command) ?? "";
if (!nodeQuery || !command) {
throw new Error("--node and --command required");
}
if (BLOCKED_NODE_INVOKE_COMMANDS.has(normalizeLowercaseStringOrEmpty(command))) {
throw new Error(
`command "${command}" is reserved for shell execution; use the exec tool with host=node instead`,
);
}
const params = parseNodeInvokeParams(opts.params);
const timeoutMs = parseOptionalNodePositiveInteger(
opts.invokeTimeout,
"--invoke-timeout",
);
const nodeId = await resolveCliNodeId(opts, nodeQuery);
const invokeParams: Record<string, unknown> = {
nodeId,
command,
params,
idempotencyKey: opts.idempotencyKey ?? randomIdempotencyKey(),
};
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) {
invokeParams.timeoutMs = timeoutMs;
}
const result = await callNodesGatewayCli("node.invoke", opts, invokeParams);
defaultRuntime.writeJson(result);
});
}),
{ timeoutMs: 30_000 },
);
}
|