# AUDIT — 2026-07-14 — Public-readiness for external AI platforms

**Goal:** certify gluecron.com can receive PRs from external AI platforms
(MCP + git-over-HTTP), self-canonically, without defaulting to GitHub.

Legend: ✅ verified live &nbsp; ⬜ pending &nbsp; ⚠️ known gap

## Executive summary

The platform is **ready for external platforms to onboard via the OAuth
connector flow today**. One critical bug was found and fixed live during
this audit: the MCP surface never signaled `invalid_token`, so any client
whose access token aged past 1 hour got stuck in a permanent
"token expired" reconnect loop even though the server's refresh grant
worked. Fixed in `b5a7bb3`, deployed 2026-07-14 02:03 UTC, verified with
live requests. The remaining ⬜ items are operator-gated (PAT-authenticated
journey run, offsite backup arming) — commands are staged below.

## 1. The critical bug — MCP "token expired" reconnect loop (FIXED)

Root cause (three interacting defects):

1. `src/middleware/auth.ts` `softAuth`: an expired/revoked `glct_`/`glc_`
   bearer was silently downgraded to **anonymous** — nothing recorded that a
   token was presented and rejected.
2. `src/routes/mcp.ts`: the 401 challenge omitted RFC 6750
   `error="invalid_token"`, so clients could not distinguish "token died →
   refresh" from "never authenticated → first-time auth". The refresh_token
   grant (fully implemented, with rotation) sat unused.
3. The MCP handshake (`initialize`/`tools/list`/`ping`) was fully open, so a
   client reconnect **falsely succeeded** on a dead token, then looped on
   `tools/call`.

Fix (`b5a7bb3`, locked-file edit to `auth.ts` owner-approved 2026-07-14):
`softAuth` now flags `bearerInvalid`; `/mcp` answers **every** method —
including the handshake — with `401` +
`WWW-Authenticate: Bearer error="invalid_token", …` when a dead bearer is
presented. Anonymous discovery/handshake unchanged; anonymous `tools/call`
still gets the connector sign-in challenge (deliberately without `error=`).

Live verification (2026-07-14, prod):

- ✅ dead `glct_` on `initialize` → 401 + `error="invalid_token"` (loop-breaker)
- ✅ dead `glct_` on `tools/list` → 401 + `error="invalid_token"`
- ✅ anonymous `tools/call` → 401 challenge with `resource_metadata=`, **no** `error=`
- ✅ anonymous `initialize` → 200 (discovery open)
- ✅ regression tests: 125 pass (mcp/oauth/well-known), 83 pass (mcp-write/git-push-auth/api-tokens/push)

## 2. Doctor run (scripts/doctor.ts, new — `bun run doctor`)

First run against `https://gluecron.com` (anonymous sections): **ALL GREEN**.

- ✅ **a. Endpoint smoke** — 15/15 (healthz, readyz, version, login/register/
  landing/explore/demo/pricing/status, api-v2 health, mcp discovery,
  manifest, sw, dxt)
- ✅ **b. OAuth provider** — 8/8: AS metadata advertises `refresh_token` +
  `registration_endpoint`; protected-resource metadata (root + /mcp); **DCR
  round-trip returns a client_id (201)**; token endpoint: garbage code →
  `invalid_grant`, garbage refresh → `invalid_grant`, bogus grant_type →
  `unsupported_grant_type`
- ✅ **c. MCP anonymous + RFC 6750** — 5/5: initialize 200; **live tools/list
  exactly matches the local 60-tool manifest (zero drift)**; anonymous
  tools/call challenge correct; dead-bearer invalid_token on both
  handshake methods
- ⬜ **d. Authenticated MCP reads** — awaits `GLUECRON_PAT`
- ⬜ **e. Read-only tool matrix (--full)** — awaits `GLUECRON_PAT`

## 3. External-platform PR journey (scripts/agent-journey.ts, new)

⬜ Awaits an admin-scope PAT. Simulates exactly what an external platform
does: PAT identity → scratch repo → git push over smart HTTP with token
basic-auth → `gluecron_create_branch`/`write_file`/`create_pr`/`comment_pr`
→ AI-review poll → `gluecron_merge_pr` + verify → activity feed →
guaranteed cleanup. Hard-guarded against ever touching the self-host repo.

## 4. Self-canonical / server-side state (verified on-box 2026-07-14)

- ✅ Deploys are cord-free from GitHub: `/opt/gluecron` `origin` = the
  box-local canonical bare repo (docker volume), NOT github.com. Pushes to
  `gluecron.com/ccantynz/Gluecron.com.git` main went live in ≤ ~2 min twice
  today (`b5a7bb3`, `2cecc08`).
- ✅ `gluecron-update.timer` firing every 60s.
- ✅ `gluecron-backup.timer` daily; last run 2026-07-14 00:00 UTC, exit 0,
  4.4M Neon dump.
- ✅ `gluecron-restore-drill.timer` weekly (first run Sun 2026-07-19 04:00 UTC).
- ⬜ **Offsite backup NOT yet armed** — dumps stay on the VPS. Staged
  operator command appends `BACKUP_SCP_TARGET=root@100.89.227.39:/opt/backups/gluecron`
  (vapron-158) to `/opt/gluecron/.env` (with `.env` backup first) and
  fires a verification run. jarvis→vapron root SSH verified working.
- ⚠️ GitHub (`ccantynz-alt/Gluecron.com`) remains the emergency deploy
  source per CUTOVER_RUNBOOK Phase 4 (needs 7 green days + armed offsite
  before retirement). Do not archive yet.

## 5. How external platforms onboard (no PAT required)

1. `GET /.well-known/oauth-authorization-server` — discovery
2. `POST /oauth/register` — RFC 7591 DCR (verified live)
3. Authorization-code + PKCE → `POST /oauth/token` (1h access / 30d rotating refresh)
4. MCP at `POST /mcp`; expired tokens now get `error="invalid_token"` →
   client refreshes transparently
5. Git over smart HTTP with `Basic <user>:<token>` (PAT) or the web/API flows

## 6. Open items / known gaps

- ⬜ Run doctor sections d/e + agent-journey with an admin PAT (blocked on
  operator minting one; classifier reserves prod-DB writes for the operator).
- ⬜ Arm offsite backup (staged command above; operator-confirmed destination).
- ⚠️ `src/middleware/api-auth.ts` (API v2): 401s invalid bearers but sends no
  `WWW-Authenticate` header and rejects `glct_` OAuth tokens as unknown —
  additive parity pass recommended (out of scope today).
- ⚠️ `/api/version` reports `sha: "unknown"` on the compose/auto-update
  deploy path — BUILD_SHA pin only happens via self-deploy.sh; staleness
  checks rely on `builtAt`. Worth stamping in the docker build.
- ⚠️ DCR leaves `doctor-*` client rows (RFC 7592 delete not implemented) —
  cosmetic residue from each doctor run.
- ⚠️ No MCP create-repo tool — journey uses `POST /api/v2/repos`; consider
  adding `gluecron_create_repo` for full MCP-only agent flows.

## Session evidence

- Commits: `b5a7bb3` (fix + tests), `2cecc08` (doctor + journey + npm script).
- Fix verified live with curl transcripts (§1); doctor output (§2) from
  `bun scripts/doctor.ts` at 2026-07-14 ~02:1x UTC.
