DURABLE-AGENT-AUTH-SPEC.md markdown
224 lines 18.4 KB
Raw
sha256:c2dbf04d56308f3bbf2d06e6d2eb022b8948b1e827195fe525a44e5e18d5f9c0 feat(auth): Phase B Connect cloud agent (RFC 8628) + Hermes… Human minor ⚠ breaking 10 days ago

Durable agent / MCP auth for remote co-founders — frozen decision

Status: Phase A DONE · Phase B DONE (2026-07-13 — RFC 8628 device connect + Hub UI + Hostinger mcp-remote interim). Phase C scoped REST credentials next.
Branch (Phase B): feat/connect-cloud-agent-device-code
Companion plan: DURABLE-AGENT-AUTH-ROADMAP.md
Approval gate: Historical — Aaron accepted §2 Verdict + §5 before Phase A/B Builds.


Simple summary

Remote always-on agents (like Hermes on Hostinger) cannot stay connected by pasting the Hub “Copy token” JWT into .env. That paste is a short-lived access token with no refresh. The Hub UI stays signed in because the browser has a refresh cookie; the paste button does not copy that. Marketing copy that makes “connect your agent” sound finished overstates what ships for cloud agents. The product gap is real: we need a Hub flow that issues refreshable, revocable, scoped credentials (prefer MCP OAuth / device connect), not longer god JWTs.

Technical summary

Verified code: btn-copy-hub-api-env pastes localStorage hub_token only (web/hub/hub.js); browser renewal uses HttpOnly cookie → POST /api/v1/auth/refresh (credentials: 'include'). Gateway access JWT default HUB_JWT_EXPIRY || '24h' (hub/gateway/server.mjs); MCP OAuth issues type: mcp_access (1h) + in-memory refresh (~24h) (hub/gateway/mcp-oauth-provider.mjs); native companion OAuth is loopback-only + body refresh via refresh-token-core (hub/gateway/native-oauth-provider.mjs). Netlify returns MCP_NETLIFY_UNSUPPORTED for /mcp; public MCP URL is https://mcp.knowtation.store/mcp (web/hub/config.js). Hermes Agent documents MCP auth: oauth with PKCE, auto-refresh, and headless paste-back — research spike still required against Aaron’s Hostinger Hermes build before claiming production readiness.


1. Incident (truth vs claims)

Claim / belief Verified truth
Paste Hub JWT into Hermes /data/.env for durable vault access False. Paste is access JWT only; no refresh token on that path (web/hub/hub.js copy handler; refresh is cookie-only for Hub UI).
Hub UI “staying logged in” means the pasted token stays valid False. UI uses refreshAccessToken()POST /api/v1/auth/refresh with credentials: 'include'.
Hosted MCP works on the Netlify gateway URL False for stateful MCP. NETLIFY/mcp 503 MCP_NETLIFY_UNSUPPORTED (hub/gateway/server.mjs). Use HUB_MCP_PUBLIC_URL / mcp.knowtation.store.
HUB-API.md: “There is no API-key-only path” True today. Login-issued JWT (or MCP/native OAuth tokens) only.
Landing “Hermes Agent” tile = connect Hermes as vault client False. Tile is under import (“Agent memory + profile”) in web/index.html.
Hosted MCP OAuth exists True on persistent gateway (mcp-oauth-provider.mjs + mcpAuthRouter). Refresh store is in-memory plaintext Map (mcp-oauth-provider.mjs:89,201-207) — no hash-at-rest, no reuse→family revoke; TTL access 3600s, refresh 86400s.
A durable, hardened refresh store must be built for MCP False — it already exists. hub/gateway/refresh-token-store.mjs (createGatewayRefreshStore()) wraps refresh-token-core (hash-at-rest, rotation, reuse→family revoke) and is already wired into native OAuth on the same persistent MCP host (server.mjs:355,670). Phase A is reuse, not build.
MCP mcp_access tokens are scope-limited on REST False. REST verifyToken returns only sub (server.mjs:221-228); authz re-derives role via roleForSubscopesForRole and ignores the token's scopes. A vault:read MCP token already writes over REST today (live scope-elevation — see §8).
Companion/native OAuth is the Hostinger answer False. Loopback-only redirect URIs (isLoopbackUri); designed for on-device companion.
Docs tell users to re-copy on 401 True (docs/AGENT-INTEGRATION.md §3; Hub Settings copy). That is ops honesty, not a consumer product for always-on agents.
Observed ~15m expiry on paste Ops observation. Code default on gateway is 24h (JWT_EXPIRY = process.env.HUB_JWT_EXPIRY \|\| '24h'). Native access is hardcoded 15m. Self-hosted Hub defaults 1h. Treat 15m as either deployment override of HUB_JWT_EXPIRY or a different token class — verify production env before changing defaults.

2. Verdict (1 paragraph)

This is a missing product surface, not user error. Aaron followed the documented power-user path (Settings → Integrations → Copy Hub URL, token & vault → Bearer / .env). That path is designed for short interactive sessions and scripts that can re-copy; it has no refresh delivery outside the browser cookie jar. Always-on remote agents need either (1) an OAuth client that stores and rotates refresh tokens, or (2) a first-class agent credential with revoke/scope/rotation. Lengthening HUB_JWT_EXPIRY alone is rejected as the sole fix: it widens the blast radius of /data/.env theft without revoke or scope. Marketing claims that imply “agents connect easily” for cloud co-founders are ahead of the durable auth story; reword now, build the connect flow next.


3. Personas — shipped vs promised

Persona Shipped durable auth Documented / promised Gap
(a) Desktop Cursor / Claude Hosted MCP OAuth Sign-in (discovery + PKCE) or Bearer paste (expires); local stdio MCP with vault path Landing + AGENT-INTEGRATION.md Paste path still over-taught vs OAuth; OAuth refresh durability weak (in-memory MCP refresh)
(b) Headless always-on (Hermes VPS/Hostinger) None durable. Static JWT paste expires; health without vault auth succeeds Landing ecosystem / “add agents” language; Hermes import tile Primary gap. Need MCP OAuth (Hermes-capable) + durable refresh and/or device-connect UX
(c) Paperclip / orchestrator Bearer JWT in env/secrets; ops must rotate Smoke scripts use KNOWTATION_HUB_TOKEN Needs agent credential or refresh-capable secret store; no Knowtation-side SSM pattern found in this workspace’s Paperclip tree
(d) Local CLI vault KNOWTATION_VAULT_PATH — no Hub JWT AGENT-INTEGRATION.md §1 Shipped. Not hosted SoT unless sync/propose added

4. Claims audit — overstatement + honest copy

Surface Quote / paraphrase (current) Problem Honest copy (proposed)
web/index.html hero “One place for you and your agents.” Implies any agent can stay connected “One place for you and your agents — desktop MCP sign-in today; cloud agents via Connect agent (shipping).”
web/index.html band “Add agents” / MCP common path in Cursor Undersells headless limits “Desktop: MCP OAuth in Cursor/Claude. Cloud agents: use Connect agent or MCP OAuth with refresh — not a pasted Hub JWT.”
web/index.html “Hosted MCP adds OAuth-gated access aligned with Hub roles.” True for MCP OAuth clients; users still taught JWT paste Keep OAuth claim; add: “Do not put Copy-Hub JWTs in always-on server .env files.”
Hermes tile “Hermes Agent — Agent memory + profile” under imports Easy to misread as connect Rename caption: “Import Hermes memory/profile into vault” (not “connect Hermes”).
Hub Integrations Copy button as “key card” for tools outside the browser Correct for short-lived tools; fails for always-on Split UI: Session token (expires) vs Connect cloud agent (refreshable).
docs/AGENT-INTEGRATION.md Re-copy on 401 Accurate but positions babysitting as normal Keep for session tokens; point always-on agents to Connect agent / MCP OAuth.
docs/HUB-API.md §1.1 “There is no API-key-only path.” Blocks agent credentials until we deliberately amend Amend when Phase C ships: “No unscoped API keys; scoped agent credentials are the machine path.”

Primary (freeze)

Hosted MCP OAuth 2.1 + durable refresh, consumed by agents that speak MCP OAuth (Hermes: auth: oauthhttps://mcp.knowtation.store/mcp).

Hardening required before this is “consumer durable”:

  1. Persist MCP client registrations + refresh tokens by wiring KnowtationOAuthProvider into the existing createGatewayRefreshStore() / refresh-token-core already used by native OAuth (server.mjs:355,670). Do not build a second store — reuse the audited one (hash-at-rest, rotation, reuse→family revoke).
  2. Align refresh lifetime with native/web policy (target: ~30d inactivity, ~90d family cap — the DEFAULT_TOKEN_TTL_MS / DEFAULT_FAMILY_TTL_MS constants already in hub/lib/refresh-token-core.mjs).
  3. Strong consistency is mandatory for MCP refresh. The store MUST be file/DB-backed on the persistent MCP host; it MUST NOT use the eventual-consistency Netlify blob path (refresh-token-store.mjs documents a ≤60s reuse-detection lag on blob). MCP OAuth already mounts only on the persistent host (server.mjs:629), so the file backend applies — freeze it explicitly.
  4. Document Hermes config + headless paste-back / SSH forward; spike Hostinger Managed Hermes version for OAuth support (hard gate — see Roadmap Phase A: if unsupported, device code becomes primary).
  5. Hub UI: “Connect cloud agent (MCP)” checklist that points at KNOWTATION_MCP_URL, not Netlify /mcp.

Fallback (freeze)

Hub “Connect agent” device authorization (RFC 8628) and/or scoped agent credentials for:

  • hosts that cannot complete loopback/paste-back OAuth,
  • REST-only runners (cron, Paperclip, curl),
  • propose-only marketing bots.

Agent credential invariants (non-negotiable):

  • Bound to sub + vault id(s) + role/scopes (vault:read / vault:write / propose ceiling).
  • Shown once at mint; stored hashed; list + revoke in Hub.
  • Rotatable; default TTL with hard max; audit events; never god-admin by default.
  • Amends “no API-key-only path” to “no unscoped long-lived keys.”

Explicitly not primary

Option Why not primary
Lengthen HUB_JWT_EXPIRY only No revoke/scope/rotation; /data/.env theft = full session until expiry
Companion native OAuth Loopback-only; not Hostinger
Vault-local CLI on agent host Fine offline; does not keep hosted vault as live SoT without sync
Human-mediated sync only Valid interim, not end-state product

6. Options ranking (security × UX)

Rank Option Security UX for (b) Notes
1 MCP OAuth 2.1 + durable refresh inside Hermes High if refresh hashed + rotatable High if Hermes version supports auth: oauth Spike required on Hostinger build; paste-back documented by Hermes
2 Device authorization / Hub “Connect agent” code High (user confirms in Hub; short device codes) Highest for non-SSH users Best “one Hub flow” product; may mint MCP or agent refresh
3 Long-lived scoped agent credential Medium–High with revoke/scope/rotation High for REST; still a secret in .env Despite today’s “no API-key path”; required for Paperclip-class runners
4 Companion-style native OAuth High on device Fails on Hostinger (no trusted loopback to user browser) Keep for companion
5 Vault-local + sync to hosted High local Medium Sync/propose still needs auth
6 Human-mediated (agent local → Cursor promotes) High (human gate) Low automation Interim only

7. Refresh token delivery threat model

Question: Can we issue refresh (or agent credential) to a remote agent without HttpOnly cookies?

Answer: Yes — that is exactly what native OAuth already does (refresh in response body, OS keychain on companion). Remote agents store secrets on disk (.env, ~/.hermes/mcp-tokens/). Cookies are a browser delivery mechanism, not a security requirement for machine clients.

Threat Mitigation (required)
Theft from /data/.env or Hostinger shared CLI Scoped credential; short access TTL; refresh rotation; Hub revoke; no admin scope by default
Telegram / chat paste of secrets Hub UI warns; prefer device-code display of user code only (not refresh secret); never log tokens
Refresh replay after theft refresh-token-core reuse detection → revoke family
Gateway restart wiping MCP OAuth Persist store (Phase A)
Confused deputy / over-scope Path/prefix + propose-only scopes (Phase D); role ceiling like native C6
Netlify MCP confusion Keep MCP on mcp.knowtation.store; never teach Netlify /mcp

Do not put web-session refresh cookies into agent .env. Issue agent-class refresh or MCP refresh only.


8. MCP URL vs REST for always-on agents

Path When
MCP https://mcp.knowtation.store/mcp Interactive tools, Hermes/Cursor with OAuth, prompts/resources
REST https://api.knowtation.store Batch writes, scripts, Paperclip, when MCP session state is undesirable

Both need durable auth. Prefer MCP OAuth for Hermes; prefer agent credential for REST-only.

Confused deputy is a live scope-elevation, not just an audience collision. mcp_access JWTs are signed with the same SESSION_SECRET; REST verifyToken (server.mjs:221-228) returns only sub, and downstream authz re-derives privileges via roleForSub(sub)scopesForRole — it never reads the token's scopes. Consequence today: a vault:read-only mcp_access token performs full read/write over REST. This directly nullifies the Phase D “propose-only” promise.

Freeze:

  • Phase A: shared SESSION_SECRET verification stays, but add scope-aware REST verification tests proving an mcp_access token cannot exceed its minted scope. (Closing the enforcement gap may land in A or the earliest phase that mints a reduced-scope token.)
  • Do NOT advertise Phase D propose-only / path-prefix scopes until scope-aware REST enforcement is closed — otherwise a reduced-scope agent token silently writes.
  • Phase C may add typ/aud separation (RFC 8707 resource indicators) when agent credentials mint separately.

9. Scopes / roles for Born Free marketing agents

Need Recommendation
Read research / drafts under projects/born-free/** vault:read + optional path prefix
Write drafts Prefer propose only (Hub review) for co-founder agents; direct write only if owner opts in
Index / import / admin Denied for agent credentials
Role mapping Start from Hub viewer / editor / evaluator ACL (mcp-tool-acl.mjs); add propose-only agent mode that exposes hub_create_proposal without direct write

10. Migration message (existing Copy-Hub → .env users)

  1. Session tokens remain for curl, one-off scripts, smoke tests — expect expiry; re-copy or use Hub UI.
  2. Always-on agents: migrate to MCP OAuth (auth: oauth) or Connect agent / agent credential when shipped.
  3. Hub Settings copy text: label paste as “Session access token (expires)”, not “API key.”
  4. Do not silently extend paste JWT lifetime as the migration.

11. Doc / marketing edits list (when approved)

  1. web/index.html — clarify Hermes import tile; soften “add agents” for cloud; mention Connect agent roadmap.
  2. web/hub/index.html + onboarding-wizard.mjs — split session token vs cloud agent connect.
  3. docs/AGENT-INTEGRATION.md — always-on section; ban .env JWT as durable path; Hermes MCP OAuth recipe pointing at mcp.knowtation.store.
  4. docs/HUB-API.md §1 — amend when agent credentials ship.
  5. docs/PARITY-MATRIX-HOSTED.md — auth row: OAuth durable vs Bearer session.
  6. Landing meta description — optional: “desktop MCP OAuth; cloud agent connect.”

(Docs-only PR to main is forbidden by owner policy; ship copy with the Build PR that implements Connect agent / Phase A.)


12. Interim Born Free runbook (≤15 lines)

  1. Do not rely on KNOWTATION_HUB_TOKEN from Copy session token in Hostinger /data/.env for vault writes — it will 401.
  2. Point Hermes MCP at https://mcp.knowtation.store/mcp, not the Netlify gateway /mcp.
  3. Preferred product path: Hub Connect cloud agent (device code) on the persistent MCP host — see AGENT-INTEGRATION § Always-on.
  4. Verified interim (2026-07-13): desktop npx mcp-remote OAuth → copy ~/.mcp-auth/mcp-remote-* to /data/.mcp-auth/ → Hermes stdio mcp-remote — evidence note in docs/evidence/durable-agent-auth/hermes-hostinger-mcp-remote-2026-07-13.md.
  5. Do not claim hermes mcp login on Hostinger works until re-verified (hang after paste observed).
  6. Prefer propose (hub_create_proposal) over direct write for co-founder drafts.
  7. Desktop Cursor: MCP OAuth Sign-in remains the best interactive path.
  8. Track end-state on docs/DURABLE-AGENT-AUTH-ROADMAP.md (Phase B DONE; Phase C scoped REST credentials next).
  9. Composio catalog packaging is future only — not required for vault auth.

13. Evidence index (files read)

  • docs/AGENT-INTEGRATION.md, docs/HUB-API.md, docs/PARITY-MATRIX-HOSTED.md
  • docs/COMPANION-APP-DESIGN-AND-AUTHORIZATION-GATE.md, docs/PHASE-8-P1B-OFFLINE-LOCKED-AUTH-SPEC.md §8
  • web/hub/hub.js (refreshAccessToken, btn-copy-hub-api-env), web/hub/config.js, web/index.html
  • hub/gateway/server.mjs, mcp-oauth-provider.mjs, native-oauth-provider.mjs, mcp-tool-acl.mjs
  • hub/lib/refresh-token-core.mjs
  • External: Hermes MCP docs (auth: oauth, headless paste-back) — spike still required on Hostinger build

14. Offline-lock interaction (Phase 8)

Verified: MCP OAuth and native OAuth endpoints mount only when !offlineLockedActive (hub/gateway/server.mjs:629). Under Phase-8 offline-locked mode, there is no durable remote-agent auth path at all — the discovery/token/refresh endpoints are not mounted.

Freeze:

  • Phase A DoD MUST explicitly document that durable agent auth is unsupported while offline-locked (a stated, tested behavior — not a silent absence).
  • This is expected and acceptable: offline-locked is a local-only security posture; always-on cloud agents are out of scope in that mode.
  • Do not attempt to “fix” this by mounting agent-auth endpoints under offline-lock without a separate Thinking freeze — it would widen the offline-locked trust boundary.
File History 1 commit
sha256:c2dbf04d56308f3bbf2d06e6d2eb022b8948b1e827195fe525a44e5e18d5f9c0 feat(auth): Phase B Connect cloud agent (RFC 8628) + Hermes… Human minor 10 days ago