Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commitfb37dd6

docs(audit): 2026-07-14 public-readiness audit — MCP invalid_token fix verified live, doctor green

docs(audit): 2026-07-14 public-readiness audit — MCP invalid_token fix verified live, doctor green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ccanty labs committed on July 14, 2026Parent: 2cecc08
1 file changed+1230fb37dd6eb1f65baa74c089414dd58083ae628c7b
1 changed file+123−0
AddedAUDIT-2026-07-14.md+123−0View fileUnifiedSplit
1# AUDIT — 2026-07-14 — Public-readiness for external AI platforms
2
3**Goal:** certify gluecron.com can receive PRs from external AI platforms
4(MCP + git-over-HTTP), self-canonically, without defaulting to GitHub.
5
6Legend: ✅ verified live &nbsp; ⬜ pending &nbsp; ⚠️ known gap
7
8## Executive summary
9
10The platform is **ready for external platforms to onboard via the OAuth
11connector flow today**. One critical bug was found and fixed live during
12this audit: the MCP surface never signaled `invalid_token`, so any client
13whose access token aged past 1 hour got stuck in a permanent
14"token expired" reconnect loop even though the server's refresh grant
15worked. Fixed in `b5a7bb3`, deployed 2026-07-14 02:03 UTC, verified with
16live requests. The remaining ⬜ items are operator-gated (PAT-authenticated
17journey run, offsite backup arming) — commands are staged below.
18
19## 1. The critical bug — MCP "token expired" reconnect loop (FIXED)
20
21Root cause (three interacting defects):
22
231. `src/middleware/auth.ts` `softAuth`: an expired/revoked `glct_`/`glc_`
24 bearer was silently downgraded to **anonymous** — nothing recorded that a
25 token was presented and rejected.
262. `src/routes/mcp.ts`: the 401 challenge omitted RFC 6750
27 `error="invalid_token"`, so clients could not distinguish "token died →
28 refresh" from "never authenticated → first-time auth". The refresh_token
29 grant (fully implemented, with rotation) sat unused.
303. The MCP handshake (`initialize`/`tools/list`/`ping`) was fully open, so a
31 client reconnect **falsely succeeded** on a dead token, then looped on
32 `tools/call`.
33
34Fix (`b5a7bb3`, locked-file edit to `auth.ts` owner-approved 2026-07-14):
35`softAuth` now flags `bearerInvalid`; `/mcp` answers **every** method —
36including the handshake — with `401` +
37`WWW-Authenticate: Bearer error="invalid_token", …` when a dead bearer is
38presented. Anonymous discovery/handshake unchanged; anonymous `tools/call`
39still gets the connector sign-in challenge (deliberately without `error=`).
40
41Live verification (2026-07-14, prod):
42
43- ✅ dead `glct_` on `initialize` → 401 + `error="invalid_token"` (loop-breaker)
44- ✅ dead `glct_` on `tools/list` → 401 + `error="invalid_token"`
45- ✅ anonymous `tools/call` → 401 challenge with `resource_metadata=`, **no** `error=`
46- ✅ anonymous `initialize` → 200 (discovery open)
47- ✅ regression tests: 125 pass (mcp/oauth/well-known), 83 pass (mcp-write/git-push-auth/api-tokens/push)
48
49## 2. Doctor run (scripts/doctor.ts, new — `bun run doctor`)
50
51First run against `https://gluecron.com` (anonymous sections): **ALL GREEN**.
52
53-**a. Endpoint smoke** — 15/15 (healthz, readyz, version, login/register/
54 landing/explore/demo/pricing/status, api-v2 health, mcp discovery,
55 manifest, sw, dxt)
56-**b. OAuth provider** — 8/8: AS metadata advertises `refresh_token` +
57 `registration_endpoint`; protected-resource metadata (root + /mcp); **DCR
58 round-trip returns a client_id (201)**; token endpoint: garbage code →
59 `invalid_grant`, garbage refresh → `invalid_grant`, bogus grant_type →
60 `unsupported_grant_type`
61- ✅ **c. MCP anonymous + RFC 6750** — 5/5: initialize 200; **live tools/list
62 exactly matches the local 60-tool manifest (zero drift)**; anonymous
63 tools/call challenge correct; dead-bearer invalid_token on both
64 handshake methods
65- ⬜ **d. Authenticated MCP reads** — awaits `GLUECRON_PAT`
66-**e. Read-only tool matrix (--full)** — awaits `GLUECRON_PAT`
67
68## 3. External-platform PR journey (scripts/agent-journey.ts, new)
69
70⬜ Awaits an admin-scope PAT. Simulates exactly what an external platform
71does: PAT identity → scratch repo → git push over smart HTTP with token
72basic-auth → `gluecron_create_branch`/`write_file`/`create_pr`/`comment_pr`
73→ AI-review poll → `gluecron_merge_pr` + verify → activity feed →
74guaranteed cleanup. Hard-guarded against ever touching the self-host repo.
75
76## 4. Self-canonical / server-side state (verified on-box 2026-07-14)
77
78- ✅ Deploys are cord-free from GitHub: `/opt/gluecron` `origin` = the
79 box-local canonical bare repo (docker volume), NOT github.com. Pushes to
80 `gluecron.com/ccantynz/Gluecron.com.git` main went live in ≤ ~2 min twice
81 today (`b5a7bb3`, `2cecc08`).
82-`gluecron-update.timer` firing every 60s.
83-`gluecron-backup.timer` daily; last run 2026-07-14 00:00 UTC, exit 0,
84 4.4M Neon dump.
85-`gluecron-restore-drill.timer` weekly (first run Sun 2026-07-19 04:00 UTC).
86-**Offsite backup NOT yet armed** — dumps stay on the VPS. Staged
87 operator command appends `BACKUP_SCP_TARGET=root@100.89.227.39:/opt/backups/gluecron`
88 (vapron-158) to `/opt/gluecron/.env` (with `.env` backup first) and
89 fires a verification run. jarvis→vapron root SSH verified working.
90- ⚠️ GitHub (`ccantynz-alt/Gluecron.com`) remains the emergency deploy
91 source per CUTOVER_RUNBOOK Phase 4 (needs 7 green days + armed offsite
92 before retirement). Do not archive yet.
93
94## 5. How external platforms onboard (no PAT required)
95
961. `GET /.well-known/oauth-authorization-server` — discovery
972. `POST /oauth/register` — RFC 7591 DCR (verified live)
983. Authorization-code + PKCE → `POST /oauth/token` (1h access / 30d rotating refresh)
994. MCP at `POST /mcp`; expired tokens now get `error="invalid_token"` →
100 client refreshes transparently
1015. Git over smart HTTP with `Basic <user>:<token>` (PAT) or the web/API flows
102
103## 6. Open items / known gaps
104
105- ⬜ Run doctor sections d/e + agent-journey with an admin PAT (blocked on
106 operator minting one; classifier reserves prod-DB writes for the operator).
107- ⬜ Arm offsite backup (staged command above; operator-confirmed destination).
108- ⚠️ `src/middleware/api-auth.ts` (API v2): 401s invalid bearers but sends no
109 `WWW-Authenticate` header and rejects `glct_` OAuth tokens as unknown —
110 additive parity pass recommended (out of scope today).
111- ⚠️ `/api/version` reports `sha: "unknown"` on the compose/auto-update
112 deploy path — BUILD_SHA pin only happens via self-deploy.sh; staleness
113 checks rely on `builtAt`. Worth stamping in the docker build.
114- ⚠️ DCR leaves `doctor-*` client rows (RFC 7592 delete not implemented) —
115 cosmetic residue from each doctor run.
116- ⚠️ No MCP create-repo tool — journey uses `POST /api/v2/repos`; consider
117 adding `gluecron_create_repo` for full MCP-only agent flows.
118
119## Session evidence
120
121- Commits: `b5a7bb3` (fix + tests), `2cecc08` (doctor + journey + npm script).
122- Fix verified live with curl transcripts (§1); doctor output (§2) from
123 `bun scripts/doctor.ts` at 2026-07-14 ~02:1x UTC.
0124