PayCargo · AI-Sandbox · Step 6.v

6.vConsole MCP — recyclable machine tokens for scheduled evaluators

A Console-hosted, multiplexed MCP endpoint that lets a scheduled (unattended) Claude Code act on cartridges — read their grounding doctrine, publish evaluations — authenticated by a recyclable machine token, not an interactive login.
Block: 6.v (Console platform service) · State: Live in AI-Sandbox (M1–M3) · Code: shared/modules/mcp/, console/lambda/mcp-tools.js, console/mcp/
Shipped
The problem: a scheduled review job on an operator's machine needs to reach the platform, but interactive OAuth (browser sign-in, refresh tokens) is fragile for unattended cron. The answer is a recyclable machine token — the same pattern /build-manager uses — verified by HMAC, gating a small multiplexed tool set. One endpoint, many cartridges, least-privilege boundaries.

Contents

  1. Why a machine token, not OAuth
  2. The auth: HMAC over a canonical request
  3. The multiplexer: boundaries and targets
  4. Token lifecycle (mint / rotate / revoke)
  5. The local client + one-paste setup
  6. The whole loop

1. Why a machine token, not OAuth

Per-user OAuth (Doctrine 4) is right for a signed-in human: the Console owns the connect/refresh flow and a cartridge acts as that user. A scheduled evaluator has no human at the keyboard — interactive sign-in and token refresh are exactly the fragile parts you don't want in a nightly cron. So this is machine-to-machine: a recyclable machine token the operator mints in the Console, stores on the scheduling machine, and rotates or revokes at will. It keeps the operator's repo access on their own desktop; only the token reaches the platform, and the token can only do what its boundaries allow.

2. The auth — HMAC over a canonical request

The endpoint is POST /api/mcp on the Console, checked before the Cognito JWT gate and behind CloudFront's X-Origin-Secret. A token is tokenId + secret; the secret is encrypted at rest (AES-256-GCM under a master key) — reversibly, so the server can recompute the HMAC, not a one-way hash. The client signs each request:

signature = HMAC-SHA256( secret, "METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY" ) headers: x-mcp-token, x-mcp-timestamp, x-mcp-nonce, x-mcp-signature, x-mcp-target replay window: ±300s TLS carries confidentiality

The server recomputes the HMAC from the stored secret and compares in constant time (timingSafeEqual). The pure auth decision (authorizeRecord) lives in shared/modules/mcp/crypto.js and is unit-tested; the DDB lookup is thin around it. Tokens live in a single-table store ai-sandbox-console-mcp-tokens (pk MTK#<tokenId>, plus a by-cartridge GSI for listing).

3. The multiplexer — boundaries and targets

One endpoint exposes a boundary-gated tool registry (shared/modules/mcp/server.js + console/lambda/mcp-tools.js). A token is granted one or more boundaries; the multiplexer only surfaces the tools whose boundary the token holds — least privilege, enforced at tools/list time.

BoundaryToolsDoes
groundinggrounding_manifest, grounding_getlist a cartridge's CLAUDE.md/skills.md with sha256, then fetch a changed one (read-only)
publishpublish_evaluation, delete_evaluationfile an HTML report into a Doc Vault project through the same discipline gate as a human upload (write); and soft-delete (hide) a report as cleanup for a mistaken publish — nothing is purged (the file is tombstoned, restorable), and the target cartridge's operator can disable this cleanup from the cartridge (off by default)
evaluatelist_evaluations, read_evaluationread prior evaluations for the target cartridge's Doc Vault project — list them, then read a version's HTML (read-only, via Doc Vault system-list / system-read). Lets a recurring evaluator do delta reviews. Opt-in in the mint form.

Two meta-tools, list_targets and switch_target, use boundary '*' (always available). A token's targets are the cartridges it may act on (default its home cartridge); switch_target sets the active one, and the grounding tools resolve and enforce membership — the active target rides an unsigned x-mcp-target header, but the server refuses any target outside the token's set, so there is no escalation.

4. Token lifecycle

Managed in the Console's MCP Tokens pane, permission-scoped like the Content CMS: admins manage every token; a user with access to cartridge X manages X's tokens. The secret is shown once at mint (and on rotate) — the store keeps only the encrypted form.

ActionRouteEffect
MintPOST /api/mcp/tokensnew tokenId + secret; pick cartridge, boundaries, target cartridges
RotatePOST /api/mcp/tokens/{id}/rotatenew secret, same id; old secret stops immediately
RevokePOST /api/mcp/tokens/{id}/revokeactive=false; stops immediately
ListGET /api/mcp/tokens[?cartridge=]public view (never the secret) + which cartridges the caller may manage

Scope key

A token carries a scope that defaults to the cartridge-wide _ (the same convention as the Content CMS keys). The id charset allows a leading underscore precisely so _ validates.

5. The local client + one-paste setup

console/mcp/client/pc-mcp-client.mjs is a zero-dependency (Node 18+) stdio↔HTTPS bridge that Claude Code launches: it speaks MCP over stdio to the client and forwards each tool call as an HMAC-signed HTTPS request to /api/mcp. The client is served from the Console origin, so mint/rotate render a single copy-paste block that needs no repo checkout:

mkdir -p ~/.pc-mcp
curl -fsSL <console>/pc-mcp-client.mjs -o ~/.pc-mcp/pc-mcp-client.mjs
cat > ~/.pc-mcp/config.json <<'JSON'
{ "endpoint": "<console>/api/mcp", "tokenId": "mtk_…", "secret": "…" }
JSON
chmod 600 ~/.pc-mcp/config.json
claude mcp add paycargo -- node ~/.pc-mcp/pc-mcp-client.mjs --config ~/.pc-mcp/config.json

A collapsible "How to set up a scheduled evaluator" guide is built into the MCP Tokens pane, and a cartridge-scoped version lives inside the Foundation Audit cartridge.

6. The whole loop

Console (mint token: cartridge=foundation-audit, boundaries=grounding+publish) │ one-paste setup ▼ operator machine: cron → claude -p "review-prompt" │ MCP over stdio → pc-mcp-client → HMAC HTTPS ▼ Console /api/mcp ├─ grounding_manifest / grounding_get → current CLAUDE.md / skills.md (Content CMS) └─ publish_evaluation → Doc Vault (system-publish, discipline gate)

The separation goal

Repo access and compute stay on the operator's machine; the platform only ever sees a scoped, revocable token and the finished report. The token can read doctrine and file a review — nothing else.

See also: 6.s Content CMS (the grounding docs the tools read) · 6.w Foundation Audit (the first real consumer) · 6.u Doc Vault (where evaluations are published).