mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:50:49 +00:00
fix(gateway): allow silent metadata-upgrade pairing for loopback CLI clients (#70224)
Loopback CLI clients (cli_container_local, shared_secret_loopback_local) with valid shared-secret auth previously got disconnected with 1008 pairing required whenever the paired device record's platform or deviceFamily string differed from what the CLI claimed at connect time. PR #69431 added the shared_secret_loopback_local locality but deferred the metadata-upgrade reason from the auto-approval allowlist. That deferral created an unrecoverable handshake loop in practice: every CLI connect triggers a fresh metadata-upgrade request, the Control UI has no approval surface for this reason, and non-interactive shells cannot complete pairing. This broke every non-interactive openclaw agent use case when paired device keys are replicated across hosts or installs are migrated across platforms. Extend shouldAllowSilentLocalPairing to auto-approve metadata-upgrade for cli_container_local and shared_secret_loopback_local localities only. Browser / Control-UI / remote paths retain existing approval- required behavior. Gateway still logs every metadata refresh via the existing security audit line for operator review. Add 4 unit tests covering the decision table for metadata-upgrade across all four localities. Related: #69397, #69431
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/pairing: shared-secret loopback CLI clients now silently auto-approve `metadata-upgrade` pairing (platform / device family refresh) instead of being disconnected with `1008 pairing required`. This matches the scope-upgrade and role-upgrade behavior added in #69431 and unblocks non-interactive CLI automation when a paired-device record has a stale platform string (e.g. device key replicated across hosts, install migrated between OSes, or platform-string format changed between OpenClaw versions). Browser / Control-UI clients keep the existing approval-required flow for metadata changes.
|
||||
- Gateway/pairing: treat any forwarded-header evidence (`Forwarded`, `X-Forwarded-*`, or `X-Real-IP`) as proxied WebSocket traffic before pairing locality checks, so reverse-proxy topologies cannot use the loopback shared-secret helper auto-pairing path.
|
||||
- Gateway/pairing webchat: render `/pair qr` replies as structured media instead of raw markdown text, preserve inline reply threading and silent-control handling on media replies, avoid persisting sensitive QR images into transcript history, and keep local webchat media embedding behind internal-only trust markers. (#70047) Thanks @BunsDev.
|
||||
- Codex harness: default app-server runs to unchained local execution, so OpenAI heartbeats can use network and shell tools without stalling behind native Codex approvals or the workspace-write sandbox.
|
||||
|
||||
@@ -541,7 +541,7 @@ describe("handshake auth helpers", () => {
|
||||
).toBe("remote");
|
||||
});
|
||||
|
||||
it("allows silent scope-upgrade for shared_secret_loopback_local", () => {
|
||||
it("allows silent scope-upgrade, role-upgrade, and metadata-upgrade for shared_secret_loopback_local", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "shared_secret_loopback_local",
|
||||
@@ -560,6 +560,8 @@ describe("handshake auth helpers", () => {
|
||||
reason: "role-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
// metadata-upgrade now auto-approves for shared_secret_loopback_local
|
||||
// (extended allowlist — see shouldAllowSilentLocalPairing).
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "shared_secret_loopback_local",
|
||||
@@ -568,7 +570,57 @@ describe("handshake auth helpers", () => {
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
describe("shouldAllowSilentLocalPairing — metadata-upgrade reason", () => {
|
||||
it("allows silent metadata-upgrade for cli_container_local CLI clients", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "cli_container_local",
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("allows silent metadata-upgrade for shared_secret_loopback_local CLI clients", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "shared_secret_loopback_local",
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still requires approval for metadata-upgrade from remote clients", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "remote",
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("still requires approval for metadata-upgrade from browser_container_local (Control UI)", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "browser_container_local",
|
||||
hasBrowserOriginHeader: true,
|
||||
isControlUi: true,
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers cli_container_local over shared_secret_loopback_local for CLI clients", () => {
|
||||
|
||||
@@ -79,13 +79,34 @@ export function shouldAllowSilentLocalPairing(params: {
|
||||
isWebchat: boolean;
|
||||
reason: "not-paired" | "role-upgrade" | "scope-upgrade" | "metadata-upgrade";
|
||||
}): boolean {
|
||||
return (
|
||||
params.locality !== "remote" &&
|
||||
(!params.hasBrowserOriginHeader || params.isControlUi || params.isWebchat) &&
|
||||
(params.reason === "not-paired" ||
|
||||
params.reason === "scope-upgrade" ||
|
||||
params.reason === "role-upgrade")
|
||||
);
|
||||
if (params.locality === "remote") {
|
||||
return false;
|
||||
}
|
||||
if (params.hasBrowserOriginHeader && !params.isControlUi && !params.isWebchat) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.reason === "not-paired" ||
|
||||
params.reason === "scope-upgrade" ||
|
||||
params.reason === "role-upgrade"
|
||||
) {
|
||||
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.
|
||||
if (
|
||||
params.reason === "metadata-upgrade" &&
|
||||
(params.locality === "cli_container_local" ||
|
||||
params.locality === "shared_secret_loopback_local")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isCliContainerLocalEquivalent(params: {
|
||||
|
||||
Reference in New Issue
Block a user