Gateway: preserve interactive pairing visibility on supersede

This commit is contained in:
joshavant
2026-03-19 19:57:45 -05:00
parent a953cb5209
commit 6309b1da6c
2 changed files with 48 additions and 1 deletions

View File

@@ -162,6 +162,36 @@ describe("device pairing tokens", () => {
expect(paired?.scopes).toEqual(["operator.read", "operator.write"]);
});
test("keeps superseded requests interactive when an existing pending request is interactive", async () => {
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
const first = await requestDevicePairing(
{
deviceId: "device-1",
publicKey: "public-key-1",
role: "node",
scopes: [],
silent: false,
},
baseDir,
);
expect(first.request.silent).toBe(false);
const second = await requestDevicePairing(
{
deviceId: "device-1",
publicKey: "public-key-1",
role: "operator",
scopes: ["operator.read"],
silent: true,
},
baseDir,
);
expect(second.created).toBe(true);
expect(second.request.requestId).not.toBe(first.request.requestId);
expect(second.request.silent).toBe(false);
});
test("rejects bootstrap token replay before pending scope escalation can be approved", async () => {
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
const issued = await issueDeviceBootstrapToken({ baseDir });

View File

@@ -236,6 +236,15 @@ function refreshPendingDevicePairingRequest(
};
}
function resolveSupersededPendingSilent(params: {
existing: readonly DevicePairingPendingRequest[];
incomingSilent: boolean | undefined;
}): boolean {
return Boolean(
params.incomingSilent && params.existing.every((pending) => pending.silent === true),
);
}
function buildPendingDevicePairingRequest(params: {
requestId?: string;
deviceId: string;
@@ -394,7 +403,15 @@ export async function requestDevicePairing(
const superseded = buildPendingDevicePairingRequest({
deviceId,
isRepair,
req,
req: {
...req,
// Preserve interactive visibility when superseding pending requests:
// if any previous pending request was interactive, keep this one interactive.
silent: resolveSupersededPendingSilent({
existing: pendingForDevice,
incomingSilent: req.silent,
}),
},
});
state.pendingById[superseded.requestId] = superseded;
await persistState(state, baseDir);