Skip to content

feat: add OpenClaw loopback HTTP fast path#793

Open
Nikhil (shadowfax92) wants to merge 6 commits intodevfrom
feat/add-openclaw-ws
Open

feat: add OpenClaw loopback HTTP fast path#793
Nikhil (shadowfax92) wants to merge 6 commits intodevfrom
feat/add-openclaw-ws

Conversation

@shadowfax92
Copy link
Copy Markdown
Contributor

@shadowfax92 Nikhil (shadowfax92) commented Apr 23, 2026

Summary

  • switch OpenClaw gateway bootstrap/runtime to gateway.auth.mode = "none" on loopback and remove token-based admin reads from service hot paths
  • add OpenClawHttpClient for /v1/models, /tools/invoke, and JSON session history reads on /sessions/:key/history
  • expose GET /claw/session/:key/history as a JSON-only route and keep session history fetches on loopback HTTP
  • remove the redundant CLI agents list after agents add; the service now refetches the created agent over HTTP

Design

OpenClaw still uses podman exec for cold-path mutations like onboarding, config writes, and agent create/delete, but all hot-path admin reads now go directly to the loopback gateway over HTTP with no auth header. The service enforces gateway.auth.mode = none, migrates existing configs once on start/restart, and keeps session history as a simple pull endpoint instead of a streamed route.

Test Plan

  • bun test tests/api/services/openclaw/openclaw-http-client.test.ts
  • bun test tests/api/services/openclaw/openclaw-service.test.ts
  • bun test tests/api/routes/openclaw.test.ts
  • bun run typecheck
  • cd apps/server && bun run test

@shadowfax92 Nikhil (shadowfax92) changed the base branch from main to dev April 23, 2026 17:23
@shadowfax92
Copy link
Copy Markdown
Contributor Author

Claude (@claude) review

@shadowfax92
Copy link
Copy Markdown
Contributor Author

Greptile (@greptileai) review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 23, 2026

Claude finished Nikhil (@shadowfax92)'s task in 2m 14s —— View job


Code Review: feat/add-openclaw-ws

  • Gather context and understand the PR
  • Read changed files
  • Analyze code quality, correctness, and potential issues
  • Post review feedback

Overall this is a clean, well-scoped refactor. The separation of "CLI for cold-path mutations, HTTP for hot-path reads" is a sound design, the error-handling helpers are thorough, and test coverage is solid. A few issues worth addressing before merge:


Bug: limit query param is not validated before forwarding

In the route handler, Number.parseInt('abc', 10) silently returns NaN, which is then forwarded to buildHistoryPath:

// packages/browseros-agent/apps/server/src/api/routes/openclaw.ts:354
const limit = limitRaw !== undefined ? Number.parseInt(limitRaw, 10) : undefined
// openclaw-http-client.ts:168
params.set('limit', String(Number(input.limit)))  // String(NaN) = "NaN"

This produces ?limit=NaN in the upstream URL. The gateway will likely error, but the BrowserOS server will return a generic 500 instead of a 400. Add a guard in the route:

const limit =
  limitRaw !== undefined
    ? (() => {
        const n = Number.parseInt(limitRaw, 10)
        return Number.isFinite(n) && n > 0 ? n : undefined
      })()
    : undefined

Fix this →


Coupling: route imports error type directly from HTTP client

// packages/browseros-agent/apps/server/src/api/routes/openclaw.ts:23
import { OpenClawSessionNotFoundError } from '../services/openclaw/openclaw-http-client'

Routes typically depend on the service layer, not on service-layer implementation details. OpenClawSessionNotFoundError could be re-exported from ./errors (alongside the other OpenClaw*Error classes imported just above it) to keep this boundary clean. Not a blocker, but easy to do.

Fix this →


Missing test: ensureAuthModeNone migration path

The new migration method has no tests for the case where an existing config has a non-none auth mode. The only coverage is in openclaw-service.test.ts where tests write mode: 'none' in the fixture, so the short-circuit branch is always hit. A test like:

"when gateway config has mode: token, calls cliClient.setConfig('gateway.auth.mode', 'none') exactly once"

would guard against the migration silently no-oping.


Minor: createAgent optimistic construction vs. findAgentById round-trip

After openclaw-cli-client.ts was simplified to return a locally-constructed record, openclaw-service.ts:createAgent immediately discards it and does an HTTP listAgents fetch to re-fetch the same agent:

// openclaw-service.ts:528-534
const created = await this.runControlPlaneCall(() =>
  this.cliClient.createAgent({ name, model }),
)
createdAgentId = created.agentId   // only the id is used
...
const agent = await this.findAgentById(createdAgentId) // full HTTP round-trip

The pattern is correct (authoritative read from the gateway), but the local created record from the CLI is now dead weight — only its agentId is read. This could be simplified to just pass name directly to findAgentById without storing created at all, since createdAgentId is initialized to name and created.agentId === input.name by construction.


Nit: protected request lacks a note on intent

// openclaw-http-client.ts:154
protected request(path: string, init: RequestInit): Promise<Response> {

protected (vs private) is intentional to allow test subclasses to intercept calls, but this is non-obvious since the tests don't visibly subclass it. A one-line comment or a // overridable in tests note would prevent future readers from tightening it back to private.


No concerns on the auth removal itself

Removing token-based auth on the loopback path is safe given that the gateway is bound to 127.0.0.1 inside the container and port-forwarded to the host loopback. The ensureAuthModeNone migration logic looks correct: it short-circuits on mode === 'none', uses existsSync to skip missing configs, and only calls setConfig when an actual migration is needed. The warning-swallowed catch is appropriate here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR replaces token-based gateway auth with a no-auth loopback fast path: it introduces OpenClawHttpClient for probe, agent listing, session listing, and session history reads; migrates existing configs to gateway.auth.mode = "none" on start/restart; and exposes a new GET /claw/session/:key/history JSON route. The OpenClawHttpChatClient is similarly simplified by dropping the getToken dependency.

  • NaN limit forwarded to gateway (openclaw.ts line 351–355): Number.parseInt('abc', 10) returns NaN; since NaN !== undefined, buildHistoryPath appends ?limit=NaN to the upstream URL. The route should reject non-finite values with a 400 before the call reaches the service.
  • Silent auth-mode migration failure (openclaw-service.ts line 619–624): if cliClient.setConfig throws, ensureAuthModeNone logs a warning and continues. The gateway stays locked on token auth, and every subsequent no-auth HTTP call returns 401 — leaving the service silently broken until the next restart.

Confidence Score: 4/5

Two P1 defects need addressing before merge: unvalidated NaN limit forwarded to gateway, and silent auth-mode migration failure that can leave the service permanently broken.

The overall architecture and new HTTP client are clean, but the unvalidated limit=NaN forwarded upstream and the swallowed migration failure that can silently break all control-plane calls are present defects on the changed code paths.

openclaw.ts (NaN limit validation) and openclaw-service.ts (ensureAuthModeNone silent failure handling)

Important Files Changed

Filename Overview
packages/browseros-agent/apps/server/src/api/routes/openclaw.ts Adds GET /claw/session/:key/history route; NaN limit not validated before forwarding to gateway; OpenClawSessionNotFoundError imported from HTTP client rather than shared errors module.
packages/browseros-agent/apps/server/src/api/services/openclaw/openclaw-http-client.ts New loopback HTTP client; well-structured with correct URL encoding, error unwrapping, and no-auth design.
packages/browseros-agent/apps/server/src/api/services/openclaw/openclaw-service.ts Removes token management; ensureAuthModeNone migration failure swallowed silently, leaving gateway locked on token auth.
packages/browseros-agent/apps/server/src/api/services/openclaw/openclaw-cli-client.ts createAgent returns synthetic record immediately; findAgentById replaces CLI list call.
packages/browseros-agent/apps/server/src/api/services/openclaw/openclaw-http-chat-client.ts Removes getToken dependency and Authorization header; consistent with no-auth loopback design.
packages/browseros-agent/apps/server/tests/api/services/openclaw/openclaw-service.test.ts Comprehensive updates; hardcoded port 42137 slightly weakens the occupied-port restart scenario.
packages/browseros-agent/apps/server/tests/api/services/openclaw/openclaw-http-client.test.ts Good coverage: probe, listAgents shapes, listSessions filters, getSessionHistory URL encoding, typed 404.
packages/browseros-agent/apps/server/tests/api/routes/openclaw.test.ts Three new route tests for session history; no issues.
packages/browseros-agent/apps/server/tests/api/services/openclaw/openclaw-cli-client.test.ts Simplified to single CLI call expectation; correct.
packages/browseros-agent/apps/server/tests/api/services/openclaw/openclaw-http-chat-client.test.ts Token/auth header assertions removed; streaming and error paths still covered.

Sequence Diagram

sequenceDiagram
    participant Route as GET /claw/session/:key/history
    participant Svc as OpenClawService
    participant HTTP as OpenClawHttpClient
    participant GW as OpenClaw Gateway (loopback)

    Route->>Svc: getSessionHistory(key, {limit, cursor, signal})
    Svc->>HTTP: getSessionHistory(key, input)
    HTTP->>GW: GET /sessions/{encoded_key}/history
    GW-->>HTTP: 200 OpenClawSessionHistory or 404
    alt 404
        HTTP-->>Svc: throw OpenClawSessionNotFoundError
        Route-->>Route: return 404
    else 2xx
        HTTP-->>Svc: OpenClawSessionHistory
        Route-->>Route: return 200 JSON
    end

    Note over Svc,GW: Boot/restart path
    Svc->>Svc: ensureAuthModeNone()
    Svc->>GW: setConfig gateway.auth.mode none
    Svc->>HTTP: probe() GET /v1/models
    HTTP->>GW: GET /v1/models
    GW-->>HTTP: 200/204
Loading

Comments Outside Diff (1)

  1. packages/browseros-agent/apps/server/src/api/services/openclaw/openclaw-service.ts, line 619-624 (link)

    P1 Silent auth-mode migration failure leaves gateway locked

    ensureAuthModeNone swallows the setConfig exception with only a warning. If the CLI call fails (e.g. the container isn't writable, or cliClient.setConfig times out), the gateway continues to run with its old token auth mode. Every subsequent no-auth HTTP call will receive a 401, but the service has no mechanism to retry the migration or surface the failure to the caller — the service will silently stay broken until the next restart.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/browseros-agent/apps/server/src/api/services/openclaw/openclaw-service.ts
    Line: 619-624
    
    Comment:
    **Silent auth-mode migration failure leaves gateway locked**
    
    `ensureAuthModeNone` swallows the `setConfig` exception with only a warning. If the CLI call fails (e.g. the container isn't writable, or `cliClient.setConfig` times out), the gateway continues to run with its old `token` auth mode. Every subsequent no-auth HTTP call will receive a 401, but the service has no mechanism to retry the migration or surface the failure to the caller — the service will silently stay broken until the next restart.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/api/routes/openclaw.ts
Line: 21-22

Comment:
**`OpenClawSessionNotFoundError` imported from implementation detail**

`OpenClawSessionNotFoundError` is imported directly from `openclaw-http-client` in the route layer rather than from the shared `errors` module. This couples the route to a specific transport implementation — if the HTTP client is ever swapped or the error type moved, the route import will break silently. Consider re-exporting the error class from the shared `errors` module alongside the other `OpenClaw*Error` types already there.

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

---

This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/api/routes/openclaw.ts
Line: 351-355

Comment:
**`NaN` forwarded to gateway when `limit` is non-numeric**

`Number.parseInt('abc', 10)` returns `NaN`, and since `NaN !== undefined`, the check in `buildHistoryPath` passes the value through, producing `?limit=NaN` in the upstream URL. Depending on gateway behaviour this could result in a 400, unintended unbounded reads, or the param being silently ignored — none of which match the caller's intent.

Add a guard after parsing:

```suggestion
      const limit =
        limitRaw !== undefined ? Number.parseInt(limitRaw, 10) : undefined
      if (limit !== undefined && !Number.isFinite(limit)) {
        return c.json({ error: 'invalid_limit' }, 400)
      }
```

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

---

This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/api/services/openclaw/openclaw-service.ts
Line: 619-624

Comment:
**Silent auth-mode migration failure leaves gateway locked**

`ensureAuthModeNone` swallows the `setConfig` exception with only a warning. If the CLI call fails (e.g. the container isn't writable, or `cliClient.setConfig` times out), the gateway continues to run with its old `token` auth mode. Every subsequent no-auth HTTP call will receive a 401, but the service has no mechanism to retry the migration or surface the failure to the caller — the service will silently stay broken until the next restart.

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

---

This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/tests/api/services/openclaw/openclaw-service.test.ts
Line: 1591

Comment:
**Hardcoded port 42137 may collide in CI**

The previous test dynamically allocated a real port and bound a server to simulate an occupied port. The replacement hardcodes `42137`. If that port is in use on a CI machine, `service.runtime.isReady` still returns `true` for it (because `hostPort === occupiedPort`), which means the test passes — but any future assertion that actually tries to bind the port will fail non-deterministically. Consider restoring dynamic allocation or using a reserved port.

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

Reviews (1): Last reviewed commit: "refactor(openclaw): keep session history..." | Re-trigger Greptile

Comment on lines +351 to +355
const cursor = c.req.query('cursor')
const limit =
limitRaw !== undefined ? Number.parseInt(limitRaw, 10) : undefined

try {
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.

P1 NaN forwarded to gateway when limit is non-numeric

Number.parseInt('abc', 10) returns NaN, and since NaN !== undefined, the check in buildHistoryPath passes the value through, producing ?limit=NaN in the upstream URL. Depending on gateway behaviour this could result in a 400, unintended unbounded reads, or the param being silently ignored — none of which match the caller's intent.

Add a guard after parsing:

Suggested change
const cursor = c.req.query('cursor')
const limit =
limitRaw !== undefined ? Number.parseInt(limitRaw, 10) : undefined
try {
const limit =
limitRaw !== undefined ? Number.parseInt(limitRaw, 10) : undefined
if (limit !== undefined && !Number.isFinite(limit)) {
return c.json({ error: 'invalid_limit' }, 400)
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/api/routes/openclaw.ts
Line: 351-355

Comment:
**`NaN` forwarded to gateway when `limit` is non-numeric**

`Number.parseInt('abc', 10)` returns `NaN`, and since `NaN !== undefined`, the check in `buildHistoryPath` passes the value through, producing `?limit=NaN` in the upstream URL. Depending on gateway behaviour this could result in a 400, unintended unbounded reads, or the param being silently ignored — none of which match the caller's intent.

Add a guard after parsing:

```suggestion
      const limit =
        limitRaw !== undefined ? Number.parseInt(limitRaw, 10) : undefined
      if (limit !== undefined && !Number.isFinite(limit)) {
        return c.json({ error: 'invalid_limit' }, 400)
      }
```

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

@github-actions
Copy link
Copy Markdown
Contributor

✅ Tests passed — 812/815

Suite Passed Failed Skipped
agent-sdk 44/44 0 0
agent 20/20 0 0
build 7/7 0 0
eval 8/8 0 0
server-agent 261/261 0 0
server-api 148/148 0 0
server-browser 3/3 0 0
server-integration 9/9 0 0
server-root 39/42 0 3
server-sdk 11/11 0 0
server-skills 31/31 0 0
server-tools 231/231 0 0

View workflow run

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant