fix(cli/server): zero-to-chat local-dev flow works without --org or browser sign-in#944
Conversation
…PATs at /oauth/userinfo `lobu run` boots via `start-local.ts`, which called `initLobuGateway()` but threw away the returned Hono sub-app. The production entry (`server.ts`) mounts that app at `/lobu`, so `lobu chat`'s `POST /lobu/api/v1/agents` worked in prod but 404'd against every local install. Mount the lobu sub-app at `/lobu` before the main app catch-all so both entries expose the same public Agent API surface. Once mounted, the gateway rejected the local-init PAT because the route's auth middleware delegates to the OIDC issuer's `/oauth/userinfo`, and that handler only validated rows in `oauth_tokens` — PATs (`owl_pat_*`, `personal_access_tokens`) returned 403. Extend `OAuthProvider.verifyAccessToken` to fall through to `PersonalAccessTokenService.verify` for `owl_pat_*` bearers and add `profile:read` to the local-init PAT scope so `getUserInfo` returns the user. With both fixes, the same `device_token` issued by `/api/local-init` now authenticates the admin REST (`/api/<orgSlug>/*`), the OAuth introspection endpoint, and the public Agent API.
`EmbeddedDeploymentManager.storeDeploymentConfigs` calls `grantStore.grant(..., orgId)` and `GrantStore.grant` refuses any call without an org id. The Agent API was constructing the queue payload from `ThreadSession`, which had no `organizationId` field, so the very first message after `lobu chat` crashed at worker spawn with "GrantStore.grant requires organizationId" even though the session itself was created cleanly. Cache the owning org on `ThreadSession` at create time (it's already resolved from agent metadata for the worker token), then stamp it on `enqueueMessage`. No metadata round-trip per message; one read per session.
`POST /api/local-init` returns the install operator's auto-provisioned personal org (slug + id), but neither `tryLocalInit` nor `announceLocalSignIn` saved it. The CLI then had no `activeOrg` for the loopback context, so `lobu apply` exited with "No organization selected. Run `lobu org set <slug>` …" until the user manually copied the slug from the boot banner. Pipe the response's `organization.slug` into `setActiveOrg(slug, contextName)` so `lobu init && lobu run && lobu apply` works end-to-end without a manual `lobu org set` step.
📝 WalkthroughWalkthroughThis PR adds PAT support and expands worker PAT scopes, returns organization slug in local-init, threads organizationId through ThreadSession and queue payloads, binds the returned org slug into CLI contexts (and may auto-switch), and mounts the Lobu gateway into local server routing. ChangesOrganization Context Threading and Local Init
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Found a correctness issue:
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/cli/src/internal/credentials.ts (2)
173-178: ⚖️ Poor tradeoffDuplicated response type and org-binding logic with
dev.ts::announceLocalSignIn.The
/api/local-initresponse type and thesetActiveOrgbinding pattern are copy-pasted between here anddev.ts. Consider extracting a shared type (e.g.,LocalInitResponse) and possibly a small helper if both call sites grow further.Also applies to: 192-200
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/src/internal/credentials.ts` around lines 173 - 178, Extract the duplicated response shape into a shared type (e.g., LocalInitResponse) and replace the inline cast in packages/cli/src/internal/credentials.ts (the const body = (await res.json()) as { ... }) and the matching cast in dev.ts::announceLocalSignIn with that type; additionally, factor the organization-binding logic that calls setActiveOrg into a small helper (e.g., bindLocalOrg(response: LocalInitResponse, setActiveOrg: ...)) and call that helper from both sites to avoid copy-paste and keep behavior consistent.
207-217: 💤 Low valueMisleading attribution in stderr message.
Line 212 hardcodes
"(lobu run)"buttryLocalInitis invoked fromgetToken(), which can be called by any CLI command (lobu chat,lobu apply, etc.) when credentials are missing and a local server happens to be running. The message would incorrectly attribute the switch tolobu run.Suggested fix
process.stderr.write( - `Switched active context to "${target.name}" (lobu run)\n` + `Switched active context to "${target.name}" (local-init)\n` );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/src/internal/credentials.ts` around lines 207 - 217, The stderr message in tryLocalInit incorrectly hardcodes "(lobu run)" causing misleading attribution when getToken (or any other CLI command like lobu chat or lobu apply) invokes tryLocalInit; update the write in the block that calls getCurrentContextName/setCurrentContext so it does not hardcode "lobu run" — either remove the parenthetical or replace it with a neutral phrase such as "(local init)" or "(local server)" so the message accurately reflects a generic local credential switch; ensure this change is made in the try/catch around getCurrentContextName and setCurrentContext in tryLocalInit/getToken.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/cli/src/internal/credentials.ts`:
- Around line 173-178: Extract the duplicated response shape into a shared type
(e.g., LocalInitResponse) and replace the inline cast in
packages/cli/src/internal/credentials.ts (the const body = (await res.json()) as
{ ... }) and the matching cast in dev.ts::announceLocalSignIn with that type;
additionally, factor the organization-binding logic that calls setActiveOrg into
a small helper (e.g., bindLocalOrg(response: LocalInitResponse, setActiveOrg:
...)) and call that helper from both sites to avoid copy-paste and keep behavior
consistent.
- Around line 207-217: The stderr message in tryLocalInit incorrectly hardcodes
"(lobu run)" causing misleading attribution when getToken (or any other CLI
command like lobu chat or lobu apply) invokes tryLocalInit; update the write in
the block that calls getCurrentContextName/setCurrentContext so it does not
hardcode "lobu run" — either remove the parenthetical or replace it with a
neutral phrase such as "(local init)" or "(local server)" so the message
accurately reflects a generic local credential switch; ensure this change is
made in the try/catch around getCurrentContextName and setCurrentContext in
tryLocalInit/getToken.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0a460dde-85da-4aef-ad4a-c624e4a4a347
📒 Files selected for processing (2)
packages/cli/src/commands/dev.tspackages/cli/src/internal/credentials.ts
…t-fixes # Conflicts: # packages/server/src/start-local.ts
Summary
After `lobu init my-agent && cd my-agent && lobu run` on a fresh `LOBU_DATA_DIR`, a developer should be able to immediately run `lobu apply && lobu chat -a "…"` and get a real response — no `--org` flag, no manual `lobu org set`, no browser sign-in.
Today this doesn't work because of three compounding bugs. This PR fixes all three.
Bugs fixed
Bug 1 — Org slug not persisted from local-init
`/api/local-init` auto-provisions a personal org for the bootstrap user (slug `dev`) and returns it. But the CLI was ignoring `body.organization` — so the context had no active org, and `lobu apply` defaulted to `local-install` (which doesn't exist).
Fix: `tryLocalInit` and `announceLocalSignIn` now call `setActiveOrg(body.organization.slug, contextName)` after a successful bootstrap. `lobu apply` resolves the org from `context.activeOrg` and just works.
Bug 2 from the original triage (apply ignores `lobu org set`) turned out to be the same bug — `resolveOrgSlug` already reads per-context `activeOrg`; it was just empty. Closed by this fix.
Bug 3 — `lobu chat` 404 / 403 / crash
Three sub-bugs stacked.
3a. `packages/server/src/start-local.ts` called `initLobuGateway()` but discarded the returned sub-app, so `/lobu/api/v1/agents` 404'd in local mode (prod's `server.ts` mounts it). Fixed by mirroring the `wrapper.route('/lobu', lobuApp)` mount before the main app catch-all.
3b. The gateway's `createApiAuthMiddleware` validates bearer tokens via the issuer's `/oauth/userinfo`, which only checked `oauth_tokens` rows — PATs returned 403. Fixed by `OAuthProvider.verifyAccessToken` delegating to `PersonalAccessTokenService.verify` for `owl_pat_*` bearers, plus adding `profile:read` to the local-init PAT scope.
3c. Once the worker spawned it crashed at `GrantStore.grant requires organizationId` — the agent route's `enqueueMessage` payload was missing `organizationId` because `ThreadSession` had no such field. Added `organizationId` to `ThreadSession`, cached it from the agent-metadata lookup at session-create time, and stamped it on the queue payload.
Reproducer
```bash
unset DATABASE_URL
export LOBU_DATA_DIR=/tmp/lobu-fresh-$(date +%s)
export PATH=/opt/homebrew/opt/node@22/bin:$PATH
export Z_AI_API_KEY=$YOUR_KEY
lobu init bot -y --no-sentry --no-slack-preview --provider z-ai
cd bot
lobu run --port 9788 &
sleep 5
lobu apply --yes --force # works without --org
lobu chat -a bot "Reply hello" # returns real LLM response
```
Verified ending with `hello there friend` from the agent.
Test plan
Summary by CodeRabbit
New Features
Bug Fixes