fix(agents): preserve pairing guidance for node invoke upgrades

This commit is contained in:
Ayaan Zaidi
2026-04-20 12:24:34 +05:30
parent c9be0ece71
commit 221e550eb9
2 changed files with 21 additions and 17 deletions

View File

@@ -320,4 +320,21 @@ describe("createNodesTool screen_record duration guardrails", () => {
}),
).rejects.toThrow('invokeCommand "system.run" is reserved for shell execution');
});
it("keeps invoke pairing guidance for scope upgrade rejections", async () => {
gatewayMocks.callGatewayTool.mockRejectedValueOnce(
new Error("scope upgrade pending approval (requestId: req-123)"),
);
const tool = createNodesTool();
await expect(
tool.execute("call-1", {
action: "invoke",
node: "macbook",
invokeCommand: "device.status",
}),
).rejects.toThrow(
"pairing required before node invoke. Approve pairing request req-123 and retry.",
);
});
});

View File

@@ -2,9 +2,9 @@ import crypto from "node:crypto";
import { Type } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { OperatorScope } from "../../gateway/method-scopes.js";
import { readConnectPairingRequiredMessage } from "../../gateway/protocol/connect-error-details.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { resolveNodePairApprovalScopes } from "../../infra/node-pairing-authz.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { resolveSessionAgentId } from "../agent-scope.js";
import { resolveImageSanitizationLimits } from "../image-sanitization.js";
@@ -74,20 +74,6 @@ async function resolveNodePairApproveScopes(
return resolveApproveScopes(match?.commands);
}
function isPairingRequiredMessage(message: string): boolean {
const lower = normalizeLowercaseStringOrEmpty(message);
return lower.includes("pairing required") || lower.includes("not_paired");
}
function extractPairingRequestId(message: string): string | null {
const match = message.match(/\(requestId:\s*([^)]+)\)/i);
if (!match) {
return null;
}
const value = (match[1] ?? "").trim();
return value.length > 0 ? value : null;
}
// Flattened schema: runtime validates per-action requirements.
const NodesToolSchema = Type.Object({
action: stringEnum(NODES_TOOL_ACTIONS),
@@ -307,8 +293,9 @@ export function createNodesTool(options?: {
: "default";
const agentLabel = agentId ?? "unknown";
let message = formatErrorMessage(err);
if (action === "invoke" && isPairingRequiredMessage(message)) {
const requestId = extractPairingRequestId(message);
const pairing = action === "invoke" ? readConnectPairingRequiredMessage(message) : null;
if (pairing) {
const requestId = pairing.requestId ?? null;
const approveHint = requestId
? `Approve pairing request ${requestId} and retry.`
: "Approve the pending pairing request and retry.";