Skip to content
Version: Current

Consumer launch spec

Status: draft, not applied (observed 2026-11-05 in this repository). This page records a specification, not an applied deployment. Nothing in the site configuration, the control plane or any service was changed while writing it. The consumer plane now exists in the binary; the public exposure described here is the plan that governs it.

Scope: publish the acumen consumer plane on https://ai.tcp.systems without widening the operator surface. Decision required before implementation: proxy authorization, DNS posture, and admin-key rotation.

Make the invite-only consumer surface reachable on the public origin while:

  1. keeping the machine API (/v1/*) and /healthz behaviour unchanged;
  2. keeping every operator surface absent from the public host: a 404 at the edge, not a 401 and not an upstream error page;
  3. moving the console’s injected admin credential out of an inline site-file literal into a rotated, file-based reference;
  4. making session cookies unambiguously Secure behind TLS;
  5. putting a bounded rate and abuse posture in front of public sign-in and invite acceptance before the routes are exposed;
  6. shipping a rollout and a rollback that can be executed from the host without guessing.
  • No new product features, self-registration, payments, email delivery, OIDC or passkeys in this change.
  • No database schema migration.
  • No change to the control plane, DNS, or the acumen listener (127.0.0.1:4020).
  • No proxy plugin or custom build in the default plan.
  • No * host allowlist, no disabling key enforcement (ACUMEN_REQUIRE_KEYS must stay enabled), no public :4020.
Fact Evidence
The edge 404s every consumer path /, /signin, /app → 404; /healthz → 200; /v1/models → 401
The consumer plane works on loopback / 200, /signin 200, /app 303, /account 303, /i/x 410, /favicon.ico 204, /robots.txt 200
Proxy has no rate-limit module version 2.6.2, module list checked
Proposed directives validate request_body { max_size 256KB }, flush_interval -1, path lists and ?Header defaults all accepted
acumen is loopback with keys armed acumen.service, 127.0.0.1:4020, env carries ACUMEN_REQUIRE_KEYS=1 and the allowed host
Disk and running config had drifted the running config held a working admin token while the on-disk file held a stale literal; a reload would have replaced the working value with the broken one
Console was publicly reachable public :8088 answered 200 for the console and key API; no source allowlist on the listener
browser ──TLS──► proxy (ai.tcp.systems) ──► 127.0.0.1:4020
├─ /healthz, /v1/* → acumen (keys still enforced)
├─ /, /i/*, /signin, /app/*,
│ /account, /favicon.ico,
│ /robots.txt → acumen (session cookie)
└─ /console*, /metrics, /health,
/readyz, /v1/router/*,
/debug/* → 404 (empty body)

The listener stays loopback with no process bearer: a browser cannot send one, and embedding it in a page would hand it to every visitor. TLS and policy live at the edge.

The order is the safety property. Each step is verifiable before the next.

Copy the running proxy config, the on-disk site files and a key inventory to a timestamped, mode 700 backup directory. Diff running against disk and record the difference.

Move the console credential out of the site file

Section titled “Move the console credential out of the site file”

Create a 0640 token file owned by the proxy group, imported by the console site. The site file keeps no credential literal.

Pin the console listener to the tailnet address and add a remote_ip fallback that answers 404 off-tailnet. Confirm the public port refuses before continuing.

Terminal window
caddy fmt --overwrite <staged files>
caddy validate --config <staged main> --adapter caddyfile
caddy validate --config <staged console> --adapter caddyfile

Validating the files separately does not prove the combination, because the main file imports the console site. Install the console file first, then validate the combined configuration.

Install, systemctl reload caddy, check systemctl is-active. A failed reload leaves the old configuration running. Read the journal before declaring success.

Verify the public plane and the closed paths

Section titled “Verify the public plane and the closed paths”

Consumer paths answer 200/303/410/204 as expected; every operator path answers 404 with no upstream markers; the tailnet console answers 200; the public console port refuses; ss shows the tailnet address, not *; /v1/models without a key answers 401.

Disable the old admin key first; rollback can re-enable it without minting a new one, then delete it after the agreed soak (24 h by default).

  • Proxy: restore the pre-change main and console files from the backup directory, caddy validate, systemctl reload caddy. The pre-bind copy is the safe one; the pre-rotation copy contains a revoked token and must be paired with a live key.
  • Keys: re-enable the old key if it was only disabled. If it was deleted, point the console at another live admin key.
  • Consumer routes: removing the @consumer handler from the public site returns the edge to 404 for those paths without touching the API, the listener or the database.
Path Change site
internal/consumer/consumer.go routes, cookie, limiter
internal/consumer/acceptance_test.go invite → session → key → revoke
internal/consumer/templates.go inline HTML, no external assets
internal/server/keys.go key auth, scopes, CRUD
internal/server/middleware.go Host/Origin guard, baseline headers
cmd/acumen/invite.go invite bootstrap
tools/edge-check.sh operator-path assertions
tools/consumer-check.sh public consumer assertions (new)

Style rules for the change: stdlib only, no new module for rate limiting; net/netip for IP canonicalisation; a mutex plus a fixed window or token bucket; table-driven tests; never log tokens, cookies, recovery codes, invite URLs or raw emails; hash client IPs in logs.

Area Must pass
Public pages / 200, /signin 200, /app 303, /account 303, /i/invalid 410, /favicon.ico 204, /robots.txt 200
API unchanged /healthz 200; /v1/models 401 without key, 200 with a chat key; one real completion 200
Operator 404 /console*, /metrics, /health, /readyz, /v1/router/*, /debug/* → 404, empty body, no X-Acumen-*
Console tailnet 200; public port never 200; listener pinned; new admin key works; old key disabled then deleted
Cookie HttpOnly, Secure, SameSite=Lax, Path=/, no Domain; HTTPS only; clearing cookie Max-Age<=0
Invite CLI bootstrap 200; accept 200; recovery code once; reuse and expiry 410
Abuse per-IP limits 429 with Retry-After; a second IP unaffected; 300 KB POST changes no state; /healthz never 429
Authz non-operator 403 on /app/people and /app/invites; chat key cannot open /app or operator paths; CSRF enforced
Ops caddy validate clean; tools/gate.sh --race green; edge and consumer checks green; rollback rehearsed; ledger updated
  1. Proxy authorization. Who authorizes editing the site files and reloading the proxy? Is a maintenance window required, or is a graceful reload acceptable?
  2. Public console containment. Is the bind plus remote_ip fix authorized in the same window as the launch, or held until it is fixed separately? It is a blocker, not a nice-to-have.
  3. DNS posture. Direct A record or Cloudflare in front for WAF and rate limiting? Cloudflare in front needs an origin firewall, trusted proxies, real-client-IP handling and a TLS-mode design. Is this the final consumer origin, or will it move; cookies are host-only?
  4. Rate limiting. App-level middleware and the proposed thresholds, a custom proxy build with a rate-limit module, or Cloudflare? Who owns tuning and the on-call response to 429 spikes?
  5. Invite policy. Who may be invited, how many active invites per operator, default and maximum TTL, and what happens on abuse. Is there a terms and privacy page, a support address, and a retention policy for public accounts?
  6. /readyz monitoring. May it leave the public edge? If an external uptime check depends on it, switch the check to /healthz.
  7. Admin key custody. Who holds the console admin key and the break-glass copy, and where is the offline copy stored?
  8. Release pin. Which commit is the release of record for any rebuild?
  9. Staging. Is there a disposable instance and database for invite acceptance, rate-limit and rollback tests, or is production the only environment?
  10. Legal and abuse. Is an invite-only public launch approved under the current terms, who is the abuse contact, and what is the freeze procedure?