fix(cli): chat/eval target the gateway Agent API under /lobu#637
Merged
Conversation
`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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This was referenced May 12, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
lobu chatandlobu evalcreate 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.ts→app.route('/lobu', lobuApp)). The bare origin only serves the org-scoped admin REST API (/api/:orgSlug/...) and OAuth.So
<origin>/api/v1/agentswas getting matched by/api/:orgSlug/agentswithorgSlug = "v1":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_PREFIXtointernal/gateway-url.tsand run the resolved gateway URL (from--gateway, a context'sapiUrl, or the.envPORT) through it, so chat/eval target<origin>/lobu/api/v1/agents. Idempotent — a URL already ending in/lobuis left alone.lobu applyand the other org-scoped commands keep using the bare origin. Updatedtemplates/TESTING.md.tmpland the chat integration-test fixtures to match.Verified
Against a local
make devgateway (officebot_dev DB, realZ_AI_API_KEYin.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.