mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-20 09:17:51 +00:00
fix(gateway): allow mobile OS metadata refresh (#83490)
Merged via squash.
Prepared head SHA: 5fae3757e9
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
This commit is contained in:
parent
384ddae86f
commit
a7ab09fa4e
6 changed files with 113 additions and 8 deletions
|
|
@ -211,6 +211,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Agents/failover: classify Moonshot/Kimi exhausted-balance HTTP 429 payloads as billing instead of generic rate limits, preserving billing guidance and fallback behavior. Fixes #43447. (#83079) Thanks @leno23.
|
||||
- Plugin SDK: bundle `openclaw/plugin-sdk/zod` into the published package artifact and verify the packed zod subpath stays self-contained, so pnpm global installs can register plugins without a package-local `zod` symlink. Fixes #78398. (#78515) Thanks @ggzeng.
|
||||
- Providers/Google: drop compaction-truncated Gemini thought signatures before replay so malformed Base64 no longer aborts the next assistant turn. (#82995) Thanks @wAngByg.
|
||||
- Gateway/mobile: allow paired iOS and Android clients to refresh same-family OS metadata on authenticated reconnect instead of requiring a new approval. (#83490) Thanks @ngutman.
|
||||
|
||||
## 2026.5.17
|
||||
|
||||
|
|
|
|||
|
|
@ -3962,7 +3962,7 @@ extension NodeAppModel {
|
|||
switch route {
|
||||
case let .agent(link):
|
||||
await self.handleAgentDeepLink(link, originalURL: url)
|
||||
case .gateway:
|
||||
case .gateway, .dashboard:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -444,4 +444,68 @@ describe("resolvePinnedClientMetadata", () => {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["openclaw-ios", "iOS 26.5.0", "iOS 26.4.2", "iPhone"],
|
||||
["openclaw-ios", "iPadOS 26.5.0", "iPadOS 26.4.2", "iPad"],
|
||||
["openclaw-ios", "iPadOS 26.5.0", "iOS 26.4.2", "iPad"],
|
||||
["openclaw-android", "Android 16", "Android 15", "Android"],
|
||||
])(
|
||||
"allows %s platform version refresh without metadata-upgrade approval",
|
||||
(clientId, claimedPlatform, pairedPlatform, deviceFamily) => {
|
||||
expect(
|
||||
__testing.resolvePinnedClientMetadata({
|
||||
clientId,
|
||||
clientMode: "node",
|
||||
claimedPlatform,
|
||||
claimedDeviceFamily: deviceFamily,
|
||||
pairedPlatform,
|
||||
pairedDeviceFamily: deviceFamily,
|
||||
}),
|
||||
).toEqual({
|
||||
platformMismatch: false,
|
||||
deviceFamilyMismatch: false,
|
||||
pinnedPlatform: claimedPlatform,
|
||||
pinnedDeviceFamily: deviceFamily,
|
||||
refreshPairedPlatform: claimedPlatform,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("still requires approval when an iOS device family changes", () => {
|
||||
expect(
|
||||
__testing.resolvePinnedClientMetadata({
|
||||
clientId: "openclaw-ios",
|
||||
clientMode: "node",
|
||||
claimedPlatform: "iOS 26.5.0",
|
||||
claimedDeviceFamily: "iPad",
|
||||
pairedPlatform: "iOS 26.4.2",
|
||||
pairedDeviceFamily: "iPhone",
|
||||
}),
|
||||
).toEqual({
|
||||
platformMismatch: false,
|
||||
deviceFamilyMismatch: true,
|
||||
pinnedPlatform: "iOS 26.5.0",
|
||||
pinnedDeviceFamily: "iPhone",
|
||||
refreshPairedPlatform: "iOS 26.5.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps non-mobile platform version changes approval-bound", () => {
|
||||
expect(
|
||||
__testing.resolvePinnedClientMetadata({
|
||||
clientId: "node-host",
|
||||
clientMode: "node",
|
||||
claimedPlatform: "linux 6.9",
|
||||
claimedDeviceFamily: "Linux",
|
||||
pairedPlatform: "linux 6.8",
|
||||
pairedDeviceFamily: "Linux",
|
||||
}),
|
||||
).toEqual({
|
||||
platformMismatch: true,
|
||||
deviceFamilyMismatch: false,
|
||||
pinnedPlatform: undefined,
|
||||
pinnedDeviceFamily: "Linux",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ function resolvePinnedClientMetadata(params: {
|
|||
deviceFamilyMismatch: boolean;
|
||||
pinnedPlatform?: string;
|
||||
pinnedDeviceFamily?: string;
|
||||
refreshPairedPlatform?: string;
|
||||
} {
|
||||
function normalizeLegacyNodeHostPlatformPin(value: string): string {
|
||||
switch (value) {
|
||||
|
|
@ -200,6 +201,16 @@ function resolvePinnedClientMetadata(params: {
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeMobileAppPlatformPin(clientId: string | undefined, value: string): string {
|
||||
if (clientId === GATEWAY_CLIENT_IDS.IOS_APP && /^(?:ios|ipados)(?:\s|$)/.test(value)) {
|
||||
return "ios-family";
|
||||
}
|
||||
if (clientId === GATEWAY_CLIENT_IDS.ANDROID_APP && /^android(?:\s|$)/.test(value)) {
|
||||
return "android";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const claimedPlatform = normalizeDeviceMetadataForAuth(params.claimedPlatform);
|
||||
const claimedDeviceFamily = normalizeDeviceMetadataForAuth(params.claimedDeviceFamily);
|
||||
const pairedPlatform = normalizeDeviceMetadataForAuth(params.pairedPlatform);
|
||||
|
|
@ -213,20 +224,32 @@ function resolvePinnedClientMetadata(params: {
|
|||
claimedPlatform !== "" &&
|
||||
normalizeLegacyNodeHostPlatformPin(claimedPlatform) ===
|
||||
normalizeLegacyNodeHostPlatformPin(pairedPlatform);
|
||||
const isMobileAppPlatformVersionRefresh =
|
||||
hasPinnedPlatform &&
|
||||
claimedPlatform !== "" &&
|
||||
claimedPlatform !== pairedPlatform &&
|
||||
normalizeMobileAppPlatformPin(params.clientId, claimedPlatform) ===
|
||||
normalizeMobileAppPlatformPin(params.clientId, pairedPlatform);
|
||||
const platformMismatch =
|
||||
hasPinnedPlatform && claimedPlatform !== pairedPlatform && !isLegacyNodeHostPlatformPin;
|
||||
hasPinnedPlatform &&
|
||||
claimedPlatform !== pairedPlatform &&
|
||||
!isLegacyNodeHostPlatformPin &&
|
||||
!isMobileAppPlatformVersionRefresh;
|
||||
const deviceFamilyMismatch = hasPinnedDeviceFamily && claimedDeviceFamily !== pairedDeviceFamily;
|
||||
const pinnedPlatform =
|
||||
claimedPlatform === pairedPlatform
|
||||
? params.pairedPlatform
|
||||
: isLegacyNodeHostPlatformPin
|
||||
? normalizeLegacyNodeHostPlatformPin(pairedPlatform)
|
||||
: undefined;
|
||||
: isMobileAppPlatformVersionRefresh
|
||||
? params.claimedPlatform
|
||||
: undefined;
|
||||
return {
|
||||
platformMismatch,
|
||||
deviceFamilyMismatch,
|
||||
pinnedPlatform: hasPinnedPlatform ? pinnedPlatform : undefined,
|
||||
pinnedDeviceFamily: hasPinnedDeviceFamily ? params.pairedDeviceFamily : undefined,
|
||||
...(isMobileAppPlatformVersionRefresh ? { refreshPairedPlatform: params.claimedPlatform } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1284,9 +1307,15 @@ export function attachGatewayWsMessageHandler(params: GatewayWsMessageHandlerPar
|
|||
}
|
||||
}
|
||||
|
||||
// Metadata pinning is approval-bound. Reconnects can update access metadata,
|
||||
// but platform/device family must stay on the approved pairing record.
|
||||
await updatePairedDeviceMetadata(device.id, clientAccessMetadata);
|
||||
// Metadata pinning is approval-bound. Reconnects can update access metadata
|
||||
// and same-family mobile OS version labels, but real platform/device-family
|
||||
// changes must stay on the approved pairing record.
|
||||
await updatePairedDeviceMetadata(device.id, {
|
||||
...clientAccessMetadata,
|
||||
...(metadataPinning.refreshPairedPlatform
|
||||
? { platform: metadataPinning.refreshPairedPlatform }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -714,7 +714,7 @@ describe("device pairing tokens", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("metadata refresh cannot mutate approved role and scope fields", async () => {
|
||||
test("metadata refresh can update display metadata but not approved role and scope fields", async () => {
|
||||
const baseDir = await makeDevicePairingDir();
|
||||
await setupPairedNodeDevice(baseDir);
|
||||
|
||||
|
|
@ -722,6 +722,7 @@ describe("device pairing tokens", () => {
|
|||
"node-1",
|
||||
{
|
||||
displayName: "renamed-node",
|
||||
platform: "iOS 26.5.0",
|
||||
role: "operator",
|
||||
roles: ["operator"],
|
||||
scopes: ["operator.admin"],
|
||||
|
|
@ -734,6 +735,7 @@ describe("device pairing tokens", () => {
|
|||
|
||||
const paired = await getPairedDevice("node-1", baseDir);
|
||||
expect(paired?.displayName).toBe("renamed-node");
|
||||
expect(paired?.platform).toBe("iOS 26.5.0");
|
||||
expect(paired?.publicKey).toBe("public-key-node-1");
|
||||
expect(paired?.role).toBe("node");
|
||||
expect(paired?.roles).toEqual(["node"]);
|
||||
|
|
|
|||
|
|
@ -97,7 +97,13 @@ export type PairedDevice = {
|
|||
|
||||
export type PairedDeviceMetadataPatch = Pick<
|
||||
PairedDevice,
|
||||
"displayName" | "clientId" | "clientMode" | "remoteIp" | "lastSeenAtMs" | "lastSeenReason"
|
||||
| "displayName"
|
||||
| "platform"
|
||||
| "clientId"
|
||||
| "clientMode"
|
||||
| "remoteIp"
|
||||
| "lastSeenAtMs"
|
||||
| "lastSeenReason"
|
||||
>;
|
||||
|
||||
export type DevicePairingList = {
|
||||
|
|
@ -855,6 +861,9 @@ export async function updatePairedDeviceMetadata(
|
|||
if ("displayName" in patch) {
|
||||
next.displayName = patch.displayName;
|
||||
}
|
||||
if ("platform" in patch) {
|
||||
next.platform = patch.platform;
|
||||
}
|
||||
if ("clientId" in patch) {
|
||||
next.clientId = patch.clientId;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue