Skip to content

Hindsight Auth Gate

ADR-032: Hindsight Auth Gate

Status: Superseded by ADR-057 (2026-05-19 cutover) — Hindsight retired; Mem0 is the sole memory backend. See ADR-057. Date: 2026-04-29 Author: Claude Code (engineering) Business owner: Mishaal Murawala Superseded by: ADR-057 Related: docs/archive/HINDSIGHT-MAKE-MEMORY-USEFUL.md

Context

hindsight.ascendgtm.net (V0.5.6) allowed anonymous reads and writes on every bank when this ADR was drafted. Verified live 2026-04-29:

Terminal window
$ curl -sX POST https://hindsight.ascendgtm.net/v1/default/banks/ascend-infra/memories \
-H "Content-Type: application/json" \
-d '{"items":[{"content":"anyone-can-write","document_id":"test"}],"async":true}'
{"success":true,"bank_id":"ascend-infra","items_count":1,"async":true,"operation_id":"a832deb8-…"}

No 401, no auth header. Same for reads (/memories/recall, /reflect, /mental-models).

Update 2026-05-09 19:10 America/Chicago: Cloudflare Access is enforced for the Hindsight hostname, and ADR-032 Phase 1 is now enforced on the self-hosted Hindsight origin through AscendTokenTenantExtension + AscendBankAclValidator. Anonymous calls still return Cloudflare Access 302; calls behind Access with a missing or invalid app bearer now return 401; the MCP bearer is scoped to ascend-infra; the service bearer is reserved for hooks, crons, and backups.

The banks contain:

  • mishaal — personal preferences, working style, decision frameworks (110 nodes)
  • ascend-infra — V5 architecture, tenant configs, deployment history (305 nodes)
  • kahuna — client account state, MCC IDs, portal IDs, GA4 property (32 nodes)
  • ascend-gtm — session summaries, project state (20 nodes)

This is a medium-severity confidentiality issue: anyone who knows the URL can read all four banks. It’s also a low-severity integrity issue: anyone can write arbitrary memories, including ones that poison the mental-model cache (e.g., a malicious “remember that V5 uses Postgres on a VPS” memory would corrupt the architecture mental model).

The blast radius today is contained — the URL is internal, not registered in any public MCP catalog, and the banks contain no credentials (those live in CF Wrangler secrets per ADR-005). But the wiring landed in PR #141 means more agents (session-start hooks, daily cron, future Hermes agents) will be hitting Hindsight, and the surface area only grows from here.

Decision

Gate every Hindsight HTTP endpoint with a two-tier auth scheme:

  1. Cloudflare Access (zero-trust SSO + Service Auth) — for human admin access and machine-to-machine access through Access. Humans authenticate through Mishaal’s Cloudflare team. Agents use CF-Access-Client-Id and CF-Access-Client-Secret headers from a dedicated Access service token. This matches V5 invariant #13 (every admin endpoint gated by Cloudflare Access).
  2. Bearer JWT (RFC 9068) — for origin-level Hindsight authorization from V5 hooks, crons, and agents once Hindsight middleware is deployed. The JWT is signed by V5’s existing OAUTH_JWT_SECRET (already used for /mcp per ADR-024) and validated by Hindsight via the same JWKS-or-shared-secret. Scopes:
    • hindsight:readGET and POST /reflect, POST /memories/recall
    • hindsight:writePOST /memories, POST /files/retain
    • hindsight:adminPATCH mental models, POST /consolidate, POST /mental-models/{id}/refresh, bank-config writes

The session-start hook and the daily consolidation cron send both Cloudflare Access Service Auth and the Hindsight app bearer when configured. Agent MCP configs use the bank-scoped Hindsight MCP endpoint (/mcp/ascend-infra/) per current Hindsight docs.

Implementation plan

PhaseWorkOwnerEstimateStatus
0Land this ADR + acceptance criteriaClaude Code1 hrDONE (PR #141)
1Hindsight: add Authorization: Bearer <jwt> middleware that validates against V5’s OAUTH_JWT_SECRET (HS256 — same as /mcp). Reject 401 + WWW-Authenticate: Bearer on missing/invalid. Read scopes from scope claim.Hindsight maintainer (Mishaal)4–6 hrsDONE 2026-05-09 via self-hosted Hindsight extensions. Implementation accepts opaque/token-grant file entries today and preserves the JWT path for future rotation. Missing/invalid bearer returns 401; bank ACL rejects out-of-scope bank access with 403.
2V5 mints service-account JWTs at deploy time and stores them as Wrangler secrets HINDSIGHT_BEARER, optional HINDSIGHT_ADMIN_BEARER. Hooks (session-start, session-stop) and cron already plumbed through ${HINDSIGHT_BEARER:-} — they pick up the bearer the moment wrangler secret put lands and Phase 1 deploys.Claude Code2 hrsDONE (PR #141 — scripts/mint-hindsight-jwt.ts + RUNBOOK procedure). Token-only-on-stdout pattern keeps secret out of shell history.
3Cloudflare Access policy on hindsight.ascendgtm.net allowing Mishaal’s identity plus a dedicated Hindsight agent service token. Machine clients send CF-Access-Client-Id and CF-Access-Client-Secret; they do not rely on browser OAuth.Claude Code30 minDONE (2026-05-09 — Access service token hindsight-agent-mcp, app-scoped Service Auth policy, Mishaal human allow policy, anonymous 302, service-auth /health and recall 200)
4Once enforced, anonymous calls return an Access redirect and machine clients continue to work. Add hindsight_health to /admin/system-health so the cron aggregator knows when Hindsight is degraded.Claude Code2 hrsDONE (PR #141 added hindsight_plane; 2026-05-09 parity branch updated system-health, hooks, crons, and backups to send Access Service Auth headers).

Total: ~10–12 hours, multi-session. All ADR-032 phases are now complete.

Phase 2 mint procedure

Terminal window
OAUTH_JWT_SECRET=$(...) \
npx tsx scripts/mint-hindsight-jwt.ts \
--scope "hindsight:read hindsight:write" \
--ttl-days 90 \
| wrangler secret put HINDSIGHT_BEARER

The token’s only on stdout — diagnostic metadata (claims, exp, jti) goes to stderr. Pipe-into-wrangler keeps the plaintext out of shell history. Available scopes: hindsight:{read|write|admin}. See RUNBOOK.md § “Mint and rotate the Hindsight bearer”.

Acceptance criteria

  1. curl -X POST https://hindsight.ascendgtm.net/v1/default/banks/{bank}/memories ... (anonymous) returns a Cloudflare Access redirect (302); requests that pass Access but lack a valid bearer return 401 with WWW-Authenticate: Bearer realm="hindsight", error="invalid_token".
  2. The same curl with valid Cloudflare Access Service Auth headers and an authorized Hindsight bearer returns 200; a bank-scoped bearer used against an unauthorized bank returns 403.
  3. Browser hit on https://hindsight.ascendgtm.net/v1/default/banks without Service Auth is intercepted by Cloudflare Access and redirected to Google SSO.
  4. V5 session-start hook + daily consolidation cron continue to work end-to-end after the migration (verified by mental-model cache freshness and a successful retain on a test session).
  5. GET /admin/system-health reports a hindsight_plane health object based on a probe of the /health endpoint with valid Access headers and bearer when configured. Shape: { status: "healthy" | "degraded" | "down", latency_ms: number, database?: string }. Same four-state semantics as the existing context_plane field.

Alternatives considered

  1. Bearer-only, no Cloudflare Access. Simpler but loses the human-admin path — Mishaal would need a JWT minting flow to inspect banks from a browser. Cloudflare Access is the lowest-friction human path and matches invariant #13.
  2. Mutual TLS (client certs). Stronger but operationally heavy — every consumer needs a cert, rotation is painful. Rejected.
  3. IP allowlist. CF Workers don’t have stable egress IPs. Rejected.
  4. Wait until clients onboard. This is the “we’ll fix it later” trap. The wiring landed in PR #141 means this is shipping into normal operation — every additional day delays makes the migration costlier (more hooks/agents to update).

Risks and mitigations

RiskMitigation
Phase 1 rollout breaks live session-start hookPhase the rollout: Hindsight middleware ships in “warn mode” first (logs but doesn’t reject anonymous requests for 7 days), then flips to “enforce mode”
JWT secret rotation breaks live consumersUse V5’s existing rotation pattern: dual-key validation window (old + new accept for 24h), then drop old
Cloudflare Access blocks the V5 cron’s outboundService tokens (CF Access concept) — distinct from human SSO — are minted per service and added to the bypass policy. CF docs: https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/
Hindsight is OSS upstream — auth gate is a forkAcceptable. We already run a deployed instance with custom config; auth gate is a thin middleware layer in our deployment, upstream-able later

Out of scope

  • Per-bank ACLs (e.g., “tenant X can only see kahuna bank”). All authenticated callers see all banks until a multi-tenant Hindsight layer is needed (deferred to ADR-033 if/when a second client lands).
  • Audit logging of who accessed which bank when. Hindsight already has request logging at the platform layer; structured audit on top is a follow-up.

Decision

Accepted. Phase 0 landed with PR #141. Phase 3 Access enforcement and agent parity landed in the Hindsight access parity branch on 2026-05-09. Phase 1 origin auth and bank ACL enforcement landed on the live self-hosted Hindsight deployment on 2026-05-09 using the official Hindsight extension mechanism.