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 roleForSub→scopesForRole 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.” |
5. Recommended end-state architecture
Primary (freeze)
Hosted MCP OAuth 2.1 + durable refresh, consumed by agents that speak MCP OAuth (Hermes: auth: oauth → https://mcp.knowtation.store/mcp).
Hardening required before this is “consumer durable”:
- Persist MCP client registrations + refresh tokens by wiring
KnowtationOAuthProviderinto the existingcreateGatewayRefreshStore()/refresh-token-corealready 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). - Align refresh lifetime with native/web policy (target: ~30d inactivity, ~90d family cap — the
DEFAULT_TOKEN_TTL_MS/DEFAULT_FAMILY_TTL_MSconstants already inhub/lib/refresh-token-core.mjs). - 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.mjsdocuments 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. - 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).
- 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/proposeceiling). - 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_SECRETverification stays, but add scope-aware REST verification tests proving anmcp_accesstoken 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/audseparation (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)
- Session tokens remain for curl, one-off scripts, smoke tests — expect expiry; re-copy or use Hub UI.
- Always-on agents: migrate to MCP OAuth (
auth: oauth) or Connect agent / agent credential when shipped. - Hub Settings copy text: label paste as “Session access token (expires)”, not “API key.”
- Do not silently extend paste JWT lifetime as the migration.
11. Doc / marketing edits list (when approved)
web/index.html— clarify Hermes import tile; soften “add agents” for cloud; mention Connect agent roadmap.web/hub/index.html+onboarding-wizard.mjs— split session token vs cloud agent connect.docs/AGENT-INTEGRATION.md— always-on section; ban.envJWT as durable path; Hermes MCP OAuth recipe pointing atmcp.knowtation.store.docs/HUB-API.md§1 — amend when agent credentials ship.docs/PARITY-MATRIX-HOSTED.md— auth row: OAuth durable vs Bearer session.- 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)
- Do not rely on
KNOWTATION_HUB_TOKENfrom Copy session token in Hostinger/data/.envfor vault writes — it will 401. - Point Hermes MCP at
https://mcp.knowtation.store/mcp, not the Netlify gateway/mcp. - Preferred product path: Hub Connect cloud agent (device code) on the persistent MCP host — see AGENT-INTEGRATION § Always-on.
- Verified interim (2026-07-13): desktop
npx mcp-remoteOAuth → copy~/.mcp-auth/mcp-remote-*to/data/.mcp-auth/→ Hermes stdio mcp-remote — evidence note indocs/evidence/durable-agent-auth/hermes-hostinger-mcp-remote-2026-07-13.md. - Do not claim
hermes mcp loginon Hostinger works until re-verified (hang after paste observed). - Prefer propose (
hub_create_proposal) over direct write for co-founder drafts. - Desktop Cursor: MCP OAuth Sign-in remains the best interactive path.
- Track end-state on
docs/DURABLE-AGENT-AUTH-ROADMAP.md(Phase B DONE; Phase C scoped REST credentials next). - 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.mddocs/COMPANION-APP-DESIGN-AND-AUTHORIZATION-GATE.md,docs/PHASE-8-P1B-OFFLINE-LOCKED-AUTH-SPEC.md§8web/hub/hub.js(refreshAccessToken,btn-copy-hub-api-env),web/hub/config.js,web/index.htmlhub/gateway/server.mjs,mcp-oauth-provider.mjs,native-oauth-provider.mjs,mcp-tool-acl.mjshub/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.