Skip to content

Commit e9509ee

Browse files
committed
fix(gateway): preserve auth-none CLI device identity
1 parent 95917ac commit e9509ee

3 files changed

Lines changed: 25 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
99
### Fixes
1010

1111
- **WhatsApp restart recovery:** stop automatic restart loops after logged-out or connection-replaced disconnects until the account reconnects. (#78511) Thanks @openperf.
12+
- **Local Gateway CLI auth:** keep loopback CLI token/password calls off durable device scopes so read probes cannot block later write/admin commands behind a stale pairing baseline. (#95997) Thanks @vincentkoc.
1213
- **iMessage group warnings:** suppress the false drop-all startup warning when an effective group sender allowlist can admit groups, and point true empty-allowlist configurations at the correct remedy. (#100046)
1314
- **Control UI mobile login:** keep Gateway recovery guidance visible after connection failures, make the disconnected gate scroll safely on constrained screens, and improve mobile keyboard and tap-target behavior. (#100208)
1415

src/gateway/call.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,19 @@ describe("callGateway url resolution", () => {
567567
expect(lastClientOptions?.deviceIdentity).toBeNull();
568568
});
569569

570+
it("keeps CLI device identity when an ambient token is inactive under auth mode none", async () => {
571+
getRuntimeConfig.mockReturnValue({
572+
gateway: { mode: "local", bind: "loopback", auth: { mode: "none" } },
573+
});
574+
setGatewayNetworkDefaults();
575+
process.env.OPENCLAW_GATEWAY_TOKEN = "inactive-env-token";
576+
577+
await callGatewayCli({ method: "health" });
578+
579+
expect(lastClientOptions?.token).toBe("inactive-env-token");
580+
expect(lastClientOptions?.deviceIdentity).toEqual(deviceIdentityState.value);
581+
});
582+
570583
it("falls back to token/password auth when device identity cannot be persisted", async () => {
571584
setLocalLoopbackGatewayConfig();
572585
deviceIdentityState.throwOnLoad = true;

src/gateway/call.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -484,13 +484,18 @@ function isLoopbackGatewayUrl(rawUrl: string): boolean {
484484
function shouldOmitDeviceIdentityForGatewayCall(params: {
485485
opts: CallGatewayBaseOptions;
486486
url: string;
487+
authMode: ReturnType<typeof resolveGatewayAuth>["mode"];
487488
token?: string;
488489
password?: string;
489490
allowAuthNone?: boolean;
490491
}): boolean {
491492
const mode = params.opts.mode ?? GATEWAY_CLIENT_MODES.CLI;
492493
const clientName = params.opts.clientName ?? GATEWAY_CLIENT_NAMES.CLI;
493-
const hasSharedSecretAuth = Boolean(params.token || params.password);
494+
// Inactive ambient credentials must not turn an auth-none CLI call device-less.
495+
// Omit identity only when the Gateway will actually authenticate the supplied secret.
496+
const hasSharedSecretAuth =
497+
(params.authMode === "token" && Boolean(params.token)) ||
498+
(params.authMode === "password" && Boolean(params.password));
494499
const isLoopback = isLoopbackGatewayUrl(params.url);
495500
const isLocalBackendSharedAuth =
496501
mode === GATEWAY_CLIENT_MODES.BACKEND &&
@@ -505,15 +510,7 @@ function shouldOmitDeviceIdentityForGatewayCall(params: {
505510
return isLocalBackendSharedAuth || isLocalCliSharedAuth;
506511
}
507512

508-
function resolveDeviceIdentityForGatewayCall(params: {
509-
opts: CallGatewayBaseOptions;
510-
url: string;
511-
token?: string;
512-
password?: string;
513-
}): ReturnType<typeof loadOrCreateDeviceIdentity> | null {
514-
if (shouldOmitDeviceIdentityForGatewayCall(params)) {
515-
return null;
516-
}
513+
function resolveDeviceIdentityForGatewayCall(): DeviceIdentity | null {
517514
try {
518515
return gatewayCallDeps.loadOrCreateDeviceIdentity();
519516
} catch {
@@ -1168,12 +1165,12 @@ async function callGatewayWithScopes<T = Record<string, unknown>>(
11681165
const tlsFingerprint = await resolveGatewayTlsFingerprint({ opts, context, url });
11691166
const token = useStoredDeviceAuth ? undefined : resolvedCredentials.token;
11701167
const password = useStoredDeviceAuth ? undefined : resolvedCredentials.password;
1171-
const allowAuthNone =
1172-
opts.requireLocalBackendSharedAuth === true &&
1173-
resolveGatewayCallAuth(context.config).mode === "none";
1168+
const authMode = resolveGatewayCallAuth(context.config).mode;
1169+
const allowAuthNone = opts.requireLocalBackendSharedAuth === true && authMode === "none";
11741170
const omitDeviceIdentity = shouldOmitDeviceIdentityForGatewayCall({
11751171
opts,
11761172
url,
1173+
authMode,
11771174
token,
11781175
password,
11791176
allowAuthNone,
@@ -1187,7 +1184,7 @@ async function callGatewayWithScopes<T = Record<string, unknown>>(
11871184
opts.deviceIdentity === undefined
11881185
? omitDeviceIdentity
11891186
? null
1190-
: resolveDeviceIdentityForGatewayCall({ opts, url, token, password })
1187+
: resolveDeviceIdentityForGatewayCall()
11911188
: opts.deviceIdentity;
11921189
if (useStoredDeviceAuth) {
11931190
const storedAuth = loadStoredOperatorDeviceAuthToken(deviceIdentity);

0 commit comments

Comments
 (0)