Skip to content

feat: per-agent scoped mailbox auth + login-with-raft (v0) - #6

Open
TennyZhuang wants to merge 9 commits into
mainfrom
feat/per-agent-auth
Open

feat: per-agent scoped mailbox auth + login-with-raft (v0)#6
TennyZhuang wants to merge 9 commits into
mainfrom
feat/per-agent-auth

Conversation

@TennyZhuang

@TennyZhuang TennyZhuang commented Jul 14, 2026

Copy link
Copy Markdown

V0 of "mail app on raft" (task #1). Backend model + login screen — ready for @gogo's security review. OAuth route + deploy are coordinated remaining work (below).

What's in (typecheck clean, npm test 33/33)

3-resource model (Owner / Mailbox / Access Key), locked with tygg/Ray/Gogo:

  • workers/lib/keyRegistry.ts — KV-backed access-key registry: resolve/mint/revoke, hashed tokens (raw shown once), per-owner mailbox index. Revoke = soft-disable (KV ~60s propagation, documented).
  • workers/app.ts — auth middleware resolves a scoped Bearer key → owner/scope context. Legacy global API_KEY kept as a temporary admin fallback (feature seam, removed at cutover). CF Access unchanged.
  • workers/index.tsPOST /api/v1/mailboxes is now a claim: requires an authed owner, enforces the tier quota (free=1, pro=10 by raft-server tier), stamps owner, mints a mailbox-scoped key (returned once). List endpoint returns only the caller's mailboxes.
  • workers/lib/mailbox.tsrequireMailbox enforces mailboxAccessAllowed for scoped-key callers (finest isolation: a key touches only its one mailbox).
  • workers/lib/auth.ts — pure, tested primitives: mailboxAccessAllowed, ownerFromRaftUserinfo (owner=raft:${server_id}:${type}:${sub}), planForOwner/canCreateMailbox, scope helpers.
  • app/routes/login.tsx — Login with Raft screen (humans + agents, one flow).
  • wrangler.jsoncAGENTIC_INBOX_KEYS KV binding + PRO_SERVER_IDS/RAFT_OAUTH_* vars.

Remaining for v0 (coordinated — do NOT merge until done)

  1. OAuth login-with-raft route (/auth/raft/login + callback/token/userinfo) — @gogo to port his proven me.build implementation; integrates with ownerFromRaftUserinfo + the claim + mintKey.
  2. Surface the minted scoped key in the home claim UI (small).
  3. @gogo infra: wrangler kv namespace create agentic-inbox-keys (fill the id), register the Connected App (client_id/secret), wrangler secret put RAFT_OAUTH_CLIENT_SECRET, npm run deploy, live E2E.

Notes

  • Security-critical (auth rewrite + cross-tenant isolation): worth a property/oracle + mutation-test pass on auth.ts before merge.
  • Merge gate per Slock norms (no gh --approve; discuss on Slock).

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Postel and others added 9 commits June 23, 2026 19:54
Foundation for task #1 (per-agent API keys + multi-mailbox ownership). Pure,
unit-tested logic with no KV/binding dependency:
- hashApiKey: sha-256 hex (keys stored/compared hashed, never raw)
- ownerCanAccess: API access scoped to mailbox owner (admins exempt; receiving
  is never owner-gated)
- reservedHandleForLocalPart/Address + createAllowedByPrefix: <handle>/<handle>-*
  prefix reservation to prevent identity squatting

Middleware/endpoint wiring, KV key registry, owner field on mailboxes, and the
migration backfill follow once the KV namespace (agentic-inbox-keys) is minted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
…rinfo, tiering

Extends the per-agent-auth foundation (task #1) with the locked 3-resource model
(Owner / Mailbox / Access Key) primitives, pure + unit-tested, for Gogo's app.ts
wiring:
- mailboxAccessAllowed(key, mailbox): scope-aware access — admin | account
  (all owner's boxes) | a single mailbox-id (finest isolation, one-key-one-box).
- ownerFromRaftUserinfo: derive owner=raft:${server_id}:${type}:${sub} from OAuth
  userinfo, trusting only type/sub/server_id (never preferred_username).
- tiering: DEFAULT_PLAN_LIMITS / maxMailboxesForPlan / canCreateMailbox — free=1
  mailbox (tygg's confirmed first rule); numbers overridable by pricing.

npm test: 31/31.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
…lation, tiering

Implements the task #1 3-resource model backend (Owner / Mailbox / Access Key):
- workers/lib/keyRegistry.ts: KV-backed access-key registry (resolve/mint/revoke,
  hashed tokens, per-owner mailbox index for quota counts). Soft-disable revoke
  (KV ~60s propagation, documented).
- workers/app.ts: auth middleware resolves a scoped Bearer key -> owner/scope
  context; legacy global API_KEY kept as a temporary admin fallback (feature seam,
  removed at cutover). CF Access unchanged (legacy human path).
- workers/index.ts: POST /mailboxes is now a CLAIM — requires an authenticated
  owner, enforces the tier quota (free=1, pro=10 by raft-server tier), stamps
  `owner`, and mints a mailbox-scoped key (returned once).
- workers/lib/mailbox.ts: requireMailbox enforces owner-scoped access for
  scoped-key callers (mailboxAccessAllowed); legacy sessions pass during transition.
- workers/lib/auth.ts: + mailboxAccessAllowed, ownerFromRaftUserinfo, tiering
  (planForOwner / canCreateMailbox), scope helpers.
- wrangler.jsonc: AGENTIC_INBOX_KEYS KV binding (Gogo mints id) + PRO_SERVER_IDS /
  RAFT_OAUTH_* vars. CLIENT_SECRET via wrangler secret.

typecheck clean; npm test 33/33. Remaining for v0: OAuth login-with-raft route,
list-endpoint scoping, frontend login/claim screens; Gogo: KV mint + Connected App
registration + secrets + deploy + live E2E.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
Scoped-key callers see only their own mailboxes (via the per-owner index);
admin/legacy sees all. Completes the v0 backend isolation surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
Adds the v0 login screen: humans and agents sign in through the same OAuth flow
(handed off to the worker's /auth/raft/login route). Wired into the router.
The mailbox list + claim UI already exists on the home route (now backed by the
scoped-key claim endpoint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
The claim endpoint returns a mailbox-scoped access key; surface it in a dialog
(copy + 'shown once, unrecoverable' warning) after claiming. Completes the v0
frontend claim tail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
…erse-only)

Per tygg (2026-07-15): v0 login restricted to botiverse users. serverAllowed
gates by raft server_id (empty allowlist = unrestricted). Enforce in the OAuth
callback after userinfo; server_id list via env. Pure + tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
- AGENTIC_INBOX_KEYS KV id = bbf03eefcab5455f9efb4496dc532a40 (minted by Gogo).
- ALLOWED_SERVER_IDS = botiverse server_id (login gate; enforce in OAuth callback
  via serverAllowed()).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
…klist

Addresses Gogo's PR #6 review finding 2: the claim endpoint didn't enforce the
(defined+tested) anti-squat logic, so an authenticated agent could claim another
agent's identity address (gogo@, postel@). Now a non-admin caller may only claim
within its own handle namespace (<handle>@ / <handle>-*, handle = session
preferred_username via new authHandle context var) and never a reserved system
name (admin/postmaster/noreply/...). Admin (provisioning) exempt.

npm test 40/40, typecheck clean. (authHandle is set by the OAuth session
middleware — Gogo's port.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Postel <postel@mail.build>
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.

1 participant