Skip to content

fix(gateway): resolve {} empty auth trap causing scope rejection in loopback probes#48950

Open
waitingsnow wants to merge 3 commits intoopenclaw:mainfrom
waitingsnow:fix/gateway-auth-empty-object-trap
Open

fix(gateway): resolve {} empty auth trap causing scope rejection in loopback probes#48950
waitingsnow wants to merge 3 commits intoopenclaw:mainfrom
waitingsnow:fix/gateway-auth-empty-object-trap

Conversation

@waitingsnow
Copy link
Copy Markdown

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: Running openclaw status locally against a running gateway with authentication enabled fails with unreachable (missing scope: operator.read).
  • Root Cause (The {} trap): In status.scan.shared.ts, when local auth credentials are not explicitly provided via flags, the resolution logic returns an empty object (auth: {}). The original logic !(opts.auth?.token || opts.auth?.password) evaluated to true when given {}, incorrectly stripping the device identity and triggering scope rejection.
  • What changed: This is NOT a De Morgan's Law refactor. This fix changes the condition to check strictly for opts.auth === undefined (a literal anonymous probe) to accurately distinguish an empty auth object from a lack of auth.
  • Why it matters: Authenticated loopback probes now correctly retain device identity, allowing users to read gateway health. Legacy anonymous pings still have their identity stripped, maintaining security.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Closes #46014
Closes #45835
Closes #48538
Closes #46000
Closes #46358
Closes #47113
Closes #48007
Closes #48444
Closes #46897
Closes #47307
Closes #46117
Closes #46568
Closes #45908
Closes #48113
Closes #46422
Closes #47640
Closes #47987
Closes #46689
Closes #46583
Closes #47650
Closes #46716
Closes #48167
Closes #46100
Closes #46650
Closes #45945
Closes #46821
Closes #48002
Closes #17745

User-visible / Behavior Changes

  • Local authenticated gateway probes now correctly retain device identity on loopback, allowing openclaw status to display diagnostic details instead of scope errors.

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation: n/a

Repro + Verification

Environment

  • OS: macOS / Linux
  • Runtime/container: Node 22
  • Relevant config (redacted): loopback gateway + token auth

Steps

  1. Run openclaw status locally on a machine with a paired device.
  2. Observe missing scope: operator.read before fix.
  3. Run with patch and observe success.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: Local openclaw status returns full JSON details when authenticated via token on loopback.
  • Edge cases checked: Anonymous loopback pings (no auth provided) still have their identity stripped as per legacy safety hardening.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

AI Assistance Transparency

  • AI-assisted PR
  • Testing degree: Targeted manual verification + existing test suite
  • I confirm I understand the changes.

@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime size: XS labels Mar 17, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR fixes a subtle but impactful bug in probeGateway where loopback probes carrying an empty auth object (auth: {}) incorrectly had their device identity stripped, causing operator.read scope rejection. The previous condition !(opts.auth?.token || opts.auth?.password) evaluated to true for {}, treating it the same as a fully anonymous probe. The fix narrows the stripping condition to opts.auth === undefined, which is the only case that truly represents an anonymous probe.

Key points:

  • The logic change is semantically correct: an explicitly provided auth object (even {}) is now distinguished from a missing auth argument.
  • The fix does not affect remote probes or probes with real credentials — both cases still set deviceIdentity: undefined (not stripped).
  • Anonymous loopback probes (auth not provided) still set deviceIdentity: null, preserving the existing security hardening.
  • The new test covers the exact auth: {} edge case that was the root cause, addressing the gap noted in the previous review round.
  • One minor style nit: the changed return statement is the only line in the file without a terminating semicolon; the semicolon inside the inline comment ((e.g. {});) is comment text, not a statement terminator.

Confidence Score: 5/5

  • Safe to merge — the fix is targeted, logically sound, and properly tested with no security regressions.
  • The change is a one-line condition narrowing with a clear and correct semantic intent. Both the pre-existing tests and the new regression test all verify the correct deviceIdentity values. No new permissions, network calls, or data access patterns are introduced. The only open item is a trivial missing semicolon.
  • No files require special attention.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/probe.ts
Line: 50

Comment:
**Missing semicolon on return statement**

The `return` statement on this line lacks a terminating semicolon. While TypeScript's ASI will handle this correctly, the trailing semicolon in the inline comment (`(e.g. {});`) is comment text, not a statement terminator — which makes this easy to mistake for an intentional omission. Every other statement in the file uses a semicolon; this one should too for consistency.

```suggestion
      return isLoopbackHost(hostname) && opts.auth === undefined; // Preserves device identity for empty but present auth objects (e.g. {})
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 40daf06

// Local authenticated probes should stay device-bound so read/detail RPCs
// are not scope-limited by the shared-auth scope stripping hardening.
return isLoopbackHost(hostname) && !(opts.auth?.token || opts.auth?.password);
return isLoopbackHost(hostname) && opts.auth === undefined // Preserves device identity for empty but present auth objects (e.g. {});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing test for the exact fixed scenario

The existing tests in probe.test.ts cover:

  • Loopback with auth: { token: "secret" }deviceIdentity preserved ✓
  • Loopback with no auth (undefined) → deviceIdentity is null

But there is no test for the specific bug case: a loopback probe with auth: {} (empty object). This is the exact scenario described in the PR as the root cause. Without it, a future regression (e.g., reverting to a truthy-check) would go undetected. Consider adding:

it("keeps device identity enabled for authenticated loopback probes with empty auth object", async () => {
  await probeGateway({
    url: "ws://127.0.0.1:18789",
    auth: {},
    timeoutMs: 1_000,
  });

  expect(gatewayClientState.options?.deviceIdentity).toBeUndefined();
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/probe.ts
Line: 50

Comment:
**Missing test for the exact fixed scenario**

The existing tests in `probe.test.ts` cover:
- Loopback with `auth: { token: "secret" }``deviceIdentity` preserved ✓
- Loopback with no `auth` (undefined) → `deviceIdentity` is `null` ✓

But there is **no test for the specific bug case**: a loopback probe with `auth: {}` (empty object). This is the exact scenario described in the PR as the root cause. Without it, a future regression (e.g., reverting to a truthy-check) would go undetected. Consider adding:

```typescript
it("keeps device identity enabled for authenticated loopback probes with empty auth object", async () => {
  await probeGateway({
    url: "ws://127.0.0.1:18789",
    auth: {},
    timeoutMs: 1_000,
  });

  expect(gatewayClientState.options?.deviceIdentity).toBeUndefined();
});
```

How can I resolve this? If you propose a fix, please make it concise.

@waitingsnow
Copy link
Copy Markdown
Author

@greptile-apps I have added the exact unit test you requested for the auth: {} empty auth object scenario in src/gateway/probe.test.ts. Please re-review.

// Local authenticated probes should stay device-bound so read/detail RPCs
// are not scope-limited by the shared-auth scope stripping hardening.
return isLoopbackHost(hostname) && !(opts.auth?.token || opts.auth?.password);
return isLoopbackHost(hostname) && opts.auth === undefined // Preserves device identity for empty but present auth objects (e.g. {});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing semicolon on return statement

The return statement on this line lacks a terminating semicolon. While TypeScript's ASI will handle this correctly, the trailing semicolon in the inline comment ((e.g. {});) is comment text, not a statement terminator — which makes this easy to mistake for an intentional omission. Every other statement in the file uses a semicolon; this one should too for consistency.

Suggested change
return isLoopbackHost(hostname) && opts.auth === undefined // Preserves device identity for empty but present auth objects (e.g. {});
return isLoopbackHost(hostname) && opts.auth === undefined; // Preserves device identity for empty but present auth objects (e.g. {})
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/probe.ts
Line: 50

Comment:
**Missing semicolon on return statement**

The `return` statement on this line lacks a terminating semicolon. While TypeScript's ASI will handle this correctly, the trailing semicolon in the inline comment (`(e.g. {});`) is comment text, not a statement terminator — which makes this easy to mistake for an intentional omission. Every other statement in the file uses a semicolon; this one should too for consistency.

```suggestion
      return isLoopbackHost(hostname) && opts.auth === undefined; // Preserves device identity for empty but present auth objects (e.g. {})
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@waliddafif
Copy link
Copy Markdown

Confirmed locally on Qubes OS / Debian 13 AppVM with OpenClaw 2026.3.13.

Repro:

  1. Local gateway on ws://127.0.0.1:18789
  2. CLI device paired with operator.read + operator.write
  3. openclaw gateway call status succeeds with the local device token
  4. openclaw gateway call status succeeds with the gateway auth token
  5. openclaw status still reports: unreachable (missing scope: operator.read)
  6. openclaw gateway probe with the gateway auth token still reports: RPC: limited - missing scope: operator.read
  7. openclaw gateway probe with the local device token fails with: unauthorized: gateway token mismatch (provide gateway auth token)

Local files are correct after openclaw devices rotate:

  • ~/.openclaw/identity/device-auth.json contains the rotated operator token with operator.read / operator.write
  • ~/.openclaw/devices/paired.json also reflects the rotated token/scopes

This points to the status / probe auth path rather than pairing or local token persistence.

@waitingsnow waitingsnow force-pushed the fix/gateway-auth-empty-object-trap branch 3 times, most recently from 4ae4fcb to d932e1c Compare March 18, 2026 07:54
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d932e1c119

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Local authenticated probes should stay device-bound so read/detail RPCs
// are not scope-limited by the shared-auth scope stripping hardening.
return isLoopbackHost(hostname) && !(opts.auth?.token || opts.auth?.password);
return isLoopbackHost(hostname) && opts.auth === undefined; // Preserves device identity for empty but present auth objects (e.g. {})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep anonymous probes anonymous when auth is empty

This condition now treats any present auth object as authenticated, but several callers build auth objects even when both fields are empty (for example unresolved SecretRef paths that explicitly say they are probing without credentials). With opts.auth === undefined as the only anonymous check, loopback probes in those paths will attach device identity and can fall back to cached device-token auth, which changes semantics from an anonymous reachability probe to an authenticated probe and can hide credential-resolution problems in status output.

Useful? React with 👍 / 👎.

@waitingsnow waitingsnow force-pushed the fix/gateway-auth-empty-object-trap branch from d932e1c to d0ab570 Compare March 18, 2026 12:00
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0ab57009f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Local authenticated probes should stay device-bound so read/detail RPCs
// are not scope-limited by the shared-auth scope stripping hardening.
return isLoopbackHost(hostname) && !(opts.auth?.token || opts.auth?.password);
return isLoopbackHost(hostname) && opts.auth === undefined; // Preserves device identity for empty but present auth objects (e.g. {})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep empty-auth loopback probes anonymous

The new opts.auth === undefined check misclassifies empty auth objects as authenticated, even though multiple call paths intentionally produce empty auth when credentials are unavailable (for example src/commands/gateway-status.ts:175-183 always builds an auth object, and src/gateway/probe-auth.ts:79-82 returns {} with a warning that it is probing without credentials). For local loopback URLs this now preserves device identity and can fall back to cached device-token auth, changing an anonymous probe into an authenticated one and hiding credential-resolution problems in status diagnostics.

Useful? React with 👍 / 👎.

@waitingsnow waitingsnow force-pushed the fix/gateway-auth-empty-object-trap branch 2 times, most recently from 3467363 to d0c9b4d Compare March 19, 2026 02:51
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0c9b4d8c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Local authenticated probes should stay device-bound so read/detail RPCs
// are not scope-limited by the shared-auth scope stripping hardening.
return isLoopbackHost(hostname) && !(opts.auth?.token || opts.auth?.password);
return isLoopbackHost(hostname) && opts.auth === undefined; // Preserves device identity for empty but present auth objects (e.g. {})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve anonymous mode when auth object has no credentials

Changing the loopback check to opts.auth === undefined makes empty auth objects ({} or { token: undefined, password: undefined }) count as authenticated, which enables device identity and allows cached device-token fallback in GatewayClient.selectConnectAuth even when no probe credentials were resolved. That regresses the intended anonymous-probe semantics for callers that always pass an auth object (for example status.scan.shared.ts and gateway-status.ts), and can suppress unresolved-SecretRef diagnostics because those paths only emit auth warnings on probe failure. Fresh evidence: status-all.ts also always passes probeAuth from resolveGatewayProbeAuthSafe, so default local status probes now lose the anonymous path when no token/password is available.

Useful? React with 👍 / 👎.

@waitingsnow waitingsnow force-pushed the fix/gateway-auth-empty-object-trap branch from d0c9b4d to 17d6e62 Compare March 19, 2026 09:56
@rbirmann
Copy link
Copy Markdown

We're running a 5-agent OpenClaw fleet across two Linux hosts (Ubuntu, systemd user services, loopback bind, token auth) managed by a custom orchestration daemon. The daemon uses openclaw agent --message to deliver inter-agent messages, and various openclaw config set / openclaw gateway install commands during agent provisioning.

On 2026.3.12/2026.3.13, every CLI↔gateway call hangs for 30+ seconds before timing out — not intermittent, effectively 100% failure rate in our environment. Gateway logs show handshake timeout / closed before connect while Slack/Telegram channels work fine on the same gateway process. HTTP endpoints respond instantly.

We're pinned to 2026.3.8 across all agents because of this. The 2026.3.22 handshake timeout raise (3s→10s) doesn't help — our CLI never sends the connect frame at all, so any timeout value fails the same way.

This PR's root cause analysis (empty auth: {} stripping device identity) matches exactly what we observed. Would love to see this merged — it's blocking us from upgrading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: XS

Projects

None yet

3 participants