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 pagearbe_* 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.