Skip to content

fix(cli): chat/eval target the gateway Agent API under /lobu#637

Merged
buremba merged 1 commit into
mainfrom
fix/chat-eval-lobu-gateway-prefix
May 12, 2026
Merged

fix(cli): chat/eval target the gateway Agent API under /lobu#637
buremba merged 1 commit into
mainfrom
fix/chat-eval-lobu-gateway-prefix

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 12, 2026

What

lobu chat and lobu eval create an agent session by POSTing to <gateway>/api/v1/agents — but the embedded Lobu server mounts its public Agent API under /lobu (packages/server/src/server.tsapp.route('/lobu', lobuApp)). The bare origin only serves the org-scoped admin REST API (/api/:orgSlug/...) and OAuth.

So <origin>/api/v1/agents was getting matched by /api/:orgSlug/agents with orgSlug = "v1":

$ lobu chat -g http://localhost:8787 -a food-ordering --new "…"
  Failed to create session (404): {"error":"invalid_request","error_description":"Organization 'v1' not found"}
$ lobu eval ping
  Eval "ping" failed: Failed to create session (404): {"error":"invalid_request","error_description":"Organization 'v1' not found"}

This affected every deployment — local lobu run/make dev, app.lobu.ai, community.lobu.ai (verified: /api/v1/agents → 404 on all three, /lobu/api/v1/agents → 401 i.e. route exists).

Fix

Add agentApiBase() / GATEWAY_AGENT_API_PREFIX to internal/gateway-url.ts and run the resolved gateway URL (from --gateway, a context's apiUrl, or the .env PORT) through it, so chat/eval target <origin>/lobu/api/v1/agents. Idempotent — a URL already ending in /lobu is left alone. lobu apply and the other org-scoped commands keep using the bare origin. Updated templates/TESTING.md.tmpl and the chat integration-test fixtures to match.

Verified

Against a local make dev gateway (officebot_dev DB, real Z_AI_API_KEY in .env):

  • lobu chat -g http://localhost:8787 -a food-ordering --new --auto-approve "…" → full round-trip, agent (z-ai/glm-4.7) replies.
  • lobu eval ping --trials 1 → connects, creates the session, runs the agent, grades (the trial itself times out at 30s — a separate eval/agent-tuning issue, not the routing path).
  • bun test packages/cli/src — 157 pass, 0 fail.

`lobu chat` and `lobu eval` POST to `<gateway>/api/v1/agents` to start a
session, but the embedded Lobu server mounts its public Agent API under
`/lobu` (server.ts `app.route('/lobu', ...)`) — the bare origin only serves
the org-scoped admin REST API + OAuth. So `<origin>/api/v1/agents` was being
matched by `/api/:orgSlug/agents` with `orgSlug = "v1"` → 404
`Organization 'v1' not found`, making both commands unusable against any
deployment (local `lobu run`, app.lobu.ai, community.lobu.ai — all `/lobu`).

Add `agentApiBase()` / `GATEWAY_AGENT_API_PREFIX` and run the resolved gateway
URL (from `--gateway`, a context's apiUrl, or `.env` PORT) through it so
chat/eval hit `<origin>/lobu/api/v1/agents`. `lobu apply` and the other
org-scoped commands keep using the bare origin. Updated the TESTING.md
template and the chat integration test fixtures to match.
* a URL that already ends in `/lobu` returns it unchanged.
*/
export function agentApiBase(gatewayUrl: string): string {
const trimmed = gatewayUrl.replace(/\/+$/, "");
@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 61.53846% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/cli/src/commands/eval.ts 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@buremba buremba merged commit 4535b79 into main May 12, 2026
17 of 18 checks passed
@buremba buremba deleted the fix/chat-eval-lobu-gateway-prefix branch May 12, 2026 23:30
buremba added a commit that referenced this pull request May 19, 2026
* fix(server): mount /lobu prefix in PGlite assembly (parity with server.ts)

start-local.ts called initLobuGateway() but threw away the returned Hono
app, so the embedded gateway's public Agent API (/lobu/api/v1/agents/*),
worker gateway, MCP proxy, and bundled API docs were all unreachable in
PGlite mode — every call returned 404. server.ts already mounts the same
app at /lobu (PR #637); this aligns the PGlite entrypoint.

Reproducer:
  Before: GET /lobu/health -> 404
  After:  GET /lobu/health -> 200

* fix(auth): accept owl_pat_ PATs in embedded Agent API auth bridge

The lobuApp middleware only hydrated (user, session) from a Better Auth
session (cookie or bearer session-token); owl_pat_* personal access tokens
were ignored, so every /lobu/api/v1/agents/* call authenticated with a PAT
minted by 'lobu token create' (or returned by /api/local-init's
device_token) fell through to the unauthenticated path and the embedded
authProvider returned null. The qmsum-demo benchmark worked around this by
forging a Better Auth session cookie from BETTER_AUTH_SECRET.

Extend the middleware to verify Authorization: Bearer owl_pat_* tokens via
PersonalAccessTokenService.verify, look up the bound user, and synthesize
the same (user, session) shape the Better Auth path produces. The downstream
org-context middleware now honours an org id pinned on the PAT (PAT minted
for org A must run against org A) before falling back to the user's default
membership. This fixes both PGlite and Postgres assemblies — they share
this auth path.

Reproducer (PGlite):
  Before: GET /lobu/api/v1/agents -H 'Authorization: Bearer owl_pat_...' -> 401
  After:  GET /lobu/api/v1/agents -H 'Authorization: Bearer owl_pat_...' -> 200
  After:  GET /lobu/api/v1/agents -H 'Authorization: Bearer owl_pat_BAD' -> 401

* fix(auth): harden embedded Agent API auth bridge (codex #1, #2, #3)

Round-2 codex review of PR #940 surfaced three defects in the PAT bridge
added by 10bb63b (`fix(auth): accept owl_pat_ PATs in embedded Agent API
auth bridge`). All three live in the same middleware closure, so fix them
together in one extracted `createLobuAuthBridge()` factory.

#1 (HIGH) — missing tenant-membership check. After PAT verification the
bridge synthesised (user, session) and set `organizationId = patInfo.organizationId`
without checking the user was still a member of that org. The canonical
REST path enforces this at `workspace/multi-tenant.ts:425` and returns
403 `forbidden`. Mirror that: query the `member` table for
`(userId, organizationId)`; reject with the same 403 shape when missing.

#2 (MED) — cookie precedence over invalid PAT. The original ordering
hydrated Better Auth first and only attempted PAT validation inside an
`if (!c.get('user'))` guard. A request carrying both a valid session cookie
and `Authorization: Bearer owl_pat_<bad>` therefore authenticated as the
cookie user and the invalid PAT was never challenged. Reverse the order:
when the `Authorization` header carries `Bearer owl_pat_*`, the PAT path
is authoritative — failure short-circuits with 401 regardless of cookie.
Better Auth only runs when the header is absent or non-PAT.

#3 (MED) — null-org PAT silent re-scoping. `personal_access_tokens.organization_id`
is `ON DELETE SET NULL`; a PAT minted for a since-deleted org would fall
through to `resolveDefaultOrgId(userId)` and silently bind to the user's
earliest membership. Treat PATs with `organizationId === null` as invalid
on this path and return 401 with a message pointing at `lobu token`.

Refactor: extract the bridge from the closure inside `initLobuGateway`
into an exported `createLobuAuthBridge()` factory. The behaviour change
is what the bullets above describe; the factory exists so the next commit
can exercise the bridge from integration tests without bootstrapping the
full gateway.

* test(auth): cover embedded Agent API auth bridge (codex #4)

Round-2 codex review of PR #940 noted that existing PAT coverage hits the
MCP routes and the token service, but not the embedded /lobu/api/v1/agents/*
auth bridge introduced by 10bb63b. Add a focused integration suite that
mounts `createLobuAuthBridge` (exported in the previous commit) on a
minimal Hono app and exercises every contract the bridge has to honour.

11 tests across four describe blocks:

  - Happy path: valid PAT → 200, organizationId pinned to the PAT's org.
  - Rejection cases: unknown hash, expired, revoked, missing owl_pat_
    prefix, empty Authorization, non-Bearer scheme — all 401 (with the
    bridge's `invalid_token` shape on actual PATs, and the test handler's
    `no-user` shape on tokens the bridge correctly ignores).
  - Cookie precedence (codex #2): valid session cookie + invalid PAT →
    401 invalid_token, not 200 via cookie fallback.
  - Tenant membership (codex #1): valid PAT for an org the user has been
    removed from → 403 forbidden, mirroring multi-tenant.ts:425. Plus a
    defensive variant for a PAT minted against an org the user never
    joined.
  - Null org PAT (codex #3): valid PAT whose organization_id was set to
    NULL after creation (mirrors the ON DELETE SET NULL collapse path) →
    401 invalid_token, not silent re-resolution to the user's earliest
    membership via resolveDefaultOrgId.

Run with LOBU_TEST_BACKEND=pglite — no external Postgres required.

* fix(auth): case-insensitive Bearer scheme parsing per RFC 7235 (codex round-2)

Pre-fix: `Authorization: bearer owl_pat_*` (lowercase scheme) failed the
`header.startsWith('Bearer ')` literal match, so the PAT path was skipped
and the bridge fell through to the Better Auth cookie path — a valid
session cookie would silently mask an invalid/revoked PAT.

RFC 7235 §2.1 makes the auth scheme token case-insensitive. Parse it that
way. Token VALUE comparison stays case-sensitive — PAT hashes are.

* test(auth): cover lowercase Bearer bypass + cookie-only happy path (codex round-2)

Three new tests against `createLobuAuthBridge`:

1. Lowercase `bearer` scheme + invalid PAT + valid session cookie → 401
   (proves the case-insensitive parse + PAT precedence hold together; was
   the evasion gap before the fix).
2. Uppercase `BEARER` scheme + valid PAT → 200 (case-insensitive parse,
   success direction).
3. Cookie-only request (no Authorization header) → bridge reaches
   `next()` instead of short-circuiting with its own 401/403
   (`error: 'invalid_token'` / `error: 'forbidden'` would indicate the
   PAT or membership path mistakenly fired). End-to-end Better Auth cookie
   verification is exercised by entities/member-privacy-contract.test.ts
   via the full app; this minimal harness only owns the bridge contract.

* fix(auth): case-insensitive PAT prefix detection (codex round-3)

Mirror the Bearer scheme fix at the inner prefix check: `Bearer OWL_PAT_*`
now flows through PAT validation instead of falling through to cookie
auth. Token value handed to verify() is unchanged — PAT hashes stay
byte-exact.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants