mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 13:54:30 +00:00
fix(gateway): tolerate legacy paired metadata in ws upgrade checks (#21447)
Fixes the pairing required regression from #21236 for legacy paired devices created without roles/scopes metadata. Detects legacy paired metadata shape and skips upgrade enforcement while backfilling metadata in place on reconnect. Co-authored-by: Josh Avant <830519+joshavant@users.noreply.github.com> Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Gateway/Pairing: tolerate legacy paired devices missing `roles`/`scopes` metadata in websocket upgrade checks and backfill metadata on reconnect. (#21447, fixes #21236) Thanks @joshavant.
|
||||||
- Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek.
|
- Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek.
|
||||||
- Provider/HTTP: treat HTTP 503 as failover-eligible for LLM provider errors. (#21086) Thanks @Protocol-zero-0.
|
- Provider/HTTP: treat HTTP 503 as failover-eligible for LLM provider errors. (#21086) Thanks @Protocol-zero-0.
|
||||||
- Slack: pass `recipient_team_id` / `recipient_user_id` through Slack native streaming calls so `chat.startStream`/`appendStream`/`stopStream` work reliably across DMs and Slack Connect setups, and disable block streaming when native streaming is active. (#20988) Thanks @Dithilli. Earlier recipient-ID groundwork was contributed in #20377 by @AsserAl1012.
|
- Slack: pass `recipient_team_id` / `recipient_user_id` through Slack native streaming calls so `chat.startStream`/`appendStream`/`stopStream` work reliably across DMs and Slack Connect setups, and disable block streaming when native streaming is active. (#20988) Thanks @Dithilli. Earlier recipient-ID groundwork was contributed in #20377 by @AsserAl1012.
|
||||||
|
|||||||
@@ -948,6 +948,71 @@ describe("gateway server auth/connect", () => {
|
|||||||
restoreGatewayToken(prevToken);
|
restoreGatewayToken(prevToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("allows legacy paired devices missing role/scope metadata", async () => {
|
||||||
|
const { resolvePairingPaths, readJsonFile } = await import("../infra/pairing-files.js");
|
||||||
|
const { writeJsonAtomic } = await import("../infra/json-files.js");
|
||||||
|
const { getPairedDevice } = await import("../infra/device-pairing.js");
|
||||||
|
const {
|
||||||
|
device,
|
||||||
|
identity: { deviceId },
|
||||||
|
} = await createSignedDevice({
|
||||||
|
token: "secret",
|
||||||
|
scopes: ["operator.read"],
|
||||||
|
clientId: TEST_OPERATOR_CLIENT.id,
|
||||||
|
clientMode: TEST_OPERATOR_CLIENT.mode,
|
||||||
|
});
|
||||||
|
const { server, ws, port, prevToken } = await startServerWithClient("secret");
|
||||||
|
let ws2: WebSocket | undefined;
|
||||||
|
try {
|
||||||
|
const initial = await connectReq(ws, {
|
||||||
|
token: "secret",
|
||||||
|
scopes: ["operator.read"],
|
||||||
|
client: TEST_OPERATOR_CLIENT,
|
||||||
|
device,
|
||||||
|
});
|
||||||
|
if (!initial.ok) {
|
||||||
|
await approvePendingPairingIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialPaired = await getPairedDevice(deviceId);
|
||||||
|
expect(initialPaired?.roles).toContain("operator");
|
||||||
|
expect(initialPaired?.scopes).toContain("operator.read");
|
||||||
|
|
||||||
|
const { pairedPath } = resolvePairingPaths(undefined, "devices");
|
||||||
|
const paired =
|
||||||
|
(await readJsonFile<Record<string, Record<string, unknown>>>(pairedPath)) ?? {};
|
||||||
|
const legacy = paired[deviceId];
|
||||||
|
if (!legacy) {
|
||||||
|
throw new Error(`Expected paired metadata for deviceId=${deviceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete legacy.roles;
|
||||||
|
delete legacy.scopes;
|
||||||
|
await writeJsonAtomic(pairedPath, paired);
|
||||||
|
ws.close();
|
||||||
|
|
||||||
|
const wsReconnect = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||||
|
ws2 = wsReconnect;
|
||||||
|
await new Promise<void>((resolve) => wsReconnect.once("open", resolve));
|
||||||
|
const reconnect = await connectReq(wsReconnect, {
|
||||||
|
token: "secret",
|
||||||
|
scopes: ["operator.read"],
|
||||||
|
client: TEST_OPERATOR_CLIENT,
|
||||||
|
device,
|
||||||
|
});
|
||||||
|
expect(reconnect.ok).toBe(true);
|
||||||
|
|
||||||
|
const repaired = await getPairedDevice(deviceId);
|
||||||
|
expect(repaired?.roles).toContain("operator");
|
||||||
|
expect(repaired?.scopes).toContain("operator.read");
|
||||||
|
} finally {
|
||||||
|
await server.close();
|
||||||
|
restoreGatewayToken(prevToken);
|
||||||
|
ws.close();
|
||||||
|
ws2?.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("rejects revoked device token", async () => {
|
test("rejects revoked device token", async () => {
|
||||||
const { revokeDeviceToken } = await import("../infra/device-pairing.js");
|
const { revokeDeviceToken } = await import("../infra/device-pairing.js");
|
||||||
const { server, ws, port, prevToken } = await startServerWithClient("secret");
|
const { server, ws, port, prevToken } = await startServerWithClient("secret");
|
||||||
|
|||||||
@@ -711,43 +711,47 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const hasLegacyPairedMetadata =
|
||||||
|
paired.roles === undefined && paired.scopes === undefined;
|
||||||
const pairedRoles = Array.isArray(paired.roles)
|
const pairedRoles = Array.isArray(paired.roles)
|
||||||
? paired.roles
|
? paired.roles
|
||||||
: paired.role
|
: paired.role
|
||||||
? [paired.role]
|
? [paired.role]
|
||||||
: [];
|
: [];
|
||||||
const allowedRoles = new Set(pairedRoles);
|
if (!hasLegacyPairedMetadata) {
|
||||||
if (allowedRoles.size === 0) {
|
const allowedRoles = new Set(pairedRoles);
|
||||||
logUpgradeAudit("role-upgrade", pairedRoles, paired.scopes);
|
if (allowedRoles.size === 0) {
|
||||||
const ok = await requirePairing("role-upgrade");
|
logUpgradeAudit("role-upgrade", pairedRoles, paired.scopes);
|
||||||
if (!ok) {
|
const ok = await requirePairing("role-upgrade");
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (!allowedRoles.has(role)) {
|
|
||||||
logUpgradeAudit("role-upgrade", pairedRoles, paired.scopes);
|
|
||||||
const ok = await requirePairing("role-upgrade");
|
|
||||||
if (!ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pairedScopes = Array.isArray(paired.scopes) ? paired.scopes : [];
|
|
||||||
if (scopes.length > 0) {
|
|
||||||
if (pairedScopes.length === 0) {
|
|
||||||
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
|
||||||
const ok = await requirePairing("scope-upgrade");
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!allowedRoles.has(role)) {
|
||||||
const allowedScopes = new Set(pairedScopes);
|
logUpgradeAudit("role-upgrade", pairedRoles, paired.scopes);
|
||||||
const missingScope = scopes.find((scope) => !allowedScopes.has(scope));
|
const ok = await requirePairing("role-upgrade");
|
||||||
if (missingScope) {
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pairedScopes = Array.isArray(paired.scopes) ? paired.scopes : [];
|
||||||
|
if (scopes.length > 0) {
|
||||||
|
if (pairedScopes.length === 0) {
|
||||||
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
||||||
const ok = await requirePairing("scope-upgrade");
|
const ok = await requirePairing("scope-upgrade");
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const allowedScopes = new Set(pairedScopes);
|
||||||
|
const missingScope = scopes.find((scope) => !allowedScopes.has(scope));
|
||||||
|
if (missingScope) {
|
||||||
|
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
||||||
|
const ok = await requirePairing("scope-upgrade");
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user