mirror of https://github.com/openclaw/openclaw.git
fix: restore local loopback role upgrades (#59092) (thanks @openperf)
* fix(gateway ): allow silent role upgrades for local loopback clients When a local loopback client connects with a role not covered by existing device tokens, listEffectivePairedDeviceRoles incorrectly returns an empty role set for devices whose tokens map is an empty object. This triggers a role-upgrade pairing request that shouldAllowSilentLocalPairing rejects because it does not recognise the role-upgrade reason. Fix listEffectivePairedDeviceRoles to fall back to legacy role fields when the tokens map has no entries, and extend shouldAllowSilentLocalPairing to accept role-upgrade for local clients. Fixes #59045 * fix: restore local loopback role upgrades (#59092) (thanks @openperf) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
parent
1ea901b107
commit
51edd30bea
|
|
@ -13,8 +13,10 @@ Docs: https://docs.openclaw.ai
|
|||
### Fixes
|
||||
|
||||
- Slack/mrkdwn formatting: add built-in Slack mrkdwn guidance in inbound context so Slack replies stop falling back to generic Markdown patterns that render poorly in Slack. Thanks @jadewon and @vincentkoc.
|
||||
- Gateway/exec loopback: restore legacy-role fallback for empty paired-device token maps and allow silent local role upgrades so local exec and node clients stop failing with pairing-required errors after `2026.3.31`. (#59092) Thanks @openperf.
|
||||
|
||||
## 2026.4.1-beta.1
|
||||
|
||||
- Plugins/runtime: stop ambient core helper and setup paths from loading non-selected bundled plugins, keep channel-setup snapshot scoping safe for custom channel plugins, and honor env-scoped plugin auth paths. (#59136) Thanks @vincentkoc.
|
||||
- Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras.
|
||||
- Gateway/pairing: prefer explicit QR bootstrap auth over earlier Tailscale auth classification so iOS `/pair qr` silent bootstrap pairing does not fall through to `pairing required`. (#59232) Thanks @ngutman.
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ describe("handshake auth helpers", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("allows silent local pairing only for not-paired and scope upgrades", () => {
|
||||
it("allows silent local pairing for not-paired, scope-upgrade and role-upgrade", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
isLocalClient: true,
|
||||
|
|
@ -75,6 +75,24 @@ describe("handshake auth helpers", () => {
|
|||
reason: "not-paired",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
isLocalClient: true,
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "role-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
isLocalClient: true,
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "scope-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
isLocalClient: true,
|
||||
|
|
@ -85,4 +103,16 @@ describe("handshake auth helpers", () => {
|
|||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects silent role-upgrade for remote clients", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
isLocalClient: false,
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "role-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ export function shouldAllowSilentLocalPairing(params: {
|
|||
return (
|
||||
params.isLocalClient &&
|
||||
(!params.hasBrowserOriginHeader || params.isControlUi || params.isWebchat) &&
|
||||
(params.reason === "not-paired" || params.reason === "scope-upgrade")
|
||||
(params.reason === "not-paired" ||
|
||||
params.reason === "scope-upgrade" ||
|
||||
params.reason === "role-upgrade")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -593,6 +593,21 @@ describe("device pairing tokens", () => {
|
|||
expect(hasEffectivePairedDeviceRole(paired, "node")).toBe(false);
|
||||
});
|
||||
|
||||
test("falls back to legacy role fields when tokens map is empty", async () => {
|
||||
const device: PairedDevice = {
|
||||
deviceId: "device-fallback",
|
||||
publicKey: "pk-fallback",
|
||||
role: "node",
|
||||
roles: ["node", "operator"],
|
||||
tokens: {},
|
||||
createdAtMs: Date.now(),
|
||||
approvedAtMs: Date.now(),
|
||||
};
|
||||
expect(listEffectivePairedDeviceRoles(device)).toEqual(["node", "operator"]);
|
||||
expect(hasEffectivePairedDeviceRole(device, "node")).toBe(true);
|
||||
expect(hasEffectivePairedDeviceRole(device, "operator")).toBe(true);
|
||||
});
|
||||
|
||||
test("removes paired devices by device id", async () => {
|
||||
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
|
||||
await setupPairedOperatorDevice(baseDir, ["operator.read"]);
|
||||
|
|
|
|||
|
|
@ -170,8 +170,15 @@ export function listEffectivePairedDeviceRoles(
|
|||
device: Pick<PairedDevice, "role" | "roles" | "tokens">,
|
||||
): string[] {
|
||||
const activeTokenRoles = listActiveTokenRoles(device.tokens);
|
||||
if (device.tokens) {
|
||||
return activeTokenRoles ?? [];
|
||||
if (activeTokenRoles && activeTokenRoles.length > 0) {
|
||||
return activeTokenRoles;
|
||||
}
|
||||
// Only fall back to legacy role fields when the tokens map is absent
|
||||
// or has no entries at all (empty object from a fresh pairing record).
|
||||
// When token entries exist but are all revoked, the revocation is
|
||||
// authoritative — do not re-grant roles from sticky historical fields.
|
||||
if (device.tokens && Object.keys(device.tokens).length > 0) {
|
||||
return [];
|
||||
}
|
||||
return mergeRoles(device.roles, device.role) ?? [];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue