Skip to content

fix(cli/server): zero-to-chat local-dev flow works without --org or browser sign-in#944

Merged
buremba merged 5 commits into
mainfrom
feat/lobu-zero-to-chat-fixes
May 20, 2026
Merged

fix(cli/server): zero-to-chat local-dev flow works without --org or browser sign-in#944
buremba merged 5 commits into
mainfrom
feat/lobu-zero-to-chat-fixes

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 19, 2026

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

  • Repro the three failure modes on fresh `LOBU_DATA_DIR`
  • Apply each fix, re-run reproducer at each step
  • `make typecheck` clean
  • `make build-packages` clean
  • Pi review

Summary by CodeRabbit

  • New Features

    • Personal Access Tokens (PATs) can be used for OAuth/API gateway authentication.
    • Local CLI now auto-resolves, persists and (best-effort) auto-switches to the resolved organization context.
    • Local init now issues a worker PAT with expanded scopes so it can access userinfo/org resolution paths.
  • Bug Fixes

    • Organization metadata is propagated for agent sessions and queued messages to ensure correct routing.
    • Restored gateway routing for local development.

Review Change Stack

buremba added 3 commits May 19, 2026 23:43
…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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Organization Context Threading and Local Init

Layer / File(s) Summary
PAT verification and worker scope expansion
packages/server/src/auth/oauth/provider.ts, packages/server/src/auth/routes.ts
OAuth provider routes owl_pat_-prefixed tokens through PersonalAccessTokenService.verify() and /api/local-init expands auto-minted worker PAT scopes to include profile:read.
Organization ID in session and queue payloads
packages/server/src/gateway/session.ts, packages/server/src/gateway/routes/public/agent.ts
ThreadSession adds optional organizationId populated at session creation; agent session creation stores tokenOrganizationId and direct message enqueues include organizationId in queue options.
CLI local-init org context binding
packages/cli/src/commands/dev.ts, packages/cli/src/internal/credentials.ts
CLI extends /api/local-init response type with optional organization.slug, imports setActiveOrg, and best-effort binds the returned slug to the local context and attempts a best-effort context switch to local.
Gateway application mounting
packages/server/src/start-local.ts
initLobuGateway() return value is captured and conditionally mounted at /lobu on the Hono wrapper before the catch-all route so gateway endpoints are registered.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • lobu-ai/lobu#902: Both PRs modify packages/server/src/auth/routes.ts's /api/local-init handler, with #902 rewriting user/org/PAT minting logic while this PR adjusts minted PAT scopes and org response data.
  • lobu-ai/lobu#909: Also touches /api/local-init; both PRs change how the endpoint responds during bootstrap (this PR adds org slug and scope changes).
  • lobu-ai/lobu#830: Related work on the local bootstrap flow where server mints credentials and CLI performs local-init; these PRs touch the same end-to-end path.

"A rabbit hops through init's light,
PATs and orgs tied snug and tight,
Sessions carry where orgs abide,
The CLI binds them by our side,
Gateway mounted — all set right."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main goal of fixing the zero-to-chat local-dev flow by removing the requirement for --org or browser sign-in, which aligns with the primary focus of the changeset.
Description check ✅ Passed The description is comprehensive with clear bug explanations, detailed fixes, and a reproducer; however, the test plan section is incomplete with only some items checked.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lobu-zero-to-chat-fixes
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/lobu-zero-to-chat-fixes

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 19, 2026

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

Codecov Report

❌ Patch coverage is 2.43902% with 40 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/cli/src/commands/dev.ts 3.57% 27 Missing ⚠️
packages/cli/src/internal/credentials.ts 0.00% 13 Missing ⚠️

📢 Thoughts on this report? Let us know!

@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 19, 2026

Found a correctness issue:

  • The advertised zero-to-chat flow still does not work without -c local in a fresh CLI config. announceLocalSignIn() creates/updates the local context and now sets its activeOrg, but addContext() does not make that context current and no project link is written. Plain lobu apply / lobu chat still call resolveContext() with no context, so they use the current/default lobu context (prod) rather than the local server. This only works if the user already happened to have local as the current context. Either switch/link the local context as part of the bootstrap, or change the docs/reproducer to require -c local.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/cli/src/internal/credentials.ts (2)

173-178: ⚖️ Poor tradeoff

Duplicated response type and org-binding logic with dev.ts::announceLocalSignIn.

The /api/local-init response type and the setActiveOrg binding pattern are copy-pasted between here and dev.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 value

Misleading attribution in stderr message.

Line 212 hardcodes "(lobu run)" but tryLocalInit is invoked from getToken(), 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 to lobu 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2643f1c and 62efff4.

📒 Files selected for processing (2)
  • packages/cli/src/commands/dev.ts
  • packages/cli/src/internal/credentials.ts

…t-fixes

# Conflicts:
#	packages/server/src/start-local.ts
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.

2 participants