Skip to content

Commit c6ece62

Browse files
committed
fix: require stable node approval matches
1 parent 73bf8b7 commit c6ece62

2 files changed

Lines changed: 128 additions & 11 deletions

File tree

src/cli/devices-cli.runtime.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,28 +206,19 @@ function stringsMatch(left: unknown, right: unknown): boolean {
206206
}
207207

208208
function nodeMatchesPairedDevice(node: NodeListNode, device: PairedDevice): boolean {
209-
return (
210-
stringsMatch(node.nodeId, device.deviceId) ||
211-
stringsMatch(node.remoteIp, device.remoteIp) ||
212-
stringsMatch(node.displayName, device.displayName)
213-
);
209+
return stringsMatch(node.nodeId, device.deviceId) || stringsMatch(node.remoteIp, device.remoteIp);
214210
}
215211

216212
function nodeMatchesQuery(node: NodeListNode, query: string): boolean {
217213
return (
218214
stringsMatch(node.nodeId, query) ||
219215
stringsMatch(node.remoteIp, query) ||
220-
stringsMatch(node.displayName, query) ||
221216
stringsMatch(node.pendingRequestId, query)
222217
);
223218
}
224219

225220
function pairedDeviceMatchesQuery(device: PairedDevice, query: string): boolean {
226-
return (
227-
stringsMatch(device.deviceId, query) ||
228-
stringsMatch(device.remoteIp, query) ||
229-
stringsMatch(device.displayName, query)
230-
);
221+
return stringsMatch(device.deviceId, query) || stringsMatch(device.remoteIp, query);
231222
}
232223

233224
async function tryReadGatewayPairingList(opts: DevicesRpcOpts): Promise<DevicePairingList | null> {

src/cli/devices-cli.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,98 @@ describe("devices cli approve", () => {
546546
expect(errorOutput).not.toContain("secret-token");
547547
expect(runtime.exit).toHaveBeenCalledWith(1);
548548
});
549+
550+
it("does not suggest node approval for a wrong-layer device IP when only display names match", async () => {
551+
callGateway
552+
.mockResolvedValueOnce({
553+
pending: [],
554+
paired: [
555+
pairedDevice({
556+
deviceId: "android-node",
557+
displayName: "Shared Phone",
558+
remoteIp: "192.168.0.202",
559+
roles: ["node"],
560+
}),
561+
],
562+
})
563+
.mockRejectedValueOnce(new Error("device pairing approval denied"))
564+
.mockRejectedValueOnce({ message: "unknown requestId", gatewayCode: "INVALID_REQUEST" })
565+
.mockResolvedValueOnce({
566+
nodes: [
567+
{
568+
nodeId: "unrelated-node",
569+
displayName: "Shared Phone",
570+
remoteIp: "10.0.0.50",
571+
approvalState: "pending-reapproval",
572+
pendingRequestId: "node-req-unrelated",
573+
},
574+
],
575+
})
576+
.mockResolvedValueOnce({
577+
pending: [],
578+
paired: [
579+
pairedDevice({
580+
deviceId: "android-node",
581+
displayName: "Shared Phone",
582+
remoteIp: "192.168.0.202",
583+
roles: ["node"],
584+
}),
585+
],
586+
});
587+
588+
await runDevicesApprove(["192.168.0.202"]);
589+
590+
expectGatewayCall(3, { method: "node.list" });
591+
expectGatewayCall(4, { method: "device.pair.list" });
592+
const errorOutput = readRuntimeErrorOutput();
593+
expect(errorOutput).toContain("unknown requestId");
594+
expect(errorOutput).not.toContain("node-req-unrelated");
595+
expect(errorOutput).not.toContain("openclaw nodes approve");
596+
});
597+
598+
it("does not suggest node approval when the query only matches a paired device display name", async () => {
599+
callGateway
600+
.mockResolvedValueOnce({
601+
pending: [],
602+
paired: [
603+
pairedDevice({
604+
deviceId: "paired-node",
605+
displayName: "Shared Phone",
606+
roles: ["node"],
607+
}),
608+
],
609+
})
610+
.mockRejectedValueOnce({ message: "unknown requestId", gatewayCode: "INVALID_REQUEST" })
611+
.mockResolvedValueOnce({
612+
nodes: [
613+
{
614+
nodeId: "paired-node",
615+
displayName: "Shared Phone",
616+
approvalState: "pending-approval",
617+
pendingRequestId: "node-req-display-name",
618+
},
619+
],
620+
})
621+
.mockResolvedValueOnce({
622+
pending: [],
623+
paired: [
624+
pairedDevice({
625+
deviceId: "paired-node",
626+
displayName: "Shared Phone",
627+
roles: ["node"],
628+
}),
629+
],
630+
});
631+
632+
await runDevicesApprove(["Shared Phone"]);
633+
634+
expectGatewayCall(2, { method: "node.list" });
635+
expectGatewayCall(3, { method: "device.pair.list" });
636+
const errorOutput = readRuntimeErrorOutput();
637+
expect(errorOutput).toContain("unknown requestId");
638+
expect(errorOutput).not.toContain("node-req-display-name");
639+
expect(errorOutput).not.toContain("openclaw nodes approve");
640+
});
549641
});
550642

551643
describe("devices cli remove", () => {
@@ -1443,6 +1535,40 @@ describe("devices cli list", () => {
14431535
expect(output).not.toContain("secret-token");
14441536
});
14451537

1538+
it("does not show node approval commands for paired node devices when only display names match", async () => {
1539+
callGateway
1540+
.mockResolvedValueOnce({
1541+
pending: [],
1542+
paired: [
1543+
pairedDevice({
1544+
deviceId: "android-node",
1545+
displayName: "Shared Phone",
1546+
remoteIp: "192.168.0.202",
1547+
role: "node",
1548+
roles: [],
1549+
}),
1550+
],
1551+
})
1552+
.mockResolvedValueOnce({
1553+
nodes: [
1554+
{
1555+
nodeId: "unrelated-node",
1556+
displayName: "Shared Phone",
1557+
remoteIp: "10.0.0.50",
1558+
approvalState: "pending-reapproval",
1559+
pendingRequestId: "node-req-unrelated",
1560+
},
1561+
],
1562+
});
1563+
1564+
await runDevicesCommand(["list"]);
1565+
1566+
expectGatewayCall(1, { method: "node.list" });
1567+
const output = readRuntimeOutput();
1568+
expect(output).not.toContain("node-req-unrelated");
1569+
expect(output).not.toContain("openclaw nodes approve");
1570+
});
1571+
14461572
it("does not show upgrade context for key-mismatched pending requests", async () => {
14471573
mockGatewayPairingList({ publicKey: "new-key" }, { publicKey: "old-key" });
14481574

0 commit comments

Comments
 (0)