mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix: allow native app metadata reconnects
This commit is contained in:
@@ -615,6 +615,10 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- macOS/Gateway pairing: silently accept same-host native app
|
||||
`metadata-upgrade` reconnects, so macOS patch-version changes update paired
|
||||
metadata instead of spamming security audit warnings and `pairing required`
|
||||
disconnects. Thanks @steipete.
|
||||
- CLI/Gateway: wait for one-shot gateway RPC clients to finish WebSocket teardown before the CLI process exits, reducing hangs where commands like `openclaw status` or `openclaw version` could finish their work but stay alive until an external timeout killed them (#70691). Thanks @Takhoffman.
|
||||
- Thinking defaults/status: raise the implicit default thinking level for reasoning-capable models from legacy `off`/`low` fallback behavior to a safe provider-supported `medium` equivalent when no explicit config default is set, preserve configured-model reasoning metadata when runtime catalog loading is empty, and make `/status` report the same resolved default as runtime (#70601). Thanks @Takhoffman.
|
||||
- Gateway/model pricing: extend OpenRouter and LiteLLM catalog fetch timeouts to 60 seconds, reducing noisy timeout warnings during slow upstream responses. Thanks @steipete.
|
||||
|
||||
@@ -148,11 +148,12 @@ Security boundary:
|
||||
When an already paired device reconnects with only non-sensitive metadata
|
||||
changes (for example, display name or client platform hints), OpenClaw treats
|
||||
that as a `metadata-upgrade`. Silent auto-approval is narrow: it applies only
|
||||
to trusted local CLI/helper reconnects that already proved possession of the
|
||||
shared token or password over loopback. Browser/Control UI clients and remote
|
||||
clients still use the explicit re-approval flow. Scope upgrades (read to
|
||||
write/admin) and public key changes are **not** eligible for metadata-upgrade
|
||||
auto-approval — they stay as explicit re-approval requests.
|
||||
to trusted non-browser local reconnects that already proved possession of local
|
||||
or shared credentials, including same-host native app reconnects after OS
|
||||
version metadata changes. Browser/Control UI clients and remote clients still
|
||||
use the explicit re-approval flow. Scope upgrades (read to write/admin) and
|
||||
public key changes are **not** eligible for metadata-upgrade auto-approval —
|
||||
they stay as explicit re-approval requests.
|
||||
|
||||
## QR pairing helpers
|
||||
|
||||
|
||||
@@ -574,6 +574,31 @@ describe("handshake auth helpers", () => {
|
||||
});
|
||||
|
||||
describe("shouldAllowSilentLocalPairing — metadata-upgrade reason", () => {
|
||||
it("allows silent metadata-upgrade for direct local native app clients without browser origin", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "direct_local",
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
isNativeAppUi: true,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still requires approval for direct local node metadata-upgrade", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "direct_local",
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("allows silent metadata-upgrade for cli_container_local CLI clients", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
@@ -621,6 +646,27 @@ describe("handshake auth helpers", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("still requires approval for direct local Browser or Control UI metadata-upgrade", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "direct_local",
|
||||
hasBrowserOriginHeader: true,
|
||||
isControlUi: true,
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "direct_local",
|
||||
hasBrowserOriginHeader: true,
|
||||
isControlUi: false,
|
||||
isWebchat: true,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers cli_container_local over shared_secret_loopback_local for CLI clients", () => {
|
||||
|
||||
@@ -77,6 +77,7 @@ export function shouldAllowSilentLocalPairing(params: {
|
||||
hasBrowserOriginHeader: boolean;
|
||||
isControlUi: boolean;
|
||||
isWebchat: boolean;
|
||||
isNativeAppUi?: boolean;
|
||||
reason: "not-paired" | "role-upgrade" | "scope-upgrade" | "metadata-upgrade";
|
||||
}): boolean {
|
||||
if (params.locality === "remote") {
|
||||
@@ -92,16 +93,18 @@ export function shouldAllowSilentLocalPairing(params: {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// metadata-upgrade auto-approves only for shared-secret loopback CLI clients.
|
||||
// On those paths the connection has already proved possession of a token or
|
||||
// password over loopback, so allowing the pinned platform/deviceFamily to be
|
||||
// refreshed on reconnect matches the "Reconnects can update access metadata"
|
||||
// comment in message-handler.ts. Browser / Control-UI clients keep the
|
||||
// existing approval-required flow — metadata pinning there is a real
|
||||
// anti-tampering surface.
|
||||
// metadata-upgrade auto-approves only for non-browser local reconnects that
|
||||
// already proved possession of local/shared credentials. Direct-local
|
||||
// metadata refresh is limited to first-party native app UI clients, covering
|
||||
// same-host app reconnects after OS version metadata changes while keeping
|
||||
// node-host, Browser, and Control-UI metadata pinning on the explicit approval path.
|
||||
if (
|
||||
params.reason === "metadata-upgrade" &&
|
||||
(params.locality === "cli_container_local" ||
|
||||
!params.hasBrowserOriginHeader &&
|
||||
!params.isControlUi &&
|
||||
!params.isWebchat &&
|
||||
((params.locality === "direct_local" && params.isNativeAppUi === true) ||
|
||||
params.locality === "cli_container_local" ||
|
||||
params.locality === "shared_secret_loopback_local")
|
||||
) {
|
||||
return true;
|
||||
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
shouldAutoApproveNodePairingFromTrustedCidrs,
|
||||
} from "../../node-pairing-auto-approve.js";
|
||||
import { checkBrowserOrigin } from "../../origin-check.js";
|
||||
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js";
|
||||
import {
|
||||
buildPairingConnectCloseReason,
|
||||
buildPairingConnectErrorDetails,
|
||||
@@ -480,6 +481,11 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
const isControlUi = isOperatorUiClient(connectParams.client);
|
||||
const isBrowserOperatorUi = isBrowserOperatorUiClient(connectParams.client);
|
||||
const isWebchat = isWebchatConnect(connectParams);
|
||||
const isNativeAppUi =
|
||||
connectParams.client.mode === GATEWAY_CLIENT_MODES.UI &&
|
||||
(connectParams.client.id === GATEWAY_CLIENT_IDS.MACOS_APP ||
|
||||
connectParams.client.id === GATEWAY_CLIENT_IDS.IOS_APP ||
|
||||
connectParams.client.id === GATEWAY_CLIENT_IDS.ANDROID_APP);
|
||||
if (enforceOriginCheckForAnyClient || isBrowserOperatorUi || isWebchat) {
|
||||
const hostHeaderOriginFallbackEnabled =
|
||||
configSnapshot.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true;
|
||||
@@ -931,6 +937,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
hasBrowserOriginHeader,
|
||||
isControlUi,
|
||||
isWebchat,
|
||||
isNativeAppUi,
|
||||
reason,
|
||||
});
|
||||
const allowSilentTrustedCidrsNodePairing = shouldAutoApproveNodePairingFromTrustedCidrs(
|
||||
@@ -1111,9 +1118,19 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
});
|
||||
const { platformMismatch, deviceFamilyMismatch } = metadataPinning;
|
||||
if (platformMismatch || deviceFamilyMismatch) {
|
||||
logGateway.warn(
|
||||
`security audit: device metadata upgrade requested reason=metadata-upgrade device=${device.id} ip=${reportedClientIp ?? "unknown-ip"} auth=${authMethod} payload=${deviceAuthPayloadVersion ?? "unknown"} claimedPlatform=${claimedPlatform ?? "<none>"} pinnedPlatform=${pairedPlatform ?? "<none>"} claimedDeviceFamily=${claimedDeviceFamily ?? "<none>"} pinnedDeviceFamily=${pairedDeviceFamily ?? "<none>"} client=${connectParams.client.id} conn=${connId}`,
|
||||
);
|
||||
const allowSilentMetadataUpgrade = shouldAllowSilentLocalPairing({
|
||||
locality: pairingLocality,
|
||||
hasBrowserOriginHeader,
|
||||
isControlUi,
|
||||
isWebchat,
|
||||
isNativeAppUi,
|
||||
reason: "metadata-upgrade",
|
||||
});
|
||||
if (!allowSilentMetadataUpgrade) {
|
||||
logGateway.warn(
|
||||
`security audit: device metadata upgrade requested reason=metadata-upgrade device=${device.id} ip=${reportedClientIp ?? "unknown-ip"} auth=${authMethod} payload=${deviceAuthPayloadVersion ?? "unknown"} claimedPlatform=${claimedPlatform ?? "<none>"} pinnedPlatform=${pairedPlatform ?? "<none>"} claimedDeviceFamily=${claimedDeviceFamily ?? "<none>"} pinnedDeviceFamily=${pairedDeviceFamily ?? "<none>"} client=${connectParams.client.id} conn=${connId}`,
|
||||
);
|
||||
}
|
||||
const ok = await requirePairing("metadata-upgrade", paired);
|
||||
if (!ok) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user