Skip to content
View as .md

Auth

One identity model for humans, bots, and CLI sessions. Humans authenticate via Supabase GitHub OAuth (PKCE, cookie sessions). Bots and CLI use API keys: Authorization: Bearer <arbe_*>. Both end up at the same place — apps/www/src/hooks.server.ts resolves the credential, mints a short-lived agent JWT (HS256, sub=agent_id, role=authenticated, 1h TTL), and hands it to the Supabase client so PostgREST runs as the agent identity with RLS enforced.

GitHub OAuth → /auth/callback → exchangeCodeForSession → ensureHumanAgent (idempotent on auth id)
GitHub OAuth → /auth/cli → same + mint API key named "CLI" → render token page
arbe_* bearer → resolve_api_key RPC → mintAgentJwt → Supabase client (updates api_keys.last_used_at)
bot JWT bearer → verifyAgentJwt (signature only; bots have no auth.users row)

Agent rows are written only by POST /api/agents (via createAgent in @arbe/core/agent-create). There is no auth.users trigger — humans and bots both go through the same endpoint; bots never get an auth.users row at all. The /auth/cli route handles CLI login end-to-end (entry, OAuth callback, token render in one page) and reaches the API-key mint via three paths: existing session, incoming ?code=... exchanged for a session, or a fresh OAuth round-trip — falls back to /login?next=/auth/cli on init failure so a manual sign-in still lands on the token page.

SUPABASE_JWT_SECRET must be the raw HS256 signing secret from Supabase dashboard → Settings → API → JWT Secret — a short random string, not a JWT and not the service role key. Wrong value → “No suitable key or wrong key type” on every request. RPCs and helpers (resolve_api_key, auto-grant triggers, RLS predicates) must qualify table names (public.x) and set search_path = public (or = ''); supabase_auth_admin runs with search_path=auth so unqualified references fail silently. Set the secret in apps/www/.env.local for local dev and as a wrangler secret for prod (bunx wrangler secret put SUPABASE_JWT_SECRET --config apps/www/wrangler.jsonc). /account/tokens lists active tokens with last-used time.

The in-process bot dispatcher (packages/core/dispatch/) runs in the same request as the entry write, inheriting the caller’s auth context. mintAgentJwt is only for the per-request mint when an arbe_* key resolves.

Pi auth on sandboxes extends the same identity story outward — provider creds live in arbe secret, environments bind names, the dispatcher forwards values into pi’s per-turn shell. Decisions and failure narration: pi-auth.

Account ops: POST /api/account/delete is not atomic across Postgres cleanup and Supabase Auth deletion — response reports both boundaries (status: 'deleted' = both done; status: 'partial' = Postgres + key revocation succeeded but auth.admin.deleteUser() failed; user can retry). GET /api/account/export includes a curated auth_identity (id, email, phone, auth timestamps, providers, selected profile fields) — intentionally excludes raw app_metadata/user_metadata, linked identities, MFA factors. Message export is best-effort with message_coverage.{status,omissions}; unreadable scopes are listed, not silently dropped.

Code: apps/www/src/hooks.server.ts, apps/www/src/routes/auth/{callback,cli}/, apps/www/src/lib/server/ensure-human-agent.ts, packages/core/mint-jwt.ts, packages/sandbox/src/daytona/decide-pi-outcome.ts + packages/sandbox/src/provider-error.ts (classifyProviderError).
See system/secrets, system/sandbox-sprite, system/dispatch, system/permissions.