From 5dae663ea445bab8ce8b4fd6a70c03d7936d18f5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 30 Mar 2026 00:39:46 +0100 Subject: [PATCH] refactor(nodes): remove nodes.run execution path --- src/agents/tools/nodes-tool.ts | 167 +---------- src/cli/nodes-cli/register.canvas.ts | 2 +- src/cli/nodes-cli/register.invoke.ts | 426 +-------------------------- src/cli/nodes-cli/register.ts | 5 +- src/cli/nodes-run.ts | 25 -- src/infra/skills-remote.ts | 4 +- 6 files changed, 24 insertions(+), 605 deletions(-) delete mode 100644 src/cli/nodes-run.ts diff --git a/src/agents/tools/nodes-tool.ts b/src/agents/tools/nodes-tool.ts index 33599e810d9..0492ea5ea6b 100644 --- a/src/agents/tools/nodes-tool.ts +++ b/src/agents/tools/nodes-tool.ts @@ -9,17 +9,16 @@ import { writeCameraClipPayloadToFile, writeCameraPayloadToFile, } from "../../cli/nodes-camera.js"; -import { parseEnvPairs, parseTimeoutMs } from "../../cli/nodes-run.js"; import { parseScreenRecordPayload, screenRecordTempPath, writeScreenRecordToFile, } from "../../cli/nodes-screen.js"; import { parseDurationMs } from "../../cli/parse-duration.js"; +import { parseTimeoutMs } from "../../cli/parse-timeout.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { OperatorScope } from "../../gateway/method-scopes.js"; import { NODE_SYSTEM_RUN_COMMANDS } from "../../infra/node-commands.js"; -import { parsePreparedSystemRunPayload } from "../../infra/system-run-approval-context.js"; import { imageMimeFromFormat } from "../../media/mime.js"; import type { GatewayMessageChannel } from "../../utils/message-channel.js"; import { resolveSessionAgentId } from "../agent-scope.js"; @@ -28,7 +27,7 @@ import { optionalStringEnum, stringEnum } from "../schema/typebox.js"; import { sanitizeToolResultImages } from "../tool-images.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; import { callGatewayTool, readGatewayCallOptions } from "./gateway.js"; -import { listNodes, resolveNode, resolveNodeId, resolveNodeIdFromList } from "./nodes-utils.js"; +import { resolveNode, resolveNodeId } from "./nodes-utils.js"; const NODES_TOOL_ACTIONS = [ "status", @@ -49,7 +48,6 @@ const NODES_TOOL_ACTIONS = [ "device_info", "device_permissions", "device_health", - "run", "invoke", ] as const; @@ -64,6 +62,7 @@ const MEDIA_INVOKE_ACTIONS = { "photos.latest": "photos_latest", "screen.record": "screen_record", } as const; +const BLOCKED_INVOKE_COMMANDS = new Set(["system.run", "system.run.prepare"]); const NODE_READ_ACTION_COMMANDS = { camera_list: "camera.list", notifications_list: "notifications.list", @@ -169,16 +168,10 @@ const NodesToolSchema = Type.Object({ notificationAction: optionalStringEnum(NOTIFICATIONS_ACTIONS), notificationKey: Type.Optional(Type.String()), notificationReplyText: Type.Optional(Type.String()), - // run - command: Type.Optional(Type.Array(Type.String())), - cwd: Type.Optional(Type.String()), - env: Type.Optional(Type.Array(Type.String())), - commandTimeoutMs: Type.Optional(Type.Number()), - invokeTimeoutMs: Type.Optional(Type.Number()), - needsScreenRecording: Type.Optional(Type.Boolean()), // invoke invokeCommand: Type.Optional(Type.String()), invokeParamsJson: Type.Optional(Type.String()), + invokeTimeoutMs: Type.Optional(Type.Number()), }); export function createNodesTool(options?: { @@ -191,11 +184,6 @@ export function createNodesTool(options?: { modelHasVision?: boolean; allowMediaInvokeCommands?: boolean; }): AnyAgentTool { - const sessionKey = options?.agentSessionKey?.trim() || undefined; - const turnSourceChannel = options?.agentChannel?.trim() || undefined; - const turnSourceTo = options?.currentChannelId?.trim() || undefined; - const turnSourceAccountId = options?.agentAccountId?.trim() || undefined; - const turnSourceThreadId = options?.currentThreadTs; const agentId = resolveSessionAgentId({ sessionKey: options?.agentSessionKey, config: options?.config, @@ -206,7 +194,7 @@ export function createNodesTool(options?: { name: "nodes", ownerOnly: true, description: - "Discover and control paired nodes (status/describe/pairing/notify/camera/photos/screen/location/notifications/run/invoke).", + "Discover and control paired nodes (status/describe/pairing/notify/camera/photos/screen/location/notifications/invoke).", parameters: NodesToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; @@ -658,151 +646,16 @@ export function createNodesTool(options?: { }); return jsonResult(payload); } - case "run": { - const node = readStringParam(params, "node", { required: true }); - const nodes = await listNodes(gatewayOpts); - if (nodes.length === 0) { - throw new Error( - "system.run requires a paired companion app or node host (no nodes available).", - ); - } - const nodeId = resolveNodeIdFromList(nodes, node); - const nodeInfo = nodes.find((entry) => entry.nodeId === nodeId); - const supportsSystemRun = Array.isArray(nodeInfo?.commands) - ? nodeInfo?.commands?.includes("system.run") - : false; - if (!supportsSystemRun) { - throw new Error( - "system.run requires a companion app or node host; the selected node does not support system.run.", - ); - } - const commandRaw = params.command; - if (!commandRaw) { - throw new Error("command required (argv array, e.g. ['echo', 'Hello'])"); - } - if (!Array.isArray(commandRaw)) { - throw new Error("command must be an array of strings (argv), e.g. ['echo', 'Hello']"); - } - const command = commandRaw.map((c) => String(c)); - if (command.length === 0) { - throw new Error("command must not be empty"); - } - const cwd = - typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : undefined; - const env = parseEnvPairs(params.env); - const commandTimeoutMs = parseTimeoutMs(params.commandTimeoutMs); - const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs); - const needsScreenRecording = - typeof params.needsScreenRecording === "boolean" - ? params.needsScreenRecording - : undefined; - const prepareRaw = await callGatewayTool<{ payload?: unknown }>( - "node.invoke", - gatewayOpts, - { - nodeId, - command: "system.run.prepare", - params: { - command, - cwd, - agentId, - sessionKey, - }, - timeoutMs: invokeTimeoutMs, - idempotencyKey: crypto.randomUUID(), - }, - ); - const prepared = parsePreparedSystemRunPayload(prepareRaw?.payload); - if (!prepared) { - throw new Error("invalid system.run.prepare response"); - } - const runParams = { - command: prepared.plan.argv, - rawCommand: prepared.plan.commandText, - cwd: prepared.plan.cwd ?? cwd, - env, - timeoutMs: commandTimeoutMs, - needsScreenRecording, - agentId: prepared.plan.agentId ?? agentId, - sessionKey: prepared.plan.sessionKey ?? sessionKey, - }; - - // First attempt without approval flags. - try { - const raw = await callGatewayTool<{ payload?: unknown }>("node.invoke", gatewayOpts, { - nodeId, - command: "system.run", - params: runParams, - timeoutMs: invokeTimeoutMs, - idempotencyKey: crypto.randomUUID(), - }); - return jsonResult(raw?.payload ?? {}); - } catch (firstErr) { - const msg = firstErr instanceof Error ? firstErr.message : String(firstErr); - if (!msg.includes("SYSTEM_RUN_DENIED: approval required")) { - throw firstErr; - } - } - - // Node requires approval – create a pending approval request on - // the gateway and wait for the user to approve/deny via the UI. - const APPROVAL_TIMEOUT_MS = 120_000; - const approvalId = crypto.randomUUID(); - const approvalResult = await callGatewayTool( - "exec.approval.request", - { ...gatewayOpts, timeoutMs: APPROVAL_TIMEOUT_MS + 5_000 }, - { - id: approvalId, - systemRunPlan: prepared.plan, - cwd: prepared.plan.cwd ?? cwd, - nodeId, - host: "node", - agentId: prepared.plan.agentId ?? agentId, - sessionKey: prepared.plan.sessionKey ?? sessionKey, - turnSourceChannel, - turnSourceTo, - turnSourceAccountId, - turnSourceThreadId, - timeoutMs: APPROVAL_TIMEOUT_MS, - }, - ); - const decisionRaw = - approvalResult && typeof approvalResult === "object" - ? (approvalResult as { decision?: unknown }).decision - : undefined; - const approvalDecision = - decisionRaw === "allow-once" || decisionRaw === "allow-always" ? decisionRaw : null; - - if (!approvalDecision) { - if (decisionRaw === "deny") { - throw new Error("exec denied: user denied"); - } - if (decisionRaw === undefined || decisionRaw === null) { - throw new Error("exec denied: approval timed out"); - } - throw new Error("exec denied: invalid approval decision"); - } - - // Retry with the approval decision. - const raw = await callGatewayTool<{ payload?: unknown }>("node.invoke", gatewayOpts, { - nodeId, - command: "system.run", - params: { - ...runParams, - runId: approvalId, - approved: true, - approvalDecision, - }, - timeoutMs: invokeTimeoutMs, - idempotencyKey: crypto.randomUUID(), - }); - return jsonResult(raw?.payload ?? {}); - } case "invoke": { const node = readStringParam(params, "node", { required: true }); const nodeId = await resolveNodeId(gatewayOpts, node); const invokeCommand = readStringParam(params, "invokeCommand", { required: true }); const invokeCommandNormalized = invokeCommand.trim().toLowerCase(); + if (BLOCKED_INVOKE_COMMANDS.has(invokeCommandNormalized)) { + throw new Error( + `invokeCommand "${invokeCommand}" is reserved for shell execution; use exec with host=node instead`, + ); + } const dedicatedAction = MEDIA_INVOKE_ACTIONS[invokeCommandNormalized as keyof typeof MEDIA_INVOKE_ACTIONS]; if (dedicatedAction && !options?.allowMediaInvokeCommands) { diff --git a/src/cli/nodes-cli/register.canvas.ts b/src/cli/nodes-cli/register.canvas.ts index 6b3a112f50b..79b6e9f7ce3 100644 --- a/src/cli/nodes-cli/register.canvas.ts +++ b/src/cli/nodes-cli/register.canvas.ts @@ -4,7 +4,7 @@ import { defaultRuntime } from "../../runtime.js"; import { shortenHomePath } from "../../utils.js"; import { writeBase64ToFile } from "../nodes-camera.js"; import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../nodes-canvas.js"; -import { parseTimeoutMs } from "../nodes-run.js"; +import { parseTimeoutMs } from "../parse-timeout.js"; import { buildA2UITextJsonl, validateA2UIJsonl } from "./a2ui-jsonl.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js"; import { buildNodeInvokeParams, callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; diff --git a/src/cli/nodes-cli/register.invoke.ts b/src/cli/nodes-cli/register.invoke.ts index 71507565f56..e663fd90075 100644 --- a/src/cli/nodes-cli/register.invoke.ts +++ b/src/cli/nodes-cli/register.invoke.ts @@ -1,301 +1,11 @@ import type { Command } from "commander"; -import { resolveAgentConfig, resolveDefaultAgentId } from "../../agents/agent-scope.js"; -import { loadConfig } from "../../config/config.js"; import { randomIdempotencyKey } from "../../gateway/call.js"; -import { - DEFAULT_EXEC_APPROVAL_TIMEOUT_MS, - type ExecApprovalsFile, - type ExecAsk, - type ExecSecurity, - loadExecApprovals, - maxAsk, - minSecurity, - normalizeExecAsk, - normalizeExecSecurity, - resolveExecApprovalsFromFile, -} from "../../infra/exec-approvals.js"; -import { buildNodeShellCommand } from "../../infra/node-shell.js"; -import { applyPathPrepend } from "../../infra/path-prepend.js"; -import { parsePreparedSystemRunPayload } from "../../infra/system-run-approval-context.js"; import { defaultRuntime } from "../../runtime.js"; -import { parseEnvPairs, parseTimeoutMs } from "../nodes-run.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js"; -import { parseNodeList } from "./format.js"; -import { callGatewayCli, nodesCallOpts, resolveNodeId, unauthorizedHintForMessage } from "./rpc.js"; +import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; -type NodesRunOpts = NodesRpcOpts & { - node?: string; - cwd?: string; - env?: string[]; - commandTimeout?: string; - needsScreenRecording?: boolean; - invokeTimeout?: string; - idempotencyKey?: string; - agent?: string; - ask?: string; - security?: string; - raw?: string; -}; - -type ExecDefaults = { - security?: ExecSecurity; - ask?: ExecAsk; - node?: string; - pathPrepend?: string[]; - safeBins?: string[]; -}; - -function resolveExecDefaults( - cfg: ReturnType, - agentId: string | undefined, -): ExecDefaults | undefined { - const globalExec = cfg?.tools?.exec; - if (!agentId) { - return globalExec - ? { - security: globalExec.security, - ask: globalExec.ask, - node: globalExec.node, - pathPrepend: globalExec.pathPrepend, - safeBins: globalExec.safeBins, - } - : undefined; - } - const agentExec = resolveAgentConfig(cfg, agentId)?.tools?.exec; - return { - security: agentExec?.security ?? globalExec?.security, - ask: agentExec?.ask ?? globalExec?.ask, - node: agentExec?.node ?? globalExec?.node, - pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend, - safeBins: agentExec?.safeBins ?? globalExec?.safeBins, - }; -} - -async function resolveNodePlatform(opts: NodesRpcOpts, nodeId: string): Promise { - try { - const res = await callGatewayCli("node.list", opts, {}); - const nodes = parseNodeList(res); - const match = nodes.find((node) => node.nodeId === nodeId); - return typeof match?.platform === "string" ? match.platform : null; - } catch { - return null; - } -} - -function requirePreparedRunPayload(payload: unknown) { - const prepared = parsePreparedSystemRunPayload(payload); - if (!prepared) { - throw new Error("invalid system.run.prepare response"); - } - return prepared; -} - -function resolveNodesRunPolicy(opts: NodesRunOpts, execDefaults: ExecDefaults | undefined) { - const configuredSecurity = normalizeExecSecurity(execDefaults?.security) ?? "allowlist"; - const requestedSecurity = normalizeExecSecurity(opts.security); - if (opts.security && !requestedSecurity) { - throw new Error("invalid --security (use deny|allowlist|full)"); - } - // Keep local exec defaults in sync with exec-approvals.json when tools.exec.ask is unset. - const configuredAsk = - normalizeExecAsk(execDefaults?.ask) ?? loadExecApprovals().defaults?.ask ?? "on-miss"; - const requestedAsk = normalizeExecAsk(opts.ask); - if (opts.ask && !requestedAsk) { - throw new Error("invalid --ask (use off|on-miss|always)"); - } - return { - security: minSecurity(configuredSecurity, requestedSecurity ?? configuredSecurity), - ask: maxAsk(configuredAsk, requestedAsk ?? configuredAsk), - }; -} - -async function prepareNodesRunContext(params: { - opts: NodesRunOpts; - command: string[]; - raw: string; - nodeId: string; - agentId: string | undefined; - execDefaults: ExecDefaults | undefined; -}) { - const env = parseEnvPairs(params.opts.env); - const timeoutMs = parseTimeoutMs(params.opts.commandTimeout); - const invokeTimeout = parseTimeoutMs(params.opts.invokeTimeout); - - let argv = Array.isArray(params.command) ? params.command : []; - let rawCommand: string | undefined; - if (params.raw) { - rawCommand = params.raw; - const platform = await resolveNodePlatform(params.opts, params.nodeId); - argv = buildNodeShellCommand(rawCommand, platform ?? undefined); - } - - const nodeEnv = env ? { ...env } : undefined; - if (nodeEnv) { - applyPathPrepend(nodeEnv, params.execDefaults?.pathPrepend, { requireExisting: true }); - } - - const prepareResponse = (await callGatewayCli("node.invoke", params.opts, { - nodeId: params.nodeId, - command: "system.run.prepare", - params: { - command: argv, - rawCommand, - cwd: params.opts.cwd, - agentId: params.agentId, - }, - idempotencyKey: `prepare-${randomIdempotencyKey()}`, - })) as { payload?: unknown } | null; - - return { - prepared: requirePreparedRunPayload(prepareResponse?.payload), - nodeEnv, - timeoutMs, - invokeTimeout, - }; -} - -async function resolveNodeApprovals(params: { - opts: NodesRunOpts; - nodeId: string; - agentId: string | undefined; - security: ExecSecurity; - ask: ExecAsk; -}) { - const approvalsSnapshot = (await callGatewayCli("exec.approvals.node.get", params.opts, { - nodeId: params.nodeId, - })) as { - file?: unknown; - } | null; - const approvalsFile = - approvalsSnapshot && typeof approvalsSnapshot === "object" ? approvalsSnapshot.file : undefined; - if (!approvalsFile || typeof approvalsFile !== "object") { - throw new Error("exec approvals unavailable"); - } - const approvals = resolveExecApprovalsFromFile({ - file: approvalsFile as ExecApprovalsFile, - agentId: params.agentId, - overrides: { security: params.security, ask: params.ask }, - }); - return { - approvals, - hostSecurity: minSecurity(params.security, approvals.agent.security), - hostAsk: maxAsk(params.ask, approvals.agent.ask), - askFallback: approvals.agent.askFallback, - }; -} - -async function maybeRequestNodesRunApproval(params: { - opts: NodesRunOpts; - nodeId: string; - agentId: string | undefined; - approvalPlan: ReturnType["plan"]; - hostSecurity: ExecSecurity; - hostAsk: ExecAsk; - askFallback: ExecSecurity; -}) { - let approvedByAsk = false; - let approvalDecision: "allow-once" | "allow-always" | null = null; - let approvalId: string | null = null; - const requiresAsk = params.hostAsk === "always" || params.hostAsk === "on-miss"; - if (!requiresAsk) { - return { approvedByAsk, approvalDecision, approvalId }; - } - - approvalId = crypto.randomUUID(); - const approvalTimeoutMs = DEFAULT_EXEC_APPROVAL_TIMEOUT_MS; - // Keep client transport alive while the approver decides. - const transportTimeoutMs = Math.max( - parseTimeoutMs(params.opts.timeout) ?? 0, - approvalTimeoutMs + 10_000, - ); - const decisionResult = (await callGatewayCli( - "exec.approval.request", - params.opts, - { - id: approvalId, - systemRunPlan: params.approvalPlan, - cwd: params.approvalPlan.cwd, - nodeId: params.nodeId, - host: "node", - security: params.hostSecurity, - ask: params.hostAsk, - agentId: params.approvalPlan.agentId ?? params.agentId, - resolvedPath: undefined, - sessionKey: params.approvalPlan.sessionKey ?? undefined, - timeoutMs: approvalTimeoutMs, - }, - { transportTimeoutMs }, - )) as { decision?: string } | null; - const decision = - decisionResult && typeof decisionResult === "object" ? (decisionResult.decision ?? null) : null; - if (decision === "deny") { - throw new Error("exec denied: user denied"); - } - if (!decision) { - if (params.askFallback === "full") { - approvedByAsk = true; - approvalDecision = "allow-once"; - } else if (params.askFallback !== "allowlist") { - throw new Error("exec denied: approval required (approval UI not available)"); - } - } - if (decision === "allow-once") { - approvedByAsk = true; - approvalDecision = "allow-once"; - } - if (decision === "allow-always") { - approvedByAsk = true; - approvalDecision = "allow-always"; - } - return { approvedByAsk, approvalDecision, approvalId }; -} - -function buildSystemRunInvokeParams(params: { - nodeId: string; - approvalPlan: ReturnType["plan"]; - nodeEnv: Record | undefined; - timeoutMs: number | undefined; - invokeTimeout: number | undefined; - approvedByAsk: boolean; - approvalDecision: "allow-once" | "allow-always" | null; - approvalId: string | null; - idempotencyKey: string | undefined; - fallbackAgentId: string | undefined; - needsScreenRecording: boolean; -}) { - const invokeParams: Record = { - nodeId: params.nodeId, - command: "system.run", - params: { - command: params.approvalPlan.argv, - rawCommand: params.approvalPlan.commandText, - cwd: params.approvalPlan.cwd, - env: params.nodeEnv, - timeoutMs: params.timeoutMs, - needsScreenRecording: params.needsScreenRecording, - }, - idempotencyKey: String(params.idempotencyKey ?? randomIdempotencyKey()), - }; - if (params.approvalPlan.agentId ?? params.fallbackAgentId) { - (invokeParams.params as Record).agentId = - params.approvalPlan.agentId ?? params.fallbackAgentId; - } - if (params.approvalPlan.sessionKey) { - (invokeParams.params as Record).sessionKey = params.approvalPlan.sessionKey; - } - (invokeParams.params as Record).approved = params.approvedByAsk; - if (params.approvalDecision) { - (invokeParams.params as Record).approvalDecision = params.approvalDecision; - } - if (params.approvedByAsk && params.approvalId) { - (invokeParams.params as Record).runId = params.approvalId; - } - if (params.invokeTimeout !== undefined) { - invokeParams.timeoutMs = params.invokeTimeout; - } - return invokeParams; -} +const BLOCKED_NODE_INVOKE_COMMANDS = new Set(["system.run", "system.run.prepare"]); export function registerNodesInvokeCommands(nodes: Command) { nodesCallOpts( @@ -317,6 +27,11 @@ export function registerNodesInvokeCommands(nodes: Command) { defaultRuntime.exit(1); return; } + if (BLOCKED_NODE_INVOKE_COMMANDS.has(command.toLowerCase())) { + throw new Error( + `command "${command}" is reserved for shell execution; use the exec tool with host=node instead`, + ); + } const params = JSON.parse(String(opts.params ?? "{}")) as unknown; const timeoutMs = opts.invokeTimeout ? Number.parseInt(String(opts.invokeTimeout), 10) @@ -338,131 +53,4 @@ export function registerNodesInvokeCommands(nodes: Command) { }), { timeoutMs: 30_000 }, ); - - nodesCallOpts( - nodes - .command("run") - .description("Run a shell command on a node (mac only)") - .option("--node ", "Node id, name, or IP") - .option("--cwd ", "Working directory") - .option( - "--env ", - "Environment override (repeatable)", - (value: string, prev: string[] = []) => [...prev, value], - ) - .option("--raw ", "Run a raw shell command string (sh -lc / cmd.exe /c)") - .option("--agent ", "Agent id (default: configured default agent)") - .option("--ask ", "Exec ask mode (off|on-miss|always)") - .option("--security ", "Exec security mode (deny|allowlist|full)") - .option("--command-timeout ", "Command timeout (ms)") - .option("--needs-screen-recording", "Require screen recording permission") - .option("--invoke-timeout ", "Node invoke timeout in ms (default 30000)", "30000") - .argument("[command...]", "Command and args") - .action(async (command: string[], opts: NodesRunOpts) => { - await runNodesCommand("run", async () => { - const cfg = loadConfig(); - const agentId = opts.agent?.trim() || resolveDefaultAgentId(cfg); - const execDefaults = resolveExecDefaults(cfg, agentId); - const raw = typeof opts.raw === "string" ? opts.raw.trim() : ""; - if (raw && Array.isArray(command) && command.length > 0) { - throw new Error("use --raw or argv, not both"); - } - if (!raw && (!Array.isArray(command) || command.length === 0)) { - throw new Error("command required"); - } - - const nodeQuery = String(opts.node ?? "").trim() || execDefaults?.node?.trim() || ""; - if (!nodeQuery) { - throw new Error("node required (set --node or tools.exec.node)"); - } - const nodeId = await resolveNodeId(opts, nodeQuery); - const preparedContext = await prepareNodesRunContext({ - opts, - command, - raw, - nodeId, - agentId, - execDefaults, - }); - const approvalPlan = preparedContext.prepared.plan; - const policy = resolveNodesRunPolicy(opts, execDefaults); - const approvals = await resolveNodeApprovals({ - opts, - nodeId, - agentId, - security: policy.security, - ask: policy.ask, - }); - if (approvals.hostSecurity === "deny") { - throw new Error("exec denied: host=node security=deny"); - } - const approvalResult = await maybeRequestNodesRunApproval({ - opts, - nodeId, - agentId, - approvalPlan, - hostSecurity: approvals.hostSecurity, - hostAsk: approvals.hostAsk, - askFallback: approvals.askFallback, - }); - const invokeParams = buildSystemRunInvokeParams({ - nodeId, - approvalPlan, - nodeEnv: preparedContext.nodeEnv, - timeoutMs: preparedContext.timeoutMs, - invokeTimeout: preparedContext.invokeTimeout, - approvedByAsk: approvalResult.approvedByAsk, - approvalDecision: approvalResult.approvalDecision, - approvalId: approvalResult.approvalId, - idempotencyKey: opts.idempotencyKey, - fallbackAgentId: agentId, - needsScreenRecording: opts.needsScreenRecording === true, - }); - - const result = await callGatewayCli("node.invoke", opts, invokeParams); - if (opts.json) { - defaultRuntime.writeJson(result); - return; - } - - const payload = - typeof result === "object" && result !== null - ? (result as { payload?: Record }).payload - : undefined; - - const stdout = typeof payload?.stdout === "string" ? payload.stdout : ""; - const stderr = typeof payload?.stderr === "string" ? payload.stderr : ""; - const exitCode = typeof payload?.exitCode === "number" ? payload.exitCode : null; - const timedOut = payload?.timedOut === true; - const success = payload?.success === true; - - if (stdout) { - process.stdout.write(stdout); - } - if (stderr) { - process.stderr.write(stderr); - } - if (timedOut) { - const { error } = getNodesTheme(); - defaultRuntime.error(error("run timed out")); - defaultRuntime.exit(1); - return; - } - if (exitCode !== null && exitCode !== 0) { - const hint = unauthorizedHintForMessage(`${stderr}\n${stdout}`); - if (hint) { - const { warn } = getNodesTheme(); - defaultRuntime.error(warn(hint)); - } - } - if (exitCode !== null && exitCode !== 0 && !success) { - const { error } = getNodesTheme(); - defaultRuntime.error(error(`run exit ${exitCode}`)); - defaultRuntime.exit(1); - return; - } - }); - }), - { timeoutMs: 35_000 }, - ); } diff --git a/src/cli/nodes-cli/register.ts b/src/cli/nodes-cli/register.ts index 5672c332a13..93d380aac6c 100644 --- a/src/cli/nodes-cli/register.ts +++ b/src/cli/nodes-cli/register.ts @@ -22,7 +22,10 @@ export function registerNodesCli(program: Command) { `\n${theme.heading("Examples:")}\n${formatHelpExamples([ ["openclaw nodes status", "List known nodes with live status."], ["openclaw nodes pairing pending", "Show pending node pairing requests."], - ['openclaw nodes run --node --raw "uname -a"', "Run a shell command on a node."], + [ + 'openclaw nodes invoke --node --command system.which --params \'{"name":"uname"}\'', + "Invoke a node command directly.", + ], ["openclaw nodes camera snap --node ", "Capture a photo from a node camera."], ])}\n\n${theme.muted("Docs:")} ${formatDocsLink("/cli/nodes", "docs.openclaw.ai/cli/nodes")}\n`, ); diff --git a/src/cli/nodes-run.ts b/src/cli/nodes-run.ts deleted file mode 100644 index 9df8deee0f5..00000000000 --- a/src/cli/nodes-run.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { parseTimeoutMs } from "./parse-timeout.js"; - -export function parseEnvPairs(pairs: unknown): Record | undefined { - if (!Array.isArray(pairs) || pairs.length === 0) { - return undefined; - } - const env: Record = {}; - for (const pair of pairs) { - if (typeof pair !== "string") { - continue; - } - const idx = pair.indexOf("="); - if (idx <= 0) { - continue; - } - const key = pair.slice(0, idx).trim(); - if (!key) { - continue; - } - env[key] = pair.slice(idx + 1); - } - return Object.keys(env).length > 0 ? env : undefined; -} - -export { parseTimeoutMs }; diff --git a/src/infra/skills-remote.ts b/src/infra/skills-remote.ts index ef0cd498c1b..f7cf926afad 100644 --- a/src/infra/skills-remote.ts +++ b/src/infra/skills-remote.ts @@ -332,8 +332,8 @@ export function getRemoteSkillEligibility(): SkillEligibilityContext["remote"] | const labels = macNodes.map((node) => node.displayName ?? node.nodeId).filter(Boolean); const note = labels.length > 0 - ? `Remote macOS node available (${labels.join(", ")}). Run macOS-only skills via nodes.run on that node.` - : "Remote macOS node available. Run macOS-only skills via nodes.run on that node."; + ? `Remote macOS node available (${labels.join(", ")}). Run macOS-only skills via exec host=node on that node.` + : "Remote macOS node available. Run macOS-only skills via exec host=node on that node."; return { platforms: ["darwin"], hasBin: (bin) => bins.has(bin),