{"owner":"mnfst","repo":"manifest","hasSkills":true,"totalSkillsCount":7,"totalTokensCount":19705,"categories":["anthropic-skill","root-instruction","claude-rule","plugin-manifest","cursor-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/mnfst-cli/SKILL.md","AGENTS.md","CLAUDE.md","packages/backend/src/routing/autofix/contract/phoenix-openapi.yaml","packages/cli/README.md","packages/manifest/README.md",".cursorrules (manifest-yaml-cursorrules-prompt-file.mdc)"],"skills":{".claude/skills/mnfst-cli/SKILL.md":"---\nname: mnfst-cli\ndescription: Use when creating or operating AI agents that make LLM calls — provisioning model routing and keys, wiring an app or automation to an LLM endpoint, testing routes, rotating leaked keys, or investigating LLM request failures and costs on a Manifest install.\n---\n\n# Operating Manifest from the CLI\n\n## Overview\n\n`mnfst` manages Manifest, the LLM gateway. Every call routed through a Manifest agent gets model routing with fallbacks, per-agent cost attribution, self-healing (Auto-fix), and a request log — for free. Default practice: never hand an app or automation a raw provider key; give it a Manifest agent.\n\nCommands emit JSON on stdout (human hints on stderr) — except `agent env`, which prints dotenv lines by design — exit 0/1, and never block on prompts when credentials come from flags or env. Full surface: `mnfst --help`.\n\n## The flow\n\n| Step | Command |\n|---|---|\n| Auth (human) | `mnfst login` — browser, one click, 30-day sliding token |\n| Auth (script/agent) | env `MANIFEST_URL` + `MANIFEST_API_KEY` — no login step |\n| Provision | `mnfst agent create --name X --platform <p> --if-absent` → response includes `setup` (the platform's config block) |\n| Check connections | `mnfst provider list [--agent X]` FIRST — the provider you need is often already connected. A connection is only usable if `cached_model_count` > 0; `is_active: true` with 0 models is hollow — run `mnfst provider refresh [<provider>]` (re-runs discovery tenant-wide, prints the new counts); still 0 means a bad credential, so reconnect, don't route on it |\n| Connect provider | `mnfst provider connect xai --auth-type api_key --credential-env KEY` · `provider catalog` lists all 30+ with auth types · subscription auth opens a browser (impossible headless — reuse an existing subscription connection instead) · own gateway: `provider custom add --name gw --endpoint <url>` |\n| Route | `mnfst agent configure X --models primary,fb1,fb2 --provider p [--auth-type a]` — first model is the route, the rest are fallbacks, and the whole chain rides the one `--provider` you name (cross-provider fallbacks are dashboard-only) · add `--tier deep` to upsert a custom tier callers select per-request with header `x-manifest-tier: deep` |\n| Verify | `mnfst routing test X` — one real request through the platform's actual API surface; broken routes fail loudly. It writes real rows to the request log — count them in later audits |\n| Wire the app | deployed → `mnfst agent env X >> .env` (one .env per service — the lines always name `MANIFEST_AGENT_KEY`, so appending two agents to one file silently keeps only the loader's winner; for several agents in one process use `mnfst run --agent X --env ROLE_KEY -- <cmd>`) |\n| Observe | `mnfst requests get --agent X [--status failed]` — paginated; pass `next_cursor` back for the next page |\n\nNamespace = scope: `provider *` acts tenant-wide, `agent *` acts on one agent, `routing *` holds readouts and custom-tier lifecycle.\n\n## Gotchas `--help` cannot tell you\n\n| Symptom | Reality |\n|---|---|\n| Proxy returns HTTP 200 but the \"answer\" starts with `[🦚 Manifest M###]` | An error wearing an assistant costume — check the content, not the status. `routing test` unmasks these automatically |\n| `cost: \"0.000000\"` on subscription-auth traffic | Flat-rate subscription: per-request cost is genuinely zero, not broken billing |\n| `agent configure`'s echo differs from later reads | Mutation bodies echo raw API rows — the canonical readback is `routing status <agent>` |\n| \"Which requests used a fallback or Auto-fix?\" | Those fields are trimmed from default output — `requests get --full` |\n| 401s missing from the request log | Rejected-auth attempts are never recorded (no tenant to attribute them to); a leaked key's blast radius cannot be proven from the log |\n| `MANIFEST_AGENT_URL` | Already ends in `/v1` — append `/chat/completions` directly |\n| Model prices needed before any agent exists | `mnfst model prices [--provider <p>]` — install-wide, no agent needed (`models <agent>` is the per-agent routable set) |\n| Auditing escalation traffic by `routing_tier` | Custom-tier requests log `routing_tier: \"standard\"` — the tier identity lives in `header_tier_name` |\n| Route resolves but requests fail on one connection | A connection can be `is_active: false` while a sibling auth-type works — pass `--auth-type` explicitly when a provider has several connections |\n| Every command answers `Invalid API key — Run mnfst login` under env auth | Not always an auth problem: a wrong `MANIFEST_URL` (dead or different install) answers exactly like a bad key. Run `mnfst doctor` — it checks config, host, credential, connections and agents in that order, so it separates \"host is dead\" from \"key is wrong for this host\" — and never tells you to re-login on an env key |\n| Need a different reasoning effort, temperature, or other model param without editing the app | `mnfst routing params get X [--tier t] [--model m]` lists what the routed model accepts and what is saved; `mnfst routing params set X [--tier t] [--model m] --set reasoning.effort=high [--unset temperature]` saves it. Saved params **override** what the app sends, per tier and per model (a fallback keeps its own), but only on requests Manifest routes: `model: \"auto\"` or a matching custom-tier header. A request naming a concrete model skips them |\n| `agent configure` / `routing custom create` reject a model as undiscovered | Both validate against the agent's *discovered* models, and a connection with `cached_model_count: 0` contributes none: run `mnfst provider refresh` (stale or empty catalog), or pass `--force` — the backend still routes uncatalogued models through provider-qualified passthrough |\n\n## Common mistakes\n\n- Handing an app a raw provider key — it loses fallbacks, cost attribution, healing, and the log. Create an agent and use `agent env`.\n- Verifying a route with hand-rolled `curl` — fake-200s read as success. Use `routing test`.\n- Slurping the whole request log — pagination is one page per call by design; loop on `next_cursor`.\n\nAlso on the surface when needed: `mnfst doctor` (first stop when anything is wrong), `agent setup <name> [--reveal]` (setup block anytime; key masked unless revealed), `agent key path|show`, `agent provider enable|disable <agent> <provider>`, `agent rotate-key` (revocation is instant), `mnfst models <agent> --cost --capabilities`.\n","AGENTS.md":"# Manifest Agent Guidelines\n\n## Domain Terminology\n\nManifest terminology is directional:\n\n- A **Manifest Request** is one logical request from an agent to Manifest and lives in `requests`.\n- A **Provider Attempt** is one request from Manifest to an AI provider and lives in `agent_messages`.\n- A **Tenant** is a user's data boundary. It is created from `user.id` on first agent creation.\n- An **Agent** is an AI agent owned by a tenant. The dashboard labels agents **Harnesses** (nav under `/harnesses`; legacy `/agents/*` URLs redirect) — this is UI copy only; backend code, database tables, and API routes still say *agent*.\n\n[`docs/glossary.md`](docs/glossary.md) is the canonical contract for statuses, ordering, recovery, database mapping, and counting rules. Do not duplicate those definitions in agent guides.\n","CLAUDE.md":"# Manifest Development Guidelines\n\nLast updated: 2026-09-14\n\n## What Manifest Is\n\nManifest is a smart model router for **AI agents**. It sits between an agent and its LLM providers, scores each request, and routes it to the cheapest model that can handle it. The dashboard tracks logical requests and their provider attempts, costs, and tokens across any agent that speaks OpenAI-compatible HTTP.\n\n**\"Harness\" is the dashboard word for an agent.** The UI now labels agents **Harnesses** (nav item \"Harnesses\", routes under `/harnesses`, categories `AI agent` / `Automation` / `App AI SDK` / `Coding Assistant` from `CATEGORY_LABELS` in `packages/shared/src/agent-type.ts`). This is a **copy-level rename only**: backend code, database tables (`agents`, `agent_messages`, …), API routes (`/api/v1/agents/*`), and entity/service names all still say *agent*. Legacy `/agents/*` dashboard URLs redirect to `/harnesses/*`. When writing UI copy say \"harness\"; when writing code or API docs keep \"agent\".\n\n**Product note:** this repo is **Manifest LLM Gateway**, the open-source LLM gateway of Manifest, available and maintained. The dashboard and the READMEs carry no promotion of any other Manifest product: not the self-healing layer at `dashboard.manifest.build` (on hold), and no API Bot banner or sidebar card (removed). `POST /api/v1/waitlist/pivot/claim` stays only for self-hosted versions that still ship the old sidebar card. The sidebar's \"Custom development\" button opens `ServicesContactModal`, which posts to `POST /api/v1/services/contact`; the backend relays it to `https://manifest.build/api/contact` (mnfst/website `api/contact.js`), which emails the team. Nothing is stored.\n\n**Supported agents**: see `AGENT_PLATFORMS` in `packages/shared/src/agent-type.ts` for the current list (OpenClaw, Hermes, Claude Code, OpenCode, generic OpenAI/Anthropic SDK slots, and others — don't duplicate the list here, it grows independently of this doc). OpenClaw remains the deepest integration, but no new code or copy should frame Manifest as OpenClaw-only. When adding examples, prefer \"AI agent\" as the noun and pick OpenClaw as the worked example rather than the sole target. Manifest is consumed as a generic OpenAI-compatible HTTP endpoint — there are no first-party OpenClaw plugins in this repo anymore.\n\nWingman — the gateway tester for sending requests against a Manifest backend while impersonating any of the supported agents (useful for routing/header-classifier reproductions) — lives in its own repo at [`mnfst/wingman`](https://github.com/mnfst/wingman) and is hosted at [`wingman.manifest.build`](https://wingman.manifest.build). The dashboard embeds it as an iframe drawer **in dev mode only** — it is dead-code-eliminated from production / self-hosted bundles via `__DEV_MODE__`. The backend allows the hosted Wingman origin through CORS in both dev and production (production also honors `WINGMAN_CORS_ORIGINS`), while the CSP `frame-src` that permits the drawer iframe stays dev-only; both are wired in `packages/backend/src/cors-csp-config.ts`.\n\n**Whenever working in dev mode (`/serve`, `npm run dev`, etc.), the Wingman drawer is expected to be available** — open the FAB at the bottom-right of the dashboard (or hit ⌘/Ctrl+Shift+W) and confirm the iframe loads `https://wingman.manifest.build` cleanly. The drawer is part of the dev surface area, so a broken iframe means the dev environment is broken. `/serve` is **dev-only** — never use it to validate production behavior.\n\n**Three management surfaces share one REST API.** The dashboard, the `mnfst` CLI (`packages/cli/`), and the remote MCP server (`packages/backend/src/mcp/`) all drive the same `/api/v1/*` services with the same tenant scoping. Add a capability to the service layer once, then expose it on whichever surfaces need it; never fork logic per surface. See [CLI login](#cli-login-mnfst-login) and [Remote MCP server](#remote-mcp-server) below.\n\n## IMPORTANT: Cloud Mode Always\n\nWhen starting the app for development or testing (e.g. `/serve`), **always use `MANIFEST_MODE=cloud`** (the default). Every dev session must use a **fresh PostgreSQL database** via Docker — multiple concurrent dev instances sharing one DB cause cross-run data pollution and intermittent test failures:\n\n```bash\n# 1. Ensure the postgres_db container is running\ndocker start postgres_db 2>/dev/null || \\\n  docker run -d --name postgres_db -e POSTGRES_USER=myuser -e POSTGRES_PASSWORD=mypassword -e POSTGRES_DB=mydatabase -p 5432:5432 postgres:16\n\n# 2. Create a pristine database with a unique name\nDB_NAME=\"manifest_$(openssl rand -hex 4)\"\ndocker exec postgres_db psql -U myuser -d postgres -c \"CREATE DATABASE $DB_NAME;\"\n\n# 3. Update DATABASE_URL in packages/backend/.env to use the new database\n# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/$DB_NAME\n\n# 4. Ensure SEED_DATA=true in .env so the database is populated on startup\n```\n\nThis guarantees each session starts with a clean, isolated database and avoids all cross-instance conflicts.\n\n## Testing OpenClaw Integration\n\nTo test routing from an OpenClaw agent against a local Manifest dev server, point OpenClaw at the dev server's OpenAI-compatible proxy directly — there is no plugin anymore:\n\n```bash\n# 1. Build and start the backend in cloud mode\nnpm run build\nPORT=38238 BIND_ADDRESS=127.0.0.1 \\\n  node -r dotenv/config packages/backend/dist/main.js\n\n# 2. Configure OpenClaw to use the dev server as a generic OpenAI-compatible provider\nopenclaw config set models.providers.manifest '{\"baseUrl\":\"http://localhost:38238/v1\",\"api\":\"openai-completions\",\"apiKey\":\"mnfst_YOUR_KEY\",\"models\":[{\"id\":\"auto\",\"name\":\"Manifest Auto\"}]}'\nopenclaw config set agents.defaults.model.primary manifest/auto\n\n# 3. Restart the gateway\nopenclaw gateway restart\n```\n\nThe `AgentKeyAuthGuard` accepts any non-`mnfst_*` token from loopback IPs in the self-hosted version, so loopback-only testing works even without a valid key. After restarting the backend, also restart the OpenClaw gateway — it doesn't reconnect automatically.\n\n## Active Technologies\n\n- **Backend**: NestJS 11, TypeORM 0.3, PostgreSQL 16, Better Auth, class-validator, class-transformer, Helmet\n- **Frontend**: SolidJS, Vite, uPlot (charts), Better Auth client, custom CSS theme\n- **Runtime**: TypeScript 5.x (strict mode). CI pins Node.js 24 (`.github/workflows/release.yml`); no `engines` field enforces this locally.\n- **Monorepo**: npm workspaces + Turborepo\n- **Release**: Changesets for version management + GitHub Actions for Docker image release\n\n## Project Structure\n\n```text\npackages/\n├── backend/\n│   ├── src/\n│   │   ├── instrument.ts                    # Sentry init, imported first (before any other import)\n│   │   ├── main.ts                          # Bootstrap: Helmet, ValidationPipe, Better Auth mount, CORS\n│   │   ├── app.module.ts                    # Root module (guards: ApiKey, Session, Throttler)\n│   │   ├── config/app.config.ts             # Environment variable config\n│   │   ├── auth/\n│   │   │   ├── auth.instance.ts             # Better Auth singleton (email/pass + 3 OAuth + jwt/mcp plugins)\n│   │   │   ├── auth.module.ts               # Registers SessionGuard as APP_GUARD\n│   │   │   ├── session.guard.ts             # Cookie session auth via Better Auth\n│   │   │   ├── current-user.decorator.ts    # @CurrentUser() param decorator\n│   │   │   ├── cli-auth.controller.ts, cli-auth.service.ts # Browser login for the mnfst CLI (PKCE code → `cli` PAT)\n│   │   │   ├── me.controller.ts             # GET /api/v1/me identity probe (tenant, user, auth method, expiry)\n│   │   │   └── mcp-scopes.ts                # mcp:read / mcp:write scope constants (leaf module, no Better Auth import)\n│   │   ├── database/\n│   │   │   ├── database.module.ts           # TypeORM PostgreSQL config\n│   │   │   ├── database-seeder.service.ts   # Seeds demo data (users, agents, security events)\n│   │   │   ├── datasource.ts               # CLI DataSource for migration commands\n│   │   │   ├── pricing-sync.service.ts      # OpenRouter pricing data sync\n│   │   │   ├── ollama-sync.service.ts       # Ollama model sync\n│   │   │   ├── quality-score.util.ts        # Model quality scoring\n│   │   │   └── seed-messages.ts             # Demo request/provider-attempt seed data\n│   │   ├── entities/                        # TypeORM entities (25 entities)\n│   │   │   ├── tenant.entity.ts             # Multi-tenant root\n│   │   │   ├── agent.entity.ts              # Agent (belongs to tenant)\n│   │   │   ├── agent-api-key.entity.ts      # OTLP ingest keys (mnfst_*)\n│   │   │   ├── cli-auth-code.entity.ts      # Short-lived PKCE codes for `mnfst login`\n│   │   │   └── ...                          # request, agent-message (provider attempt), tenant-provider, tier-assignment, header-tier, etc.\n│   │   ├── common/\n│   │   │   ├── guards/api-key.guard.ts      # X-API-Key header auth (timing-safe)\n│   │   │   ├── decorators/public.decorator.ts\n│   │   │   ├── dto/                         # create-agent, range-query, rename-agent DTOs\n│   │   │   ├── filters/spa-fallback.filter.ts\n│   │   │   ├── interceptors/               # agent-cache, user-cache\n│   │   │   ├── constants/                   # api-key, cache, ollama, providers, openai-models, xai-models, subscription-clients\n│   │   │   ├── services/                    # ingest-event-bus, manifest-runtime, tenant-cache, request-recording-storage\n│   │   │   └── utils/                       # crypto, hash, range, period, slugify, url-validation, provider-inference, postgres-sql, cost-calculator, detect-self-hosted, frontend-path, og-rewrite, secret-scrub, ttl-cache, local-ip, etc.\n│   │   ├── health/                          # @Public() health check\n│   │   ├── analytics/                       # Dashboard analytics\n│   │   │   ├── controllers/                 # overview, tokens, costs, messages, agents\n│   │   │   └── services/                    # aggregation + timeseries-queries + query-helpers\n│   │   ├── otlp/                            # Agent key auth + onboarding\n│   │   │   ├── guards/agent-key-auth.guard.ts # Bearer token auth (agent API keys)\n│   │   │   └── services/api-key.service.ts  # Agent onboarding (creates tenant+agent+key)\n│   │   ├── routing/                         # LLM routing (providers, tiers, proxy, scorer)\n│   │   │   ├── proxy/                       # OpenAI-compatible proxy (anthropic/google adapters, fallback chain, attempt recording, caller-classifier)\n│   │   │   ├── autofix/                     # Autofix self-healing (Phoenix client + heal-once flow)\n│   │   │   ├── routing-core/               # Tier, provider, specificity services + cache\n│   │   │   ├── resolve/                     # Scoring-based tier + specificity resolution\n│   │   │   ├── custom-provider/             # Custom provider CRUD (+ `alias`, the public model-id prefix in /v1/models)\n│   │   │   ├── header-tiers/               # Header-based tier overrides\n│   │   │   ├── oauth/                       # OAuth flows (Gemini, OpenAI, Kiro, MiniMax)\n│   │   │   └── specificity.controller.ts   # Specificity routing CRUD endpoints\n│   │   ├── scoring/                         # Request complexity scoring engine\n│   │   │   ├── keywords.ts                 # Keyword lists for all dimensions (complexity + specificity)\n│   │   │   ├── specificity-detector.ts     # Task-type detection (coding, trading, etc.)\n│   │   │   └── scan-messages.ts            # Message scanner for specificity detection\n│   │   ├── model-prices/                    # Model pricing management + sync\n│   │   ├── notifications/                   # Alert rules, email providers, cron\n│   │   ├── playground/                      # Prompt playground (runs, columns, starred/best)\n│   │   ├── github/                          # GitHub stars endpoint\n│   │   ├── sse/                             # Server-Sent Events for real-time updates\n│   │   ├── setup/                           # First-run admin setup wizard\n│   │   ├── free-models/                     # Free LLM model catalog\n│   │   ├── model-discovery/                 # Per-provider model fetching + fallback\n│   │   ├── billing/                         # Stripe billing status + plan limits\n│   │   ├── error-pages/                     # Custom error-page config (internal + public)\n│   │   ├── waitlist/                        # Pivot waiting-list claims + legacy Autofix claim compatibility route\n│   │   ├── discovery/                       # Self-hosted discovery onboarding (forwarded to Peacock)\n│   │   ├── crm-metrics/                     # Cloud-only internal feed: Autofix-healed cohort + waitlist claims + corporate signups (secret-gated)\n│   │   ├── cors-csp-config.ts               # Wingman CORS/CSP origin allowlists\n│   │   ├── sentry/                          # Sentry init-options builder (SENTRY_DSN-gated)\n│   │   ├── telemetry/                       # Anonymous self-hosted telemetry\n│   │   ├── version/                         # GET /api/v1/version — self-hosted \"new version available\" check\n│   │   └── mcp/                             # Remote MCP server (OAuth 2.1 bearer, stateless per-POST, tools/ per domain)\n│   └── test/                                # E2E tests (supertest)\n├── frontend/\n│   ├── src/\n│   │   ├── index.tsx                        # Router setup (App + AuthLayout)\n│   │   ├── components/\n│   │   │   ├── AuthGuard.tsx                # Session check, redirect to /login\n│   │   │   ├── GuestGuard.tsx               # Redirect authenticated users away from auth pages\n│   │   │   ├── SocialButtons.tsx            # 3 OAuth provider buttons\n│   │   │   ├── Header.tsx                   # User session data, logout\n│   │   │   ├── Sidebar.tsx                  # Navigation sidebar\n│   │   │   ├── SetupModal.tsx               # Agent setup wizard modal\n│   │   │   └── ...                          # Charts, modals, pagination, etc.\n│   │   ├── pages/\n│   │   │   ├── Login.tsx, Register.tsx       # Auth pages\n│   │   │   ├── ResetPassword.tsx            # Password reset flow\n│   │   │   ├── Workspace.tsx                # Harness grid/table at /harnesses + create harness\n│   │   │   ├── GlobalOverview.tsx, AgentOverview.tsx # Cross-harness + per-harness dashboards (split from one Overview.tsx)\n│   │   │   ├── AgentDetail.tsx, AgentProviders.tsx   # Per-harness detail shell (tabs) + provider connections\n│   │   │   ├── MessageLog.tsx               # Paginated Requests log (legacy filename)\n│   │   │   ├── Account.tsx                  # User profile (session data)\n│   │   │   ├── Settings.tsx, SettingsAutofixSection.tsx, SettingsRecordingSection.tsx # Harness settings + Autofix + request-recording toggles\n│   │   │   ├── Routing.tsx, RoutingPanels.tsx, RoutingActions.tsx, RoutingDefaultTierSection.tsx, RoutingHeaderTiersSection.tsx, RoutingSpecificitySection.tsx, RoutingTierCard.tsx # LLM routing config (split by concern)\n│   │   │   ├── Limits.tsx                   # Alert rule management — mounted at /harnesses/:agentName/guardrails (\"Guardrails\" tab)\n│   │   │   ├── ModelPrices.tsx              # Model pricing table\n│   │   │   ├── Playground.tsx               # Prompt playground\n│   │   │   ├── ConnectProvider.tsx          # Provider connection flow\n│   │   │   ├── providers/                   # Subscriptions.tsx, Byok.tsx, Local.tsx, ConnectionDetail.tsx, ProviderConnectionsPage.tsx (/providers/* pages)\n│   │   │   ├── Discovery.tsx                # Self-hosted discovery form (one-time, post-signup)\n│   │   │   ├── Welcome.tsx                  # Post-signup welcome flow (+ welcome-helpers.ts)\n│   │   │   ├── FreeModels.tsx               # Free model catalog\n│   │   │   ├── Setup.tsx                    # First-run setup wizard\n│   │   │   ├── Upgrade.tsx                  # Billing/plan upgrade page\n│   │   │   ├── Help.tsx                     # Help page\n│   │   │   ├── AgentLimitsRedirect.tsx, AgentMessagesRedirect.tsx # Legacy per-harness URL redirects\n│   │   │   ├── CliAuth.tsx                  # /cli/auth — approve a `mnfst login` browser request\n│   │   │   ├── Consent.tsx                  # /consent — OAuth consent screen for MCP clients\n│   │   │   └── NotFound.tsx                 # 404 page\n│   │   ├── services/\n│   │   │   ├── auth-client.ts               # Better Auth SolidJS client\n│   │   │   ├── api.ts                       # API functions (credentials: include)\n│   │   │   ├── providers.ts                 # ProviderDef list + SPECIFICITY_STAGES + STAGES\n│   │   │   ├── model-display.ts             # Model display-name cache\n│   │   │   ├── formatters.ts               # Number/cost formatting\n│   │   │   ├── provider-utils.ts            # LLM provider helpers\n│   │   │   ├── routing.ts, routing-utils.ts # Routing config helpers\n│   │   │   ├── theme.ts                     # Theme management\n│   │   │   ├── toast-store.ts               # Toast notification state\n│   │   │   └── ...                          # setup-status, playground-store, pagination, sse, oauth-popup, etc.\n│   │   ├── layouts/                         # Layout components\n│   │   └── styles/\n│   └── tests/\n├── shared/                           # Shared TypeScript types + helpers (consumed by backend and frontend)\n├── cli/                              # `mnfst` management CLI (private; bin/mnfst.js, commands/, generated provider catalog + skill content)\n└── manifest/                         # Code-free shell package holding the canonical Manifest version (see Releases)\n```\n\nBeyond `packages/`, the repo root also carries:\n\n- `integrations/n8n-nodes-manifest/` — the published n8n community node (`n8n-nodes-manifest` on npm); root-level `nodes/` and `credentials/` are identical mirrors of its sources for n8n's community-node scanner.\n- `contracts/gateway/` — gateway API contract tests.\n- `design-system/` — machine-readable design registry (generated `registry/components.json` + `registry/llms.txt`; generator `generate-registry.mjs`).\n- `deploy/` — one-click deployment templates (apple-containers, aws, coolify, digitalocean, easypanel, fly, gcp, heroku, koyeb).\n- `docs/` — `glossary.md` (canonical terminology contract), `autofix-self-healing-poc.md`, `model-parameters-schema.md`, `providers/subscription-based-providers.md`.\n\n## Single-Service Deployment\n\nThe app deploys as a **single service**. In production, NestJS serves both the API and the frontend static files from the same port.\n\n```bash\nnpm run build     # Turborepo: frontend (Vite) then backend (Nest)\nnpm start         # node packages/backend/dist/main.js — serves frontend + API\n```\n\n- API routes (`/api/*`, `/otlp/*`) are excluded from static file serving.\n- Dev mode: Vite on `:3000` proxies `/api` and `/otlp` to backend on `:3001`.\n- **Manifest Cloud** runs on Cloud Run (`manifest`, project `manifest-platform-457808`, `us-east4`) behind the shared Google HTTPS load balancer, with Cloud SQL Postgres on a private IP. Every push to `main` deploys through `.github/workflows/deploy.yml` (keyless Workload Identity Federation; `mnfst/*` on `main` only): it builds `docker/Dockerfile.cloud` (the self-hosted `docker/Dockerfile` minus `VITE_MANIFEST_SELFHOSTED`, keep both in step), runs the `manifest-migrate` job, then swaps the service image. Env vars, secrets (Secret Manager) and scaling live on the service; it sets `MANIFEST_MODE=cloud`, `RUN_MIGRATIONS_ON_BOOT=false`, `TRUST_PROXY=2` (load balancer + Cloud Run hops) and `SHUTDOWN_DRAIN_MS=0`. `railway.toml` is the old Railway config, kept until Railway is decommissioned.\n\n## Commands\n\n### Starting the Dev Server\n\nThe backend requires a `.env` file at `packages/backend/.env` with at least `BETTER_AUTH_SECRET` (32+ chars). The `auth.instance.ts` reads `process.env` at import time, before NestJS `ConfigModule` loads `.env`, so env vars must be available to the Node process.\n\n**Quick start (run these in parallel):**\n\n```bash\n# Backend — must preload dotenv since auth.instance.ts reads process.env at import time\ncd packages/backend && NODE_OPTIONS='-r dotenv/config' npx nest start --watch\n\n# Frontend\ncd packages/frontend && npx vite\n```\n\n**Note:** `npm run dev` (turbo) starts the frontend but NOT the backend, because the backend's script is `start:dev` not `dev`. Start the backend separately as shown above.\n\n### Seeding Dev Data\n\nSet `SEED_DATA=true` in `packages/backend/.env` to seed on startup (dev/test only). This creates:\n\n- **Admin user**: `admin@manifest.build` / `manifest` (email verification email is skipped if Mailgun is not configured — user is created but unverified)\n- **Tenant**: `seed-tenant-001` linked to the admin user\n- **Agent**: `demo-agent` with OTLP key `dev-otlp-key-001`\n- **API key**: `dev-api-key-manifest-001`\n- **Security events**: 12 sample events for the security dashboard\n- **Requests and provider attempts**: Sample telemetry for the demo agent\n\nSeeding is idempotent — it checks for existing records before inserting.\n\n**Dev-login shortcut:** when running under the Vite dev server the login page shows a\nprominent one-click **⚡ Sign in as dev** button that submits the seeded\n`admin@manifest.build` / `manifest` credentials — no copy-paste. It's gated by\n`import.meta.env.DEV`, so Vite strips the button and the credential literals from\nproduction builds, and no password ever rides in a URL. See\n`packages/frontend/src/pages/Login.tsx`.\n\n**Minimal `.env` for development:**\n\n```env\nPORT=3001\nBIND_ADDRESS=127.0.0.1\nNODE_ENV=development\nBETTER_AUTH_SECRET=<random-hex-64-chars>\nDATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase\nAPI_KEY=dev-api-key-12345\nSEED_DATA=true\n```\n\nGenerate a secret with: `openssl rand -hex 32`\n\n**Database naming convention:** Always create uniquely-named databases to avoid overlapping other dev/test instances. Use the pattern `manifest_<context>_<random>` (e.g., `manifest_sse_49821`, `manifest_dev_83712`). Create databases via Docker:\n\n```bash\ndocker exec postgres_db psql -U myuser -d postgres -c \"CREATE DATABASE manifest_<name>;\"\n```\n\nThen set `DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/manifest_<name>` in `.env`.\n\n```bash\n# Production build + start (single server)\nnpm run build && npm start\n\n# Tests\nnpm test --workspace=packages/backend          # Jest unit tests\nnpm run test:e2e --workspace=packages/backend  # Jest e2e tests\nnpm test --workspace=packages/frontend         # Vitest tests\nnpm test --workspace=packages/shared           # Jest unit tests\nnpm test --workspace=packages/cli              # Jest unit tests (mnfst CLI)\n```\n\n### Database Migrations\n\nTypeORM migrations run automatically on app startup by default (gated by `RUN_MIGRATIONS_ON_BOOT`, default `true`). Schema sync (`synchronize`) is permanently disabled — all schema changes must go through migrations.\n\n**Dev workflow:** modify entity → generate migration → commit both.\n\n```bash\n# Generate a migration after changing an entity\ncd packages/backend\nnpm run migration:generate -- src/database/migrations/DescriptiveName\n\n# Other migration commands\nnpm run migration:run       # Run pending migrations\nnpm run migration:revert    # Revert the last migration\nnpm run migration:show      # Show migration status ([X] = applied)\nnpm run migration:create -- src/database/migrations/Name  # Create empty migration\n```\n\nNew migrations must be imported in `database.module.ts` and added to the `migrations` array.\n\n**Important**: Always use unique timestamps for new migrations. Never reuse a timestamp from an existing migration file.\n\n## Authentication Architecture\n\n### Guard Chain\n\nThree global guards run on every request (order matters):\n\n1. **SessionGuard** (`auth/session.guard.ts`) — Checks `@Public()` first. If not public, validates the Better Auth cookie session via `auth.api.getSession()`. Attaches `request.user` and `request.session`.\n2. **ApiKeyGuard** (`common/guards/api-key.guard.ts`) — Falls through if session already set. Otherwise reads the `X-API-Key` header and first looks it up against the tenant-scoped `ApiKey` entity (`api_keys` table, hashed with scrypt) — this is the primary multi-tenant credential path. Only if no DB match is found does it fall back to a timing-safe compare against the single `API_KEY` env var. Use `@Public()` to skip both guards.\n3. **ThrottlerGuard** — Rate limiting.\n\n`mnfst` CLI tokens are ordinary `api_keys` rows named `cli` (`CLI_KEY_NAME` in `auth/cli-auth.service.ts`), so they ride the `ApiKeyGuard` path with no special casing; the guard also enforces their sliding and absolute TTLs. The remote MCP route is `@Public()` because it carries its own credential (an OAuth 2.1 bearer) — see [Remote MCP server](#remote-mcp-server).\n\n### Better Auth Setup\n\n- **Instance**: `auth/auth.instance.ts` — `betterAuth()` with `emailAndPassword` + 3 social providers (Google, GitHub, Discord). Each provider only activates when both `CLIENT_ID` and `CLIENT_SECRET` env vars are set. Plugins: `jwt()` and `@better-auth/mcp`'s `mcp()` (the OAuth 2.1 authorization server for the remote MCP endpoint, resource-bound to `${BETTER_AUTH_URL}/api/v1/mcp`, scopes from `auth/mcp-scopes.ts`).\n- **Mounting**: In `main.ts`, Better Auth is mounted as Express middleware at `/api/auth/*splat` **before** `express.json()` (it needs raw body control). NestJS body parsing is re-added after for all other routes.\n- **Frontend client**: `services/auth-client.ts` — `createAuthClient()` from `better-auth/solid`.\n- **Social login in dev**: OAuth callback URLs point to `:3001` (`BETTER_AUTH_URL`). Social login only works when accessing the app on port **3001** (production build), not on Vite's `:3000` dev server.\n\n### Auth Types\n\n```typescript\n// backend/src/auth/auth.instance.ts\nexport type AuthSession = typeof auth.$Infer.Session;\nexport type AuthUser = typeof auth.$Infer.Session.user;\n\n// Use in controllers:\n@Get('something')\nasync handler(@CurrentUser() user: AuthUser) {\n  // user.id, user.name, user.email\n}\n```\n\n### CLI login (`mnfst login`)\n\n`packages/cli/` is the `mnfst` management CLI: a thin wrapper over the `/api/v1` REST API (JSON on stdout, exit `0`/`1`, never interactive mid-task). One global tenant credential unlocks every command; per-agent `mnfst_*` keys are outputs only (written to `--key-file` paths with mode `0600`), never inputs. The package publishes to npm as `mnfst-gateway-cli` (`npm i -g mnfst-gateway-cli`, command still `mnfst`); from the monorepo, build with `npm run build --workspace=packages/shared && npm run build --workspace=packages/cli` and run `node packages/cli/bin/mnfst.js`. `packages/cli/README.md` is the user-facing reference; the repo also ships a `mnfst-cli` skill under `.claude/skills/`.\n\nThe default login is a browser flow, PKCE-protected so an intercepted code is useless without the verifier:\n\n1. The CLI opens the dashboard at `/cli/auth` (`pages/CliAuth.tsx`) with a `state` and an S256 `code_challenge`.\n2. The signed-in user approves; the page calls `POST /api/v1/cli/authorize` (session-only — an API key caller gets 403) and receives a short-lived code stored in `cli_auth_codes`.\n3. The CLI exchanges it at `POST /api/v1/cli/token` (public — the code is the credential, with `state` + `code_verifier`) for a `cli` PAT in `api_keys`; `expiresAt` is ISO-8601 UTC.\n4. `DELETE /api/v1/cli/token` revokes the calling PAT on `mnfst logout` (best-effort).\n\nNon-interactive alternatives: `--token-stdin` or `--token-env`. The secret is never a CLI argument. `GET /api/v1/me` is the identity probe (`{ tenantId, userId, authMethod, expiresAt }`); it fails closed with 401 for the env `API_KEY` operator fallback because that credential attaches no tenant. Token lifetime: `CLI_TOKEN_TTL_DAYS` (sliding, default 30) capped by `CLI_TOKEN_ABSOLUTE_TTL_DAYS` (hard ceiling from issuance, default 90).\n\nThe CLI refuses HTTP redirects (so the key cannot leak cross-origin) and strips inherited `MANIFEST_AGENT_KEY` / `MANIFEST_API_KEY` from `mnfst run` children. `mnfst routing test` sends `User-Agent: mnfst-cli/<version>` so gateway caller attribution (`routing/proxy/caller-classifier.ts`, which also recognises the n8n node's `n8n-nodes-manifest/<version>` UA) can tell CLI test traffic apart from real agent traffic in request details.\n\n### Remote MCP server\n\n`POST /api/v1/mcp` (`mcp/mcp.controller.ts`) is a remote MCP server over stateless Streamable HTTP: a fresh `McpServer` per POST (`mcp-server.factory.ts`), closed when the request ends, so no session affinity is needed. GET and DELETE answer **405 + `Allow: POST`** as the spec requires for a stateless JSON transport (a 404 made clients log an error on every connect).\n\n- **Auth**: the route is `@Public()` to skip the session/API-key guards, and `requireMcpAuth` from `@better-auth/mcp` is the real gate. Better Auth's MCP plugin is the authorization server (PKCE, resource-bound JWT access tokens, CIMD client identity, RFC 8414/9728 discovery). A missing or dead token answers 401 with `WWW-Authenticate: … resource_metadata=…`, which is how a client learns it must run OAuth. `mcp-auth.ts` maps the verified token's `sub` to a tenant through `TenantCacheService`, so tools are scoped exactly like the dashboard and CLI; a tenant-less user gets 401.\n- **Availability**: MCP is on by default and off in two cases, both decided by `auth/mcp-availability.ts` before Better Auth is built: the operator set `MCP_ENABLED=false`, or the MCP resource (`${BETTER_AUTH_URL}/api/v1/mcp`) is not HTTPS and not loopback. The second case is **not** opt-in on purpose — `mcp()` validates its resource URL as it is constructed and throws, and that throw happens while `auth.instance.ts` is being imported, so an HTTP-only self-hosted install would refuse to boot at all (issue #2939). When MCP is off, `buildPlugins()` omits `mcp()` and `cimd()`, `app.module.ts` leaves `McpModule` unregistered so `/api/v1/mcp` does not exist, `main.ts` mounts `mountMcpUnavailable()` so the well-known paths answer a JSON 404 rather than the SPA shell, and the dashboard hides the MCP nav entry (`GET /api/v1/setup/status` carries `mcpEnabled`). `mcp-availability.ts` is a leaf module for the same reason `mcp-scopes.ts` is — it must be importable from the unit suite, which cannot load Better Auth's ESM.\n- **Discovery**: `mcp-discovery.ts` (mounted in `main.ts`) serves the OAuth documents at the well-known **root** paths (`/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource[/api/v1/mcp]`) in addition to Better Auth's `/api/auth/.well-known/…`, because clients start from the MCP origin's root.\n- **Consent**: `pages/Consent.tsx` at `/consent` is the OAuth consent screen.\n- **Scopes**: `mcp:read` is required for every token; each `register*Tools()` returns before registering its write tools unless `operator.scopes` has `mcp:write`, so a read-only client never sees them (the server `instructions` string tells it to reconnect with the write scope). Revealing an ingest key also requires `mcp:write` because it is a live secret.\n- **Tools** (`mcp/tools/*.tools.ts`): identity, agents, environment (+ a `doctor` that walks dependencies in order), providers (including custom providers), routing (status, fallbacks, Autofix, recording, custom/header tiers, per-tier model params), models/pricing, and the request ledger. They call the same services as the REST controllers through `McpToolDeps` (`tool-deps.ts`); `tool-result.ts` is the shared result envelope.\n- **Testing**: `auth.instance.ts` is ESM-only and cannot load under Jest on Node 22, which is why the scope constants live in the leaf module `auth/mcp-scopes.ts` — import from there in guards, tools, and specs.\n\n## Multi-Tenancy Model\n\n```\nUser (Better Auth) ──→ Tenant ──→ Agent ──→ AgentApiKey (mnfst_*)\n                                    │\n                                    └──→ requests ──→ agent_messages (telemetry data)\n```\n\n- **Tenant** (`tenants` table): Created automatically on first agent creation. `tenant.owner_user_id` = `user.id` is the ONLY user→tenant link (resolved through `TenantCacheService`); `tenant.name` mirrors it for display until repurposed as a slug.\n- **Agent** (`agents` table): Belongs to a tenant. Unique constraint on `[tenant_id, name]`.\n- **AgentApiKey** (`agent_api_keys` table): One-to-one with agent. `mnfst_*` format key for OTLP ingestion.\n- **ApiKey** (`api_keys` table): A separate, tenant-scoped credential (not per-agent) used for dashboard/API access — the primary key `ApiKeyGuard` checks. Distinct from `AgentApiKey`. `mnfst login` PATs are rows of this table named `cli`.\n- **Onboarding flow**: `ApiKeyGeneratorService.onboardAgent()` creates tenant (if new) + agent + API key via three sequential inserts.\n\n### Data Isolation\n\nEvery resource belongs to a tenant; users only authenticate and (optionally) appear as `created_by_user_id` audit metadata. Guards (SessionGuard/ApiKeyGuard) resolve the tenant once per request and attach a `TenantContext` (`{ tenantId, userId }`), injected in controllers via `@TenantCtx()`. All analytics queries filter by tenant via `addTenantFilter(qb, tenantId)` from `query-helpers.ts`. Never scope, key, cache, or authorize by user id.\n\n## API Endpoints\n\n| Method                    | Route                                           | Auth                                | Purpose                                                                                                     |\n| ------------------------- | ----------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |\n| GET                       | `/api/v1/health`                                | Public                              | Health check                                                                                                |\n| ALL                       | `/api/auth/*`                                   | Public                              | Better Auth (login, register, OAuth, sessions)                                                              |\n| GET                       | `/api/v1/overview`                              | Session/API Key                     | Dashboard summary                                                                                           |\n| GET                       | `/api/v1/tokens`                                | Session/API Key                     | Token usage analytics                                                                                       |\n| GET                       | `/api/v1/costs`                                 | Session/API Key                     | Cost analytics                                                                                              |\n| GET                       | `/api/v1/agents`                                | Session/API Key                     | Agent list with sparklines                                                                                  |\n| POST                      | `/api/v1/agents`                                | Session/API Key                     | Create agent + API key                                                                                      |\n| GET                       | `/api/v1/agents/:agentName`                     | Session/API Key                     | Single agent detail                                                                                         |\n| GET/POST                  | `/api/v1/agents/:agentName/duplicate*`          | Session/API Key                     | Duplicate agent (preview + confirm)                                                                         |\n| DELETE                    | `/api/v1/agents/:agentName`                     | Session/API Key                     | Delete agent                                                                                                |\n| GET                       | `/api/v1/agents/:agentName/key`                 | Session/API Key                     | Get agent API key                                                                                           |\n| POST                      | `/api/v1/agents/:agentName/rotate-key`          | Session/API Key                     | Rotate API key                                                                                              |\n| PATCH                     | `/api/v1/agents/:agentName`                     | Session/API Key                     | Rename agent                                                                                                |\n| GET                       | `/api/v1/messages`                              | Session/API Key                     | Paginated Requests log (legacy route name)                                                                  |\n| GET/PATCH/DELETE          | `/api/v1/messages/:id/*`                        | Session/API Key                     | Request details, feedback, miscategorized flag (legacy route name)                                          |\n| GET                       | `/api/v1/security`                              | Session/API Key                     | Security score + events                                                                                     |\n| GET                       | `/api/v1/model-prices`                          | Session/API Key                     | Model pricing list                                                                                          |\n| GET                       | `/api/v1/free-models`                           | Session/API Key                     | Free LLM model catalog                                                                                      |\n| GET                       | `/api/v1/agent/usage`                           | Bearer (mnfst\\_\\*)                  | Token usage for the calling agent                                                                           |\n| GET                       | `/api/v1/agent/costs`                           | Bearer (mnfst\\_\\*)                  | Cost data for the calling agent                                                                             |\n| GET                       | `/api/v1/overview/*`                            | Session/API Key                     | Overview timeseries/breakdown sub-endpoints                                                                 |\n| GET                       | `/api/v1/providers` / `/api/v1/providers/usage` | Session/API Key                     | Connected provider list + usage                                                                             |\n| GET                       | `/api/v1/provider-analytics/*`                  | Session/API Key                     | Per-provider analytics                                                                                      |\n| GET                       | `/api/v1/errors/breakdown`                      | Session/API Key                     | Error breakdown analytics                                                                                   |\n| GET/PATCH                 | `/api/v1/billing/*`                             | Session/API Key                     | Billing status, light `plan` endpoint, email preferences (Stripe)                                           |\n| POST                      | `/api/v1/waitlist/autofix/claim`                | Public                              | Deprecated no-op compatibility route for older self-hosted versions                                         |\n| POST                      | `/api/v1/waitlist/pivot/claim`                  | Public                              | Closed pivot waiting-list claim, kept for older self-hosted versions; records origin (cloud/self-hosted)   |\n| GET/POST                  | `/api/v1/autofix/status` / `.../enable-all`     | Session/API Key                     | Workspace Autofix coverage + API-only fleet enable (no dashboard caller)                                    |\n| GET                       | `/api/v1/overview/autofix-*`                    | Session/API Key                     | Autofix analytics (stats, timeseries, per-agent/provider/model)                                             |\n| POST                      | `/api/v1/discovery/complete`                    | Session/API Key                     | Best-effort self-hosted discovery submission forwarded to Peacock                                           |\n| GET/POST/DELETE           | `/api/v1/internal/error-pages*`                 | Public (`x-internal-secret` header) | Custom error-page config (Peacock CMS push API)                                                             |\n| GET                       | `/api/v1/internal/crm-metrics*`                 | Public (`x-internal-secret` header) | **Cloud only.** Autofix-healed user cohort, pivot waitlist claims, and corporate signups, for the outreach CRM (`CRM_METRICS_SECRET`) |\n| GET/PUT/DELETE            | `/api/v1/agents/:agentName/enabled-providers*`  | Session/API Key                     | Per-agent provider enable/disable + impact preview                                                          |\n| GET/POST/PATCH/DELETE     | `/api/v1/notifications/*`                       | Session/API Key                     | Notification rules CRUD + email provider config                                                             |\n| GET/POST/PUT/PATCH/DELETE | `/api/v1/routing/:agentName/*`                  | Session/API Key                     | Routing config (tiers, providers, model-params, header-tiers, custom-providers, specificity, autofix, recording, etc.) |\n| GET/PATCH                 | `/api/v1/routing/:agentName/tiers/:tier/model-params` | Session/API Key                     | Model params addressed by tier (`default` or a custom tier name) + `?model=` (defaults to the tier primary); backs `mnfst routing params` and the MCP `manifest_routing_params_*` tools. Writes the same rows as the scope-keyed `…/model-params` |\n| POST                      | `/api/v1/routing/ollama/sync`                   | Session/API Key                     | Sync Ollama models                                                                                          |\n| GET                       | `/api/v1/routing/pricing-health`                | Session/API Key                     | OpenRouter pricing sync health                                                                              |\n| POST                      | `/api/v1/routing/pricing/refresh`               | Session/API Key                     | Force pricing cache refresh                                                                                 |\n| GET/POST/DELETE           | `/api/v1/oauth/:provider/*`                     | Session/API Key                     | OAuth flows (Gemini, OpenAI, Anthropic, xAI, Kiro, MiniMax)                                                 |\n| POST                      | `/api/v1/routing/resolve`                       | Bearer (mnfst\\_\\*)                  | Model resolution                                                                                            |\n| POST                      | `/api/v1/routing/subscription-providers`        | Bearer (mnfst\\_\\*)                  | Subscription provider config                                                                                |\n| GET                       | `/api/v1/setup/status`                          | Public                              | First-run setup status                                                                                      |\n| POST                      | `/api/v1/setup/admin`                           | Public                              | Create initial admin user                                                                                   |\n| GET                       | `/api/v1/public/error-pages*`                   | Public (opt-in)                     | Published error pages for the marketing site (controlled by `MANIFEST_PUBLIC_STATS`)                        |\n| GET                       | `/v1/models`                                    | Bearer (mnfst\\_\\*)                  | Available model list (proxy)                                                                                |\n| POST                      | `/v1/chat/completions`                          | Bearer (mnfst\\_\\*)                  | LLM proxy (OpenAI-compatible)                                                                               |\n| POST                      | `/v1/responses`                                 | Bearer (mnfst\\_\\*)                  | LLM proxy (OpenAI Responses API)                                                                            |\n| POST                      | `/v1/messages`                                  | Bearer (mnfst\\_\\*)                  | LLM proxy (Anthropic Messages API)                                                                          |\n| POST                      | `/chat/completions`                             | Public                              | Structured 404 pointing callers at `/v1/chat/completions` (missing `/v1` basePath — not a proxy alias)      |\n| ALL                       | `/otlp/v1/*`, `/v1/{traces,metrics,logs}`       | Public                              | Structured **410 Gone** — OTLP ingest is removed (`otlp-deprecated.controller.ts`)                          |\n| GET/POST/PATCH            | `/api/v1/playground/*`                          | Session/API Key                     | Playground runs (run, list, star, mark best)                                                                |\n| GET                       | `/api/v1/events`                                | Session                             | SSE real-time events                                                                                        |\n| GET                       | `/api/v1/github/stars`                          | Public                              | GitHub star count                                                                                           |\n| GET                       | `/api/v1/version`                               | Session/API Key                     | Running version + latest GitHub release for the self-hosted update badge (24h cache; off in cloud)          |\n| GET                       | `/api/v1/me`                                    | Session/API Key (tenant required)   | Identity probe for CLI/API clients: `{ tenantId, userId, authMethod, expiresAt }`; env `API_KEY` gets 401    |\n| POST                      | `/api/v1/cli/authorize`                         | Session only                        | Browser side of `mnfst login`: mints a PKCE-bound short-lived code                                          |\n| POST/DELETE               | `/api/v1/cli/token`                             | Public (code + verifier) / API Key  | Exchange the code for a `cli` PAT; DELETE revokes the calling PAT on logout                                 |\n| POST                      | `/api/v1/mcp`                                   | OAuth 2.1 bearer (`mcp:read`/`mcp:write`) | Remote MCP server (stateless Streamable HTTP); GET/DELETE answer 405 + `Allow: POST`                  |\n| GET                       | `/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource[/api/v1/mcp]` | Public | OAuth discovery for MCP clients (root aliases of Better Auth's `/api/auth/.well-known/…`)   |\n\n## Environment Variables\n\nSee `packages/backend/.env.example` for all variables. Key ones:\n\n- `BETTER_AUTH_SECRET` — **Required.** Secret for Better Auth session signing (min 32 chars). Generate with `openssl rand -hex 32`.\n- `DATABASE_URL` — **Required** in every environment except `NODE_ENV=test` (which falls back to `postgresql://myuser:mypassword@localhost:5432/mydatabase`, matching the local Docker command). Dev and production both throw on boot if unset. Format: `postgresql://user:password@host:port/database`.\n- `MANIFEST_ENCRYPTION_KEY` — Recommended. AES-256-GCM key (min 32 chars) for encrypting stored provider API keys and OAuth tokens. Defaults to `BETTER_AUTH_SECRET` if unset — set this independently so a session-cookie leak doesn't also expose provider credentials.\n- `MANIFEST_ENCRYPTION_KEY_PREVIOUS` — Optional, rotation only. A secret that stored values may still be encrypted under. Decryption always tries `MANIFEST_ENCRYPTION_KEY`, then this, then `BETTER_AUTH_SECRET`, so old rows stay readable. While it is set, one replica rewrites every row found under an older secret onto the current one after boot (`database/secret-reencryption.service.ts`, advisory-locked, never blocks or fails boot); a normal boot without it never scans. Rotation: set the new `MANIFEST_ENCRYPTION_KEY` and `MANIFEST_ENCRYPTION_KEY_PREVIOUS=<old>`, deploy, wait for the log line `Nothing left under an older secret`, then remove `PREVIOUS`. Introducing a dedicated key on an install that used the `BETTER_AUTH_SECRET` fallback: set `MANIFEST_ENCRYPTION_KEY` (old rows keep decrypting via the session secret); to move them onto the new key, set `PREVIOUS` to the session secret's value for one deploy. Request recordings are not rewritten by this pass: they decrypt under the current key only, so a key change makes older recordings unreadable until retention removes them.\n- `PORT` — Server port. Default: `3001`\n- `BIND_ADDRESS` — Bind address. Default: `127.0.0.1` (use `0.0.0.0` for Railway/Docker)\n- `NODE_ENV` — `development` or `production`. Dev allows broad CORS (local dashboard + Wingman); production allows the hosted Wingman origin plus any `WINGMAN_CORS_ORIGINS` entries.\n- `CORS_ORIGIN` — Allowed CORS origin (dev). Default: `http://localhost:3000`\n- `WINGMAN_CORS_ORIGINS` — Production only. Extra browser origins allowed to call the gateway (comma-separated). The hosted Wingman (`https://wingman.manifest.build`) is always allowed.\n- `BETTER_AUTH_URL` — Base URL for Better Auth. Default: `http://localhost:{PORT}`\n- `FRONTEND_PORT` \n\n<!-- [TRUNCATED AT 50KB] -->","packages/backend/src/routing/autofix/contract/phoenix-openapi.yaml":"openapi: 3.1.0\ninfo:\n  title: Phoenix MVP — request-healing API\n  version: 0.0.1\n  description: >\n    Advisory request-healing for LLM providers. Submit a failed request plus the\n    provider error to get a corrected request body, then report whether the retry\n    worked. See README.md for the full loop and data model.\nservers:\n  - url: http://127.0.0.1:3000\n    description: Local development\n  - url: https://your-app.up.railway.app\n    description: Production — set to your Railway deployment URL\n\nsecurity:\n  - ApiKeyAuth: []\n\npaths:\n  /api/heal:\n    post:\n      summary: Resolve a patch for a failed request\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      description: >\n        Fingerprints the error and returns a healed request body if a\n        deterministic patch exists. Only request-side errors are healable.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/HealRequest'\n      responses:\n        '200':\n          description: A heal decision (patched, pending confirmation, still resolving, or no patch).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HealResult'\n        '400':\n          description: Malformed body (failed schema validation).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ZodError'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '422':\n          description: Error is not request-side (5xx / auth / rate-limit); nothing to heal.\n\n  /api/heal/observe:\n    post:\n      summary: Record failed requests as diagnostic evidence\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ObservationBatch'\n      responses:\n        '200':\n          description: Observation batch accepted.\n        '401':\n          description: Missing or invalid static key or instance credential.\n\n  /api/heal-attempts/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read a heal attempt and its parent issue\n      responses:\n        '200':\n          description: The heal attempt.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HealAttemptView'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n    patch:\n      summary: Report the outcome of a retried request\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/HealOutcome'\n      responses:\n        '200':\n          description: Recorded outcome (idempotent).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OutcomeResult'\n        '400':\n          description: Malformed body, or error omitted when retryStatusCode >= 400.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ZodError'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: No such heal attempt.\n\n  /api/issues/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read an issue and its resolved patch\n      responses:\n        '200':\n          description: The issue.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/IssueView'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n\n  /api/issues:\n    get:\n      summary: List issues (most-recently-updated first)\n      parameters:\n        - name: patchId\n          in: query\n          schema: { type: string, format: uuid }\n          description: Only issues fixed by this patch.\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Issues.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/IssueListItem' }\n        '400':\n          description: Malformed query.\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/heal-attempts:\n    get:\n      summary: List heal attempts (most-recent first)\n      parameters:\n        - name: issueId\n          in: query\n          schema: { type: string, format: uuid }\n        - name: patchId\n          in: query\n          schema: { type: string, format: uuid }\n        - name: traceId\n          in: query\n          schema: { type: string }\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Heal attempts.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/AttemptListItem' }\n        '400':\n          description: Malformed query.\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/patches:\n    get:\n      summary: List patches with usage counts (most-recent first)\n      parameters:\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Patches.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/PatchListItem' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/patches/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read a patch and the issues it fixes\n      responses:\n        '200':\n          description: The patch.\n          content:\n            application/json:\n              schema: { $ref: '#/components/schemas/PatchDetail' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n\n  /api/traces:\n    get:\n      summary: List traces with per-trace outcome counts (most-recent first)\n      parameters:\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Traces.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/TraceListItem' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/traces/{traceId}:\n    parameters:\n      - name: traceId\n        in: path\n        required: true\n        schema: { type: string }\n    get:\n      summary: A trace's heal attempts, oldest-first\n      responses:\n        '200':\n          description: The trace timeline.\n          content:\n            application/json:\n              schema: { $ref: '#/components/schemas/TraceDetail' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: No such trace.\n\n  /api/health:\n    get:\n      summary: Liveness probe (public — no API key)\n      security: []\n      responses:\n        '200':\n          description: Process is up.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  status: { type: string, example: ok }\n                  uptimeSeconds: { type: integer }\n\n  /api/ready:\n    get:\n      summary: Readiness probe — process up and database reachable (public)\n      security: []\n      responses:\n        '200':\n          description: Ready.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  status: { type: string, example: ready }\n        '503':\n          description: Database not ready.\n\ncomponents:\n  securitySchemes:\n    ApiKeyAuth:\n      type: apiKey\n      in: header\n      name: x-api-key\n      description: >\n        Static API key. Send as `x-api-key: <key>` (an `Authorization: Bearer\n        <key>` header is also accepted). Enforced in deployed environments where\n        the API_KEY env var is set; disabled locally when it is unset.\n    ManifestInstanceAuth:\n      type: apiKey\n      in: header\n      name: X-Manifest-Instance\n      description: >\n        Anonymous install identifier announced by a self-hosted Manifest. This\n        is an identifier, not a credential: it is not secret and no registration\n        handshake issues it. Phoenix creates the instance row the first time it\n        sees an id. Accepted in place of ApiKeyAuth on the gateway write\n        operations.\n\n  parameters:\n    Id:\n      name: id\n      in: path\n      required: true\n      schema:\n        type: string\n        format: uuid\n    Limit:\n      name: limit\n      in: query\n      description: Page size (default 50, max 200).\n      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }\n    Offset:\n      name: offset\n      in: query\n      description: Rows to skip for pagination.\n      schema: { type: integer, minimum: 0, default: 0 }\n    ManifestVersion:\n      name: X-Manifest-Version\n      in: header\n      required: false\n      description: Running Manifest release version. Invalid values become `unknown` in analytics.\n      schema:\n        type: string\n        maxLength: 32\n        pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'\n    ManifestHarness:\n      name: X-Manifest-Harness\n      in: header\n      required: false\n      description: Agent platform slug. Missing or unknown values become `other` in analytics.\n      schema:\n        type: string\n        enum:\n          [\n            openclaw,\n            hermes,\n            nanobot,\n            craft,\n            n8n,\n            claude-code,\n            opencode,\n            codex,\n            openai-sdk,\n            anthropic-sdk,\n            vercel-ai-sdk,\n            langchain,\n            curl,\n            other,\n          ]\n  schemas:\n    InstanceRegistration:\n      type: object\n      required: [version, schema_version]\n      properties:\n        version:\n          type: string\n          maxLength: 32\n          pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'\n        schema_version:\n          type: integer\n          const: 1\n      additionalProperties: true\n\n    InstanceCredential:\n      type: object\n      required: [instance_id, secret]\n      properties:\n        instance_id: { type: string, format: uuid }\n        secret:\n          type: string\n          description: Opaque 256-bit base64url bearer token. Returned only once.\n      additionalProperties: false\n\n    ObservationBatch:\n      type: object\n      required: [observations]\n      properties:\n        observations:\n          type: array\n          minItems: 1\n          maxItems: 200\n          items: { $ref: '#/components/schemas/HealRequest' }\n\n    ProviderError:\n      type: object\n      required: [message]\n      properties:\n        message: { type: string }\n        type: { type: [string, 'null'] }\n        param: { type: [string, 'null'] }\n        code: { type: [string, 'null'] }\n\n    Operation:\n      type: object\n      description: A deterministic transform applied to the request body.\n      required: [type]\n      properties:\n        type: { type: string }\n      additionalProperties: true\n      example: { type: rename_param, from: max_tokens, to: max_output_tokens }\n\n    HealRequest:\n      type: object\n      required: [traceId, provider, api, request, response]\n      properties:\n        traceId:\n          type: string\n          minLength: 1\n          description: >\n            Caller-supplied id of the logical request being healed. Send the\n            same value on every heal of that request's retry chain (not the\n            provider's per-call request id) — it groups the attempts into one\n            trace.\n        tenantId:\n          type: string\n          minLength: 1\n          description: >\n            Opaque id of the gateway tenant this request belongs to (Manifest's\n            Tenant.id). Optional on the wire; recorded so a failure can be\n            attributed to a tenant. Manifest always sends it on the proxy path.\n        provider: { type: string, minLength: 1, example: openai }\n        model:\n          type: string\n          minLength: 1\n          maxLength: 1024\n          description: >\n            Logical provider model identity. Native transports may encode it in\n            the URL rather than in the JSON request body.\n        authType:\n          type: string\n          enum: [api_key, subscription, local]\n          description: Provider credential route. Optional during the Manifest rollout.\n        api:\n          type: string\n          enum: [responses, chat_completions, messages]\n        url: { type: string, format: uri }\n        request:\n          type: object\n          description: The original outgoing request body, verbatim.\n          additionalProperties: true\n        response:\n          type: object\n          required: [statusCode, error]\n          properties:\n            statusCode: { type: integer, example: 400 }\n            error: { $ref: '#/components/schemas/ProviderError' }\n        providerExchange:\n          type: object\n          description: Final provider-native transport exchange, separate from the patch input.\n          required: [format, request, response]\n          properties:\n            format:\n              type: string\n              enum:\n                - openai_chat_completions\n                - openai_responses\n                - anthropic_messages\n                - google_generate_content\n                - google_code_assist\n            url: { type: string, format: uri }\n            request:\n              type: object\n              required: [body]\n              properties:\n                body: { type: object, additionalProperties: true }\n                redactedFields: { type: array, items: { type: string } }\n            response:\n              type: object\n              required: [statusCode, body]\n              properties:\n                statusCode: { type: integer }\n                body: {}\n        responseTimeMs: { type: integer, minimum: 0 }\n        responseSizeBytes: { type: integer, minimum: 0 }\n\n    HealResult:\n      type: object\n      required: [status, issueId]\n      properties:\n        status:\n          type: string\n          enum: [patched, unverified, resolving, no_patch]\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        healAttemptId: { type: string, format: uuid }\n        operations:\n          type: array\n          items: { $ref: '#/components/schemas/Operation' }\n        healedBody:\n          type: object\n          additionalProperties: true\n        explanation:\n          type: object\n          description: >\n            Human-readable \"why\" for a served heal (present on \"patched\" /\n            \"unverified\"). operations[].detail is always deterministic (built from\n            the operations); summary prefers an authored rationale (source \"agent\" /\n            \"operator\") and falls back to the deterministic composition (source\n            \"deterministic\").\n          required: [summary, operations, source]\n          properties:\n            summary: { type: string }\n            operations:\n              type: array\n              items:\n                type: object\n                required: [type, detail]\n                properties:\n                  type: { type: string }\n                  detail: { type: string }\n            source:\n              type: string\n              enum: [deterministic, agent, operator]\n        retryAfterMs:\n          type: integer\n          description: Present only when status is \"resolving\".\n\n    HealOutcome:\n      type: object\n      required: [retryStatusCode]\n      description: error is required when retryStatusCode >= 400.\n      properties:\n        retryStatusCode: { type: integer, example: 200 }\n        error: { $ref: '#/components/schemas/ProviderError' }\n\n    OutcomeResult:\n      type: object\n      required: [healAttemptId, status, issueStatus]\n      properties:\n        healAttemptId: { type: string, format: uuid }\n        status:\n          type: string\n          enum: [succeeded, failed, expired]\n        issueStatus: { type: string }\n\n    IssueView:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        fingerprint: { type: string }\n        provider: { type: string }\n        api: { type: string }\n        model: { type: [string, 'null'] }\n        error:\n          type: object\n          properties:\n            code: { type: [string, 'null'] }\n            type: { type: [string, 'null'] }\n            param: { type: [string, 'null'] }\n        status:\n          type: string\n          enum: [resolving, unverified, verified, ineffective, no_fix_found]\n        occurrenceCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n        patch:\n          type: [object, 'null']\n          properties:\n            id: { type: string, format: uuid }\n            operations:\n              type: array\n              items: { $ref: '#/components/schemas/Operation' }\n\n    HealAttemptView:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        traceId: { type: string }\n        status:\n          type: string\n          enum: [pending, succeeded, failed, expired]\n        retryStatusCode: { type: [integer, 'null'] }\n        retryErrorFingerprint: { type: [string, 'null'] }\n        confirmedAt: { type: [string, 'null'], format: date-time }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n        issue:\n          type: [object, 'null']\n          properties:\n            id: { type: string, format: uuid }\n            status: { type: string }\n            provider: { type: string }\n            api: { type: string }\n            model: { type: [string, 'null'] }\n            fingerprint: { type: string }\n\n    IssueListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        fingerprint: { type: string }\n        provider: { type: string }\n        api: { type: string }\n        model: { type: [string, 'null'] }\n        errorCode: { type: [string, 'null'] }\n        errorType: { type: [string, 'null'] }\n        errorParam: { type: [string, 'null'] }\n        status:\n          type: string\n          enum: [resolving, unverified, verified, ineffective, no_fix_found]\n        patchId: { type: [string, 'null'], format: uuid }\n        occurrenceCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    AttemptListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        traceId: { type: string }\n        status:\n          type: string\n          enum: [pending, succeeded, failed, expired]\n        retryStatusCode: { type: [integer, 'null'] }\n        retryErrorFingerprint: { type: [string, 'null'] }\n        confirmedAt: { type: [string, 'null'], format: date-time }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    PatchListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        operations:\n          type: array\n          items: { $ref: '#/components/schemas/Operation' }\n        operationsHash: { type: string }\n        issueCount: { type: integer }\n        attemptCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    PatchDetail:\n      allOf:\n        - $ref: '#/components/schemas/PatchListItem'\n        - type: object\n          properties:\n            issues:\n              type: array\n              items: { $ref: '#/components/schemas/IssueListItem' }\n\n    TraceListItem:\n      type: object\n      properties:\n        traceId: { type: string }\n        attemptCount: { type: integer }\n        pendingCount: { type: integer }\n        succeededCount: { type: integer }\n        failedCount: { type: integer }\n        firstAt: { type: string, format: date-time }\n        lastAt: { type: string, format: date-time }\n\n    TraceAttempt:\n      allOf:\n        - $ref: '#/components/schemas/AttemptListItem'\n        - type: object\n          properties:\n            issue:\n              type: object\n              properties:\n                id: { type: string, format: uuid }\n                status: { type: string }\n                provider: { type: string }\n                api: { type: string }\n                model: { type: [string, 'null'] }\n                errorCode: { type: [string, 'null'] }\n                errorType: { type: [string, 'null'] }\n                errorParam: { type: [string, 'null'] }\n            patch:\n              type: object\n              properties:\n                id: { type: string, format: uuid }\n                operations:\n                  type: array\n                  items: { $ref: '#/components/schemas/Operation' }\n\n    TraceDetail:\n      type: object\n      properties:\n        traceId: { type: string }\n        attempts:\n          type: array\n          items: { $ref: '#/components/schemas/TraceAttempt' }\n\n    ZodError:\n      type: object\n      description: Flattened zod validation error.\n      properties:\n        formErrors:\n          type: array\n          items: { type: string }\n        fieldErrors:\n          type: object\n          additionalProperties:\n            type: array\n            items: { type: string }\n","packages/cli/README.md":"# Manifest CLI\n\n`mnfst` manages a Manifest gateway from your terminal. Use it to create agents, connect AI providers, configure model routing, and inspect requests. The dashboard calls agents **Harnesses**; CLI commands use `agent`.\n\nThe npm package is `mnfst-gateway-cli`. The command it installs is `mnfst`. Node.js 20 or later is required.\n\n## Install\n\n```bash\nnpm install -g mnfst-gateway-cli\nmnfst --help\n```\n\nTo run it without a global install:\n\n```bash\nnpx mnfst-gateway-cli --help\n```\n\n## Quick start\n\nSign in to the hosted Manifest gateway, then create an agent and connect a provider:\n\n```bash\nmnfst login\nmnfst agent create --name my-app --platform openai-sdk\nmnfst provider connect openai --agent my-app --credential-env OPENAI_API_KEY\nmnfst models my-app --provider openai\nmnfst agent configure my-app --models gpt-4o-mini --provider openai\nmnfst routing status my-app\nmnfst agent setup my-app\n```\n\nSet `OPENAI_API_KEY` in your environment before connecting the provider. Choose a model shown by `mnfst models` if `gpt-4o-mini` is unavailable to your account. `mnfst agent setup` returns wiring instructions with the agent key masked.\n\nFor a self-hosted gateway, pass its URL when you sign in:\n\n```bash\nmnfst login --url https://gateway.example.com\n```\n\nThe CLI uses that gateway for later commands. You can select another gateway with `--url` or `MANIFEST_URL`.\n\n## Useful commands\n\n| Task                           | Command                                        |\n| ------------------------------ | ---------------------------------------------- |\n| Check your connection          | `mnfst doctor`                                 |\n| List available agent platforms | `mnfst agent platforms`                        |\n| List connectable providers     | `mnfst provider catalog`                       |\n| Review an agent's routing      | `mnfst routing status my-app`                  |\n| Read or set model params       | `mnfst routing params get my-app`              |\n| Read recent requests           | `mnfst requests get --agent my-app --range 7d` |\n| See all commands and options   | `mnfst --help`                                 |\n\nMost commands write JSON to stdout for scripts and coding agents. `mnfst agent env` and `mnfst skill show` write text. `mnfst routing test` sends a real provider request, which may incur provider charges. Destructive commands require `--yes`.\n\n## Credentials\n\nBrowser login is the default. For a script, provide a management credential through standard input or an environment variable:\n\n```bash\nprintf '%s' \"$MY_KEY\" | mnfst login --token-stdin\n# or\nmnfst login --token-env MY_KEY\n```\n\nThe CLI stores login credentials for each gateway host in `~/.config/manifest/config.json` with file mode `0600`. A stored credential is only sent to the host where it was saved. `MANIFEST_API_KEY` overrides the stored credential for the selected host.\n\nAgent keys are separate from management credentials. The CLI stores a newly created agent key in its local keystore and does not print it in `mnfst agent setup` unless you pass `--reveal`. Use `mnfst agent env my-app` to get the key and URL for your application; its output contains the key, so handle it as a secret.\n\n## Usage telemetry\n\nThe CLI records usage telemetry by default. It batches events locally and normally sends one request per install per day. The payload contains a random, persistent install ID; CLI version; operating system; Cloud or self-hosted target; and each command's name, result, duration, and minute-level time. It may include the detected coding-agent runtime. It does not include command arguments, agent or provider names, gateway URLs, prompts, or credentials.\n\nSet `MANIFEST_TELEMETRY_DISABLED=1` to opt out. Telemetry files are stored beside the CLI config. You can set `MANIFEST_CLI_TELEMETRY_ENDPOINT` to send usage data to your own endpoint.\n\n## Documentation\n\nSee the [CLI documentation](https://manifest.build/llm-gateway/docs/cli/) for more examples and the [GitHub repository](https://github.com/mnfst/llm-gateway) for source and issues.\n","packages/manifest/README.md":"# manifest\n\nThis package exists to hold the canonical **Manifest release version**.\n\nIt contains no code. The real backend and frontend live in [`../backend`](../backend) and [`../frontend`](../frontend), and shared types in [`../shared`](../shared). The Docker image at [`manifestdotbuild/manifest`](https://hub.docker.com/r/manifestdotbuild/manifest) bundles the built backend + frontend.\n\n## What it's for\n\n- **Changesets target**: `.changeset/config.json` ignores `manifest-backend`, `manifest-frontend`, and `manifest-shared`, so when contributors run `npx changeset` the only selectable package is this one. Always target `manifest` regardless of which files you actually changed — this is the single source of truth for \"the version of Manifest as a whole\".\n- **Docker release version**: `.github/workflows/docker.yml` reads the `version` field from this package's `package.json` via `jq` and uses it to tag the image (`manifestdotbuild/manifest:{version}` + derived `{major}.{minor}` / `{major}` / `sha-<short>` tags).\n- **Changelog**: `CHANGELOG.md` here (auto-generated by changesets) is the unified Manifest release history.\n\n## Adding a changeset\n\n```bash\nnpx changeset\n# → select \"manifest\"\n# → choose patch / minor / major\n# → write a one-line summary\n```\n\nCommit the generated file. On merge to `main`, the release workflow opens a `chore: version packages` PR bumping `version` and appending to `CHANGELOG.md`. Merge that PR to land the version bump, then manually run the Docker workflow (`Actions → Docker → Run workflow` with a blank input) to publish.\n",".cursorrules (manifest-yaml-cursorrules-prompt-file.mdc)":"---\ndescription: \"Cursor rules for manifest development with YAML integration.\"\nglobs: **/*\nalwaysApply: false\n---\n**Prompt for Expert Manifest Developer**\n\n**You are an assistant for app creation. You are going to use the backend Manifest. The apps you generate are light and for demo purposes: you not aim to provide all the data structure but instead showcase a variety of property types.**\n\n**Code structure**\nWhen asked to create a backend, execute the following actions:\n\n1. Install the `manifest` npm package\n2. Add the following scripts to `pacakge.json`: \"manifest\": \"node node_modules/manifest/scripts/watch/watch.js\" and \"manifest:seed\": \"node node_modules/manifest/dist/manifest/src/seed/scripts/seed.js\"\n3. Create the `manifest/backend.yml` file and add the manifest code to it.\n4. Add the `redhat.vscode-yaml` as recommendation in `.vscode/extensions.json`\n5. Configure `yaml.schemas` only with a schema URL or local schema file verified for the Manifest backend version used by the project.\n\n**Backend file**\nOn the `manifest/backend.yml`, follow those rules:\n- Strictly follow the Manifest JSON Schema shipped with the project or verified from the current Manifest backend documentation.\n- Start by addind a quick name to the app\n- Limit to 2 or 3 entities maximum\n- Limit to 4 properties maximum per entity\n- Try to showcase different property types\n- Only use validation properties once or twice\n- No entity should be called admin\n- Do not use authenticable entities\n- Add an emoji after each entity name, but do not use the emoji it on relationships references\n- Add a linebreak before each entity object\n- Each entity only appears once. Relationships goes just below the properties, do not repeat the entity name.\n- Do not use special characters.\n. Do not use middlewares, endpoints or hooks.\n- Use YAML abbreviated form for objects, with spaces. Example: { name: issueDate, type: date }\n- Do not add relationships to single entities\n- For relationships, use the short form. Ex: ' belongsTo:\n      - Author'\n- Add policies. Most projects only have \"read\" public policies. Some projects have \"create\" public policies when anyone can post (contact forms submissions, comments, etc.)\n- If using the \"choice\" property type, use \"options.values\" property to list choices. Example:  `{ name: type, type: choice, options: { values: [\"Fire\", \"Water\", \"Grass\"] } }`\n- Do not add \"seedCount\" and \"mainProp\" to entities\n\n**Documentation**\nRefer to the Manifest backend documentation that matches the project's installed version.\n\n**Example**\nThis is an example of the content of a `backend.yml` file:\nname: My pet app 🐾\nentities:\n  Owner:\n    properties:\n      - name\n      - { name: birthdate, type: date }\n\n  Cat:\n    properties:\n      - name\n      - { name: age, type: number }\n      - { name: birthdate, type: date }\n    belongsTo:\n      - Owner\n\n  Homepage:\n    nameSingular: Home content\n    single: true\n    properties:\n      - title\n      - { name: description, type: richText }\n      - { name: cover, type: image }\n"},"files":{".claude/skills/mnfst-cli/SKILL.md":"---\nname: mnfst-cli\ndescription: Use when creating or operating AI agents that make LLM calls — provisioning model routing and keys, wiring an app or automation to an LLM endpoint, testing routes, rotating leaked keys, or investigating LLM request failures and costs on a Manifest install.\n---\n\n# Operating Manifest from the CLI\n\n## Overview\n\n`mnfst` manages Manifest, the LLM gateway. Every call routed through a Manifest agent gets model routing with fallbacks, per-agent cost attribution, self-healing (Auto-fix), and a request log — for free. Default practice: never hand an app or automation a raw provider key; give it a Manifest agent.\n\nCommands emit JSON on stdout (human hints on stderr) — except `agent env`, which prints dotenv lines by design — exit 0/1, and never block on prompts when credentials come from flags or env. Full surface: `mnfst --help`.\n\n## The flow\n\n| Step | Command |\n|---|---|\n| Auth (human) | `mnfst login` — browser, one click, 30-day sliding token |\n| Auth (script/agent) | env `MANIFEST_URL` + `MANIFEST_API_KEY` — no login step |\n| Provision | `mnfst agent create --name X --platform <p> --if-absent` → response includes `setup` (the platform's config block) |\n| Check connections | `mnfst provider list [--agent X]` FIRST — the provider you need is often already connected. A connection is only usable if `cached_model_count` > 0; `is_active: true` with 0 models is hollow — run `mnfst provider refresh [<provider>]` (re-runs discovery tenant-wide, prints the new counts); still 0 means a bad credential, so reconnect, don't route on it |\n| Connect provider | `mnfst provider connect xai --auth-type api_key --credential-env KEY` · `provider catalog` lists all 30+ with auth types · subscription auth opens a browser (impossible headless — reuse an existing subscription connection instead) · own gateway: `provider custom add --name gw --endpoint <url>` |\n| Route | `mnfst agent configure X --models primary,fb1,fb2 --provider p [--auth-type a]` — first model is the route, the rest are fallbacks, and the whole chain rides the one `--provider` you name (cross-provider fallbacks are dashboard-only) · add `--tier deep` to upsert a custom tier callers select per-request with header `x-manifest-tier: deep` |\n| Verify | `mnfst routing test X` — one real request through the platform's actual API surface; broken routes fail loudly. It writes real rows to the request log — count them in later audits |\n| Wire the app | deployed → `mnfst agent env X >> .env` (one .env per service — the lines always name `MANIFEST_AGENT_KEY`, so appending two agents to one file silently keeps only the loader's winner; for several agents in one process use `mnfst run --agent X --env ROLE_KEY -- <cmd>`) |\n| Observe | `mnfst requests get --agent X [--status failed]` — paginated; pass `next_cursor` back for the next page |\n\nNamespace = scope: `provider *` acts tenant-wide, `agent *` acts on one agent, `routing *` holds readouts and custom-tier lifecycle.\n\n## Gotchas `--help` cannot tell you\n\n| Symptom | Reality |\n|---|---|\n| Proxy returns HTTP 200 but the \"answer\" starts with `[🦚 Manifest M###]` | An error wearing an assistant costume — check the content, not the status. `routing test` unmasks these automatically |\n| `cost: \"0.000000\"` on subscription-auth traffic | Flat-rate subscription: per-request cost is genuinely zero, not broken billing |\n| `agent configure`'s echo differs from later reads | Mutation bodies echo raw API rows — the canonical readback is `routing status <agent>` |\n| \"Which requests used a fallback or Auto-fix?\" | Those fields are trimmed from default output — `requests get --full` |\n| 401s missing from the request log | Rejected-auth attempts are never recorded (no tenant to attribute them to); a leaked key's blast radius cannot be proven from the log |\n| `MANIFEST_AGENT_URL` | Already ends in `/v1` — append `/chat/completions` directly |\n| Model prices needed before any agent exists | `mnfst model prices [--provider <p>]` — install-wide, no agent needed (`models <agent>` is the per-agent routable set) |\n| Auditing escalation traffic by `routing_tier` | Custom-tier requests log `routing_tier: \"standard\"` — the tier identity lives in `header_tier_name` |\n| Route resolves but requests fail on one connection | A connection can be `is_active: false` while a sibling auth-type works — pass `--auth-type` explicitly when a provider has several connections |\n| Every command answers `Invalid API key — Run mnfst login` under env auth | Not always an auth problem: a wrong `MANIFEST_URL` (dead or different install) answers exactly like a bad key. Run `mnfst doctor` — it checks config, host, credential, connections and agents in that order, so it separates \"host is dead\" from \"key is wrong for this host\" — and never tells you to re-login on an env key |\n| Need a different reasoning effort, temperature, or other model param without editing the app | `mnfst routing params get X [--tier t] [--model m]` lists what the routed model accepts and what is saved; `mnfst routing params set X [--tier t] [--model m] --set reasoning.effort=high [--unset temperature]` saves it. Saved params **override** what the app sends, per tier and per model (a fallback keeps its own), but only on requests Manifest routes: `model: \"auto\"` or a matching custom-tier header. A request naming a concrete model skips them |\n| `agent configure` / `routing custom create` reject a model as undiscovered | Both validate against the agent's *discovered* models, and a connection with `cached_model_count: 0` contributes none: run `mnfst provider refresh` (stale or empty catalog), or pass `--force` — the backend still routes uncatalogued models through provider-qualified passthrough |\n\n## Common mistakes\n\n- Handing an app a raw provider key — it loses fallbacks, cost attribution, healing, and the log. Create an agent and use `agent env`.\n- Verifying a route with hand-rolled `curl` — fake-200s read as success. Use `routing test`.\n- Slurping the whole request log — pagination is one page per call by design; loop on `next_cursor`.\n\nAlso on the surface when needed: `mnfst doctor` (first stop when anything is wrong), `agent setup <name> [--reveal]` (setup block anytime; key masked unless revealed), `agent key path|show`, `agent provider enable|disable <agent> <provider>`, `agent rotate-key` (revocation is instant), `mnfst models <agent> --cost --capabilities`.\n","AGENTS.md":"# Manifest Agent Guidelines\n\n## Domain Terminology\n\nManifest terminology is directional:\n\n- A **Manifest Request** is one logical request from an agent to Manifest and lives in `requests`.\n- A **Provider Attempt** is one request from Manifest to an AI provider and lives in `agent_messages`.\n- A **Tenant** is a user's data boundary. It is created from `user.id` on first agent creation.\n- An **Agent** is an AI agent owned by a tenant. The dashboard labels agents **Harnesses** (nav under `/harnesses`; legacy `/agents/*` URLs redirect) — this is UI copy only; backend code, database tables, and API routes still say *agent*.\n\n[`docs/glossary.md`](docs/glossary.md) is the canonical contract for statuses, ordering, recovery, database mapping, and counting rules. Do not duplicate those definitions in agent guides.\n","CLAUDE.md":"# Manifest Development Guidelines\n\nLast updated: 2026-09-14\n\n## What Manifest Is\n\nManifest is a smart model router for **AI agents**. It sits between an agent and its LLM providers, scores each request, and routes it to the cheapest model that can handle it. The dashboard tracks logical requests and their provider attempts, costs, and tokens across any agent that speaks OpenAI-compatible HTTP.\n\n**\"Harness\" is the dashboard word for an agent.** The UI now labels agents **Harnesses** (nav item \"Harnesses\", routes under `/harnesses`, categories `AI agent` / `Automation` / `App AI SDK` / `Coding Assistant` from `CATEGORY_LABELS` in `packages/shared/src/agent-type.ts`). This is a **copy-level rename only**: backend code, database tables (`agents`, `agent_messages`, …), API routes (`/api/v1/agents/*`), and entity/service names all still say *agent*. Legacy `/agents/*` dashboard URLs redirect to `/harnesses/*`. When writing UI copy say \"harness\"; when writing code or API docs keep \"agent\".\n\n**Product note:** this repo is **Manifest LLM Gateway**, the open-source LLM gateway of Manifest, available and maintained. The dashboard and the READMEs carry no promotion of any other Manifest product: not the self-healing layer at `dashboard.manifest.build` (on hold), and no API Bot banner or sidebar card (removed). `POST /api/v1/waitlist/pivot/claim` stays only for self-hosted versions that still ship the old sidebar card. The sidebar's \"Custom development\" button opens `ServicesContactModal`, which posts to `POST /api/v1/services/contact`; the backend relays it to `https://manifest.build/api/contact` (mnfst/website `api/contact.js`), which emails the team. Nothing is stored.\n\n**Supported agents**: see `AGENT_PLATFORMS` in `packages/shared/src/agent-type.ts` for the current list (OpenClaw, Hermes, Claude Code, OpenCode, generic OpenAI/Anthropic SDK slots, and others — don't duplicate the list here, it grows independently of this doc). OpenClaw remains the deepest integration, but no new code or copy should frame Manifest as OpenClaw-only. When adding examples, prefer \"AI agent\" as the noun and pick OpenClaw as the worked example rather than the sole target. Manifest is consumed as a generic OpenAI-compatible HTTP endpoint — there are no first-party OpenClaw plugins in this repo anymore.\n\nWingman — the gateway tester for sending requests against a Manifest backend while impersonating any of the supported agents (useful for routing/header-classifier reproductions) — lives in its own repo at [`mnfst/wingman`](https://github.com/mnfst/wingman) and is hosted at [`wingman.manifest.build`](https://wingman.manifest.build). The dashboard embeds it as an iframe drawer **in dev mode only** — it is dead-code-eliminated from production / self-hosted bundles via `__DEV_MODE__`. The backend allows the hosted Wingman origin through CORS in both dev and production (production also honors `WINGMAN_CORS_ORIGINS`), while the CSP `frame-src` that permits the drawer iframe stays dev-only; both are wired in `packages/backend/src/cors-csp-config.ts`.\n\n**Whenever working in dev mode (`/serve`, `npm run dev`, etc.), the Wingman drawer is expected to be available** — open the FAB at the bottom-right of the dashboard (or hit ⌘/Ctrl+Shift+W) and confirm the iframe loads `https://wingman.manifest.build` cleanly. The drawer is part of the dev surface area, so a broken iframe means the dev environment is broken. `/serve` is **dev-only** — never use it to validate production behavior.\n\n**Three management surfaces share one REST API.** The dashboard, the `mnfst` CLI (`packages/cli/`), and the remote MCP server (`packages/backend/src/mcp/`) all drive the same `/api/v1/*` services with the same tenant scoping. Add a capability to the service layer once, then expose it on whichever surfaces need it; never fork logic per surface. See [CLI login](#cli-login-mnfst-login) and [Remote MCP server](#remote-mcp-server) below.\n\n## IMPORTANT: Cloud Mode Always\n\nWhen starting the app for development or testing (e.g. `/serve`), **always use `MANIFEST_MODE=cloud`** (the default). Every dev session must use a **fresh PostgreSQL database** via Docker — multiple concurrent dev instances sharing one DB cause cross-run data pollution and intermittent test failures:\n\n```bash\n# 1. Ensure the postgres_db container is running\ndocker start postgres_db 2>/dev/null || \\\n  docker run -d --name postgres_db -e POSTGRES_USER=myuser -e POSTGRES_PASSWORD=mypassword -e POSTGRES_DB=mydatabase -p 5432:5432 postgres:16\n\n# 2. Create a pristine database with a unique name\nDB_NAME=\"manifest_$(openssl rand -hex 4)\"\ndocker exec postgres_db psql -U myuser -d postgres -c \"CREATE DATABASE $DB_NAME;\"\n\n# 3. Update DATABASE_URL in packages/backend/.env to use the new database\n# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/$DB_NAME\n\n# 4. Ensure SEED_DATA=true in .env so the database is populated on startup\n```\n\nThis guarantees each session starts with a clean, isolated database and avoids all cross-instance conflicts.\n\n## Testing OpenClaw Integration\n\nTo test routing from an OpenClaw agent against a local Manifest dev server, point OpenClaw at the dev server's OpenAI-compatible proxy directly — there is no plugin anymore:\n\n```bash\n# 1. Build and start the backend in cloud mode\nnpm run build\nPORT=38238 BIND_ADDRESS=127.0.0.1 \\\n  node -r dotenv/config packages/backend/dist/main.js\n\n# 2. Configure OpenClaw to use the dev server as a generic OpenAI-compatible provider\nopenclaw config set models.providers.manifest '{\"baseUrl\":\"http://localhost:38238/v1\",\"api\":\"openai-completions\",\"apiKey\":\"mnfst_YOUR_KEY\",\"models\":[{\"id\":\"auto\",\"name\":\"Manifest Auto\"}]}'\nopenclaw config set agents.defaults.model.primary manifest/auto\n\n# 3. Restart the gateway\nopenclaw gateway restart\n```\n\nThe `AgentKeyAuthGuard` accepts any non-`mnfst_*` token from loopback IPs in the self-hosted version, so loopback-only testing works even without a valid key. After restarting the backend, also restart the OpenClaw gateway — it doesn't reconnect automatically.\n\n## Active Technologies\n\n- **Backend**: NestJS 11, TypeORM 0.3, PostgreSQL 16, Better Auth, class-validator, class-transformer, Helmet\n- **Frontend**: SolidJS, Vite, uPlot (charts), Better Auth client, custom CSS theme\n- **Runtime**: TypeScript 5.x (strict mode). CI pins Node.js 24 (`.github/workflows/release.yml`); no `engines` field enforces this locally.\n- **Monorepo**: npm workspaces + Turborepo\n- **Release**: Changesets for version management + GitHub Actions for Docker image release\n\n## Project Structure\n\n```text\npackages/\n├── backend/\n│   ├── src/\n│   │   ├── instrument.ts                    # Sentry init, imported first (before any other import)\n│   │   ├── main.ts                          # Bootstrap: Helmet, ValidationPipe, Better Auth mount, CORS\n│   │   ├── app.module.ts                    # Root module (guards: ApiKey, Session, Throttler)\n│   │   ├── config/app.config.ts             # Environment variable config\n│   │   ├── auth/\n│   │   │   ├── auth.instance.ts             # Better Auth singleton (email/pass + 3 OAuth + jwt/mcp plugins)\n│   │   │   ├── auth.module.ts               # Registers SessionGuard as APP_GUARD\n│   │   │   ├── session.guard.ts             # Cookie session auth via Better Auth\n│   │   │   ├── current-user.decorator.ts    # @CurrentUser() param decorator\n│   │   │   ├── cli-auth.controller.ts, cli-auth.service.ts # Browser login for the mnfst CLI (PKCE code → `cli` PAT)\n│   │   │   ├── me.controller.ts             # GET /api/v1/me identity probe (tenant, user, auth method, expiry)\n│   │   │   └── mcp-scopes.ts                # mcp:read / mcp:write scope constants (leaf module, no Better Auth import)\n│   │   ├── database/\n│   │   │   ├── database.module.ts           # TypeORM PostgreSQL config\n│   │   │   ├── database-seeder.service.ts   # Seeds demo data (users, agents, security events)\n│   │   │   ├── datasource.ts               # CLI DataSource for migration commands\n│   │   │   ├── pricing-sync.service.ts      # OpenRouter pricing data sync\n│   │   │   ├── ollama-sync.service.ts       # Ollama model sync\n│   │   │   ├── quality-score.util.ts        # Model quality scoring\n│   │   │   └── seed-messages.ts             # Demo request/provider-attempt seed data\n│   │   ├── entities/                        # TypeORM entities (25 entities)\n│   │   │   ├── tenant.entity.ts             # Multi-tenant root\n│   │   │   ├── agent.entity.ts              # Agent (belongs to tenant)\n│   │   │   ├── agent-api-key.entity.ts      # OTLP ingest keys (mnfst_*)\n│   │   │   ├── cli-auth-code.entity.ts      # Short-lived PKCE codes for `mnfst login`\n│   │   │   └── ...                          # request, agent-message (provider attempt), tenant-provider, tier-assignment, header-tier, etc.\n│   │   ├── common/\n│   │   │   ├── guards/api-key.guard.ts      # X-API-Key header auth (timing-safe)\n│   │   │   ├── decorators/public.decorator.ts\n│   │   │   ├── dto/                         # create-agent, range-query, rename-agent DTOs\n│   │   │   ├── filters/spa-fallback.filter.ts\n│   │   │   ├── interceptors/               # agent-cache, user-cache\n│   │   │   ├── constants/                   # api-key, cache, ollama, providers, openai-models, xai-models, subscription-clients\n│   │   │   ├── services/                    # ingest-event-bus, manifest-runtime, tenant-cache, request-recording-storage\n│   │   │   └── utils/                       # crypto, hash, range, period, slugify, url-validation, provider-inference, postgres-sql, cost-calculator, detect-self-hosted, frontend-path, og-rewrite, secret-scrub, ttl-cache, local-ip, etc.\n│   │   ├── health/                          # @Public() health check\n│   │   ├── analytics/                       # Dashboard analytics\n│   │   │   ├── controllers/                 # overview, tokens, costs, messages, agents\n│   │   │   └── services/                    # aggregation + timeseries-queries + query-helpers\n│   │   ├── otlp/                            # Agent key auth + onboarding\n│   │   │   ├── guards/agent-key-auth.guard.ts # Bearer token auth (agent API keys)\n│   │   │   └── services/api-key.service.ts  # Agent onboarding (creates tenant+agent+key)\n│   │   ├── routing/                         # LLM routing (providers, tiers, proxy, scorer)\n│   │   │   ├── proxy/                       # OpenAI-compatible proxy (anthropic/google adapters, fallback chain, attempt recording, caller-classifier)\n│   │   │   ├── autofix/                     # Autofix self-healing (Phoenix client + heal-once flow)\n│   │   │   ├── routing-core/               # Tier, provider, specificity services + cache\n│   │   │   ├── resolve/                     # Scoring-based tier + specificity resolution\n│   │   │   ├── custom-provider/             # Custom provider CRUD (+ `alias`, the public model-id prefix in /v1/models)\n│   │   │   ├── header-tiers/               # Header-based tier overrides\n│   │   │   ├── oauth/                       # OAuth flows (Gemini, OpenAI, Kiro, MiniMax)\n│   │   │   └── specificity.controller.ts   # Specificity routing CRUD endpoints\n│   │   ├── scoring/                         # Request complexity scoring engine\n│   │   │   ├── keywords.ts                 # Keyword lists for all dimensions (complexity + specificity)\n│   │   │   ├── specificity-detector.ts     # Task-type detection (coding, trading, etc.)\n│   │   │   └── scan-messages.ts            # Message scanner for specificity detection\n│   │   ├── model-prices/                    # Model pricing management + sync\n│   │   ├── notifications/                   # Alert rules, email providers, cron\n│   │   ├── playground/                      # Prompt playground (runs, columns, starred/best)\n│   │   ├── github/                          # GitHub stars endpoint\n│   │   ├── sse/                             # Server-Sent Events for real-time updates\n│   │   ├── setup/                           # First-run admin setup wizard\n│   │   ├── free-models/                     # Free LLM model catalog\n│   │   ├── model-discovery/                 # Per-provider model fetching + fallback\n│   │   ├── billing/                         # Stripe billing status + plan limits\n│   │   ├── error-pages/                     # Custom error-page config (internal + public)\n│   │   ├── waitlist/                        # Pivot waiting-list claims + legacy Autofix claim compatibility route\n│   │   ├── discovery/                       # Self-hosted discovery onboarding (forwarded to Peacock)\n│   │   ├── crm-metrics/                     # Cloud-only internal feed: Autofix-healed cohort + waitlist claims + corporate signups (secret-gated)\n│   │   ├── cors-csp-config.ts               # Wingman CORS/CSP origin allowlists\n│   │   ├── sentry/                          # Sentry init-options builder (SENTRY_DSN-gated)\n│   │   ├── telemetry/                       # Anonymous self-hosted telemetry\n│   │   ├── version/                         # GET /api/v1/version — self-hosted \"new version available\" check\n│   │   └── mcp/                             # Remote MCP server (OAuth 2.1 bearer, stateless per-POST, tools/ per domain)\n│   └── test/                                # E2E tests (supertest)\n├── frontend/\n│   ├── src/\n│   │   ├── index.tsx                        # Router setup (App + AuthLayout)\n│   │   ├── components/\n│   │   │   ├── AuthGuard.tsx                # Session check, redirect to /login\n│   │   │   ├── GuestGuard.tsx               # Redirect authenticated users away from auth pages\n│   │   │   ├── SocialButtons.tsx            # 3 OAuth provider buttons\n│   │   │   ├── Header.tsx                   # User session data, logout\n│   │   │   ├── Sidebar.tsx                  # Navigation sidebar\n│   │   │   ├── SetupModal.tsx               # Agent setup wizard modal\n│   │   │   └── ...                          # Charts, modals, pagination, etc.\n│   │   ├── pages/\n│   │   │   ├── Login.tsx, Register.tsx       # Auth pages\n│   │   │   ├── ResetPassword.tsx            # Password reset flow\n│   │   │   ├── Workspace.tsx                # Harness grid/table at /harnesses + create harness\n│   │   │   ├── GlobalOverview.tsx, AgentOverview.tsx # Cross-harness + per-harness dashboards (split from one Overview.tsx)\n│   │   │   ├── AgentDetail.tsx, AgentProviders.tsx   # Per-harness detail shell (tabs) + provider connections\n│   │   │   ├── MessageLog.tsx               # Paginated Requests log (legacy filename)\n│   │   │   ├── Account.tsx                  # User profile (session data)\n│   │   │   ├── Settings.tsx, SettingsAutofixSection.tsx, SettingsRecordingSection.tsx # Harness settings + Autofix + request-recording toggles\n│   │   │   ├── Routing.tsx, RoutingPanels.tsx, RoutingActions.tsx, RoutingDefaultTierSection.tsx, RoutingHeaderTiersSection.tsx, RoutingSpecificitySection.tsx, RoutingTierCard.tsx # LLM routing config (split by concern)\n│   │   │   ├── Limits.tsx                   # Alert rule management — mounted at /harnesses/:agentName/guardrails (\"Guardrails\" tab)\n│   │   │   ├── ModelPrices.tsx              # Model pricing table\n│   │   │   ├── Playground.tsx               # Prompt playground\n│   │   │   ├── ConnectProvider.tsx          # Provider connection flow\n│   │   │   ├── providers/                   # Subscriptions.tsx, Byok.tsx, Local.tsx, ConnectionDetail.tsx, ProviderConnectionsPage.tsx (/providers/* pages)\n│   │   │   ├── Discovery.tsx                # Self-hosted discovery form (one-time, post-signup)\n│   │   │   ├── Welcome.tsx                  # Post-signup welcome flow (+ welcome-helpers.ts)\n│   │   │   ├── FreeModels.tsx               # Free model catalog\n│   │   │   ├── Setup.tsx                    # First-run setup wizard\n│   │   │   ├── Upgrade.tsx                  # Billing/plan upgrade page\n│   │   │   ├── Help.tsx                     # Help page\n│   │   │   ├── AgentLimitsRedirect.tsx, AgentMessagesRedirect.tsx # Legacy per-harness URL redirects\n│   │   │   ├── CliAuth.tsx                  # /cli/auth — approve a `mnfst login` browser request\n│   │   │   ├── Consent.tsx                  # /consent — OAuth consent screen for MCP clients\n│   │   │   └── NotFound.tsx                 # 404 page\n│   │   ├── services/\n│   │   │   ├── auth-client.ts               # Better Auth SolidJS client\n│   │   │   ├── api.ts                       # API functions (credentials: include)\n│   │   │   ├── providers.ts                 # ProviderDef list + SPECIFICITY_STAGES + STAGES\n│   │   │   ├── model-display.ts             # Model display-name cache\n│   │   │   ├── formatters.ts               # Number/cost formatting\n│   │   │   ├── provider-utils.ts            # LLM provider helpers\n│   │   │   ├── routing.ts, routing-utils.ts # Routing config helpers\n│   │   │   ├── theme.ts                     # Theme management\n│   │   │   ├── toast-store.ts               # Toast notification state\n│   │   │   └── ...                          # setup-status, playground-store, pagination, sse, oauth-popup, etc.\n│   │   ├── layouts/                         # Layout components\n│   │   └── styles/\n│   └── tests/\n├── shared/                           # Shared TypeScript types + helpers (consumed by backend and frontend)\n├── cli/                              # `mnfst` management CLI (private; bin/mnfst.js, commands/, generated provider catalog + skill content)\n└── manifest/                         # Code-free shell package holding the canonical Manifest version (see Releases)\n```\n\nBeyond `packages/`, the repo root also carries:\n\n- `integrations/n8n-nodes-manifest/` — the published n8n community node (`n8n-nodes-manifest` on npm); root-level `nodes/` and `credentials/` are identical mirrors of its sources for n8n's community-node scanner.\n- `contracts/gateway/` — gateway API contract tests.\n- `design-system/` — machine-readable design registry (generated `registry/components.json` + `registry/llms.txt`; generator `generate-registry.mjs`).\n- `deploy/` — one-click deployment templates (apple-containers, aws, coolify, digitalocean, easypanel, fly, gcp, heroku, koyeb).\n- `docs/` — `glossary.md` (canonical terminology contract), `autofix-self-healing-poc.md`, `model-parameters-schema.md`, `providers/subscription-based-providers.md`.\n\n## Single-Service Deployment\n\nThe app deploys as a **single service**. In production, NestJS serves both the API and the frontend static files from the same port.\n\n```bash\nnpm run build     # Turborepo: frontend (Vite) then backend (Nest)\nnpm start         # node packages/backend/dist/main.js — serves frontend + API\n```\n\n- API routes (`/api/*`, `/otlp/*`) are excluded from static file serving.\n- Dev mode: Vite on `:3000` proxies `/api` and `/otlp` to backend on `:3001`.\n- **Manifest Cloud** runs on Cloud Run (`manifest`, project `manifest-platform-457808`, `us-east4`) behind the shared Google HTTPS load balancer, with Cloud SQL Postgres on a private IP. Every push to `main` deploys through `.github/workflows/deploy.yml` (keyless Workload Identity Federation; `mnfst/*` on `main` only): it builds `docker/Dockerfile.cloud` (the self-hosted `docker/Dockerfile` minus `VITE_MANIFEST_SELFHOSTED`, keep both in step), runs the `manifest-migrate` job, then swaps the service image. Env vars, secrets (Secret Manager) and scaling live on the service; it sets `MANIFEST_MODE=cloud`, `RUN_MIGRATIONS_ON_BOOT=false`, `TRUST_PROXY=2` (load balancer + Cloud Run hops) and `SHUTDOWN_DRAIN_MS=0`. `railway.toml` is the old Railway config, kept until Railway is decommissioned.\n\n## Commands\n\n### Starting the Dev Server\n\nThe backend requires a `.env` file at `packages/backend/.env` with at least `BETTER_AUTH_SECRET` (32+ chars). The `auth.instance.ts` reads `process.env` at import time, before NestJS `ConfigModule` loads `.env`, so env vars must be available to the Node process.\n\n**Quick start (run these in parallel):**\n\n```bash\n# Backend — must preload dotenv since auth.instance.ts reads process.env at import time\ncd packages/backend && NODE_OPTIONS='-r dotenv/config' npx nest start --watch\n\n# Frontend\ncd packages/frontend && npx vite\n```\n\n**Note:** `npm run dev` (turbo) starts the frontend but NOT the backend, because the backend's script is `start:dev` not `dev`. Start the backend separately as shown above.\n\n### Seeding Dev Data\n\nSet `SEED_DATA=true` in `packages/backend/.env` to seed on startup (dev/test only). This creates:\n\n- **Admin user**: `admin@manifest.build` / `manifest` (email verification email is skipped if Mailgun is not configured — user is created but unverified)\n- **Tenant**: `seed-tenant-001` linked to the admin user\n- **Agent**: `demo-agent` with OTLP key `dev-otlp-key-001`\n- **API key**: `dev-api-key-manifest-001`\n- **Security events**: 12 sample events for the security dashboard\n- **Requests and provider attempts**: Sample telemetry for the demo agent\n\nSeeding is idempotent — it checks for existing records before inserting.\n\n**Dev-login shortcut:** when running under the Vite dev server the login page shows a\nprominent one-click **⚡ Sign in as dev** button that submits the seeded\n`admin@manifest.build` / `manifest` credentials — no copy-paste. It's gated by\n`import.meta.env.DEV`, so Vite strips the button and the credential literals from\nproduction builds, and no password ever rides in a URL. See\n`packages/frontend/src/pages/Login.tsx`.\n\n**Minimal `.env` for development:**\n\n```env\nPORT=3001\nBIND_ADDRESS=127.0.0.1\nNODE_ENV=development\nBETTER_AUTH_SECRET=<random-hex-64-chars>\nDATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase\nAPI_KEY=dev-api-key-12345\nSEED_DATA=true\n```\n\nGenerate a secret with: `openssl rand -hex 32`\n\n**Database naming convention:** Always create uniquely-named databases to avoid overlapping other dev/test instances. Use the pattern `manifest_<context>_<random>` (e.g., `manifest_sse_49821`, `manifest_dev_83712`). Create databases via Docker:\n\n```bash\ndocker exec postgres_db psql -U myuser -d postgres -c \"CREATE DATABASE manifest_<name>;\"\n```\n\nThen set `DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/manifest_<name>` in `.env`.\n\n```bash\n# Production build + start (single server)\nnpm run build && npm start\n\n# Tests\nnpm test --workspace=packages/backend          # Jest unit tests\nnpm run test:e2e --workspace=packages/backend  # Jest e2e tests\nnpm test --workspace=packages/frontend         # Vitest tests\nnpm test --workspace=packages/shared           # Jest unit tests\nnpm test --workspace=packages/cli              # Jest unit tests (mnfst CLI)\n```\n\n### Database Migrations\n\nTypeORM migrations run automatically on app startup by default (gated by `RUN_MIGRATIONS_ON_BOOT`, default `true`). Schema sync (`synchronize`) is permanently disabled — all schema changes must go through migrations.\n\n**Dev workflow:** modify entity → generate migration → commit both.\n\n```bash\n# Generate a migration after changing an entity\ncd packages/backend\nnpm run migration:generate -- src/database/migrations/DescriptiveName\n\n# Other migration commands\nnpm run migration:run       # Run pending migrations\nnpm run migration:revert    # Revert the last migration\nnpm run migration:show      # Show migration status ([X] = applied)\nnpm run migration:create -- src/database/migrations/Name  # Create empty migration\n```\n\nNew migrations must be imported in `database.module.ts` and added to the `migrations` array.\n\n**Important**: Always use unique timestamps for new migrations. Never reuse a timestamp from an existing migration file.\n\n## Authentication Architecture\n\n### Guard Chain\n\nThree global guards run on every request (order matters):\n\n1. **SessionGuard** (`auth/session.guard.ts`) — Checks `@Public()` first. If not public, validates the Better Auth cookie session via `auth.api.getSession()`. Attaches `request.user` and `request.session`.\n2. **ApiKeyGuard** (`common/guards/api-key.guard.ts`) — Falls through if session already set. Otherwise reads the `X-API-Key` header and first looks it up against the tenant-scoped `ApiKey` entity (`api_keys` table, hashed with scrypt) — this is the primary multi-tenant credential path. Only if no DB match is found does it fall back to a timing-safe compare against the single `API_KEY` env var. Use `@Public()` to skip both guards.\n3. **ThrottlerGuard** — Rate limiting.\n\n`mnfst` CLI tokens are ordinary `api_keys` rows named `cli` (`CLI_KEY_NAME` in `auth/cli-auth.service.ts`), so they ride the `ApiKeyGuard` path with no special casing; the guard also enforces their sliding and absolute TTLs. The remote MCP route is `@Public()` because it carries its own credential (an OAuth 2.1 bearer) — see [Remote MCP server](#remote-mcp-server).\n\n### Better Auth Setup\n\n- **Instance**: `auth/auth.instance.ts` — `betterAuth()` with `emailAndPassword` + 3 social providers (Google, GitHub, Discord). Each provider only activates when both `CLIENT_ID` and `CLIENT_SECRET` env vars are set. Plugins: `jwt()` and `@better-auth/mcp`'s `mcp()` (the OAuth 2.1 authorization server for the remote MCP endpoint, resource-bound to `${BETTER_AUTH_URL}/api/v1/mcp`, scopes from `auth/mcp-scopes.ts`).\n- **Mounting**: In `main.ts`, Better Auth is mounted as Express middleware at `/api/auth/*splat` **before** `express.json()` (it needs raw body control). NestJS body parsing is re-added after for all other routes.\n- **Frontend client**: `services/auth-client.ts` — `createAuthClient()` from `better-auth/solid`.\n- **Social login in dev**: OAuth callback URLs point to `:3001` (`BETTER_AUTH_URL`). Social login only works when accessing the app on port **3001** (production build), not on Vite's `:3000` dev server.\n\n### Auth Types\n\n```typescript\n// backend/src/auth/auth.instance.ts\nexport type AuthSession = typeof auth.$Infer.Session;\nexport type AuthUser = typeof auth.$Infer.Session.user;\n\n// Use in controllers:\n@Get('something')\nasync handler(@CurrentUser() user: AuthUser) {\n  // user.id, user.name, user.email\n}\n```\n\n### CLI login (`mnfst login`)\n\n`packages/cli/` is the `mnfst` management CLI: a thin wrapper over the `/api/v1` REST API (JSON on stdout, exit `0`/`1`, never interactive mid-task). One global tenant credential unlocks every command; per-agent `mnfst_*` keys are outputs only (written to `--key-file` paths with mode `0600`), never inputs. The package publishes to npm as `mnfst-gateway-cli` (`npm i -g mnfst-gateway-cli`, command still `mnfst`); from the monorepo, build with `npm run build --workspace=packages/shared && npm run build --workspace=packages/cli` and run `node packages/cli/bin/mnfst.js`. `packages/cli/README.md` is the user-facing reference; the repo also ships a `mnfst-cli` skill under `.claude/skills/`.\n\nThe default login is a browser flow, PKCE-protected so an intercepted code is useless without the verifier:\n\n1. The CLI opens the dashboard at `/cli/auth` (`pages/CliAuth.tsx`) with a `state` and an S256 `code_challenge`.\n2. The signed-in user approves; the page calls `POST /api/v1/cli/authorize` (session-only — an API key caller gets 403) and receives a short-lived code stored in `cli_auth_codes`.\n3. The CLI exchanges it at `POST /api/v1/cli/token` (public — the code is the credential, with `state` + `code_verifier`) for a `cli` PAT in `api_keys`; `expiresAt` is ISO-8601 UTC.\n4. `DELETE /api/v1/cli/token` revokes the calling PAT on `mnfst logout` (best-effort).\n\nNon-interactive alternatives: `--token-stdin` or `--token-env`. The secret is never a CLI argument. `GET /api/v1/me` is the identity probe (`{ tenantId, userId, authMethod, expiresAt }`); it fails closed with 401 for the env `API_KEY` operator fallback because that credential attaches no tenant. Token lifetime: `CLI_TOKEN_TTL_DAYS` (sliding, default 30) capped by `CLI_TOKEN_ABSOLUTE_TTL_DAYS` (hard ceiling from issuance, default 90).\n\nThe CLI refuses HTTP redirects (so the key cannot leak cross-origin) and strips inherited `MANIFEST_AGENT_KEY` / `MANIFEST_API_KEY` from `mnfst run` children. `mnfst routing test` sends `User-Agent: mnfst-cli/<version>` so gateway caller attribution (`routing/proxy/caller-classifier.ts`, which also recognises the n8n node's `n8n-nodes-manifest/<version>` UA) can tell CLI test traffic apart from real agent traffic in request details.\n\n### Remote MCP server\n\n`POST /api/v1/mcp` (`mcp/mcp.controller.ts`) is a remote MCP server over stateless Streamable HTTP: a fresh `McpServer` per POST (`mcp-server.factory.ts`), closed when the request ends, so no session affinity is needed. GET and DELETE answer **405 + `Allow: POST`** as the spec requires for a stateless JSON transport (a 404 made clients log an error on every connect).\n\n- **Auth**: the route is `@Public()` to skip the session/API-key guards, and `requireMcpAuth` from `@better-auth/mcp` is the real gate. Better Auth's MCP plugin is the authorization server (PKCE, resource-bound JWT access tokens, CIMD client identity, RFC 8414/9728 discovery). A missing or dead token answers 401 with `WWW-Authenticate: … resource_metadata=…`, which is how a client learns it must run OAuth. `mcp-auth.ts` maps the verified token's `sub` to a tenant through `TenantCacheService`, so tools are scoped exactly like the dashboard and CLI; a tenant-less user gets 401.\n- **Availability**: MCP is on by default and off in two cases, both decided by `auth/mcp-availability.ts` before Better Auth is built: the operator set `MCP_ENABLED=false`, or the MCP resource (`${BETTER_AUTH_URL}/api/v1/mcp`) is not HTTPS and not loopback. The second case is **not** opt-in on purpose — `mcp()` validates its resource URL as it is constructed and throws, and that throw happens while `auth.instance.ts` is being imported, so an HTTP-only self-hosted install would refuse to boot at all (issue #2939). When MCP is off, `buildPlugins()` omits `mcp()` and `cimd()`, `app.module.ts` leaves `McpModule` unregistered so `/api/v1/mcp` does not exist, `main.ts` mounts `mountMcpUnavailable()` so the well-known paths answer a JSON 404 rather than the SPA shell, and the dashboard hides the MCP nav entry (`GET /api/v1/setup/status` carries `mcpEnabled`). `mcp-availability.ts` is a leaf module for the same reason `mcp-scopes.ts` is — it must be importable from the unit suite, which cannot load Better Auth's ESM.\n- **Discovery**: `mcp-discovery.ts` (mounted in `main.ts`) serves the OAuth documents at the well-known **root** paths (`/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource[/api/v1/mcp]`) in addition to Better Auth's `/api/auth/.well-known/…`, because clients start from the MCP origin's root.\n- **Consent**: `pages/Consent.tsx` at `/consent` is the OAuth consent screen.\n- **Scopes**: `mcp:read` is required for every token; each `register*Tools()` returns before registering its write tools unless `operator.scopes` has `mcp:write`, so a read-only client never sees them (the server `instructions` string tells it to reconnect with the write scope). Revealing an ingest key also requires `mcp:write` because it is a live secret.\n- **Tools** (`mcp/tools/*.tools.ts`): identity, agents, environment (+ a `doctor` that walks dependencies in order), providers (including custom providers), routing (status, fallbacks, Autofix, recording, custom/header tiers, per-tier model params), models/pricing, and the request ledger. They call the same services as the REST controllers through `McpToolDeps` (`tool-deps.ts`); `tool-result.ts` is the shared result envelope.\n- **Testing**: `auth.instance.ts` is ESM-only and cannot load under Jest on Node 22, which is why the scope constants live in the leaf module `auth/mcp-scopes.ts` — import from there in guards, tools, and specs.\n\n## Multi-Tenancy Model\n\n```\nUser (Better Auth) ──→ Tenant ──→ Agent ──→ AgentApiKey (mnfst_*)\n                                    │\n                                    └──→ requests ──→ agent_messages (telemetry data)\n```\n\n- **Tenant** (`tenants` table): Created automatically on first agent creation. `tenant.owner_user_id` = `user.id` is the ONLY user→tenant link (resolved through `TenantCacheService`); `tenant.name` mirrors it for display until repurposed as a slug.\n- **Agent** (`agents` table): Belongs to a tenant. Unique constraint on `[tenant_id, name]`.\n- **AgentApiKey** (`agent_api_keys` table): One-to-one with agent. `mnfst_*` format key for OTLP ingestion.\n- **ApiKey** (`api_keys` table): A separate, tenant-scoped credential (not per-agent) used for dashboard/API access — the primary key `ApiKeyGuard` checks. Distinct from `AgentApiKey`. `mnfst login` PATs are rows of this table named `cli`.\n- **Onboarding flow**: `ApiKeyGeneratorService.onboardAgent()` creates tenant (if new) + agent + API key via three sequential inserts.\n\n### Data Isolation\n\nEvery resource belongs to a tenant; users only authenticate and (optionally) appear as `created_by_user_id` audit metadata. Guards (SessionGuard/ApiKeyGuard) resolve the tenant once per request and attach a `TenantContext` (`{ tenantId, userId }`), injected in controllers via `@TenantCtx()`. All analytics queries filter by tenant via `addTenantFilter(qb, tenantId)` from `query-helpers.ts`. Never scope, key, cache, or authorize by user id.\n\n## API Endpoints\n\n| Method                    | Route                                           | Auth                                | Purpose                                                                                                     |\n| ------------------------- | ----------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |\n| GET                       | `/api/v1/health`                                | Public                              | Health check                                                                                                |\n| ALL                       | `/api/auth/*`                                   | Public                              | Better Auth (login, register, OAuth, sessions)                                                              |\n| GET                       | `/api/v1/overview`                              | Session/API Key                     | Dashboard summary                                                                                           |\n| GET                       | `/api/v1/tokens`                                | Session/API Key                     | Token usage analytics                                                                                       |\n| GET                       | `/api/v1/costs`                                 | Session/API Key                     | Cost analytics                                                                                              |\n| GET                       | `/api/v1/agents`                                | Session/API Key                     | Agent list with sparklines                                                                                  |\n| POST                      | `/api/v1/agents`                                | Session/API Key                     | Create agent + API key                                                                                      |\n| GET                       | `/api/v1/agents/:agentName`                     | Session/API Key                     | Single agent detail                                                                                         |\n| GET/POST                  | `/api/v1/agents/:agentName/duplicate*`          | Session/API Key                     | Duplicate agent (preview + confirm)                                                                         |\n| DELETE                    | `/api/v1/agents/:agentName`                     | Session/API Key                     | Delete agent                                                                                                |\n| GET                       | `/api/v1/agents/:agentName/key`                 | Session/API Key                     | Get agent API key                                                                                           |\n| POST                      | `/api/v1/agents/:agentName/rotate-key`          | Session/API Key                     | Rotate API key                                                                                              |\n| PATCH                     | `/api/v1/agents/:agentName`                     | Session/API Key                     | Rename agent                                                                                                |\n| GET                       | `/api/v1/messages`                              | Session/API Key                     | Paginated Requests log (legacy route name)                                                                  |\n| GET/PATCH/DELETE          | `/api/v1/messages/:id/*`                        | Session/API Key                     | Request details, feedback, miscategorized flag (legacy route name)                                          |\n| GET                       | `/api/v1/security`                              | Session/API Key                     | Security score + events                                                                                     |\n| GET                       | `/api/v1/model-prices`                          | Session/API Key                     | Model pricing list                                                                                          |\n| GET                       | `/api/v1/free-models`                           | Session/API Key                     | Free LLM model catalog                                                                                      |\n| GET                       | `/api/v1/agent/usage`                           | Bearer (mnfst\\_\\*)                  | Token usage for the calling agent                                                                           |\n| GET                       | `/api/v1/agent/costs`                           | Bearer (mnfst\\_\\*)                  | Cost data for the calling agent                                                                             |\n| GET                       | `/api/v1/overview/*`                            | Session/API Key                     | Overview timeseries/breakdown sub-endpoints                                                                 |\n| GET                       | `/api/v1/providers` / `/api/v1/providers/usage` | Session/API Key                     | Connected provider list + usage                                                                             |\n| GET                       | `/api/v1/provider-analytics/*`                  | Session/API Key                     | Per-provider analytics                                                                                      |\n| GET                       | `/api/v1/errors/breakdown`                      | Session/API Key                     | Error breakdown analytics                                                                                   |\n| GET/PATCH                 | `/api/v1/billing/*`                             | Session/API Key                     | Billing status, light `plan` endpoint, email preferences (Stripe)                                           |\n| POST                      | `/api/v1/waitlist/autofix/claim`                | Public                              | Deprecated no-op compatibility route for older self-hosted versions                                         |\n| POST                      | `/api/v1/waitlist/pivot/claim`                  | Public                              | Closed pivot waiting-list claim, kept for older self-hosted versions; records origin (cloud/self-hosted)   |\n| GET/POST                  | `/api/v1/autofix/status` / `.../enable-all`     | Session/API Key                     | Workspace Autofix coverage + API-only fleet enable (no dashboard caller)                                    |\n| GET                       | `/api/v1/overview/autofix-*`                    | Session/API Key                     | Autofix analytics (stats, timeseries, per-agent/provider/model)                                             |\n| POST                      | `/api/v1/discovery/complete`                    | Session/API Key                     | Best-effort self-hosted discovery submission forwarded to Peacock                                           |\n| GET/POST/DELETE           | `/api/v1/internal/error-pages*`                 | Public (`x-internal-secret` header) | Custom error-page config (Peacock CMS push API)                                                             |\n| GET                       | `/api/v1/internal/crm-metrics*`                 | Public (`x-internal-secret` header) | **Cloud only.** Autofix-healed user cohort, pivot waitlist claims, and corporate signups, for the outreach CRM (`CRM_METRICS_SECRET`) |\n| GET/PUT/DELETE            | `/api/v1/agents/:agentName/enabled-providers*`  | Session/API Key                     | Per-agent provider enable/disable + impact preview                                                          |\n| GET/POST/PATCH/DELETE     | `/api/v1/notifications/*`                       | Session/API Key                     | Notification rules CRUD + email provider config                                                             |\n| GET/POST/PUT/PATCH/DELETE | `/api/v1/routing/:agentName/*`                  | Session/API Key                     | Routing config (tiers, providers, model-params, header-tiers, custom-providers, specificity, autofix, recording, etc.) |\n| GET/PATCH                 | `/api/v1/routing/:agentName/tiers/:tier/model-params` | Session/API Key                     | Model params addressed by tier (`default` or a custom tier name) + `?model=` (defaults to the tier primary); backs `mnfst routing params` and the MCP `manifest_routing_params_*` tools. Writes the same rows as the scope-keyed `…/model-params` |\n| POST                      | `/api/v1/routing/ollama/sync`                   | Session/API Key                     | Sync Ollama models                                                                                          |\n| GET                       | `/api/v1/routing/pricing-health`                | Session/API Key                     | OpenRouter pricing sync health                                                                              |\n| POST                      | `/api/v1/routing/pricing/refresh`               | Session/API Key                     | Force pricing cache refresh                                                                                 |\n| GET/POST/DELETE           | `/api/v1/oauth/:provider/*`                     | Session/API Key                     | OAuth flows (Gemini, OpenAI, Anthropic, xAI, Kiro, MiniMax)                                                 |\n| POST                      | `/api/v1/routing/resolve`                       | Bearer (mnfst\\_\\*)                  | Model resolution                                                                                            |\n| POST                      | `/api/v1/routing/subscription-providers`        | Bearer (mnfst\\_\\*)                  | Subscription provider config                                                                                |\n| GET                       | `/api/v1/setup/status`                          | Public                              | First-run setup status                                                                                      |\n| POST                      | `/api/v1/setup/admin`                           | Public                              | Create initial admin user                                                                                   |\n| GET                       | `/api/v1/public/error-pages*`                   | Public (opt-in)                     | Published error pages for the marketing site (controlled by `MANIFEST_PUBLIC_STATS`)                        |\n| GET                       | `/v1/models`                                    | Bearer (mnfst\\_\\*)                  | Available model list (proxy)                                                                                |\n| POST                      | `/v1/chat/completions`                          | Bearer (mnfst\\_\\*)                  | LLM proxy (OpenAI-compatible)                                                                               |\n| POST                      | `/v1/responses`                                 | Bearer (mnfst\\_\\*)                  | LLM proxy (OpenAI Responses API)                                                                            |\n| POST                      | `/v1/messages`                                  | Bearer (mnfst\\_\\*)                  | LLM proxy (Anthropic Messages API)                                                                          |\n| POST                      | `/chat/completions`                             | Public                              | Structured 404 pointing callers at `/v1/chat/completions` (missing `/v1` basePath — not a proxy alias)      |\n| ALL                       | `/otlp/v1/*`, `/v1/{traces,metrics,logs}`       | Public                              | Structured **410 Gone** — OTLP ingest is removed (`otlp-deprecated.controller.ts`)                          |\n| GET/POST/PATCH            | `/api/v1/playground/*`                          | Session/API Key                     | Playground runs (run, list, star, mark best)                                                                |\n| GET                       | `/api/v1/events`                                | Session                             | SSE real-time events                                                                                        |\n| GET                       | `/api/v1/github/stars`                          | Public                              | GitHub star count                                                                                           |\n| GET                       | `/api/v1/version`                               | Session/API Key                     | Running version + latest GitHub release for the self-hosted update badge (24h cache; off in cloud)          |\n| GET                       | `/api/v1/me`                                    | Session/API Key (tenant required)   | Identity probe for CLI/API clients: `{ tenantId, userId, authMethod, expiresAt }`; env `API_KEY` gets 401    |\n| POST                      | `/api/v1/cli/authorize`                         | Session only                        | Browser side of `mnfst login`: mints a PKCE-bound short-lived code                                          |\n| POST/DELETE               | `/api/v1/cli/token`                             | Public (code + verifier) / API Key  | Exchange the code for a `cli` PAT; DELETE revokes the calling PAT on logout                                 |\n| POST                      | `/api/v1/mcp`                                   | OAuth 2.1 bearer (`mcp:read`/`mcp:write`) | Remote MCP server (stateless Streamable HTTP); GET/DELETE answer 405 + `Allow: POST`                  |\n| GET                       | `/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource[/api/v1/mcp]` | Public | OAuth discovery for MCP clients (root aliases of Better Auth's `/api/auth/.well-known/…`)   |\n\n## Environment Variables\n\nSee `packages/backend/.env.example` for all variables. Key ones:\n\n- `BETTER_AUTH_SECRET` — **Required.** Secret for Better Auth session signing (min 32 chars). Generate with `openssl rand -hex 32`.\n- `DATABASE_URL` — **Required** in every environment except `NODE_ENV=test` (which falls back to `postgresql://myuser:mypassword@localhost:5432/mydatabase`, matching the local Docker command). Dev and production both throw on boot if unset. Format: `postgresql://user:password@host:port/database`.\n- `MANIFEST_ENCRYPTION_KEY` — Recommended. AES-256-GCM key (min 32 chars) for encrypting stored provider API keys and OAuth tokens. Defaults to `BETTER_AUTH_SECRET` if unset — set this independently so a session-cookie leak doesn't also expose provider credentials.\n- `MANIFEST_ENCRYPTION_KEY_PREVIOUS` — Optional, rotation only. A secret that stored values may still be encrypted under. Decryption always tries `MANIFEST_ENCRYPTION_KEY`, then this, then `BETTER_AUTH_SECRET`, so old rows stay readable. While it is set, one replica rewrites every row found under an older secret onto the current one after boot (`database/secret-reencryption.service.ts`, advisory-locked, never blocks or fails boot); a normal boot without it never scans. Rotation: set the new `MANIFEST_ENCRYPTION_KEY` and `MANIFEST_ENCRYPTION_KEY_PREVIOUS=<old>`, deploy, wait for the log line `Nothing left under an older secret`, then remove `PREVIOUS`. Introducing a dedicated key on an install that used the `BETTER_AUTH_SECRET` fallback: set `MANIFEST_ENCRYPTION_KEY` (old rows keep decrypting via the session secret); to move them onto the new key, set `PREVIOUS` to the session secret's value for one deploy. Request recordings are not rewritten by this pass: they decrypt under the current key only, so a key change makes older recordings unreadable until retention removes them.\n- `PORT` — Server port. Default: `3001`\n- `BIND_ADDRESS` — Bind address. Default: `127.0.0.1` (use `0.0.0.0` for Railway/Docker)\n- `NODE_ENV` — `development` or `production`. Dev allows broad CORS (local dashboard + Wingman); production allows the hosted Wingman origin plus any `WINGMAN_CORS_ORIGINS` entries.\n- `CORS_ORIGIN` — Allowed CORS origin (dev). Default: `http://localhost:3000`\n- `WINGMAN_CORS_ORIGINS` — Production only. Extra browser origins allowed to call the gateway (comma-separated). The hosted Wingman (`https://wingman.manifest.build`) is always allowed.\n- `BETTER_AUTH_URL` — Base URL for Better Auth. Default: `http://localhost:{PORT}`\n- `FRONTEND_PORT` \n\n<!-- [TRUNCATED AT 50KB] -->","packages/backend/src/routing/autofix/contract/phoenix-openapi.yaml":"openapi: 3.1.0\ninfo:\n  title: Phoenix MVP — request-healing API\n  version: 0.0.1\n  description: >\n    Advisory request-healing for LLM providers. Submit a failed request plus the\n    provider error to get a corrected request body, then report whether the retry\n    worked. See README.md for the full loop and data model.\nservers:\n  - url: http://127.0.0.1:3000\n    description: Local development\n  - url: https://your-app.up.railway.app\n    description: Production — set to your Railway deployment URL\n\nsecurity:\n  - ApiKeyAuth: []\n\npaths:\n  /api/heal:\n    post:\n      summary: Resolve a patch for a failed request\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      description: >\n        Fingerprints the error and returns a healed request body if a\n        deterministic patch exists. Only request-side errors are healable.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/HealRequest'\n      responses:\n        '200':\n          description: A heal decision (patched, pending confirmation, still resolving, or no patch).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HealResult'\n        '400':\n          description: Malformed body (failed schema validation).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ZodError'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '422':\n          description: Error is not request-side (5xx / auth / rate-limit); nothing to heal.\n\n  /api/heal/observe:\n    post:\n      summary: Record failed requests as diagnostic evidence\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ObservationBatch'\n      responses:\n        '200':\n          description: Observation batch accepted.\n        '401':\n          description: Missing or invalid static key or instance credential.\n\n  /api/heal-attempts/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read a heal attempt and its parent issue\n      responses:\n        '200':\n          description: The heal attempt.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HealAttemptView'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n    patch:\n      summary: Report the outcome of a retried request\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/HealOutcome'\n      responses:\n        '200':\n          description: Recorded outcome (idempotent).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OutcomeResult'\n        '400':\n          description: Malformed body, or error omitted when retryStatusCode >= 400.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ZodError'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: No such heal attempt.\n\n  /api/issues/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read an issue and its resolved patch\n      responses:\n        '200':\n          description: The issue.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/IssueView'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n\n  /api/issues:\n    get:\n      summary: List issues (most-recently-updated first)\n      parameters:\n        - name: patchId\n          in: query\n          schema: { type: string, format: uuid }\n          description: Only issues fixed by this patch.\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Issues.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/IssueListItem' }\n        '400':\n          description: Malformed query.\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/heal-attempts:\n    get:\n      summary: List heal attempts (most-recent first)\n      parameters:\n        - name: issueId\n          in: query\n          schema: { type: string, format: uuid }\n        - name: patchId\n          in: query\n          schema: { type: string, format: uuid }\n        - name: traceId\n          in: query\n          schema: { type: string }\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Heal attempts.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/AttemptListItem' }\n        '400':\n          description: Malformed query.\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/patches:\n    get:\n      summary: List patches with usage counts (most-recent first)\n      parameters:\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Patches.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/PatchListItem' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/patches/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read a patch and the issues it fixes\n      responses:\n        '200':\n          description: The patch.\n          content:\n            application/json:\n              schema: { $ref: '#/components/schemas/PatchDetail' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n\n  /api/traces:\n    get:\n      summary: List traces with per-trace outcome counts (most-recent first)\n      parameters:\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Traces.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/TraceListItem' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/traces/{traceId}:\n    parameters:\n      - name: traceId\n        in: path\n        required: true\n        schema: { type: string }\n    get:\n      summary: A trace's heal attempts, oldest-first\n      responses:\n        '200':\n          description: The trace timeline.\n          content:\n            application/json:\n              schema: { $ref: '#/components/schemas/TraceDetail' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: No such trace.\n\n  /api/health:\n    get:\n      summary: Liveness probe (public — no API key)\n      security: []\n      responses:\n        '200':\n          description: Process is up.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  status: { type: string, example: ok }\n                  uptimeSeconds: { type: integer }\n\n  /api/ready:\n    get:\n      summary: Readiness probe — process up and database reachable (public)\n      security: []\n      responses:\n        '200':\n          description: Ready.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  status: { type: string, example: ready }\n        '503':\n          description: Database not ready.\n\ncomponents:\n  securitySchemes:\n    ApiKeyAuth:\n      type: apiKey\n      in: header\n      name: x-api-key\n      description: >\n        Static API key. Send as `x-api-key: <key>` (an `Authorization: Bearer\n        <key>` header is also accepted). Enforced in deployed environments where\n        the API_KEY env var is set; disabled locally when it is unset.\n    ManifestInstanceAuth:\n      type: apiKey\n      in: header\n      name: X-Manifest-Instance\n      description: >\n        Anonymous install identifier announced by a self-hosted Manifest. This\n        is an identifier, not a credential: it is not secret and no registration\n        handshake issues it. Phoenix creates the instance row the first time it\n        sees an id. Accepted in place of ApiKeyAuth on the gateway write\n        operations.\n\n  parameters:\n    Id:\n      name: id\n      in: path\n      required: true\n      schema:\n        type: string\n        format: uuid\n    Limit:\n      name: limit\n      in: query\n      description: Page size (default 50, max 200).\n      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }\n    Offset:\n      name: offset\n      in: query\n      description: Rows to skip for pagination.\n      schema: { type: integer, minimum: 0, default: 0 }\n    ManifestVersion:\n      name: X-Manifest-Version\n      in: header\n      required: false\n      description: Running Manifest release version. Invalid values become `unknown` in analytics.\n      schema:\n        type: string\n        maxLength: 32\n        pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'\n    ManifestHarness:\n      name: X-Manifest-Harness\n      in: header\n      required: false\n      description: Agent platform slug. Missing or unknown values become `other` in analytics.\n      schema:\n        type: string\n        enum:\n          [\n            openclaw,\n            hermes,\n            nanobot,\n            craft,\n            n8n,\n            claude-code,\n            opencode,\n            codex,\n            openai-sdk,\n            anthropic-sdk,\n            vercel-ai-sdk,\n            langchain,\n            curl,\n            other,\n          ]\n  schemas:\n    InstanceRegistration:\n      type: object\n      required: [version, schema_version]\n      properties:\n        version:\n          type: string\n          maxLength: 32\n          pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'\n        schema_version:\n          type: integer\n          const: 1\n      additionalProperties: true\n\n    InstanceCredential:\n      type: object\n      required: [instance_id, secret]\n      properties:\n        instance_id: { type: string, format: uuid }\n        secret:\n          type: string\n          description: Opaque 256-bit base64url bearer token. Returned only once.\n      additionalProperties: false\n\n    ObservationBatch:\n      type: object\n      required: [observations]\n      properties:\n        observations:\n          type: array\n          minItems: 1\n          maxItems: 200\n          items: { $ref: '#/components/schemas/HealRequest' }\n\n    ProviderError:\n      type: object\n      required: [message]\n      properties:\n        message: { type: string }\n        type: { type: [string, 'null'] }\n        param: { type: [string, 'null'] }\n        code: { type: [string, 'null'] }\n\n    Operation:\n      type: object\n      description: A deterministic transform applied to the request body.\n      required: [type]\n      properties:\n        type: { type: string }\n      additionalProperties: true\n      example: { type: rename_param, from: max_tokens, to: max_output_tokens }\n\n    HealRequest:\n      type: object\n      required: [traceId, provider, api, request, response]\n      properties:\n        traceId:\n          type: string\n          minLength: 1\n          description: >\n            Caller-supplied id of the logical request being healed. Send the\n            same value on every heal of that request's retry chain (not the\n            provider's per-call request id) — it groups the attempts into one\n            trace.\n        tenantId:\n          type: string\n          minLength: 1\n          description: >\n            Opaque id of the gateway tenant this request belongs to (Manifest's\n            Tenant.id). Optional on the wire; recorded so a failure can be\n            attributed to a tenant. Manifest always sends it on the proxy path.\n        provider: { type: string, minLength: 1, example: openai }\n        model:\n          type: string\n          minLength: 1\n          maxLength: 1024\n          description: >\n            Logical provider model identity. Native transports may encode it in\n            the URL rather than in the JSON request body.\n        authType:\n          type: string\n          enum: [api_key, subscription, local]\n          description: Provider credential route. Optional during the Manifest rollout.\n        api:\n          type: string\n          enum: [responses, chat_completions, messages]\n        url: { type: string, format: uri }\n        request:\n          type: object\n          description: The original outgoing request body, verbatim.\n          additionalProperties: true\n        response:\n          type: object\n          required: [statusCode, error]\n          properties:\n            statusCode: { type: integer, example: 400 }\n            error: { $ref: '#/components/schemas/ProviderError' }\n        providerExchange:\n          type: object\n          description: Final provider-native transport exchange, separate from the patch input.\n          required: [format, request, response]\n          properties:\n            format:\n              type: string\n              enum:\n                - openai_chat_completions\n                - openai_responses\n                - anthropic_messages\n                - google_generate_content\n                - google_code_assist\n            url: { type: string, format: uri }\n            request:\n              type: object\n              required: [body]\n              properties:\n                body: { type: object, additionalProperties: true }\n                redactedFields: { type: array, items: { type: string } }\n            response:\n              type: object\n              required: [statusCode, body]\n              properties:\n                statusCode: { type: integer }\n                body: {}\n        responseTimeMs: { type: integer, minimum: 0 }\n        responseSizeBytes: { type: integer, minimum: 0 }\n\n    HealResult:\n      type: object\n      required: [status, issueId]\n      properties:\n        status:\n          type: string\n          enum: [patched, unverified, resolving, no_patch]\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        healAttemptId: { type: string, format: uuid }\n        operations:\n          type: array\n          items: { $ref: '#/components/schemas/Operation' }\n        healedBody:\n          type: object\n          additionalProperties: true\n        explanation:\n          type: object\n          description: >\n            Human-readable \"why\" for a served heal (present on \"patched\" /\n            \"unverified\"). operations[].detail is always deterministic (built from\n            the operations); summary prefers an authored rationale (source \"agent\" /\n            \"operator\") and falls back to the deterministic composition (source\n            \"deterministic\").\n          required: [summary, operations, source]\n          properties:\n            summary: { type: string }\n            operations:\n              type: array\n              items:\n                type: object\n                required: [type, detail]\n                properties:\n                  type: { type: string }\n                  detail: { type: string }\n            source:\n              type: string\n              enum: [deterministic, agent, operator]\n        retryAfterMs:\n          type: integer\n          description: Present only when status is \"resolving\".\n\n    HealOutcome:\n      type: object\n      required: [retryStatusCode]\n      description: error is required when retryStatusCode >= 400.\n      properties:\n        retryStatusCode: { type: integer, example: 200 }\n        error: { $ref: '#/components/schemas/ProviderError' }\n\n    OutcomeResult:\n      type: object\n      required: [healAttemptId, status, issueStatus]\n      properties:\n        healAttemptId: { type: string, format: uuid }\n        status:\n          type: string\n          enum: [succeeded, failed, expired]\n        issueStatus: { type: string }\n\n    IssueView:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        fingerprint: { type: string }\n        provider: { type: string }\n        api: { type: string }\n        model: { type: [string, 'null'] }\n        error:\n          type: object\n          properties:\n            code: { type: [string, 'null'] }\n            type: { type: [string, 'null'] }\n            param: { type: [string, 'null'] }\n        status:\n          type: string\n          enum: [resolving, unverified, verified, ineffective, no_fix_found]\n        occurrenceCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n        patch:\n          type: [object, 'null']\n          properties:\n            id: { type: string, format: uuid }\n            operations:\n              type: array\n              items: { $ref: '#/components/schemas/Operation' }\n\n    HealAttemptView:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        traceId: { type: string }\n        status:\n          type: string\n          enum: [pending, succeeded, failed, expired]\n        retryStatusCode: { type: [integer, 'null'] }\n        retryErrorFingerprint: { type: [string, 'null'] }\n        confirmedAt: { type: [string, 'null'], format: date-time }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n        issue:\n          type: [object, 'null']\n          properties:\n            id: { type: string, format: uuid }\n            status: { type: string }\n            provider: { type: string }\n            api: { type: string }\n            model: { type: [string, 'null'] }\n            fingerprint: { type: string }\n\n    IssueListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        fingerprint: { type: string }\n        provider: { type: string }\n        api: { type: string }\n        model: { type: [string, 'null'] }\n        errorCode: { type: [string, 'null'] }\n        errorType: { type: [string, 'null'] }\n        errorParam: { type: [string, 'null'] }\n        status:\n          type: string\n          enum: [resolving, unverified, verified, ineffective, no_fix_found]\n        patchId: { type: [string, 'null'], format: uuid }\n        occurrenceCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    AttemptListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        traceId: { type: string }\n        status:\n          type: string\n          enum: [pending, succeeded, failed, expired]\n        retryStatusCode: { type: [integer, 'null'] }\n        retryErrorFingerprint: { type: [string, 'null'] }\n        confirmedAt: { type: [string, 'null'], format: date-time }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    PatchListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        operations:\n          type: array\n          items: { $ref: '#/components/schemas/Operation' }\n        operationsHash: { type: string }\n        issueCount: { type: integer }\n        attemptCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    PatchDetail:\n      allOf:\n        - $ref: '#/components/schemas/PatchListItem'\n        - type: object\n          properties:\n            issues:\n              type: array\n              items: { $ref: '#/components/schemas/IssueListItem' }\n\n    TraceListItem:\n      type: object\n      properties:\n        traceId: { type: string }\n        attemptCount: { type: integer }\n        pendingCount: { type: integer }\n        succeededCount: { type: integer }\n        failedCount: { type: integer }\n        firstAt: { type: string, format: date-time }\n        lastAt: { type: string, format: date-time }\n\n    TraceAttempt:\n      allOf:\n        - $ref: '#/components/schemas/AttemptListItem'\n        - type: object\n          properties:\n            issue:\n              type: object\n              properties:\n                id: { type: string, format: uuid }\n                status: { type: string }\n                provider: { type: string }\n                api: { type: string }\n                model: { type: [string, 'null'] }\n                errorCode: { type: [string, 'null'] }\n                errorType: { type: [string, 'null'] }\n                errorParam: { type: [string, 'null'] }\n            patch:\n              type: object\n              properties:\n                id: { type: string, format: uuid }\n                operations:\n                  type: array\n                  items: { $ref: '#/components/schemas/Operation' }\n\n    TraceDetail:\n      type: object\n      properties:\n        traceId: { type: string }\n        attempts:\n          type: array\n          items: { $ref: '#/components/schemas/TraceAttempt' }\n\n    ZodError:\n      type: object\n      description: Flattened zod validation error.\n      properties:\n        formErrors:\n          type: array\n          items: { type: string }\n        fieldErrors:\n          type: object\n          additionalProperties:\n            type: array\n            items: { type: string }\n","packages/cli/README.md":"# Manifest CLI\n\n`mnfst` manages a Manifest gateway from your terminal. Use it to create agents, connect AI providers, configure model routing, and inspect requests. The dashboard calls agents **Harnesses**; CLI commands use `agent`.\n\nThe npm package is `mnfst-gateway-cli`. The command it installs is `mnfst`. Node.js 20 or later is required.\n\n## Install\n\n```bash\nnpm install -g mnfst-gateway-cli\nmnfst --help\n```\n\nTo run it without a global install:\n\n```bash\nnpx mnfst-gateway-cli --help\n```\n\n## Quick start\n\nSign in to the hosted Manifest gateway, then create an agent and connect a provider:\n\n```bash\nmnfst login\nmnfst agent create --name my-app --platform openai-sdk\nmnfst provider connect openai --agent my-app --credential-env OPENAI_API_KEY\nmnfst models my-app --provider openai\nmnfst agent configure my-app --models gpt-4o-mini --provider openai\nmnfst routing status my-app\nmnfst agent setup my-app\n```\n\nSet `OPENAI_API_KEY` in your environment before connecting the provider. Choose a model shown by `mnfst models` if `gpt-4o-mini` is unavailable to your account. `mnfst agent setup` returns wiring instructions with the agent key masked.\n\nFor a self-hosted gateway, pass its URL when you sign in:\n\n```bash\nmnfst login --url https://gateway.example.com\n```\n\nThe CLI uses that gateway for later commands. You can select another gateway with `--url` or `MANIFEST_URL`.\n\n## Useful commands\n\n| Task                           | Command                                        |\n| ------------------------------ | ---------------------------------------------- |\n| Check your connection          | `mnfst doctor`                                 |\n| List available agent platforms | `mnfst agent platforms`                        |\n| List connectable providers     | `mnfst provider catalog`                       |\n| Review an agent's routing      | `mnfst routing status my-app`                  |\n| Read or set model params       | `mnfst routing params get my-app`              |\n| Read recent requests           | `mnfst requests get --agent my-app --range 7d` |\n| See all commands and options   | `mnfst --help`                                 |\n\nMost commands write JSON to stdout for scripts and coding agents. `mnfst agent env` and `mnfst skill show` write text. `mnfst routing test` sends a real provider request, which may incur provider charges. Destructive commands require `--yes`.\n\n## Credentials\n\nBrowser login is the default. For a script, provide a management credential through standard input or an environment variable:\n\n```bash\nprintf '%s' \"$MY_KEY\" | mnfst login --token-stdin\n# or\nmnfst login --token-env MY_KEY\n```\n\nThe CLI stores login credentials for each gateway host in `~/.config/manifest/config.json` with file mode `0600`. A stored credential is only sent to the host where it was saved. `MANIFEST_API_KEY` overrides the stored credential for the selected host.\n\nAgent keys are separate from management credentials. The CLI stores a newly created agent key in its local keystore and does not print it in `mnfst agent setup` unless you pass `--reveal`. Use `mnfst agent env my-app` to get the key and URL for your application; its output contains the key, so handle it as a secret.\n\n## Usage telemetry\n\nThe CLI records usage telemetry by default. It batches events locally and normally sends one request per install per day. The payload contains a random, persistent install ID; CLI version; operating system; Cloud or self-hosted target; and each command's name, result, duration, and minute-level time. It may include the detected coding-agent runtime. It does not include command arguments, agent or provider names, gateway URLs, prompts, or credentials.\n\nSet `MANIFEST_TELEMETRY_DISABLED=1` to opt out. Telemetry files are stored beside the CLI config. You can set `MANIFEST_CLI_TELEMETRY_ENDPOINT` to send usage data to your own endpoint.\n\n## Documentation\n\nSee the [CLI documentation](https://manifest.build/llm-gateway/docs/cli/) for more examples and the [GitHub repository](https://github.com/mnfst/llm-gateway) for source and issues.\n","packages/manifest/README.md":"# manifest\n\nThis package exists to hold the canonical **Manifest release version**.\n\nIt contains no code. The real backend and frontend live in [`../backend`](../backend) and [`../frontend`](../frontend), and shared types in [`../shared`](../shared). The Docker image at [`manifestdotbuild/manifest`](https://hub.docker.com/r/manifestdotbuild/manifest) bundles the built backend + frontend.\n\n## What it's for\n\n- **Changesets target**: `.changeset/config.json` ignores `manifest-backend`, `manifest-frontend`, and `manifest-shared`, so when contributors run `npx changeset` the only selectable package is this one. Always target `manifest` regardless of which files you actually changed — this is the single source of truth for \"the version of Manifest as a whole\".\n- **Docker release version**: `.github/workflows/docker.yml` reads the `version` field from this package's `package.json` via `jq` and uses it to tag the image (`manifestdotbuild/manifest:{version}` + derived `{major}.{minor}` / `{major}` / `sha-<short>` tags).\n- **Changelog**: `CHANGELOG.md` here (auto-generated by changesets) is the unified Manifest release history.\n\n## Adding a changeset\n\n```bash\nnpx changeset\n# → select \"manifest\"\n# → choose patch / minor / major\n# → write a one-line summary\n```\n\nCommit the generated file. On merge to `main`, the release workflow opens a `chore: version packages` PR bumping `version` and appending to `CHANGELOG.md`. Merge that PR to land the version bump, then manually run the Docker workflow (`Actions → Docker → Run workflow` with a blank input) to publish.\n",".cursorrules (manifest-yaml-cursorrules-prompt-file.mdc)":"---\ndescription: \"Cursor rules for manifest development with YAML integration.\"\nglobs: **/*\nalwaysApply: false\n---\n**Prompt for Expert Manifest Developer**\n\n**You are an assistant for app creation. You are going to use the backend Manifest. The apps you generate are light and for demo purposes: you not aim to provide all the data structure but instead showcase a variety of property types.**\n\n**Code structure**\nWhen asked to create a backend, execute the following actions:\n\n1. Install the `manifest` npm package\n2. Add the following scripts to `pacakge.json`: \"manifest\": \"node node_modules/manifest/scripts/watch/watch.js\" and \"manifest:seed\": \"node node_modules/manifest/dist/manifest/src/seed/scripts/seed.js\"\n3. Create the `manifest/backend.yml` file and add the manifest code to it.\n4. Add the `redhat.vscode-yaml` as recommendation in `.vscode/extensions.json`\n5. Configure `yaml.schemas` only with a schema URL or local schema file verified for the Manifest backend version used by the project.\n\n**Backend file**\nOn the `manifest/backend.yml`, follow those rules:\n- Strictly follow the Manifest JSON Schema shipped with the project or verified from the current Manifest backend documentation.\n- Start by addind a quick name to the app\n- Limit to 2 or 3 entities maximum\n- Limit to 4 properties maximum per entity\n- Try to showcase different property types\n- Only use validation properties once or twice\n- No entity should be called admin\n- Do not use authenticable entities\n- Add an emoji after each entity name, but do not use the emoji it on relationships references\n- Add a linebreak before each entity object\n- Each entity only appears once. Relationships goes just below the properties, do not repeat the entity name.\n- Do not use special characters.\n. Do not use middlewares, endpoints or hooks.\n- Use YAML abbreviated form for objects, with spaces. Example: { name: issueDate, type: date }\n- Do not add relationships to single entities\n- For relationships, use the short form. Ex: ' belongsTo:\n      - Author'\n- Add policies. Most projects only have \"read\" public policies. Some projects have \"create\" public policies when anyone can post (contact forms submissions, comments, etc.)\n- If using the \"choice\" property type, use \"options.values\" property to list choices. Example:  `{ name: type, type: choice, options: { values: [\"Fire\", \"Water\", \"Grass\"] } }`\n- Do not add \"seedCount\" and \"mainProp\" to entities\n\n**Documentation**\nRefer to the Manifest backend documentation that matches the project's installed version.\n\n**Example**\nThis is an example of the content of a `backend.yml` file:\nname: My pet app 🐾\nentities:\n  Owner:\n    properties:\n      - name\n      - { name: birthdate, type: date }\n\n  Cat:\n    properties:\n      - name\n      - { name: age, type: number }\n      - { name: birthdate, type: date }\n    belongsTo:\n      - Owner\n\n  Homepage:\n    nameSingular: Home content\n    single: true\n    properties:\n      - title\n      - { name: description, type: richText }\n      - { name: cover, type: image }\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/mnfst/manifest/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Manifest Agent Guidelines\n\n## Domain Terminology\n\nManifest terminology is directional:\n\n- A **Manifest Request** is one logical request from an agent to Manifest and lives in `requests`.\n- A **Provider Attempt** is one request from Manifest to an AI provider and lives in `agent_messages`.\n- A **Tenant** is a user's data boundary. It is created from `user.id` on first agent creation.\n- An **Agent** is an AI agent owned by a tenant. The dashboard labels agents **Harnesses** (nav under `/harnesses`; legacy `/agents/*` URLs redirect) — this is UI copy only; backend code, database tables, and API routes still say *agent*.\n\n[`docs/glossary.md`](docs/glossary.md) is the canonical contract for statuses, ordering, recovery, database mapping, and counting rules. Do not duplicate those definitions in agent guides.\n","isInternal":false,"tokens":183,"sizeBytes":819},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/mnfst/manifest/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Manifest Development Guidelines\n\nLast updated: 2026-09-14\n\n## What Manifest Is\n\nManifest is a smart model router for **AI agents**. It sits between an agent and its LLM providers, scores each request, and routes it to the cheapest model that can handle it. The dashboard tracks logical requests and their provider attempts, costs, and tokens across any agent that speaks OpenAI-compatible HTTP.\n\n**\"Harness\" is the dashboard word for an agent.** The UI now labels agents **Harnesses** (nav item \"Harnesses\", routes under `/harnesses`, categories `AI agent` / `Automation` / `App AI SDK` / `Coding Assistant` from `CATEGORY_LABELS` in `packages/shared/src/agent-type.ts`). This is a **copy-level rename only**: backend code, database tables (`agents`, `agent_messages`, …), API routes (`/api/v1/agents/*`), and entity/service names all still say *agent*. Legacy `/agents/*` dashboard URLs redirect to `/harnesses/*`. When writing UI copy say \"harness\"; when writing code or API docs keep \"agent\".\n\n**Product note:** this repo is **Manifest LLM Gateway**, the open-source LLM gateway of Manifest, available and maintained. The dashboard and the READMEs carry no promotion of any other Manifest product: not the self-healing layer at `dashboard.manifest.build` (on hold), and no API Bot banner or sidebar card (removed). `POST /api/v1/waitlist/pivot/claim` stays only for self-hosted versions that still ship the old sidebar card. The sidebar's \"Custom development\" button opens `ServicesContactModal`, which posts to `POST /api/v1/services/contact`; the backend relays it to `https://manifest.build/api/contact` (mnfst/website `api/contact.js`), which emails the team. Nothing is stored.\n\n**Supported agents**: see `AGENT_PLATFORMS` in `packages/shared/src/agent-type.ts` for the current list (OpenClaw, Hermes, Claude Code, OpenCode, generic OpenAI/Anthropic SDK slots, and others — don't duplicate the list here, it grows independently of this doc). OpenClaw remains the deepest integration, but no new code or copy should frame Manifest as OpenClaw-only. When adding examples, prefer \"AI agent\" as the noun and pick OpenClaw as the worked example rather than the sole target. Manifest is consumed as a generic OpenAI-compatible HTTP endpoint — there are no first-party OpenClaw plugins in this repo anymore.\n\nWingman — the gateway tester for sending requests against a Manifest backend while impersonating any of the supported agents (useful for routing/header-classifier reproductions) — lives in its own repo at [`mnfst/wingman`](https://github.com/mnfst/wingman) and is hosted at [`wingman.manifest.build`](https://wingman.manifest.build). The dashboard embeds it as an iframe drawer **in dev mode only** — it is dead-code-eliminated from production / self-hosted bundles via `__DEV_MODE__`. The backend allows the hosted Wingman origin through CORS in both dev and production (production also honors `WINGMAN_CORS_ORIGINS`), while the CSP `frame-src` that permits the drawer iframe stays dev-only; both are wired in `packages/backend/src/cors-csp-config.ts`.\n\n**Whenever working in dev mode (`/serve`, `npm run dev`, etc.), the Wingman drawer is expected to be available** — open the FAB at the bottom-right of the dashboard (or hit ⌘/Ctrl+Shift+W) and confirm the iframe loads `https://wingman.manifest.build` cleanly. The drawer is part of the dev surface area, so a broken iframe means the dev environment is broken. `/serve` is **dev-only** — never use it to validate production behavior.\n\n**Three management surfaces share one REST API.** The dashboard, the `mnfst` CLI (`packages/cli/`), and the remote MCP server (`packages/backend/src/mcp/`) all drive the same `/api/v1/*` services with the same tenant scoping. Add a capability to the service layer once, then expose it on whichever surfaces need it; never fork logic per surface. See [CLI login](#cli-login-mnfst-login) and [Remote MCP server](#remote-mcp-server) below.\n\n## IMPORTANT: Cloud Mode Always\n\nWhen starting the app for development or testing (e.g. `/serve`), **always use `MANIFEST_MODE=cloud`** (the default). Every dev session must use a **fresh PostgreSQL database** via Docker — multiple concurrent dev instances sharing one DB cause cross-run data pollution and intermittent test failures:\n\n```bash\n# 1. Ensure the postgres_db container is running\ndocker start postgres_db 2>/dev/null || \\\n  docker run -d --name postgres_db -e POSTGRES_USER=myuser -e POSTGRES_PASSWORD=mypassword -e POSTGRES_DB=mydatabase -p 5432:5432 postgres:16\n\n# 2. Create a pristine database with a unique name\nDB_NAME=\"manifest_$(openssl rand -hex 4)\"\ndocker exec postgres_db psql -U myuser -d postgres -c \"CREATE DATABASE $DB_NAME;\"\n\n# 3. Update DATABASE_URL in packages/backend/.env to use the new database\n# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/$DB_NAME\n\n# 4. Ensure SEED_DATA=true in .env so the database is populated on startup\n```\n\nThis guarantees each session starts with a clean, isolated database and avoids all cross-instance conflicts.\n\n## Testing OpenClaw Integration\n\nTo test routing from an OpenClaw agent against a local Manifest dev server, point OpenClaw at the dev server's OpenAI-compatible proxy directly — there is no plugin anymore:\n\n```bash\n# 1. Build and start the backend in cloud mode\nnpm run build\nPORT=38238 BIND_ADDRESS=127.0.0.1 \\\n  node -r dotenv/config packages/backend/dist/main.js\n\n# 2. Configure OpenClaw to use the dev server as a generic OpenAI-compatible provider\nopenclaw config set models.providers.manifest '{\"baseUrl\":\"http://localhost:38238/v1\",\"api\":\"openai-completions\",\"apiKey\":\"mnfst_YOUR_KEY\",\"models\":[{\"id\":\"auto\",\"name\":\"Manifest Auto\"}]}'\nopenclaw config set agents.defaults.model.primary manifest/auto\n\n# 3. Restart the gateway\nopenclaw gateway restart\n```\n\nThe `AgentKeyAuthGuard` accepts any non-`mnfst_*` token from loopback IPs in the self-hosted version, so loopback-only testing works even without a valid key. After restarting the backend, also restart the OpenClaw gateway — it doesn't reconnect automatically.\n\n## Active Technologies\n\n- **Backend**: NestJS 11, TypeORM 0.3, PostgreSQL 16, Better Auth, class-validator, class-transformer, Helmet\n- **Frontend**: SolidJS, Vite, uPlot (charts), Better Auth client, custom CSS theme\n- **Runtime**: TypeScript 5.x (strict mode). CI pins Node.js 24 (`.github/workflows/release.yml`); no `engines` field enforces this locally.\n- **Monorepo**: npm workspaces + Turborepo\n- **Release**: Changesets for version management + GitHub Actions for Docker image release\n\n## Project Structure\n\n```text\npackages/\n├── backend/\n│   ├── src/\n│   │   ├── instrument.ts                    # Sentry init, imported first (before any other import)\n│   │   ├── main.ts                          # Bootstrap: Helmet, ValidationPipe, Better Auth mount, CORS\n│   │   ├── app.module.ts                    # Root module (guards: ApiKey, Session, Throttler)\n│   │   ├── config/app.config.ts             # Environment variable config\n│   │   ├── auth/\n│   │   │   ├── auth.instance.ts             # Better Auth singleton (email/pass + 3 OAuth + jwt/mcp plugins)\n│   │   │   ├── auth.module.ts               # Registers SessionGuard as APP_GUARD\n│   │   │   ├── session.guard.ts             # Cookie session auth via Better Auth\n│   │   │   ├── current-user.decorator.ts    # @CurrentUser() param decorator\n│   │   │   ├── cli-auth.controller.ts, cli-auth.service.ts # Browser login for the mnfst CLI (PKCE code → `cli` PAT)\n│   │   │   ├── me.controller.ts             # GET /api/v1/me identity probe (tenant, user, auth method, expiry)\n│   │   │   └── mcp-scopes.ts                # mcp:read / mcp:write scope constants (leaf module, no Better Auth import)\n│   │   ├── database/\n│   │   │   ├── database.module.ts           # TypeORM PostgreSQL config\n│   │   │   ├── database-seeder.service.ts   # Seeds demo data (users, agents, security events)\n│   │   │   ├── datasource.ts               # CLI DataSource for migration commands\n│   │   │   ├── pricing-sync.service.ts      # OpenRouter pricing data sync\n│   │   │   ├── ollama-sync.service.ts       # Ollama model sync\n│   │   │   ├── quality-score.util.ts        # Model quality scoring\n│   │   │   └── seed-messages.ts             # Demo request/provider-attempt seed data\n│   │   ├── entities/                        # TypeORM entities (25 entities)\n│   │   │   ├── tenant.entity.ts             # Multi-tenant root\n│   │   │   ├── agent.entity.ts              # Agent (belongs to tenant)\n│   │   │   ├── agent-api-key.entity.ts      # OTLP ingest keys (mnfst_*)\n│   │   │   ├── cli-auth-code.entity.ts      # Short-lived PKCE codes for `mnfst login`\n│   │   │   └── ...                          # request, agent-message (provider attempt), tenant-provider, tier-assignment, header-tier, etc.\n│   │   ├── common/\n│   │   │   ├── guards/api-key.guard.ts      # X-API-Key header auth (timing-safe)\n│   │   │   ├── decorators/public.decorator.ts\n│   │   │   ├── dto/                         # create-agent, range-query, rename-agent DTOs\n│   │   │   ├── filters/spa-fallback.filter.ts\n│   │   │   ├── interceptors/               # agent-cache, user-cache\n│   │   │   ├── constants/                   # api-key, cache, ollama, providers, openai-models, xai-models, subscription-clients\n│   │   │   ├── services/                    # ingest-event-bus, manifest-runtime, tenant-cache, request-recording-storage\n│   │   │   └── utils/                       # crypto, hash, range, period, slugify, url-validation, provider-inference, postgres-sql, cost-calculator, detect-self-hosted, frontend-path, og-rewrite, secret-scrub, ttl-cache, local-ip, etc.\n│   │   ├── health/                          # @Public() health check\n│   │   ├── analytics/                       # Dashboard analytics\n│   │   │   ├── controllers/                 # overview, tokens, costs, messages, agents\n│   │   │   └── services/                    # aggregation + timeseries-queries + query-helpers\n│   │   ├── otlp/                            # Agent key auth + onboarding\n│   │   │   ├── guards/agent-key-auth.guard.ts # Bearer token auth (agent API keys)\n│   │   │   └── services/api-key.service.ts  # Agent onboarding (creates tenant+agent+key)\n│   │   ├── routing/                         # LLM routing (providers, tiers, proxy, scorer)\n│   │   │   ├── proxy/                       # OpenAI-compatible proxy (anthropic/google adapters, fallback chain, attempt recording, caller-classifier)\n│   │   │   ├── autofix/                     # Autofix self-healing (Phoenix client + heal-once flow)\n│   │   │   ├── routing-core/               # Tier, provider, specificity services + cache\n│   │   │   ├── resolve/                     # Scoring-based tier + specificity resolution\n│   │   │   ├── custom-provider/             # Custom provider CRUD (+ `alias`, the public model-id prefix in /v1/models)\n│   │   │   ├── header-tiers/               # Header-based tier overrides\n│   │   │   ├── oauth/                       # OAuth flows (Gemini, OpenAI, Kiro, MiniMax)\n│   │   │   └── specificity.controller.ts   # Specificity routing CRUD endpoints\n│   │   ├── scoring/                         # Request complexity scoring engine\n│   │   │   ├── keywords.ts                 # Keyword lists for all dimensions (complexity + specificity)\n│   │   │   ├── specificity-detector.ts     # Task-type detection (coding, trading, etc.)\n│   │   │   └── scan-messages.ts            # Message scanner for specificity detection\n│   │   ├── model-prices/                    # Model pricing management + sync\n│   │   ├── notifications/                   # Alert rules, email providers, cron\n│   │   ├── playground/                      # Prompt playground (runs, columns, starred/best)\n│   │   ├── github/                          # GitHub stars endpoint\n│   │   ├── sse/                             # Server-Sent Events for real-time updates\n│   │   ├── setup/                           # First-run admin setup wizard\n│   │   ├── free-models/                     # Free LLM model catalog\n│   │   ├── model-discovery/                 # Per-provider model fetching + fallback\n│   │   ├── billing/                         # Stripe billing status + plan limits\n│   │   ├── error-pages/                     # Custom error-page config (internal + public)\n│   │   ├── waitlist/                        # Pivot waiting-list claims + legacy Autofix claim compatibility route\n│   │   ├── discovery/                       # Self-hosted discovery onboarding (forwarded to Peacock)\n│   │   ├── crm-metrics/                     # Cloud-only internal feed: Autofix-healed cohort + waitlist claims + corporate signups (secret-gated)\n│   │   ├── cors-csp-config.ts               # Wingman CORS/CSP origin allowlists\n│   │   ├── sentry/                          # Sentry init-options builder (SENTRY_DSN-gated)\n│   │   ├── telemetry/                       # Anonymous self-hosted telemetry\n│   │   ├── version/                         # GET /api/v1/version — self-hosted \"new version available\" check\n│   │   └── mcp/                             # Remote MCP server (OAuth 2.1 bearer, stateless per-POST, tools/ per domain)\n│   └── test/                                # E2E tests (supertest)\n├── frontend/\n│   ├── src/\n│   │   ├── index.tsx                        # Router setup (App + AuthLayout)\n│   │   ├── components/\n│   │   │   ├── AuthGuard.tsx                # Session check, redirect to /login\n│   │   │   ├── GuestGuard.tsx               # Redirect authenticated users away from auth pages\n│   │   │   ├── SocialButtons.tsx            # 3 OAuth provider buttons\n│   │   │   ├── Header.tsx                   # User session data, logout\n│   │   │   ├── Sidebar.tsx                  # Navigation sidebar\n│   │   │   ├── SetupModal.tsx               # Agent setup wizard modal\n│   │   │   └── ...                          # Charts, modals, pagination, etc.\n│   │   ├── pages/\n│   │   │   ├── Login.tsx, Register.tsx       # Auth pages\n│   │   │   ├── ResetPassword.tsx            # Password reset flow\n│   │   │   ├── Workspace.tsx                # Harness grid/table at /harnesses + create harness\n│   │   │   ├── GlobalOverview.tsx, AgentOverview.tsx # Cross-harness + per-harness dashboards (split from one Overview.tsx)\n│   │   │   ├── AgentDetail.tsx, AgentProviders.tsx   # Per-harness detail shell (tabs) + provider connections\n│   │   │   ├── MessageLog.tsx               # Paginated Requests log (legacy filename)\n│   │   │   ├── Account.tsx                  # User profile (session data)\n│   │   │   ├── Settings.tsx, SettingsAutofixSection.tsx, SettingsRecordingSection.tsx # Harness settings + Autofix + request-recording toggles\n│   │   │   ├── Routing.tsx, RoutingPanels.tsx, RoutingActions.tsx, RoutingDefaultTierSection.tsx, RoutingHeaderTiersSection.tsx, RoutingSpecificitySection.tsx, RoutingTierCard.tsx # LLM routing config (split by concern)\n│   │   │   ├── Limits.tsx                   # Alert rule management — mounted at /harnesses/:agentName/guardrails (\"Guardrails\" tab)\n│   │   │   ├── ModelPrices.tsx              # Model pricing table\n│   │   │   ├── Playground.tsx               # Prompt playground\n│   │   │   ├── ConnectProvider.tsx          # Provider connection flow\n│   │   │   ├── providers/                   # Subscriptions.tsx, Byok.tsx, Local.tsx, ConnectionDetail.tsx, ProviderConnectionsPage.tsx (/providers/* pages)\n│   │   │   ├── Discovery.tsx                # Self-hosted discovery form (one-time, post-signup)\n│   │   │   ├── Welcome.tsx                  # Post-signup welcome flow (+ welcome-helpers.ts)\n│   │   │   ├── FreeModels.tsx               # Free model catalog\n│   │   │   ├── Setup.tsx                    # First-run setup wizard\n│   │   │   ├── Upgrade.tsx                  # Billing/plan upgrade page\n│   │   │   ├── Help.tsx                     # Help page\n│   │   │   ├── AgentLimitsRedirect.tsx, AgentMessagesRedirect.tsx # Legacy per-harness URL redirects\n│   │   │   ├── CliAuth.tsx                  # /cli/auth — approve a `mnfst login` browser request\n│   │   │   ├── Consent.tsx                  # /consent — OAuth consent screen for MCP clients\n│   │   │   └── NotFound.tsx                 # 404 page\n│   │   ├── services/\n│   │   │   ├── auth-client.ts               # Better Auth SolidJS client\n│   │   │   ├── api.ts                       # API functions (credentials: include)\n│   │   │   ├── providers.ts                 # ProviderDef list + SPECIFICITY_STAGES + STAGES\n│   │   │   ├── model-display.ts             # Model display-name cache\n│   │   │   ├── formatters.ts               # Number/cost formatting\n│   │   │   ├── provider-utils.ts            # LLM provider helpers\n│   │   │   ├── routing.ts, routing-utils.ts # Routing config helpers\n│   │   │   ├── theme.ts                     # Theme management\n│   │   │   ├── toast-store.ts               # Toast notification state\n│   │   │   └── ...                          # setup-status, playground-store, pagination, sse, oauth-popup, etc.\n│   │   ├── layouts/                         # Layout components\n│   │   └── styles/\n│   └── tests/\n├── shared/                           # Shared TypeScript types + helpers (consumed by backend and frontend)\n├── cli/                              # `mnfst` management CLI (private; bin/mnfst.js, commands/, generated provider catalog + skill content)\n└── manifest/                         # Code-free shell package holding the canonical Manifest version (see Releases)\n```\n\nBeyond `packages/`, the repo root also carries:\n\n- `integrations/n8n-nodes-manifest/` — the published n8n community node (`n8n-nodes-manifest` on npm); root-level `nodes/` and `credentials/` are identical mirrors of its sources for n8n's community-node scanner.\n- `contracts/gateway/` — gateway API contract tests.\n- `design-system/` — machine-readable design registry (generated `registry/components.json` + `registry/llms.txt`; generator `generate-registry.mjs`).\n- `deploy/` — one-click deployment templates (apple-containers, aws, coolify, digitalocean, easypanel, fly, gcp, heroku, koyeb).\n- `docs/` — `glossary.md` (canonical terminology contract), `autofix-self-healing-poc.md`, `model-parameters-schema.md`, `providers/subscription-based-providers.md`.\n\n## Single-Service Deployment\n\nThe app deploys as a **single service**. In production, NestJS serves both the API and the frontend static files from the same port.\n\n```bash\nnpm run build     # Turborepo: frontend (Vite) then backend (Nest)\nnpm start         # node packages/backend/dist/main.js — serves frontend + API\n```\n\n- API routes (`/api/*`, `/otlp/*`) are excluded from static file serving.\n- Dev mode: Vite on `:3000` proxies `/api` and `/otlp` to backend on `:3001`.\n- **Manifest Cloud** runs on Cloud Run (`manifest`, project `manifest-platform-457808`, `us-east4`) behind the shared Google HTTPS load balancer, with Cloud SQL Postgres on a private IP. Every push to `main` deploys through `.github/workflows/deploy.yml` (keyless Workload Identity Federation; `mnfst/*` on `main` only): it builds `docker/Dockerfile.cloud` (the self-hosted `docker/Dockerfile` minus `VITE_MANIFEST_SELFHOSTED`, keep both in step), runs the `manifest-migrate` job, then swaps the service image. Env vars, secrets (Secret Manager) and scaling live on the service; it sets `MANIFEST_MODE=cloud`, `RUN_MIGRATIONS_ON_BOOT=false`, `TRUST_PROXY=2` (load balancer + Cloud Run hops) and `SHUTDOWN_DRAIN_MS=0`. `railway.toml` is the old Railway config, kept until Railway is decommissioned.\n\n## Commands\n\n### Starting the Dev Server\n\nThe backend requires a `.env` file at `packages/backend/.env` with at least `BETTER_AUTH_SECRET` (32+ chars). The `auth.instance.ts` reads `process.env` at import time, before NestJS `ConfigModule` loads `.env`, so env vars must be available to the Node process.\n\n**Quick start (run these in parallel):**\n\n```bash\n# Backend — must preload dotenv since auth.instance.ts reads process.env at import time\ncd packages/backend && NODE_OPTIONS='-r dotenv/config' npx nest start --watch\n\n# Frontend\ncd packages/frontend && npx vite\n```\n\n**Note:** `npm run dev` (turbo) starts the frontend but NOT the backend, because the backend's script is `start:dev` not `dev`. Start the backend separately as shown above.\n\n### Seeding Dev Data\n\nSet `SEED_DATA=true` in `packages/backend/.env` to seed on startup (dev/test only). This creates:\n\n- **Admin user**: `admin@manifest.build` / `manifest` (email verification email is skipped if Mailgun is not configured — user is created but unverified)\n- **Tenant**: `seed-tenant-001` linked to the admin user\n- **Agent**: `demo-agent` with OTLP key `dev-otlp-key-001`\n- **API key**: `dev-api-key-manifest-001`\n- **Security events**: 12 sample events for the security dashboard\n- **Requests and provider attempts**: Sample telemetry for the demo agent\n\nSeeding is idempotent — it checks for existing records before inserting.\n\n**Dev-login shortcut:** when running under the Vite dev server the login page shows a\nprominent one-click **⚡ Sign in as dev** button that submits the seeded\n`admin@manifest.build` / `manifest` credentials — no copy-paste. It's gated by\n`import.meta.env.DEV`, so Vite strips the button and the credential literals from\nproduction builds, and no password ever rides in a URL. See\n`packages/frontend/src/pages/Login.tsx`.\n\n**Minimal `.env` for development:**\n\n```env\nPORT=3001\nBIND_ADDRESS=127.0.0.1\nNODE_ENV=development\nBETTER_AUTH_SECRET=<random-hex-64-chars>\nDATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase\nAPI_KEY=dev-api-key-12345\nSEED_DATA=true\n```\n\nGenerate a secret with: `openssl rand -hex 32`\n\n**Database naming convention:** Always create uniquely-named databases to avoid overlapping other dev/test instances. Use the pattern `manifest_<context>_<random>` (e.g., `manifest_sse_49821`, `manifest_dev_83712`). Create databases via Docker:\n\n```bash\ndocker exec postgres_db psql -U myuser -d postgres -c \"CREATE DATABASE manifest_<name>;\"\n```\n\nThen set `DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/manifest_<name>` in `.env`.\n\n```bash\n# Production build + start (single server)\nnpm run build && npm start\n\n# Tests\nnpm test --workspace=packages/backend          # Jest unit tests\nnpm run test:e2e --workspace=packages/backend  # Jest e2e tests\nnpm test --workspace=packages/frontend         # Vitest tests\nnpm test --workspace=packages/shared           # Jest unit tests\nnpm test --workspace=packages/cli              # Jest unit tests (mnfst CLI)\n```\n\n### Database Migrations\n\nTypeORM migrations run automatically on app startup by default (gated by `RUN_MIGRATIONS_ON_BOOT`, default `true`). Schema sync (`synchronize`) is permanently disabled — all schema changes must go through migrations.\n\n**Dev workflow:** modify entity → generate migration → commit both.\n\n```bash\n# Generate a migration after changing an entity\ncd packages/backend\nnpm run migration:generate -- src/database/migrations/DescriptiveName\n\n# Other migration commands\nnpm run migration:run       # Run pending migrations\nnpm run migration:revert    # Revert the last migration\nnpm run migration:show      # Show migration status ([X] = applied)\nnpm run migration:create -- src/database/migrations/Name  # Create empty migration\n```\n\nNew migrations must be imported in `database.module.ts` and added to the `migrations` array.\n\n**Important**: Always use unique timestamps for new migrations. Never reuse a timestamp from an existing migration file.\n\n## Authentication Architecture\n\n### Guard Chain\n\nThree global guards run on every request (order matters):\n\n1. **SessionGuard** (`auth/session.guard.ts`) — Checks `@Public()` first. If not public, validates the Better Auth cookie session via `auth.api.getSession()`. Attaches `request.user` and `request.session`.\n2. **ApiKeyGuard** (`common/guards/api-key.guard.ts`) — Falls through if session already set. Otherwise reads the `X-API-Key` header and first looks it up against the tenant-scoped `ApiKey` entity (`api_keys` table, hashed with scrypt) — this is the primary multi-tenant credential path. Only if no DB match is found does it fall back to a timing-safe compare against the single `API_KEY` env var. Use `@Public()` to skip both guards.\n3. **ThrottlerGuard** — Rate limiting.\n\n`mnfst` CLI tokens are ordinary `api_keys` rows named `cli` (`CLI_KEY_NAME` in `auth/cli-auth.service.ts`), so they ride the `ApiKeyGuard` path with no special casing; the guard also enforces their sliding and absolute TTLs. The remote MCP route is `@Public()` because it carries its own credential (an OAuth 2.1 bearer) — see [Remote MCP server](#remote-mcp-server).\n\n### Better Auth Setup\n\n- **Instance**: `auth/auth.instance.ts` — `betterAuth()` with `emailAndPassword` + 3 social providers (Google, GitHub, Discord). Each provider only activates when both `CLIENT_ID` and `CLIENT_SECRET` env vars are set. Plugins: `jwt()` and `@better-auth/mcp`'s `mcp()` (the OAuth 2.1 authorization server for the remote MCP endpoint, resource-bound to `${BETTER_AUTH_URL}/api/v1/mcp`, scopes from `auth/mcp-scopes.ts`).\n- **Mounting**: In `main.ts`, Better Auth is mounted as Express middleware at `/api/auth/*splat` **before** `express.json()` (it needs raw body control). NestJS body parsing is re-added after for all other routes.\n- **Frontend client**: `services/auth-client.ts` — `createAuthClient()` from `better-auth/solid`.\n- **Social login in dev**: OAuth callback URLs point to `:3001` (`BETTER_AUTH_URL`). Social login only works when accessing the app on port **3001** (production build), not on Vite's `:3000` dev server.\n\n### Auth Types\n\n```typescript\n// backend/src/auth/auth.instance.ts\nexport type AuthSession = typeof auth.$Infer.Session;\nexport type AuthUser = typeof auth.$Infer.Session.user;\n\n// Use in controllers:\n@Get('something')\nasync handler(@CurrentUser() user: AuthUser) {\n  // user.id, user.name, user.email\n}\n```\n\n### CLI login (`mnfst login`)\n\n`packages/cli/` is the `mnfst` management CLI: a thin wrapper over the `/api/v1` REST API (JSON on stdout, exit `0`/`1`, never interactive mid-task). One global tenant credential unlocks every command; per-agent `mnfst_*` keys are outputs only (written to `--key-file` paths with mode `0600`), never inputs. The package publishes to npm as `mnfst-gateway-cli` (`npm i -g mnfst-gateway-cli`, command still `mnfst`); from the monorepo, build with `npm run build --workspace=packages/shared && npm run build --workspace=packages/cli` and run `node packages/cli/bin/mnfst.js`. `packages/cli/README.md` is the user-facing reference; the repo also ships a `mnfst-cli` skill under `.claude/skills/`.\n\nThe default login is a browser flow, PKCE-protected so an intercepted code is useless without the verifier:\n\n1. The CLI opens the dashboard at `/cli/auth` (`pages/CliAuth.tsx`) with a `state` and an S256 `code_challenge`.\n2. The signed-in user approves; the page calls `POST /api/v1/cli/authorize` (session-only — an API key caller gets 403) and receives a short-lived code stored in `cli_auth_codes`.\n3. The CLI exchanges it at `POST /api/v1/cli/token` (public — the code is the credential, with `state` + `code_verifier`) for a `cli` PAT in `api_keys`; `expiresAt` is ISO-8601 UTC.\n4. `DELETE /api/v1/cli/token` revokes the calling PAT on `mnfst logout` (best-effort).\n\nNon-interactive alternatives: `--token-stdin` or `--token-env`. The secret is never a CLI argument. `GET /api/v1/me` is the identity probe (`{ tenantId, userId, authMethod, expiresAt }`); it fails closed with 401 for the env `API_KEY` operator fallback because that credential attaches no tenant. Token lifetime: `CLI_TOKEN_TTL_DAYS` (sliding, default 30) capped by `CLI_TOKEN_ABSOLUTE_TTL_DAYS` (hard ceiling from issuance, default 90).\n\nThe CLI refuses HTTP redirects (so the key cannot leak cross-origin) and strips inherited `MANIFEST_AGENT_KEY` / `MANIFEST_API_KEY` from `mnfst run` children. `mnfst routing test` sends `User-Agent: mnfst-cli/<version>` so gateway caller attribution (`routing/proxy/caller-classifier.ts`, which also recognises the n8n node's `n8n-nodes-manifest/<version>` UA) can tell CLI test traffic apart from real agent traffic in request details.\n\n### Remote MCP server\n\n`POST /api/v1/mcp` (`mcp/mcp.controller.ts`) is a remote MCP server over stateless Streamable HTTP: a fresh `McpServer` per POST (`mcp-server.factory.ts`), closed when the request ends, so no session affinity is needed. GET and DELETE answer **405 + `Allow: POST`** as the spec requires for a stateless JSON transport (a 404 made clients log an error on every connect).\n\n- **Auth**: the route is `@Public()` to skip the session/API-key guards, and `requireMcpAuth` from `@better-auth/mcp` is the real gate. Better Auth's MCP plugin is the authorization server (PKCE, resource-bound JWT access tokens, CIMD client identity, RFC 8414/9728 discovery). A missing or dead token answers 401 with `WWW-Authenticate: … resource_metadata=…`, which is how a client learns it must run OAuth. `mcp-auth.ts` maps the verified token's `sub` to a tenant through `TenantCacheService`, so tools are scoped exactly like the dashboard and CLI; a tenant-less user gets 401.\n- **Availability**: MCP is on by default and off in two cases, both decided by `auth/mcp-availability.ts` before Better Auth is built: the operator set `MCP_ENABLED=false`, or the MCP resource (`${BETTER_AUTH_URL}/api/v1/mcp`) is not HTTPS and not loopback. The second case is **not** opt-in on purpose — `mcp()` validates its resource URL as it is constructed and throws, and that throw happens while `auth.instance.ts` is being imported, so an HTTP-only self-hosted install would refuse to boot at all (issue #2939). When MCP is off, `buildPlugins()` omits `mcp()` and `cimd()`, `app.module.ts` leaves `McpModule` unregistered so `/api/v1/mcp` does not exist, `main.ts` mounts `mountMcpUnavailable()` so the well-known paths answer a JSON 404 rather than the SPA shell, and the dashboard hides the MCP nav entry (`GET /api/v1/setup/status` carries `mcpEnabled`). `mcp-availability.ts` is a leaf module for the same reason `mcp-scopes.ts` is — it must be importable from the unit suite, which cannot load Better Auth's ESM.\n- **Discovery**: `mcp-discovery.ts` (mounted in `main.ts`) serves the OAuth documents at the well-known **root** paths (`/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource[/api/v1/mcp]`) in addition to Better Auth's `/api/auth/.well-known/…`, because clients start from the MCP origin's root.\n- **Consent**: `pages/Consent.tsx` at `/consent` is the OAuth consent screen.\n- **Scopes**: `mcp:read` is required for every token; each `register*Tools()` returns before registering its write tools unless `operator.scopes` has `mcp:write`, so a read-only client never sees them (the server `instructions` string tells it to reconnect with the write scope). Revealing an ingest key also requires `mcp:write` because it is a live secret.\n- **Tools** (`mcp/tools/*.tools.ts`): identity, agents, environment (+ a `doctor` that walks dependencies in order), providers (including custom providers), routing (status, fallbacks, Autofix, recording, custom/header tiers, per-tier model params), models/pricing, and the request ledger. They call the same services as the REST controllers through `McpToolDeps` (`tool-deps.ts`); `tool-result.ts` is the shared result envelope.\n- **Testing**: `auth.instance.ts` is ESM-only and cannot load under Jest on Node 22, which is why the scope constants live in the leaf module `auth/mcp-scopes.ts` — import from there in guards, tools, and specs.\n\n## Multi-Tenancy Model\n\n```\nUser (Better Auth) ──→ Tenant ──→ Agent ──→ AgentApiKey (mnfst_*)\n                                    │\n                                    └──→ requests ──→ agent_messages (telemetry data)\n```\n\n- **Tenant** (`tenants` table): Created automatically on first agent creation. `tenant.owner_user_id` = `user.id` is the ONLY user→tenant link (resolved through `TenantCacheService`); `tenant.name` mirrors it for display until repurposed as a slug.\n- **Agent** (`agents` table): Belongs to a tenant. Unique constraint on `[tenant_id, name]`.\n- **AgentApiKey** (`agent_api_keys` table): One-to-one with agent. `mnfst_*` format key for OTLP ingestion.\n- **ApiKey** (`api_keys` table): A separate, tenant-scoped credential (not per-agent) used for dashboard/API access — the primary key `ApiKeyGuard` checks. Distinct from `AgentApiKey`. `mnfst login` PATs are rows of this table named `cli`.\n- **Onboarding flow**: `ApiKeyGeneratorService.onboardAgent()` creates tenant (if new) + agent + API key via three sequential inserts.\n\n### Data Isolation\n\nEvery resource belongs to a tenant; users only authenticate and (optionally) appear as `created_by_user_id` audit metadata. Guards (SessionGuard/ApiKeyGuard) resolve the tenant once per request and attach a `TenantContext` (`{ tenantId, userId }`), injected in controllers via `@TenantCtx()`. All analytics queries filter by tenant via `addTenantFilter(qb, tenantId)` from `query-helpers.ts`. Never scope, key, cache, or authorize by user id.\n\n## API Endpoints\n\n| Method                    | Route                                           | Auth                                | Purpose                                                                                                     |\n| ------------------------- | ----------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |\n| GET                       | `/api/v1/health`                                | Public                              | Health check                                                                                                |\n| ALL                       | `/api/auth/*`                                   | Public                              | Better Auth (login, register, OAuth, sessions)                                                              |\n| GET                       | `/api/v1/overview`                              | Session/API Key                     | Dashboard summary                                                                                           |\n| GET                       | `/api/v1/tokens`                                | Session/API Key                     | Token usage analytics                                                                                       |\n| GET                       | `/api/v1/costs`                                 | Session/API Key                     | Cost analytics                                                                                              |\n| GET                       | `/api/v1/agents`                                | Session/API Key                     | Agent list with sparklines                                                                                  |\n| POST                      | `/api/v1/agents`                                | Session/API Key                     | Create agent + API key                                                                                      |\n| GET                       | `/api/v1/agents/:agentName`                     | Session/API Key                     | Single agent detail                                                                                         |\n| GET/POST                  | `/api/v1/agents/:agentName/duplicate*`          | Session/API Key                     | Duplicate agent (preview + confirm)                                                                         |\n| DELETE                    | `/api/v1/agents/:agentName`                     | Session/API Key                     | Delete agent                                                                                                |\n| GET                       | `/api/v1/agents/:agentName/key`                 | Session/API Key                     | Get agent API key                                                                                           |\n| POST                      | `/api/v1/agents/:agentName/rotate-key`          | Session/API Key                     | Rotate API key                                                                                              |\n| PATCH                     | `/api/v1/agents/:agentName`                     | Session/API Key                     | Rename agent                                                                                                |\n| GET                       | `/api/v1/messages`                              | Session/API Key                     | Paginated Requests log (legacy route name)                                                                  |\n| GET/PATCH/DELETE          | `/api/v1/messages/:id/*`                        | Session/API Key                     | Request details, feedback, miscategorized flag (legacy route name)                                          |\n| GET                       | `/api/v1/security`                              | Session/API Key                     | Security score + events                                                                                     |\n| GET                       | `/api/v1/model-prices`                          | Session/API Key                     | Model pricing list                                                                                          |\n| GET                       | `/api/v1/free-models`                           | Session/API Key                     | Free LLM model catalog                                                                                      |\n| GET                       | `/api/v1/agent/usage`                           | Bearer (mnfst\\_\\*)                  | Token usage for the calling agent                                                                           |\n| GET                       | `/api/v1/agent/costs`                           | Bearer (mnfst\\_\\*)                  | Cost data for the calling agent                                                                             |\n| GET                       | `/api/v1/overview/*`                            | Session/API Key                     | Overview timeseries/breakdown sub-endpoints                                                                 |\n| GET                       | `/api/v1/providers` / `/api/v1/providers/usage` | Session/API Key                     | Connected provider list + usage                                                                             |\n| GET                       | `/api/v1/provider-analytics/*`                  | Session/API Key                     | Per-provider analytics                                                                                      |\n| GET                       | `/api/v1/errors/breakdown`                      | Session/API Key                     | Error breakdown analytics                                                                                   |\n| GET/PATCH                 | `/api/v1/billing/*`                             | Session/API Key                     | Billing status, light `plan` endpoint, email preferences (Stripe)                                           |\n| POST                      | `/api/v1/waitlist/autofix/claim`                | Public                              | Deprecated no-op compatibility route for older self-hosted versions                                         |\n| POST                      | `/api/v1/waitlist/pivot/claim`                  | Public                              | Closed pivot waiting-list claim, kept for older self-hosted versions; records origin (cloud/self-hosted)   |\n| GET/POST                  | `/api/v1/autofix/status` / `.../enable-all`     | Session/API Key                     | Workspace Autofix coverage + API-only fleet enable (no dashboard caller)                                    |\n| GET                       | `/api/v1/overview/autofix-*`                    | Session/API Key                     | Autofix analytics (stats, timeseries, per-agent/provider/model)                                             |\n| POST                      | `/api/v1/discovery/complete`                    | Session/API Key                     | Best-effort self-hosted discovery submission forwarded to Peacock                                           |\n| GET/POST/DELETE           | `/api/v1/internal/error-pages*`                 | Public (`x-internal-secret` header) | Custom error-page config (Peacock CMS push API)                                                             |\n| GET                       | `/api/v1/internal/crm-metrics*`                 | Public (`x-internal-secret` header) | **Cloud only.** Autofix-healed user cohort, pivot waitlist claims, and corporate signups, for the outreach CRM (`CRM_METRICS_SECRET`) |\n| GET/PUT/DELETE            | `/api/v1/agents/:agentName/enabled-providers*`  | Session/API Key                     | Per-agent provider enable/disable + impact preview                                                          |\n| GET/POST/PATCH/DELETE     | `/api/v1/notifications/*`                       | Session/API Key                     | Notification rules CRUD + email provider config                                                             |\n| GET/POST/PUT/PATCH/DELETE | `/api/v1/routing/:agentName/*`                  | Session/API Key                     | Routing config (tiers, providers, model-params, header-tiers, custom-providers, specificity, autofix, recording, etc.) |\n| GET/PATCH                 | `/api/v1/routing/:agentName/tiers/:tier/model-params` | Session/API Key                     | Model params addressed by tier (`default` or a custom tier name) + `?model=` (defaults to the tier primary); backs `mnfst routing params` and the MCP `manifest_routing_params_*` tools. Writes the same rows as the scope-keyed `…/model-params` |\n| POST                      | `/api/v1/routing/ollama/sync`                   | Session/API Key                     | Sync Ollama models                                                                                          |\n| GET                       | `/api/v1/routing/pricing-health`                | Session/API Key                     | OpenRouter pricing sync health                                                                              |\n| POST                      | `/api/v1/routing/pricing/refresh`               | Session/API Key                     | Force pricing cache refresh                                                                                 |\n| GET/POST/DELETE           | `/api/v1/oauth/:provider/*`                     | Session/API Key                     | OAuth flows (Gemini, OpenAI, Anthropic, xAI, Kiro, MiniMax)                                                 |\n| POST                      | `/api/v1/routing/resolve`                       | Bearer (mnfst\\_\\*)                  | Model resolution                                                                                            |\n| POST                      | `/api/v1/routing/subscription-providers`        | Bearer (mnfst\\_\\*)                  | Subscription provider config                                                                                |\n| GET                       | `/api/v1/setup/status`                          | Public                              | First-run setup status                                                                                      |\n| POST                      | `/api/v1/setup/admin`                           | Public                              | Create initial admin user                                                                                   |\n| GET                       | `/api/v1/public/error-pages*`                   | Public (opt-in)                     | Published error pages for the marketing site (controlled by `MANIFEST_PUBLIC_STATS`)                        |\n| GET                       | `/v1/models`                                    | Bearer (mnfst\\_\\*)                  | Available model list (proxy)                                                                                |\n| POST                      | `/v1/chat/completions`                          | Bearer (mnfst\\_\\*)                  | LLM proxy (OpenAI-compatible)                                                                               |\n| POST                      | `/v1/responses`                                 | Bearer (mnfst\\_\\*)                  | LLM proxy (OpenAI Responses API)                                                                            |\n| POST                      | `/v1/messages`                                  | Bearer (mnfst\\_\\*)                  | LLM proxy (Anthropic Messages API)                                                                          |\n| POST                      | `/chat/completions`                             | Public                              | Structured 404 pointing callers at `/v1/chat/completions` (missing `/v1` basePath — not a proxy alias)      |\n| ALL                       | `/otlp/v1/*`, `/v1/{traces,metrics,logs}`       | Public                              | Structured **410 Gone** — OTLP ingest is removed (`otlp-deprecated.controller.ts`)                          |\n| GET/POST/PATCH            | `/api/v1/playground/*`                          | Session/API Key                     | Playground runs (run, list, star, mark best)                                                                |\n| GET                       | `/api/v1/events`                                | Session                             | SSE real-time events                                                                                        |\n| GET                       | `/api/v1/github/stars`                          | Public                              | GitHub star count                                                                                           |\n| GET                       | `/api/v1/version`                               | Session/API Key                     | Running version + latest GitHub release for the self-hosted update badge (24h cache; off in cloud)          |\n| GET                       | `/api/v1/me`                                    | Session/API Key (tenant required)   | Identity probe for CLI/API clients: `{ tenantId, userId, authMethod, expiresAt }`; env `API_KEY` gets 401    |\n| POST                      | `/api/v1/cli/authorize`                         | Session only                        | Browser side of `mnfst login`: mints a PKCE-bound short-lived code                                          |\n| POST/DELETE               | `/api/v1/cli/token`                             | Public (code + verifier) / API Key  | Exchange the code for a `cli` PAT; DELETE revokes the calling PAT on logout                                 |\n| POST                      | `/api/v1/mcp`                                   | OAuth 2.1 bearer (`mcp:read`/`mcp:write`) | Remote MCP server (stateless Streamable HTTP); GET/DELETE answer 405 + `Allow: POST`                  |\n| GET                       | `/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource[/api/v1/mcp]` | Public | OAuth discovery for MCP clients (root aliases of Better Auth's `/api/auth/.well-known/…`)   |\n\n## Environment Variables\n\nSee `packages/backend/.env.example` for all variables. Key ones:\n\n- `BETTER_AUTH_SECRET` — **Required.** Secret for Better Auth session signing (min 32 chars). Generate with `openssl rand -hex 32`.\n- `DATABASE_URL` — **Required** in every environment except `NODE_ENV=test` (which falls back to `postgresql://myuser:mypassword@localhost:5432/mydatabase`, matching the local Docker command). Dev and production both throw on boot if unset. Format: `postgresql://user:password@host:port/database`.\n- `MANIFEST_ENCRYPTION_KEY` — Recommended. AES-256-GCM key (min 32 chars) for encrypting stored provider API keys and OAuth tokens. Defaults to `BETTER_AUTH_SECRET` if unset — set this independently so a session-cookie leak doesn't also expose provider credentials.\n- `MANIFEST_ENCRYPTION_KEY_PREVIOUS` — Optional, rotation only. A secret that stored values may still be encrypted under. Decryption always tries `MANIFEST_ENCRYPTION_KEY`, then this, then `BETTER_AUTH_SECRET`, so old rows stay readable. While it is set, one replica rewrites every row found under an older secret onto the current one after boot (`database/secret-reencryption.service.ts`, advisory-locked, never blocks or fails boot); a normal boot without it never scans. Rotation: set the new `MANIFEST_ENCRYPTION_KEY` and `MANIFEST_ENCRYPTION_KEY_PREVIOUS=<old>`, deploy, wait for the log line `Nothing left under an older secret`, then remove `PREVIOUS`. Introducing a dedicated key on an install that used the `BETTER_AUTH_SECRET` fallback: set `MANIFEST_ENCRYPTION_KEY` (old rows keep decrypting via the session secret); to move them onto the new key, set `PREVIOUS` to the session secret's value for one deploy. Request recordings are not rewritten by this pass: they decrypt under the current key only, so a key change makes older recordings unreadable until retention removes them.\n- `PORT` — Server port. Default: `3001`\n- `BIND_ADDRESS` — Bind address. Default: `127.0.0.1` (use `0.0.0.0` for Railway/Docker)\n- `NODE_ENV` — `development` or `production`. Dev allows broad CORS (local dashboard + Wingman); production allows the hosted Wingman origin plus any `WINGMAN_CORS_ORIGINS` entries.\n- `CORS_ORIGIN` — Allowed CORS origin (dev). Default: `http://localhost:3000`\n- `WINGMAN_CORS_ORIGINS` — Production only. Extra browser origins allowed to call the gateway (comma-separated). The hosted Wingman (`https://wingman.manifest.build`) is always allowed.\n- `BETTER_AUTH_URL` — Base URL for Better Auth. Default: `http://localhost:{PORT}`\n- `FRONTEND_PORT` \n\n<!-- [TRUNCATED AT 50KB] -->","isInternal":false,"tokens":10910,"sizeBytes":51618},{"name":"SKILL.md","path":".claude/skills/mnfst-cli/SKILL.md","rawUrl":"https://raw.githubusercontent.com/mnfst/manifest/HEAD/.claude/skills/mnfst-cli/SKILL.md","title":"mnfst-cli","category":"anthropic-skill","format":"markdown","content":"---\nname: mnfst-cli\ndescription: Use when creating or operating AI agents that make LLM calls — provisioning model routing and keys, wiring an app or automation to an LLM endpoint, testing routes, rotating leaked keys, or investigating LLM request failures and costs on a Manifest install.\n---\n\n# Operating Manifest from the CLI\n\n## Overview\n\n`mnfst` manages Manifest, the LLM gateway. Every call routed through a Manifest agent gets model routing with fallbacks, per-agent cost attribution, self-healing (Auto-fix), and a request log — for free. Default practice: never hand an app or automation a raw provider key; give it a Manifest agent.\n\nCommands emit JSON on stdout (human hints on stderr) — except `agent env`, which prints dotenv lines by design — exit 0/1, and never block on prompts when credentials come from flags or env. Full surface: `mnfst --help`.\n\n## The flow\n\n| Step | Command |\n|---|---|\n| Auth (human) | `mnfst login` — browser, one click, 30-day sliding token |\n| Auth (script/agent) | env `MANIFEST_URL` + `MANIFEST_API_KEY` — no login step |\n| Provision | `mnfst agent create --name X --platform <p> --if-absent` → response includes `setup` (the platform's config block) |\n| Check connections | `mnfst provider list [--agent X]` FIRST — the provider you need is often already connected. A connection is only usable if `cached_model_count` > 0; `is_active: true` with 0 models is hollow — run `mnfst provider refresh [<provider>]` (re-runs discovery tenant-wide, prints the new counts); still 0 means a bad credential, so reconnect, don't route on it |\n| Connect provider | `mnfst provider connect xai --auth-type api_key --credential-env KEY` · `provider catalog` lists all 30+ with auth types · subscription auth opens a browser (impossible headless — reuse an existing subscription connection instead) · own gateway: `provider custom add --name gw --endpoint <url>` |\n| Route | `mnfst agent configure X --models primary,fb1,fb2 --provider p [--auth-type a]` — first model is the route, the rest are fallbacks, and the whole chain rides the one `--provider` you name (cross-provider fallbacks are dashboard-only) · add `--tier deep` to upsert a custom tier callers select per-request with header `x-manifest-tier: deep` |\n| Verify | `mnfst routing test X` — one real request through the platform's actual API surface; broken routes fail loudly. It writes real rows to the request log — count them in later audits |\n| Wire the app | deployed → `mnfst agent env X >> .env` (one .env per service — the lines always name `MANIFEST_AGENT_KEY`, so appending two agents to one file silently keeps only the loader's winner; for several agents in one process use `mnfst run --agent X --env ROLE_KEY -- <cmd>`) |\n| Observe | `mnfst requests get --agent X [--status failed]` — paginated; pass `next_cursor` back for the next page |\n\nNamespace = scope: `provider *` acts tenant-wide, `agent *` acts on one agent, `routing *` holds readouts and custom-tier lifecycle.\n\n## Gotchas `--help` cannot tell you\n\n| Symptom | Reality |\n|---|---|\n| Proxy returns HTTP 200 but the \"answer\" starts with `[🦚 Manifest M###]` | An error wearing an assistant costume — check the content, not the status. `routing test` unmasks these automatically |\n| `cost: \"0.000000\"` on subscription-auth traffic | Flat-rate subscription: per-request cost is genuinely zero, not broken billing |\n| `agent configure`'s echo differs from later reads | Mutation bodies echo raw API rows — the canonical readback is `routing status <agent>` |\n| \"Which requests used a fallback or Auto-fix?\" | Those fields are trimmed from default output — `requests get --full` |\n| 401s missing from the request log | Rejected-auth attempts are never recorded (no tenant to attribute them to); a leaked key's blast radius cannot be proven from the log |\n| `MANIFEST_AGENT_URL` | Already ends in `/v1` — append `/chat/completions` directly |\n| Model prices needed before any agent exists | `mnfst model prices [--provider <p>]` — install-wide, no agent needed (`models <agent>` is the per-agent routable set) |\n| Auditing escalation traffic by `routing_tier` | Custom-tier requests log `routing_tier: \"standard\"` — the tier identity lives in `header_tier_name` |\n| Route resolves but requests fail on one connection | A connection can be `is_active: false` while a sibling auth-type works — pass `--auth-type` explicitly when a provider has several connections |\n| Every command answers `Invalid API key — Run mnfst login` under env auth | Not always an auth problem: a wrong `MANIFEST_URL` (dead or different install) answers exactly like a bad key. Run `mnfst doctor` — it checks config, host, credential, connections and agents in that order, so it separates \"host is dead\" from \"key is wrong for this host\" — and never tells you to re-login on an env key |\n| Need a different reasoning effort, temperature, or other model param without editing the app | `mnfst routing params get X [--tier t] [--model m]` lists what the routed model accepts and what is saved; `mnfst routing params set X [--tier t] [--model m] --set reasoning.effort=high [--unset temperature]` saves it. Saved params **override** what the app sends, per tier and per model (a fallback keeps its own), but only on requests Manifest routes: `model: \"auto\"` or a matching custom-tier header. A request naming a concrete model skips them |\n| `agent configure` / `routing custom create` reject a model as undiscovered | Both validate against the agent's *discovered* models, and a connection with `cached_model_count: 0` contributes none: run `mnfst provider refresh` (stale or empty catalog), or pass `--force` — the backend still routes uncatalogued models through provider-qualified passthrough |\n\n## Common mistakes\n\n- Handing an app a raw provider key — it loses fallbacks, cost attribution, healing, and the log. Create an agent and use `agent env`.\n- Verifying a route with hand-rolled `curl` — fake-200s read as success. Use `routing test`.\n- Slurping the whole request log — pagination is one page per call by design; loop on `next_cursor`.\n\nAlso on the surface when needed: `mnfst doctor` (first stop when anything is wrong), `agent setup <name> [--reveal]` (setup block anytime; key masked unless revealed), `agent key path|show`, `agent provider enable|disable <agent> <provider>`, `agent rotate-key` (revocation is instant), `mnfst models <agent> --cost --capabilities`.\n","frontmatter":{"name":"mnfst-cli","description":"Use when creating or operating AI agents that make LLM calls — provisioning model routing and keys, wiring an app or automation to an LLM endpoint, testing routes, rotating leaked keys, or investigating LLM request failures and costs on a Manifest install."},"isInternal":false,"tokens":1533,"sizeBytes":6481},{"name":"phoenix-openapi.yaml","path":"packages/backend/src/routing/autofix/contract/phoenix-openapi.yaml","rawUrl":"https://raw.githubusercontent.com/mnfst/manifest/HEAD/packages/backend/src/routing/autofix/contract/phoenix-openapi.yaml","title":"Contract Skill","category":"anthropic-skill","format":"yaml","content":"openapi: 3.1.0\ninfo:\n  title: Phoenix MVP — request-healing API\n  version: 0.0.1\n  description: >\n    Advisory request-healing for LLM providers. Submit a failed request plus the\n    provider error to get a corrected request body, then report whether the retry\n    worked. See README.md for the full loop and data model.\nservers:\n  - url: http://127.0.0.1:3000\n    description: Local development\n  - url: https://your-app.up.railway.app\n    description: Production — set to your Railway deployment URL\n\nsecurity:\n  - ApiKeyAuth: []\n\npaths:\n  /api/heal:\n    post:\n      summary: Resolve a patch for a failed request\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      description: >\n        Fingerprints the error and returns a healed request body if a\n        deterministic patch exists. Only request-side errors are healable.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/HealRequest'\n      responses:\n        '200':\n          description: A heal decision (patched, pending confirmation, still resolving, or no patch).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HealResult'\n        '400':\n          description: Malformed body (failed schema validation).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ZodError'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '422':\n          description: Error is not request-side (5xx / auth / rate-limit); nothing to heal.\n\n  /api/heal/observe:\n    post:\n      summary: Record failed requests as diagnostic evidence\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ObservationBatch'\n      responses:\n        '200':\n          description: Observation batch accepted.\n        '401':\n          description: Missing or invalid static key or instance credential.\n\n  /api/heal-attempts/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read a heal attempt and its parent issue\n      responses:\n        '200':\n          description: The heal attempt.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HealAttemptView'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n    patch:\n      summary: Report the outcome of a retried request\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/HealOutcome'\n      responses:\n        '200':\n          description: Recorded outcome (idempotent).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OutcomeResult'\n        '400':\n          description: Malformed body, or error omitted when retryStatusCode >= 400.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ZodError'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: No such heal attempt.\n\n  /api/issues/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read an issue and its resolved patch\n      responses:\n        '200':\n          description: The issue.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/IssueView'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n\n  /api/issues:\n    get:\n      summary: List issues (most-recently-updated first)\n      parameters:\n        - name: patchId\n          in: query\n          schema: { type: string, format: uuid }\n          description: Only issues fixed by this patch.\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Issues.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/IssueListItem' }\n        '400':\n          description: Malformed query.\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/heal-attempts:\n    get:\n      summary: List heal attempts (most-recent first)\n      parameters:\n        - name: issueId\n          in: query\n          schema: { type: string, format: uuid }\n        - name: patchId\n          in: query\n          schema: { type: string, format: uuid }\n        - name: traceId\n          in: query\n          schema: { type: string }\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Heal attempts.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/AttemptListItem' }\n        '400':\n          description: Malformed query.\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/patches:\n    get:\n      summary: List patches with usage counts (most-recent first)\n      parameters:\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Patches.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/PatchListItem' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/patches/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read a patch and the issues it fixes\n      responses:\n        '200':\n          description: The patch.\n          content:\n            application/json:\n              schema: { $ref: '#/components/schemas/PatchDetail' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n\n  /api/traces:\n    get:\n      summary: List traces with per-trace outcome counts (most-recent first)\n      parameters:\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Traces.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/TraceListItem' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/traces/{traceId}:\n    parameters:\n      - name: traceId\n        in: path\n        required: true\n        schema: { type: string }\n    get:\n      summary: A trace's heal attempts, oldest-first\n      responses:\n        '200':\n          description: The trace timeline.\n          content:\n            application/json:\n              schema: { $ref: '#/components/schemas/TraceDetail' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: No such trace.\n\n  /api/health:\n    get:\n      summary: Liveness probe (public — no API key)\n      security: []\n      responses:\n        '200':\n          description: Process is up.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  status: { type: string, example: ok }\n                  uptimeSeconds: { type: integer }\n\n  /api/ready:\n    get:\n      summary: Readiness probe — process up and database reachable (public)\n      security: []\n      responses:\n        '200':\n          description: Ready.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  status: { type: string, example: ready }\n        '503':\n          description: Database not ready.\n\ncomponents:\n  securitySchemes:\n    ApiKeyAuth:\n      type: apiKey\n      in: header\n      name: x-api-key\n      description: >\n        Static API key. Send as `x-api-key: <key>` (an `Authorization: Bearer\n        <key>` header is also accepted). Enforced in deployed environments where\n        the API_KEY env var is set; disabled locally when it is unset.\n    ManifestInstanceAuth:\n      type: apiKey\n      in: header\n      name: X-Manifest-Instance\n      description: >\n        Anonymous install identifier announced by a self-hosted Manifest. This\n        is an identifier, not a credential: it is not secret and no registration\n        handshake issues it. Phoenix creates the instance row the first time it\n        sees an id. Accepted in place of ApiKeyAuth on the gateway write\n        operations.\n\n  parameters:\n    Id:\n      name: id\n      in: path\n      required: true\n      schema:\n        type: string\n        format: uuid\n    Limit:\n      name: limit\n      in: query\n      description: Page size (default 50, max 200).\n      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }\n    Offset:\n      name: offset\n      in: query\n      description: Rows to skip for pagination.\n      schema: { type: integer, minimum: 0, default: 0 }\n    ManifestVersion:\n      name: X-Manifest-Version\n      in: header\n      required: false\n      description: Running Manifest release version. Invalid values become `unknown` in analytics.\n      schema:\n        type: string\n        maxLength: 32\n        pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'\n    ManifestHarness:\n      name: X-Manifest-Harness\n      in: header\n      required: false\n      description: Agent platform slug. Missing or unknown values become `other` in analytics.\n      schema:\n        type: string\n        enum:\n          [\n            openclaw,\n            hermes,\n            nanobot,\n            craft,\n            n8n,\n            claude-code,\n            opencode,\n            codex,\n            openai-sdk,\n            anthropic-sdk,\n            vercel-ai-sdk,\n            langchain,\n            curl,\n            other,\n          ]\n  schemas:\n    InstanceRegistration:\n      type: object\n      required: [version, schema_version]\n      properties:\n        version:\n          type: string\n          maxLength: 32\n          pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'\n        schema_version:\n          type: integer\n          const: 1\n      additionalProperties: true\n\n    InstanceCredential:\n      type: object\n      required: [instance_id, secret]\n      properties:\n        instance_id: { type: string, format: uuid }\n        secret:\n          type: string\n          description: Opaque 256-bit base64url bearer token. Returned only once.\n      additionalProperties: false\n\n    ObservationBatch:\n      type: object\n      required: [observations]\n      properties:\n        observations:\n          type: array\n          minItems: 1\n          maxItems: 200\n          items: { $ref: '#/components/schemas/HealRequest' }\n\n    ProviderError:\n      type: object\n      required: [message]\n      properties:\n        message: { type: string }\n        type: { type: [string, 'null'] }\n        param: { type: [string, 'null'] }\n        code: { type: [string, 'null'] }\n\n    Operation:\n      type: object\n      description: A deterministic transform applied to the request body.\n      required: [type]\n      properties:\n        type: { type: string }\n      additionalProperties: true\n      example: { type: rename_param, from: max_tokens, to: max_output_tokens }\n\n    HealRequest:\n      type: object\n      required: [traceId, provider, api, request, response]\n      properties:\n        traceId:\n          type: string\n          minLength: 1\n          description: >\n            Caller-supplied id of the logical request being healed. Send the\n            same value on every heal of that request's retry chain (not the\n            provider's per-call request id) — it groups the attempts into one\n            trace.\n        tenantId:\n          type: string\n          minLength: 1\n          description: >\n            Opaque id of the gateway tenant this request belongs to (Manifest's\n            Tenant.id). Optional on the wire; recorded so a failure can be\n            attributed to a tenant. Manifest always sends it on the proxy path.\n        provider: { type: string, minLength: 1, example: openai }\n        model:\n          type: string\n          minLength: 1\n          maxLength: 1024\n          description: >\n            Logical provider model identity. Native transports may encode it in\n            the URL rather than in the JSON request body.\n        authType:\n          type: string\n          enum: [api_key, subscription, local]\n          description: Provider credential route. Optional during the Manifest rollout.\n        api:\n          type: string\n          enum: [responses, chat_completions, messages]\n        url: { type: string, format: uri }\n        request:\n          type: object\n          description: The original outgoing request body, verbatim.\n          additionalProperties: true\n        response:\n          type: object\n          required: [statusCode, error]\n          properties:\n            statusCode: { type: integer, example: 400 }\n            error: { $ref: '#/components/schemas/ProviderError' }\n        providerExchange:\n          type: object\n          description: Final provider-native transport exchange, separate from the patch input.\n          required: [format, request, response]\n          properties:\n            format:\n              type: string\n              enum:\n                - openai_chat_completions\n                - openai_responses\n                - anthropic_messages\n                - google_generate_content\n                - google_code_assist\n            url: { type: string, format: uri }\n            request:\n              type: object\n              required: [body]\n              properties:\n                body: { type: object, additionalProperties: true }\n                redactedFields: { type: array, items: { type: string } }\n            response:\n              type: object\n              required: [statusCode, body]\n              properties:\n                statusCode: { type: integer }\n                body: {}\n        responseTimeMs: { type: integer, minimum: 0 }\n        responseSizeBytes: { type: integer, minimum: 0 }\n\n    HealResult:\n      type: object\n      required: [status, issueId]\n      properties:\n        status:\n          type: string\n          enum: [patched, unverified, resolving, no_patch]\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        healAttemptId: { type: string, format: uuid }\n        operations:\n          type: array\n          items: { $ref: '#/components/schemas/Operation' }\n        healedBody:\n          type: object\n          additionalProperties: true\n        explanation:\n          type: object\n          description: >\n            Human-readable \"why\" for a served heal (present on \"patched\" /\n            \"unverified\"). operations[].detail is always deterministic (built from\n            the operations); summary prefers an authored rationale (source \"agent\" /\n            \"operator\") and falls back to the deterministic composition (source\n            \"deterministic\").\n          required: [summary, operations, source]\n          properties:\n            summary: { type: string }\n            operations:\n              type: array\n              items:\n                type: object\n                required: [type, detail]\n                properties:\n                  type: { type: string }\n                  detail: { type: string }\n            source:\n              type: string\n              enum: [deterministic, agent, operator]\n        retryAfterMs:\n          type: integer\n          description: Present only when status is \"resolving\".\n\n    HealOutcome:\n      type: object\n      required: [retryStatusCode]\n      description: error is required when retryStatusCode >= 400.\n      properties:\n        retryStatusCode: { type: integer, example: 200 }\n        error: { $ref: '#/components/schemas/ProviderError' }\n\n    OutcomeResult:\n      type: object\n      required: [healAttemptId, status, issueStatus]\n      properties:\n        healAttemptId: { type: string, format: uuid }\n        status:\n          type: string\n          enum: [succeeded, failed, expired]\n        issueStatus: { type: string }\n\n    IssueView:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        fingerprint: { type: string }\n        provider: { type: string }\n        api: { type: string }\n        model: { type: [string, 'null'] }\n        error:\n          type: object\n          properties:\n            code: { type: [string, 'null'] }\n            type: { type: [string, 'null'] }\n            param: { type: [string, 'null'] }\n        status:\n          type: string\n          enum: [resolving, unverified, verified, ineffective, no_fix_found]\n        occurrenceCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n        patch:\n          type: [object, 'null']\n          properties:\n            id: { type: string, format: uuid }\n            operations:\n              type: array\n              items: { $ref: '#/components/schemas/Operation' }\n\n    HealAttemptView:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        traceId: { type: string }\n        status:\n          type: string\n          enum: [pending, succeeded, failed, expired]\n        retryStatusCode: { type: [integer, 'null'] }\n        retryErrorFingerprint: { type: [string, 'null'] }\n        confirmedAt: { type: [string, 'null'], format: date-time }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n        issue:\n          type: [object, 'null']\n          properties:\n            id: { type: string, format: uuid }\n            status: { type: string }\n            provider: { type: string }\n            api: { type: string }\n            model: { type: [string, 'null'] }\n            fingerprint: { type: string }\n\n    IssueListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        fingerprint: { type: string }\n        provider: { type: string }\n        api: { type: string }\n        model: { type: [string, 'null'] }\n        errorCode: { type: [string, 'null'] }\n        errorType: { type: [string, 'null'] }\n        errorParam: { type: [string, 'null'] }\n        status:\n          type: string\n          enum: [resolving, unverified, verified, ineffective, no_fix_found]\n        patchId: { type: [string, 'null'], format: uuid }\n        occurrenceCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    AttemptListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        traceId: { type: string }\n        status:\n          type: string\n          enum: [pending, succeeded, failed, expired]\n        retryStatusCode: { type: [integer, 'null'] }\n        retryErrorFingerprint: { type: [string, 'null'] }\n        confirmedAt: { type: [string, 'null'], format: date-time }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    PatchListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        operations:\n          type: array\n          items: { $ref: '#/components/schemas/Operation' }\n        operationsHash: { type: string }\n        issueCount: { type: integer }\n        attemptCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    PatchDetail:\n      allOf:\n        - $ref: '#/components/schemas/PatchListItem'\n        - type: object\n          properties:\n            issues:\n              type: array\n              items: { $ref: '#/components/schemas/IssueListItem' }\n\n    TraceListItem:\n      type: object\n      properties:\n        traceId: { type: string }\n        attemptCount: { type: integer }\n        pendingCount: { type: integer }\n        succeededCount: { type: integer }\n        failedCount: { type: integer }\n        firstAt: { type: string, format: date-time }\n        lastAt: { type: string, format: date-time }\n\n    TraceAttempt:\n      allOf:\n        - $ref: '#/components/schemas/AttemptListItem'\n        - type: object\n          properties:\n            issue:\n              type: object\n              properties:\n                id: { type: string, format: uuid }\n                status: { type: string }\n                provider: { type: string }\n                api: { type: string }\n                model: { type: [string, 'null'] }\n                errorCode: { type: [string, 'null'] }\n                errorType: { type: [string, 'null'] }\n                errorParam: { type: [string, 'null'] }\n            patch:\n              type: object\n              properties:\n                id: { type: string, format: uuid }\n                operations:\n                  type: array\n                  items: { $ref: '#/components/schemas/Operation' }\n\n    TraceDetail:\n      type: object\n      properties:\n        traceId: { type: string }\n        attempts:\n          type: array\n          items: { $ref: '#/components/schemas/TraceAttempt' }\n\n    ZodError:\n      type: object\n      description: Flattened zod validation error.\n      properties:\n        formErrors:\n          type: array\n          items: { type: string }\n        fieldErrors:\n          type: object\n          additionalProperties:\n            type: array\n            items: { type: string }\n","isInternal":false,"tokens":5108,"sizeBytes":23002},{"name":"README.md","path":"packages/cli/README.md","rawUrl":"https://raw.githubusercontent.com/mnfst/manifest/HEAD/packages/cli/README.md","title":"cli Documentation","category":"plugin-manifest","format":"markdown","content":"# Manifest CLI\n\n`mnfst` manages a Manifest gateway from your terminal. Use it to create agents, connect AI providers, configure model routing, and inspect requests. The dashboard calls agents **Harnesses**; CLI commands use `agent`.\n\nThe npm package is `mnfst-gateway-cli`. The command it installs is `mnfst`. Node.js 20 or later is required.\n\n## Install\n\n```bash\nnpm install -g mnfst-gateway-cli\nmnfst --help\n```\n\nTo run it without a global install:\n\n```bash\nnpx mnfst-gateway-cli --help\n```\n\n## Quick start\n\nSign in to the hosted Manifest gateway, then create an agent and connect a provider:\n\n```bash\nmnfst login\nmnfst agent create --name my-app --platform openai-sdk\nmnfst provider connect openai --agent my-app --credential-env OPENAI_API_KEY\nmnfst models my-app --provider openai\nmnfst agent configure my-app --models gpt-4o-mini --provider openai\nmnfst routing status my-app\nmnfst agent setup my-app\n```\n\nSet `OPENAI_API_KEY` in your environment before connecting the provider. Choose a model shown by `mnfst models` if `gpt-4o-mini` is unavailable to your account. `mnfst agent setup` returns wiring instructions with the agent key masked.\n\nFor a self-hosted gateway, pass its URL when you sign in:\n\n```bash\nmnfst login --url https://gateway.example.com\n```\n\nThe CLI uses that gateway for later commands. You can select another gateway with `--url` or `MANIFEST_URL`.\n\n## Useful commands\n\n| Task                           | Command                                        |\n| ------------------------------ | ---------------------------------------------- |\n| Check your connection          | `mnfst doctor`                                 |\n| List available agent platforms | `mnfst agent platforms`                        |\n| List connectable providers     | `mnfst provider catalog`                       |\n| Review an agent's routing      | `mnfst routing status my-app`                  |\n| Read or set model params       | `mnfst routing params get my-app`              |\n| Read recent requests           | `mnfst requests get --agent my-app --range 7d` |\n| See all commands and options   | `mnfst --help`                                 |\n\nMost commands write JSON to stdout for scripts and coding agents. `mnfst agent env` and `mnfst skill show` write text. `mnfst routing test` sends a real provider request, which may incur provider charges. Destructive commands require `--yes`.\n\n## Credentials\n\nBrowser login is the default. For a script, provide a management credential through standard input or an environment variable:\n\n```bash\nprintf '%s' \"$MY_KEY\" | mnfst login --token-stdin\n# or\nmnfst login --token-env MY_KEY\n```\n\nThe CLI stores login credentials for each gateway host in `~/.config/manifest/config.json` with file mode `0600`. A stored credential is only sent to the host where it was saved. `MANIFEST_API_KEY` overrides the stored credential for the selected host.\n\nAgent keys are separate from management credentials. The CLI stores a newly created agent key in its local keystore and does not print it in `mnfst agent setup` unless you pass `--reveal`. Use `mnfst agent env my-app` to get the key and URL for your application; its output contains the key, so handle it as a secret.\n\n## Usage telemetry\n\nThe CLI records usage telemetry by default. It batches events locally and normally sends one request per install per day. The payload contains a random, persistent install ID; CLI version; operating system; Cloud or self-hosted target; and each command's name, result, duration, and minute-level time. It may include the detected coding-agent runtime. It does not include command arguments, agent or provider names, gateway URLs, prompts, or credentials.\n\nSet `MANIFEST_TELEMETRY_DISABLED=1` to opt out. Telemetry files are stored beside the CLI config. You can set `MANIFEST_CLI_TELEMETRY_ENDPOINT` to send usage data to your own endpoint.\n\n## Documentation\n\nSee the [CLI documentation](https://manifest.build/llm-gateway/docs/cli/) for more examples and the [GitHub repository](https://github.com/mnfst/llm-gateway) for source and issues.\n","isInternal":false,"tokens":905,"sizeBytes":4075},{"name":"README.md","path":"packages/manifest/README.md","rawUrl":"https://raw.githubusercontent.com/mnfst/manifest/HEAD/packages/manifest/README.md","title":"manifest Documentation","category":"plugin-manifest","format":"markdown","content":"# manifest\n\nThis package exists to hold the canonical **Manifest release version**.\n\nIt contains no code. The real backend and frontend live in [`../backend`](../backend) and [`../frontend`](../frontend), and shared types in [`../shared`](../shared). The Docker image at [`manifestdotbuild/manifest`](https://hub.docker.com/r/manifestdotbuild/manifest) bundles the built backend + frontend.\n\n## What it's for\n\n- **Changesets target**: `.changeset/config.json` ignores `manifest-backend`, `manifest-frontend`, and `manifest-shared`, so when contributors run `npx changeset` the only selectable package is this one. Always target `manifest` regardless of which files you actually changed — this is the single source of truth for \"the version of Manifest as a whole\".\n- **Docker release version**: `.github/workflows/docker.yml` reads the `version` field from this package's `package.json` via `jq` and uses it to tag the image (`manifestdotbuild/manifest:{version}` + derived `{major}.{minor}` / `{major}` / `sha-<short>` tags).\n- **Changelog**: `CHANGELOG.md` here (auto-generated by changesets) is the unified Manifest release history.\n\n## Adding a changeset\n\n```bash\nnpx changeset\n# → select \"manifest\"\n# → choose patch / minor / major\n# → write a one-line summary\n```\n\nCommit the generated file. On merge to `main`, the release workflow opens a `chore: version packages` PR bumping `version` and appending to `CHANGELOG.md`. Merge that PR to land the version bump, then manually run the Docker workflow (`Actions → Docker → Run workflow` with a blank input) to publish.\n","isInternal":false,"tokens":366,"sizeBytes":1584},{"name":".cursorrules","path":".cursorrules (manifest-yaml-cursorrules-prompt-file.mdc)","rawUrl":"https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/manifest-yaml-cursorrules-prompt-file.mdc","title":"Awesome Cursorrules: manifest yaml","category":"cursor-rule","format":"markdown","content":"---\ndescription: \"Cursor rules for manifest development with YAML integration.\"\nglobs: **/*\nalwaysApply: false\n---\n**Prompt for Expert Manifest Developer**\n\n**You are an assistant for app creation. You are going to use the backend Manifest. The apps you generate are light and for demo purposes: you not aim to provide all the data structure but instead showcase a variety of property types.**\n\n**Code structure**\nWhen asked to create a backend, execute the following actions:\n\n1. Install the `manifest` npm package\n2. Add the following scripts to `pacakge.json`: \"manifest\": \"node node_modules/manifest/scripts/watch/watch.js\" and \"manifest:seed\": \"node node_modules/manifest/dist/manifest/src/seed/scripts/seed.js\"\n3. Create the `manifest/backend.yml` file and add the manifest code to it.\n4. Add the `redhat.vscode-yaml` as recommendation in `.vscode/extensions.json`\n5. Configure `yaml.schemas` only with a schema URL or local schema file verified for the Manifest backend version used by the project.\n\n**Backend file**\nOn the `manifest/backend.yml`, follow those rules:\n- Strictly follow the Manifest JSON Schema shipped with the project or verified from the current Manifest backend documentation.\n- Start by addind a quick name to the app\n- Limit to 2 or 3 entities maximum\n- Limit to 4 properties maximum per entity\n- Try to showcase different property types\n- Only use validation properties once or twice\n- No entity should be called admin\n- Do not use authenticable entities\n- Add an emoji after each entity name, but do not use the emoji it on relationships references\n- Add a linebreak before each entity object\n- Each entity only appears once. Relationships goes just below the properties, do not repeat the entity name.\n- Do not use special characters.\n. Do not use middlewares, endpoints or hooks.\n- Use YAML abbreviated form for objects, with spaces. Example: { name: issueDate, type: date }\n- Do not add relationships to single entities\n- For relationships, use the short form. Ex: ' belongsTo:\n      - Author'\n- Add policies. Most projects only have \"read\" public policies. Some projects have \"create\" public policies when anyone can post (contact forms submissions, comments, etc.)\n- If using the \"choice\" property type, use \"options.values\" property to list choices. Example:  `{ name: type, type: choice, options: { values: [\"Fire\", \"Water\", \"Grass\"] } }`\n- Do not add \"seedCount\" and \"mainProp\" to entities\n\n**Documentation**\nRefer to the Manifest backend documentation that matches the project's installed version.\n\n**Example**\nThis is an example of the content of a `backend.yml` file:\nname: My pet app 🐾\nentities:\n  Owner:\n    properties:\n      - name\n      - { name: birthdate, type: date }\n\n  Cat:\n    properties:\n      - name\n      - { name: age, type: number }\n      - { name: birthdate, type: date }\n    belongsTo:\n      - Owner\n\n  Homepage:\n    nameSingular: Home content\n    single: true\n    properties:\n      - title\n      - { name: description, type: richText }\n      - { name: cover, type: image }\n","tokens":700,"sizeBytes":3036}],"systemPromptSnippet":"<yakaai_skills repo=\"mnfst/manifest\">\n<!-- File: AGENTS.md (Tokens: ~183 | Category: root-instruction) -->\n# Manifest Agent Guidelines\n\n## Domain Terminology\n\nManifest terminology is directional:\n\n- A **Manifest Request** is one logical request from an agent to Manifest and lives in `requests`.\n- A **Provider Attempt** is one request from Manifest to an AI provider and lives in `agent_messages`.\n- A **Tenant** is a user's data boundary. It is created from `user.id` on first agent creation.\n- An **Agent** is an AI agent owned by a tenant. The dashboard labels agents **Harnesses** (nav under `/harnesses`; legacy `/agents/*` URLs redirect) — this is UI copy only; backend code, database tables, and API routes still say *agent*.\n\n[`docs/glossary.md`](docs/glossary.md) is the canonical contract for statuses, ordering, recovery, database mapping, and counting rules. Do not duplicate those definitions in agent guides.\n\n<!-- File: CLAUDE.md (Tokens: ~10910 | Category: claude-rule) -->\n# Manifest Development Guidelines\n\nLast updated: 2026-09-14\n\n## What Manifest Is\n\nManifest is a smart model router for **AI agents**. It sits between an agent and its LLM providers, scores each request, and routes it to the cheapest model that can handle it. The dashboard tracks logical requests and their provider attempts, costs, and tokens across any agent that speaks OpenAI-compatible HTTP.\n\n**\"Harness\" is the dashboard word for an agent.** The UI now labels agents **Harnesses** (nav item \"Harnesses\", routes under `/harnesses`, categories `AI agent` / `Automation` / `App AI SDK` / `Coding Assistant` from `CATEGORY_LABELS` in `packages/shared/src/agent-type.ts`). This is a **copy-level rename only**: backend code, database tables (`agents`, `agent_messages`, …), API routes (`/api/v1/agents/*`), and entity/service names all still say *agent*. Legacy `/agents/*` dashboard URLs redirect to `/harnesses/*`. When writing UI copy say \"harness\"; when writing code or API docs keep \"agent\".\n\n**Product note:** this repo is **Manifest LLM Gateway**, the open-source LLM gateway of Manifest, available and maintained. The dashboard and the READMEs carry no promotion of any other Manifest product: not the self-healing layer at `dashboard.manifest.build` (on hold), and no API Bot banner or sidebar card (removed). `POST /api/v1/waitlist/pivot/claim` stays only for self-hosted versions that still ship the old sidebar card. The sidebar's \"Custom development\" button opens `ServicesContactModal`, which posts to `POST /api/v1/services/contact`; the backend relays it to `https://manifest.build/api/contact` (mnfst/website `api/contact.js`), which emails the team. Nothing is stored.\n\n**Supported agents**: see `AGENT_PLATFORMS` in `packages/shared/src/agent-type.ts` for the current list (OpenClaw, Hermes, Claude Code, OpenCode, generic OpenAI/Anthropic SDK slots, and others — don't duplicate the list here, it grows independently of this doc). OpenClaw remains the deepest integration, but no new code or copy should frame Manifest as OpenClaw-only. When adding examples, prefer \"AI agent\" as the noun and pick OpenClaw as the worked example rather than the sole target. Manifest is consumed as a generic OpenAI-compatible HTTP endpoint — there are no first-party OpenClaw plugins in this repo anymore.\n\nWingman — the gateway tester for sending requests against a Manifest backend while impersonating any of the supported agents (useful for routing/header-classifier reproductions) — lives in its own repo at [`mnfst/wingman`](https://github.com/mnfst/wingman) and is hosted at [`wingman.manifest.build`](https://wingman.manifest.build). The dashboard embeds it as an iframe drawer **in dev mode only** — it is dead-code-eliminated from production / self-hosted bundles via `__DEV_MODE__`. The backend allows the hosted Wingman origin through CORS in both dev and production (production also honors `WINGMAN_CORS_ORIGINS`), while the CSP `frame-src` that permits the drawer iframe stays dev-only; both are wired in `packages/backend/src/cors-csp-config.ts`.\n\n**Whenever working in dev mode (`/serve`, `npm run dev`, etc.), the Wingman drawer is expected to be available** — open the FAB at the bottom-right of the dashboard (or hit ⌘/Ctrl+Shift+W) and confirm the iframe loads `https://wingman.manifest.build` cleanly. The drawer is part of the dev surface area, so a broken iframe means the dev environment is broken. `/serve` is **dev-only** — never use it to validate production behavior.\n\n**Three management surfaces share one REST API.** The dashboard, the `mnfst` CLI (`packages/cli/`), and the remote MCP server (`packages/backend/src/mcp/`) all drive the same `/api/v1/*` services with the same tenant scoping. Add a capability to the service layer once, then expose it on whichever surfaces need it; never fork logic per surface. See [CLI login](#cli-login-mnfst-login) and [Remote MCP server](#remote-mcp-server) below.\n\n## IMPORTANT: Cloud Mode Always\n\nWhen starting the app for development or testing (e.g. `/serve`), **always use `MANIFEST_MODE=cloud`** (the default). Every dev session must use a **fresh PostgreSQL database** via Docker — multiple concurrent dev instances sharing one DB cause cross-run data pollution and intermittent test failures:\n\n```bash\n# 1. Ensure the postgres_db container is running\ndocker start postgres_db 2>/dev/null || \\\n  docker run -d --name postgres_db -e POSTGRES_USER=myuser -e POSTGRES_PASSWORD=mypassword -e POSTGRES_DB=mydatabase -p 5432:5432 postgres:16\n\n# 2. Create a pristine database with a unique name\nDB_NAME=\"manifest_$(openssl rand -hex 4)\"\ndocker exec postgres_db psql -U myuser -d postgres -c \"CREATE DATABASE $DB_NAME;\"\n\n# 3. Update DATABASE_URL in packages/backend/.env to use the new database\n# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/$DB_NAME\n\n# 4. Ensure SEED_DATA=true in .env so the database is populated on startup\n```\n\nThis guarantees each session starts with a clean, isolated database and avoids all cross-instance conflicts.\n\n## Testing OpenClaw Integration\n\nTo test routing from an OpenClaw agent against a local Manifest dev server, point OpenClaw at the dev server's OpenAI-compatible proxy directly — there is no plugin anymore:\n\n```bash\n# 1. Build and start the backend in cloud mode\nnpm run build\nPORT=38238 BIND_ADDRESS=127.0.0.1 \\\n  node -r dotenv/config packages/backend/dist/main.js\n\n# 2. Configure OpenClaw to use the dev server as a generic OpenAI-compatible provider\nopenclaw config set models.providers.manifest '{\"baseUrl\":\"http://localhost:38238/v1\",\"api\":\"openai-completions\",\"apiKey\":\"mnfst_YOUR_KEY\",\"models\":[{\"id\":\"auto\",\"name\":\"Manifest Auto\"}]}'\nopenclaw config set agents.defaults.model.primary manifest/auto\n\n# 3. Restart the gateway\nopenclaw gateway restart\n```\n\nThe `AgentKeyAuthGuard` accepts any non-`mnfst_*` token from loopback IPs in the self-hosted version, so loopback-only testing works even without a valid key. After restarting the backend, also restart the OpenClaw gateway — it doesn't reconnect automatically.\n\n## Active Technologies\n\n- **Backend**: NestJS 11, TypeORM 0.3, PostgreSQL 16, Better Auth, class-validator, class-transformer, Helmet\n- **Frontend**: SolidJS, Vite, uPlot (charts), Better Auth client, custom CSS theme\n- **Runtime**: TypeScript 5.x (strict mode). CI pins Node.js 24 (`.github/workflows/release.yml`); no `engines` field enforces this locally.\n- **Monorepo**: npm workspaces + Turborepo\n- **Release**: Changesets for version management + GitHub Actions for Docker image release\n\n## Project Structure\n\n```text\npackages/\n├── backend/\n│   ├── src/\n│   │   ├── instrument.ts                    # Sentry init, imported first (before any other import)\n│   │   ├── main.ts                          # Bootstrap: Helmet, ValidationPipe, Better Auth mount, CORS\n│   │   ├── app.module.ts                    # Root module (guards: ApiKey, Session, Throttler)\n│   │   ├── config/app.config.ts             # Environment variable config\n│   │   ├── auth/\n│   │   │   ├── auth.instance.ts             # Better Auth singleton (email/pass + 3 OAuth + jwt/mcp plugins)\n│   │   │   ├── auth.module.ts               # Registers SessionGuard as APP_GUARD\n│   │   │   ├── session.guard.ts             # Cookie session auth via Better Auth\n│   │   │   ├── current-user.decorator.ts    # @CurrentUser() param decorator\n│   │   │   ├── cli-auth.controller.ts, cli-auth.service.ts # Browser login for the mnfst CLI (PKCE code → `cli` PAT)\n│   │   │   ├── me.controller.ts             # GET /api/v1/me identity probe (tenant, user, auth method, expiry)\n│   │   │   └── mcp-scopes.ts                # mcp:read / mcp:write scope constants (leaf module, no Better Auth import)\n│   │   ├── database/\n│   │   │   ├── database.module.ts           # TypeORM PostgreSQL config\n│   │   │   ├── database-seeder.service.ts   # Seeds demo data (users, agents, security events)\n│   │   │   ├── datasource.ts               # CLI DataSource for migration commands\n│   │   │   ├── pricing-sync.service.ts      # OpenRouter pricing data sync\n│   │   │   ├── ollama-sync.service.ts       # Ollama model sync\n│   │   │   ├── quality-score.util.ts        # Model quality scoring\n│   │   │   └── seed-messages.ts             # Demo request/provider-attempt seed data\n│   │   ├── entities/                        # TypeORM entities (25 entities)\n│   │   │   ├── tenant.entity.ts             # Multi-tenant root\n│   │   │   ├── agent.entity.ts              # Agent (belongs to tenant)\n│   │   │   ├── agent-api-key.entity.ts      # OTLP ingest keys (mnfst_*)\n│   │   │   ├── cli-auth-code.entity.ts      # Short-lived PKCE codes for `mnfst login`\n│   │   │   └── ...                          # request, agent-message (provider attempt), tenant-provider, tier-assignment, header-tier, etc.\n│   │   ├── common/\n│   │   │   ├── guards/api-key.guard.ts      # X-API-Key header auth (timing-safe)\n│   │   │   ├── decorators/public.decorator.ts\n│   │   │   ├── dto/                         # create-agent, range-query, rename-agent DTOs\n│   │   │   ├── filters/spa-fallback.filter.ts\n│   │   │   ├── interceptors/               # agent-cache, user-cache\n│   │   │   ├── constants/                   # api-key, cache, ollama, providers, openai-models, xai-models, subscription-clients\n│   │   │   ├── services/                    # ingest-event-bus, manifest-runtime, tenant-cache, request-recording-storage\n│   │   │   └── utils/                       # crypto, hash, range, period, slugify, url-validation, provider-inference, postgres-sql, cost-calculator, detect-self-hosted, frontend-path, og-rewrite, secret-scrub, ttl-cache, local-ip, etc.\n│   │   ├── health/                          # @Public() health check\n│   │   ├── analytics/                       # Dashboard analytics\n│   │   │   ├── controllers/                 # overview, tokens, costs, messages, agents\n│   │   │   └── services/                    # aggregation + timeseries-queries + query-helpers\n│   │   ├── otlp/                            # Agent key auth + onboarding\n│   │   │   ├── guards/agent-key-auth.guard.ts # Bearer token auth (agent API keys)\n│   │   │   └── services/api-key.service.ts  # Agent onboarding (creates tenant+agent+key)\n│   │   ├── routing/                         # LLM routing (providers, tiers, proxy, scorer)\n│   │   │   ├── proxy/                       # OpenAI-compatible proxy (anthropic/google adapters, fallback chain, attempt recording, caller-classifier)\n│   │   │   ├── autofix/                     # Autofix self-healing (Phoenix client + heal-once flow)\n│   │   │   ├── routing-core/               # Tier, provider, specificity services + cache\n│   │   │   ├── resolve/                     # Scoring-based tier + specificity resolution\n│   │   │   ├── custom-provider/             # Custom provider CRUD (+ `alias`, the public model-id prefix in /v1/models)\n│   │   │   ├── header-tiers/               # Header-based tier overrides\n│   │   │   ├── oauth/                       # OAuth flows (Gemini, OpenAI, Kiro, MiniMax)\n│   │   │   └── specificity.controller.ts   # Specificity routing CRUD endpoints\n│   │   ├── scoring/                         # Request complexity scoring engine\n│   │   │   ├── keywords.ts                 # Keyword lists for all dimensions (complexity + specificity)\n│   │   │   ├── specificity-detector.ts     # Task-type detection (coding, trading, etc.)\n│   │   │   └── scan-messages.ts            # Message scanner for specificity detection\n│   │   ├── model-prices/                    # Model pricing management + sync\n│   │   ├── notifications/                   # Alert rules, email providers, cron\n│   │   ├── playground/                      # Prompt playground (runs, columns, starred/best)\n│   │   ├── github/                          # GitHub stars endpoint\n│   │   ├── sse/                             # Server-Sent Events for real-time updates\n│   │   ├── setup/                           # First-run admin setup wizard\n│   │   ├── free-models/                     # Free LLM model catalog\n│   │   ├── model-discovery/                 # Per-provider model fetching + fallback\n│   │   ├── billing/                         # Stripe billing status + plan limits\n│   │   ├── error-pages/                     # Custom error-page config (internal + public)\n│   │   ├── waitlist/                        # Pivot waiting-list claims + legacy Autofix claim compatibility route\n│   │   ├── discovery/                       # Self-hosted discovery onboarding (forwarded to Peacock)\n│   │   ├── crm-metrics/                     # Cloud-only internal feed: Autofix-healed cohort + waitlist claims + corporate signups (secret-gated)\n│   │   ├── cors-csp-config.ts               # Wingman CORS/CSP origin allowlists\n│   │   ├── sentry/                          # Sentry init-options builder (SENTRY_DSN-gated)\n│   │   ├── telemetry/                       # Anonymous self-hosted telemetry\n│   │   ├── version/                         # GET /api/v1/version — self-hosted \"new version available\" check\n│   │   └── mcp/                             # Remote MCP server (OAuth 2.1 bearer, stateless per-POST, tools/ per domain)\n│   └── test/                                # E2E tests (supertest)\n├── frontend/\n│   ├── src/\n│   │   ├── index.tsx                        # Router setup (App + AuthLayout)\n│   │   ├── components/\n│   │   │   ├── AuthGuard.tsx                # Session check, redirect to /login\n│   │   │   ├── GuestGuard.tsx               # Redirect authenticated users away from auth pages\n│   │   │   ├── SocialButtons.tsx            # 3 OAuth provider buttons\n│   │   │   ├── Header.tsx                   # User session data, logout\n│   │   │   ├── Sidebar.tsx                  # Navigation sidebar\n│   │   │   ├── SetupModal.tsx               # Agent setup wizard modal\n│   │   │   └── ...                          # Charts, modals, pagination, etc.\n│   │   ├── pages/\n│   │   │   ├── Login.tsx, Register.tsx       # Auth pages\n│   │   │   ├── ResetPassword.tsx            # Password reset flow\n│   │   │   ├── Workspace.tsx                # Harness grid/table at /harnesses + create harness\n│   │   │   ├── GlobalOverview.tsx, AgentOverview.tsx # Cross-harness + per-harness dashboards (split from one Overview.tsx)\n│   │   │   ├── AgentDetail.tsx, AgentProviders.tsx   # Per-harness detail shell (tabs) + provider connections\n│   │   │   ├── MessageLog.tsx               # Paginated Requests log (legacy filename)\n│   │   │   ├── Account.tsx                  # User profile (session data)\n│   │   │   ├── Settings.tsx, SettingsAutofixSection.tsx, SettingsRecordingSection.tsx # Harness settings + Autofix + request-recording toggles\n│   │   │   ├── Routing.tsx, RoutingPanels.tsx, RoutingActions.tsx, RoutingDefaultTierSection.tsx, RoutingHeaderTiersSection.tsx, RoutingSpecificitySection.tsx, RoutingTierCard.tsx # LLM routing config (split by concern)\n│   │   │   ├── Limits.tsx                   # Alert rule management — mounted at /harnesses/:agentName/guardrails (\"Guardrails\" tab)\n│   │   │   ├── ModelPrices.tsx              # Model pricing table\n│   │   │   ├── Playground.tsx               # Prompt playground\n│   │   │   ├── ConnectProvider.tsx          # Provider connection flow\n│   │   │   ├── providers/                   # Subscriptions.tsx, Byok.tsx, Local.tsx, ConnectionDetail.tsx, ProviderConnectionsPage.tsx (/providers/* pages)\n│   │   │   ├── Discovery.tsx                # Self-hosted discovery form (one-time, post-signup)\n│   │   │   ├── Welcome.tsx                  # Post-signup welcome flow (+ welcome-helpers.ts)\n│   │   │   ├── FreeModels.tsx               # Free model catalog\n│   │   │   ├── Setup.tsx                    # First-run setup wizard\n│   │   │   ├── Upgrade.tsx                  # Billing/plan upgrade page\n│   │   │   ├── Help.tsx                     # Help page\n│   │   │   ├── AgentLimitsRedirect.tsx, AgentMessagesRedirect.tsx # Legacy per-harness URL redirects\n│   │   │   ├── CliAuth.tsx                  # /cli/auth — approve a `mnfst login` browser request\n│   │   │   ├── Consent.tsx                  # /consent — OAuth consent screen for MCP clients\n│   │   │   └── NotFound.tsx                 # 404 page\n│   │   ├── services/\n│   │   │   ├── auth-client.ts               # Better Auth SolidJS client\n│   │   │   ├── api.ts                       # API functions (credentials: include)\n│   │   │   ├── providers.ts                 # ProviderDef list + SPECIFICITY_STAGES + STAGES\n│   │   │   ├── model-display.ts             # Model display-name cache\n│   │   │   ├── formatters.ts               # Number/cost formatting\n│   │   │   ├── provider-utils.ts            # LLM provider helpers\n│   │   │   ├── routing.ts, routing-utils.ts # Routing config helpers\n│   │   │   ├── theme.ts                     # Theme management\n│   │   │   ├── toast-store.ts               # Toast notification state\n│   │   │   └── ...                          # setup-status, playground-store, pagination, sse, oauth-popup, etc.\n│   │   ├── layouts/                         # Layout components\n│   │   └── styles/\n│   └── tests/\n├── shared/                           # Shared TypeScript types + helpers (consumed by backend and frontend)\n├── cli/                              # `mnfst` management CLI (private; bin/mnfst.js, commands/, generated provider catalog + skill content)\n└── manifest/                         # Code-free shell package holding the canonical Manifest version (see Releases)\n```\n\nBeyond `packages/`, the repo root also carries:\n\n- `integrations/n8n-nodes-manifest/` — the published n8n community node (`n8n-nodes-manifest` on npm); root-level `nodes/` and `credentials/` are identical mirrors of its sources for n8n's community-node scanner.\n- `contracts/gateway/` — gateway API contract tests.\n- `design-system/` — machine-readable design registry (generated `registry/components.json` + `registry/llms.txt`; generator `generate-registry.mjs`).\n- `deploy/` — one-click deployment templates (apple-containers, aws, coolify, digitalocean, easypanel, fly, gcp, heroku, koyeb).\n- `docs/` — `glossary.md` (canonical terminology contract), `autofix-self-healing-poc.md`, `model-parameters-schema.md`, `providers/subscription-based-providers.md`.\n\n## Single-Service Deployment\n\nThe app deploys as a **single service**. In production, NestJS serves both the API and the frontend static files from the same port.\n\n```bash\nnpm run build     # Turborepo: frontend (Vite) then backend (Nest)\nnpm start         # node packages/backend/dist/main.js — serves frontend + API\n```\n\n- API routes (`/api/*`, `/otlp/*`) are excluded from static file serving.\n- Dev mode: Vite on `:3000` proxies `/api` and `/otlp` to backend on `:3001`.\n- **Manifest Cloud** runs on Cloud Run (`manifest`, project `manifest-platform-457808`, `us-east4`) behind the shared Google HTTPS load balancer, with Cloud SQL Postgres on a private IP. Every push to `main` deploys through `.github/workflows/deploy.yml` (keyless Workload Identity Federation; `mnfst/*` on `main` only): it builds `docker/Dockerfile.cloud` (the self-hosted `docker/Dockerfile` minus `VITE_MANIFEST_SELFHOSTED`, keep both in step), runs the `manifest-migrate` job, then swaps the service image. Env vars, secrets (Secret Manager) and scaling live on the service; it sets `MANIFEST_MODE=cloud`, `RUN_MIGRATIONS_ON_BOOT=false`, `TRUST_PROXY=2` (load balancer + Cloud Run hops) and `SHUTDOWN_DRAIN_MS=0`. `railway.toml` is the old Railway config, kept until Railway is decommissioned.\n\n## Commands\n\n### Starting the Dev Server\n\nThe backend requires a `.env` file at `packages/backend/.env` with at least `BETTER_AUTH_SECRET` (32+ chars). The `auth.instance.ts` reads `process.env` at import time, before NestJS `ConfigModule` loads `.env`, so env vars must be available to the Node process.\n\n**Quick start (run these in parallel):**\n\n```bash\n# Backend — must preload dotenv since auth.instance.ts reads process.env at import time\ncd packages/backend && NODE_OPTIONS='-r dotenv/config' npx nest start --watch\n\n# Frontend\ncd packages/frontend && npx vite\n```\n\n**Note:** `npm run dev` (turbo) starts the frontend but NOT the backend, because the backend's script is `start:dev` not `dev`. Start the backend separately as shown above.\n\n### Seeding Dev Data\n\nSet `SEED_DATA=true` in `packages/backend/.env` to seed on startup (dev/test only). This creates:\n\n- **Admin user**: `admin@manifest.build` / `manifest` (email verification email is skipped if Mailgun is not configured — user is created but unverified)\n- **Tenant**: `seed-tenant-001` linked to the admin user\n- **Agent**: `demo-agent` with OTLP key `dev-otlp-key-001`\n- **API key**: `dev-api-key-manifest-001`\n- **Security events**: 12 sample events for the security dashboard\n- **Requests and provider attempts**: Sample telemetry for the demo agent\n\nSeeding is idempotent — it checks for existing records before inserting.\n\n**Dev-login shortcut:** when running under the Vite dev server the login page shows a\nprominent one-click **⚡ Sign in as dev** button that submits the seeded\n`admin@manifest.build` / `manifest` credentials — no copy-paste. It's gated by\n`import.meta.env.DEV`, so Vite strips the button and the credential literals from\nproduction builds, and no password ever rides in a URL. See\n`packages/frontend/src/pages/Login.tsx`.\n\n**Minimal `.env` for development:**\n\n```env\nPORT=3001\nBIND_ADDRESS=127.0.0.1\nNODE_ENV=development\nBETTER_AUTH_SECRET=<random-hex-64-chars>\nDATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase\nAPI_KEY=dev-api-key-12345\nSEED_DATA=true\n```\n\nGenerate a secret with: `openssl rand -hex 32`\n\n**Database naming convention:** Always create uniquely-named databases to avoid overlapping other dev/test instances. Use the pattern `manifest_<context>_<random>` (e.g., `manifest_sse_49821`, `manifest_dev_83712`). Create databases via Docker:\n\n```bash\ndocker exec postgres_db psql -U myuser -d postgres -c \"CREATE DATABASE manifest_<name>;\"\n```\n\nThen set `DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/manifest_<name>` in `.env`.\n\n```bash\n# Production build + start (single server)\nnpm run build && npm start\n\n# Tests\nnpm test --workspace=packages/backend          # Jest unit tests\nnpm run test:e2e --workspace=packages/backend  # Jest e2e tests\nnpm test --workspace=packages/frontend         # Vitest tests\nnpm test --workspace=packages/shared           # Jest unit tests\nnpm test --workspace=packages/cli              # Jest unit tests (mnfst CLI)\n```\n\n### Database Migrations\n\nTypeORM migrations run automatically on app startup by default (gated by `RUN_MIGRATIONS_ON_BOOT`, default `true`). Schema sync (`synchronize`) is permanently disabled — all schema changes must go through migrations.\n\n**Dev workflow:** modify entity → generate migration → commit both.\n\n```bash\n# Generate a migration after changing an entity\ncd packages/backend\nnpm run migration:generate -- src/database/migrations/DescriptiveName\n\n# Other migration commands\nnpm run migration:run       # Run pending migrations\nnpm run migration:revert    # Revert the last migration\nnpm run migration:show      # Show migration status ([X] = applied)\nnpm run migration:create -- src/database/migrations/Name  # Create empty migration\n```\n\nNew migrations must be imported in `database.module.ts` and added to the `migrations` array.\n\n**Important**: Always use unique timestamps for new migrations. Never reuse a timestamp from an existing migration file.\n\n## Authentication Architecture\n\n### Guard Chain\n\nThree global guards run on every request (order matters):\n\n1. **SessionGuard** (`auth/session.guard.ts`) — Checks `@Public()` first. If not public, validates the Better Auth cookie session via `auth.api.getSession()`. Attaches `request.user` and `request.session`.\n2. **ApiKeyGuard** (`common/guards/api-key.guard.ts`) — Falls through if session already set. Otherwise reads the `X-API-Key` header and first looks it up against the tenant-scoped `ApiKey` entity (`api_keys` table, hashed with scrypt) — this is the primary multi-tenant credential path. Only if no DB match is found does it fall back to a timing-safe compare against the single `API_KEY` env var. Use `@Public()` to skip both guards.\n3. **ThrottlerGuard** — Rate limiting.\n\n`mnfst` CLI tokens are ordinary `api_keys` rows named `cli` (`CLI_KEY_NAME` in `auth/cli-auth.service.ts`), so they ride the `ApiKeyGuard` path with no special casing; the guard also enforces their sliding and absolute TTLs. The remote MCP route is `@Public()` because it carries its own credential (an OAuth 2.1 bearer) — see [Remote MCP server](#remote-mcp-server).\n\n### Better Auth Setup\n\n- **Instance**: `auth/auth.instance.ts` — `betterAuth()` with `emailAndPassword` + 3 social providers (Google, GitHub, Discord). Each provider only activates when both `CLIENT_ID` and `CLIENT_SECRET` env vars are set. Plugins: `jwt()` and `@better-auth/mcp`'s `mcp()` (the OAuth 2.1 authorization server for the remote MCP endpoint, resource-bound to `${BETTER_AUTH_URL}/api/v1/mcp`, scopes from `auth/mcp-scopes.ts`).\n- **Mounting**: In `main.ts`, Better Auth is mounted as Express middleware at `/api/auth/*splat` **before** `express.json()` (it needs raw body control). NestJS body parsing is re-added after for all other routes.\n- **Frontend client**: `services/auth-client.ts` — `createAuthClient()` from `better-auth/solid`.\n- **Social login in dev**: OAuth callback URLs point to `:3001` (`BETTER_AUTH_URL`). Social login only works when accessing the app on port **3001** (production build), not on Vite's `:3000` dev server.\n\n### Auth Types\n\n```typescript\n// backend/src/auth/auth.instance.ts\nexport type AuthSession = typeof auth.$Infer.Session;\nexport type AuthUser = typeof auth.$Infer.Session.user;\n\n// Use in controllers:\n@Get('something')\nasync handler(@CurrentUser() user: AuthUser) {\n  // user.id, user.name, user.email\n}\n```\n\n### CLI login (`mnfst login`)\n\n`packages/cli/` is the `mnfst` management CLI: a thin wrapper over the `/api/v1` REST API (JSON on stdout, exit `0`/`1`, never interactive mid-task). One global tenant credential unlocks every command; per-agent `mnfst_*` keys are outputs only (written to `--key-file` paths with mode `0600`), never inputs. The package publishes to npm as `mnfst-gateway-cli` (`npm i -g mnfst-gateway-cli`, command still `mnfst`); from the monorepo, build with `npm run build --workspace=packages/shared && npm run build --workspace=packages/cli` and run `node packages/cli/bin/mnfst.js`. `packages/cli/README.md` is the user-facing reference; the repo also ships a `mnfst-cli` skill under `.claude/skills/`.\n\nThe default login is a browser flow, PKCE-protected so an intercepted code is useless without the verifier:\n\n1. The CLI opens the dashboard at `/cli/auth` (`pages/CliAuth.tsx`) with a `state` and an S256 `code_challenge`.\n2. The signed-in user approves; the page calls `POST /api/v1/cli/authorize` (session-only — an API key caller gets 403) and receives a short-lived code stored in `cli_auth_codes`.\n3. The CLI exchanges it at `POST /api/v1/cli/token` (public — the code is the credential, with `state` + `code_verifier`) for a `cli` PAT in `api_keys`; `expiresAt` is ISO-8601 UTC.\n4. `DELETE /api/v1/cli/token` revokes the calling PAT on `mnfst logout` (best-effort).\n\nNon-interactive alternatives: `--token-stdin` or `--token-env`. The secret is never a CLI argument. `GET /api/v1/me` is the identity probe (`{ tenantId, userId, authMethod, expiresAt }`); it fails closed with 401 for the env `API_KEY` operator fallback because that credential attaches no tenant. Token lifetime: `CLI_TOKEN_TTL_DAYS` (sliding, default 30) capped by `CLI_TOKEN_ABSOLUTE_TTL_DAYS` (hard ceiling from issuance, default 90).\n\nThe CLI refuses HTTP redirects (so the key cannot leak cross-origin) and strips inherited `MANIFEST_AGENT_KEY` / `MANIFEST_API_KEY` from `mnfst run` children. `mnfst routing test` sends `User-Agent: mnfst-cli/<version>` so gateway caller attribution (`routing/proxy/caller-classifier.ts`, which also recognises the n8n node's `n8n-nodes-manifest/<version>` UA) can tell CLI test traffic apart from real agent traffic in request details.\n\n### Remote MCP server\n\n`POST /api/v1/mcp` (`mcp/mcp.controller.ts`) is a remote MCP server over stateless Streamable HTTP: a fresh `McpServer` per POST (`mcp-server.factory.ts`), closed when the request ends, so no session affinity is needed. GET and DELETE answer **405 + `Allow: POST`** as the spec requires for a stateless JSON transport (a 404 made clients log an error on every connect).\n\n- **Auth**: the route is `@Public()` to skip the session/API-key guards, and `requireMcpAuth` from `@better-auth/mcp` is the real gate. Better Auth's MCP plugin is the authorization server (PKCE, resource-bound JWT access tokens, CIMD client identity, RFC 8414/9728 discovery). A missing or dead token answers 401 with `WWW-Authenticate: … resource_metadata=…`, which is how a client learns it must run OAuth. `mcp-auth.ts` maps the verified token's `sub` to a tenant through `TenantCacheService`, so tools are scoped exactly like the dashboard and CLI; a tenant-less user gets 401.\n- **Availability**: MCP is on by default and off in two cases, both decided by `auth/mcp-availability.ts` before Better Auth is built: the operator set `MCP_ENABLED=false`, or the MCP resource (`${BETTER_AUTH_URL}/api/v1/mcp`) is not HTTPS and not loopback. The second case is **not** opt-in on purpose — `mcp()` validates its resource URL as it is constructed and throws, and that throw happens while `auth.instance.ts` is being imported, so an HTTP-only self-hosted install would refuse to boot at all (issue #2939). When MCP is off, `buildPlugins()` omits `mcp()` and `cimd()`, `app.module.ts` leaves `McpModule` unregistered so `/api/v1/mcp` does not exist, `main.ts` mounts `mountMcpUnavailable()` so the well-known paths answer a JSON 404 rather than the SPA shell, and the dashboard hides the MCP nav entry (`GET /api/v1/setup/status` carries `mcpEnabled`). `mcp-availability.ts` is a leaf module for the same reason `mcp-scopes.ts` is — it must be importable from the unit suite, which cannot load Better Auth's ESM.\n- **Discovery**: `mcp-discovery.ts` (mounted in `main.ts`) serves the OAuth documents at the well-known **root** paths (`/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource[/api/v1/mcp]`) in addition to Better Auth's `/api/auth/.well-known/…`, because clients start from the MCP origin's root.\n- **Consent**: `pages/Consent.tsx` at `/consent` is the OAuth consent screen.\n- **Scopes**: `mcp:read` is required for every token; each `register*Tools()` returns before registering its write tools unless `operator.scopes` has `mcp:write`, so a read-only client never sees them (the server `instructions` string tells it to reconnect with the write scope). Revealing an ingest key also requires `mcp:write` because it is a live secret.\n- **Tools** (`mcp/tools/*.tools.ts`): identity, agents, environment (+ a `doctor` that walks dependencies in order), providers (including custom providers), routing (status, fallbacks, Autofix, recording, custom/header tiers, per-tier model params), models/pricing, and the request ledger. They call the same services as the REST controllers through `McpToolDeps` (`tool-deps.ts`); `tool-result.ts` is the shared result envelope.\n- **Testing**: `auth.instance.ts` is ESM-only and cannot load under Jest on Node 22, which is why the scope constants live in the leaf module `auth/mcp-scopes.ts` — import from there in guards, tools, and specs.\n\n## Multi-Tenancy Model\n\n```\nUser (Better Auth) ──→ Tenant ──→ Agent ──→ AgentApiKey (mnfst_*)\n                                    │\n                                    └──→ requests ──→ agent_messages (telemetry data)\n```\n\n- **Tenant** (`tenants` table): Created automatically on first agent creation. `tenant.owner_user_id` = `user.id` is the ONLY user→tenant link (resolved through `TenantCacheService`); `tenant.name` mirrors it for display until repurposed as a slug.\n- **Agent** (`agents` table): Belongs to a tenant. Unique constraint on `[tenant_id, name]`.\n- **AgentApiKey** (`agent_api_keys` table): One-to-one with agent. `mnfst_*` format key for OTLP ingestion.\n- **ApiKey** (`api_keys` table): A separate, tenant-scoped credential (not per-agent) used for dashboard/API access — the primary key `ApiKeyGuard` checks. Distinct from `AgentApiKey`. `mnfst login` PATs are rows of this table named `cli`.\n- **Onboarding flow**: `ApiKeyGeneratorService.onboardAgent()` creates tenant (if new) + agent + API key via three sequential inserts.\n\n### Data Isolation\n\nEvery resource belongs to a tenant; users only authenticate and (optionally) appear as `created_by_user_id` audit metadata. Guards (SessionGuard/ApiKeyGuard) resolve the tenant once per request and attach a `TenantContext` (`{ tenantId, userId }`), injected in controllers via `@TenantCtx()`. All analytics queries filter by tenant via `addTenantFilter(qb, tenantId)` from `query-helpers.ts`. Never scope, key, cache, or authorize by user id.\n\n## API Endpoints\n\n| Method                    | Route                                           | Auth                                | Purpose                                                                                                     |\n| ------------------------- | ----------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |\n| GET                       | `/api/v1/health`                                | Public                              | Health check                                                                                                |\n| ALL                       | `/api/auth/*`                                   | Public                              | Better Auth (login, register, OAuth, sessions)                                                              |\n| GET                       | `/api/v1/overview`                              | Session/API Key                     | Dashboard summary                                                                                           |\n| GET                       | `/api/v1/tokens`                                | Session/API Key                     | Token usage analytics                                                                                       |\n| GET                       | `/api/v1/costs`                                 | Session/API Key                     | Cost analytics                                                                                              |\n| GET                       | `/api/v1/agents`                                | Session/API Key                     | Agent list with sparklines                                                                                  |\n| POST                      | `/api/v1/agents`                                | Session/API Key                     | Create agent + API key                                                                                      |\n| GET                       | `/api/v1/agents/:agentName`                     | Session/API Key                     | Single agent detail                                                                                         |\n| GET/POST                  | `/api/v1/agents/:agentName/duplicate*`          | Session/API Key                     | Duplicate agent (preview + confirm)                                                                         |\n| DELETE                    | `/api/v1/agents/:agentName`                     | Session/API Key                     | Delete agent                                                                                                |\n| GET                       | `/api/v1/agents/:agentName/key`                 | Session/API Key                     | Get agent API key                                                                                           |\n| POST                      | `/api/v1/agents/:agentName/rotate-key`          | Session/API Key                     | Rotate API key                                                                                              |\n| PATCH                     | `/api/v1/agents/:agentName`                     | Session/API Key                     | Rename agent                                                                                                |\n| GET                       | `/api/v1/messages`                              | Session/API Key                     | Paginated Requests log (legacy route name)                                                                  |\n| GET/PATCH/DELETE          | `/api/v1/messages/:id/*`                        | Session/API Key                     | Request details, feedback, miscategorized flag (legacy route name)                                          |\n| GET                       | `/api/v1/security`                              | Session/API Key                     | Security score + events                                                                                     |\n| GET                       | `/api/v1/model-prices`                          | Session/API Key                     | Model pricing list                                                                                          |\n| GET                       | `/api/v1/free-models`                           | Session/API Key                     | Free LLM model catalog                                                                                      |\n| GET                       | `/api/v1/agent/usage`                           | Bearer (mnfst\\_\\*)                  | Token usage for the calling agent                                                                           |\n| GET                       | `/api/v1/agent/costs`                           | Bearer (mnfst\\_\\*)                  | Cost data for the calling agent                                                                             |\n| GET                       | `/api/v1/overview/*`                            | Session/API Key                     | Overview timeseries/breakdown sub-endpoints                                                                 |\n| GET                       | `/api/v1/providers` / `/api/v1/providers/usage` | Session/API Key                     | Connected provider list + usage                                                                             |\n| GET                       | `/api/v1/provider-analytics/*`                  | Session/API Key                     | Per-provider analytics                                                                                      |\n| GET                       | `/api/v1/errors/breakdown`                      | Session/API Key                     | Error breakdown analytics                                                                                   |\n| GET/PATCH                 | `/api/v1/billing/*`                             | Session/API Key                     | Billing status, light `plan` endpoint, email preferences (Stripe)                                           |\n| POST                      | `/api/v1/waitlist/autofix/claim`                | Public                              | Deprecated no-op compatibility route for older self-hosted versions                                         |\n| POST                      | `/api/v1/waitlist/pivot/claim`                  | Public                              | Closed pivot waiting-list claim, kept for older self-hosted versions; records origin (cloud/self-hosted)   |\n| GET/POST                  | `/api/v1/autofix/status` / `.../enable-all`     | Session/API Key                     | Workspace Autofix coverage + API-only fleet enable (no dashboard caller)                                    |\n| GET                       | `/api/v1/overview/autofix-*`                    | Session/API Key                     | Autofix analytics (stats, timeseries, per-agent/provider/model)                                             |\n| POST                      | `/api/v1/discovery/complete`                    | Session/API Key                     | Best-effort self-hosted discovery submission forwarded to Peacock                                           |\n| GET/POST/DELETE           | `/api/v1/internal/error-pages*`                 | Public (`x-internal-secret` header) | Custom error-page config (Peacock CMS push API)                                                             |\n| GET                       | `/api/v1/internal/crm-metrics*`                 | Public (`x-internal-secret` header) | **Cloud only.** Autofix-healed user cohort, pivot waitlist claims, and corporate signups, for the outreach CRM (`CRM_METRICS_SECRET`) |\n| GET/PUT/DELETE            | `/api/v1/agents/:agentName/enabled-providers*`  | Session/API Key                     | Per-agent provider enable/disable + impact preview                                                          |\n| GET/POST/PATCH/DELETE     | `/api/v1/notifications/*`                       | Session/API Key                     | Notification rules CRUD + email provider config                                                             |\n| GET/POST/PUT/PATCH/DELETE | `/api/v1/routing/:agentName/*`                  | Session/API Key                     | Routing config (tiers, providers, model-params, header-tiers, custom-providers, specificity, autofix, recording, etc.) |\n| GET/PATCH                 | `/api/v1/routing/:agentName/tiers/:tier/model-params` | Session/API Key                     | Model params addressed by tier (`default` or a custom tier name) + `?model=` (defaults to the tier primary); backs `mnfst routing params` and the MCP `manifest_routing_params_*` tools. Writes the same rows as the scope-keyed `…/model-params` |\n| POST                      | `/api/v1/routing/ollama/sync`                   | Session/API Key                     | Sync Ollama models                                                                                          |\n| GET                       | `/api/v1/routing/pricing-health`                | Session/API Key                     | OpenRouter pricing sync health                                                                              |\n| POST                      | `/api/v1/routing/pricing/refresh`               | Session/API Key                     | Force pricing cache refresh                                                                                 |\n| GET/POST/DELETE           | `/api/v1/oauth/:provider/*`                     | Session/API Key                     | OAuth flows (Gemini, OpenAI, Anthropic, xAI, Kiro, MiniMax)                                                 |\n| POST                      | `/api/v1/routing/resolve`                       | Bearer (mnfst\\_\\*)                  | Model resolution                                                                                            |\n| POST                      | `/api/v1/routing/subscription-providers`        | Bearer (mnfst\\_\\*)                  | Subscription provider config                                                                                |\n| GET                       | `/api/v1/setup/status`                          | Public                              | First-run setup status                                                                                      |\n| POST                      | `/api/v1/setup/admin`                           | Public                              | Create initial admin user                                                                                   |\n| GET                       | `/api/v1/public/error-pages*`                   | Public (opt-in)                     | Published error pages for the marketing site (controlled by `MANIFEST_PUBLIC_STATS`)                        |\n| GET                       | `/v1/models`                                    | Bearer (mnfst\\_\\*)                  | Available model list (proxy)                                                                                |\n| POST                      | `/v1/chat/completions`                          | Bearer (mnfst\\_\\*)                  | LLM proxy (OpenAI-compatible)                                                                               |\n| POST                      | `/v1/responses`                                 | Bearer (mnfst\\_\\*)                  | LLM proxy (OpenAI Responses API)                                                                            |\n| POST                      | `/v1/messages`                                  | Bearer (mnfst\\_\\*)                  | LLM proxy (Anthropic Messages API)                                                                          |\n| POST                      | `/chat/completions`                             | Public                              | Structured 404 pointing callers at `/v1/chat/completions` (missing `/v1` basePath — not a proxy alias)      |\n| ALL                       | `/otlp/v1/*`, `/v1/{traces,metrics,logs}`       | Public                              | Structured **410 Gone** — OTLP ingest is removed (`otlp-deprecated.controller.ts`)                          |\n| GET/POST/PATCH            | `/api/v1/playground/*`                          | Session/API Key                     | Playground runs (run, list, star, mark best)                                                                |\n| GET                       | `/api/v1/events`                                | Session                             | SSE real-time events                                                                                        |\n| GET                       | `/api/v1/github/stars`                          | Public                              | GitHub star count                                                                                           |\n| GET                       | `/api/v1/version`                               | Session/API Key                     | Running version + latest GitHub release for the self-hosted update badge (24h cache; off in cloud)          |\n| GET                       | `/api/v1/me`                                    | Session/API Key (tenant required)   | Identity probe for CLI/API clients: `{ tenantId, userId, authMethod, expiresAt }`; env `API_KEY` gets 401    |\n| POST                      | `/api/v1/cli/authorize`                         | Session only                        | Browser side of `mnfst login`: mints a PKCE-bound short-lived code                                          |\n| POST/DELETE               | `/api/v1/cli/token`                             | Public (code + verifier) / API Key  | Exchange the code for a `cli` PAT; DELETE revokes the calling PAT on logout                                 |\n| POST                      | `/api/v1/mcp`                                   | OAuth 2.1 bearer (`mcp:read`/`mcp:write`) | Remote MCP server (stateless Streamable HTTP); GET/DELETE answer 405 + `Allow: POST`                  |\n| GET                       | `/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource[/api/v1/mcp]` | Public | OAuth discovery for MCP clients (root aliases of Better Auth's `/api/auth/.well-known/…`)   |\n\n## Environment Variables\n\nSee `packages/backend/.env.example` for all variables. Key ones:\n\n- `BETTER_AUTH_SECRET` — **Required.** Secret for Better Auth session signing (min 32 chars). Generate with `openssl rand -hex 32`.\n- `DATABASE_URL` — **Required** in every environment except `NODE_ENV=test` (which falls back to `postgresql://myuser:mypassword@localhost:5432/mydatabase`, matching the local Docker command). Dev and production both throw on boot if unset. Format: `postgresql://user:password@host:port/database`.\n- `MANIFEST_ENCRYPTION_KEY` — Recommended. AES-256-GCM key (min 32 chars) for encrypting stored provider API keys and OAuth tokens. Defaults to `BETTER_AUTH_SECRET` if unset — set this independently so a session-cookie leak doesn't also expose provider credentials.\n- `MANIFEST_ENCRYPTION_KEY_PREVIOUS` — Optional, rotation only. A secret that stored values may still be encrypted under. Decryption always tries `MANIFEST_ENCRYPTION_KEY`, then this, then `BETTER_AUTH_SECRET`, so old rows stay readable. While it is set, one replica rewrites every row found under an older secret onto the current one after boot (`database/secret-reencryption.service.ts`, advisory-locked, never blocks or fails boot); a normal boot without it never scans. Rotation: set the new `MANIFEST_ENCRYPTION_KEY` and `MANIFEST_ENCRYPTION_KEY_PREVIOUS=<old>`, deploy, wait for the log line `Nothing left under an older secret`, then remove `PREVIOUS`. Introducing a dedicated key on an install that used the `BETTER_AUTH_SECRET` fallback: set `MANIFEST_ENCRYPTION_KEY` (old rows keep decrypting via the session secret); to move them onto the new key, set `PREVIOUS` to the session secret's value for one deploy. Request recordings are not rewritten by this pass: they decrypt under the current key only, so a key change makes older recordings unreadable until retention removes them.\n- `PORT` — Server port. Default: `3001`\n- `BIND_ADDRESS` — Bind address. Default: `127.0.0.1` (use `0.0.0.0` for Railway/Docker)\n- `NODE_ENV` — `development` or `production`. Dev allows broad CORS (local dashboard + Wingman); production allows the hosted Wingman origin plus any `WINGMAN_CORS_ORIGINS` entries.\n- `CORS_ORIGIN` — Allowed CORS origin (dev). Default: `http://localhost:3000`\n- `WINGMAN_CORS_ORIGINS` — Production only. Extra browser origins allowed to call the gateway (comma-separated). The hosted Wingman (`https://wingman.manifest.build`) is always allowed.\n- `BETTER_AUTH_URL` — Base URL for Better Auth. Default: `http://localhost:{PORT}`\n- `FRONTEND_PORT` \n\n<!-- [TRUNCATED AT 50KB] -->\n\n<!-- File: .claude/skills/mnfst-cli/SKILL.md (Tokens: ~1533 | Category: anthropic-skill) -->\n---\nname: mnfst-cli\ndescription: Use when creating or operating AI agents that make LLM calls — provisioning model routing and keys, wiring an app or automation to an LLM endpoint, testing routes, rotating leaked keys, or investigating LLM request failures and costs on a Manifest install.\n---\n\n# Operating Manifest from the CLI\n\n## Overview\n\n`mnfst` manages Manifest, the LLM gateway. Every call routed through a Manifest agent gets model routing with fallbacks, per-agent cost attribution, self-healing (Auto-fix), and a request log — for free. Default practice: never hand an app or automation a raw provider key; give it a Manifest agent.\n\nCommands emit JSON on stdout (human hints on stderr) — except `agent env`, which prints dotenv lines by design — exit 0/1, and never block on prompts when credentials come from flags or env. Full surface: `mnfst --help`.\n\n## The flow\n\n| Step | Command |\n|---|---|\n| Auth (human) | `mnfst login` — browser, one click, 30-day sliding token |\n| Auth (script/agent) | env `MANIFEST_URL` + `MANIFEST_API_KEY` — no login step |\n| Provision | `mnfst agent create --name X --platform <p> --if-absent` → response includes `setup` (the platform's config block) |\n| Check connections | `mnfst provider list [--agent X]` FIRST — the provider you need is often already connected. A connection is only usable if `cached_model_count` > 0; `is_active: true` with 0 models is hollow — run `mnfst provider refresh [<provider>]` (re-runs discovery tenant-wide, prints the new counts); still 0 means a bad credential, so reconnect, don't route on it |\n| Connect provider | `mnfst provider connect xai --auth-type api_key --credential-env KEY` · `provider catalog` lists all 30+ with auth types · subscription auth opens a browser (impossible headless — reuse an existing subscription connection instead) · own gateway: `provider custom add --name gw --endpoint <url>` |\n| Route | `mnfst agent configure X --models primary,fb1,fb2 --provider p [--auth-type a]` — first model is the route, the rest are fallbacks, and the whole chain rides the one `--provider` you name (cross-provider fallbacks are dashboard-only) · add `--tier deep` to upsert a custom tier callers select per-request with header `x-manifest-tier: deep` |\n| Verify | `mnfst routing test X` — one real request through the platform's actual API surface; broken routes fail loudly. It writes real rows to the request log — count them in later audits |\n| Wire the app | deployed → `mnfst agent env X >> .env` (one .env per service — the lines always name `MANIFEST_AGENT_KEY`, so appending two agents to one file silently keeps only the loader's winner; for several agents in one process use `mnfst run --agent X --env ROLE_KEY -- <cmd>`) |\n| Observe | `mnfst requests get --agent X [--status failed]` — paginated; pass `next_cursor` back for the next page |\n\nNamespace = scope: `provider *` acts tenant-wide, `agent *` acts on one agent, `routing *` holds readouts and custom-tier lifecycle.\n\n## Gotchas `--help` cannot tell you\n\n| Symptom | Reality |\n|---|---|\n| Proxy returns HTTP 200 but the \"answer\" starts with `[🦚 Manifest M###]` | An error wearing an assistant costume — check the content, not the status. `routing test` unmasks these automatically |\n| `cost: \"0.000000\"` on subscription-auth traffic | Flat-rate subscription: per-request cost is genuinely zero, not broken billing |\n| `agent configure`'s echo differs from later reads | Mutation bodies echo raw API rows — the canonical readback is `routing status <agent>` |\n| \"Which requests used a fallback or Auto-fix?\" | Those fields are trimmed from default output — `requests get --full` |\n| 401s missing from the request log | Rejected-auth attempts are never recorded (no tenant to attribute them to); a leaked key's blast radius cannot be proven from the log |\n| `MANIFEST_AGENT_URL` | Already ends in `/v1` — append `/chat/completions` directly |\n| Model prices needed before any agent exists | `mnfst model prices [--provider <p>]` — install-wide, no agent needed (`models <agent>` is the per-agent routable set) |\n| Auditing escalation traffic by `routing_tier` | Custom-tier requests log `routing_tier: \"standard\"` — the tier identity lives in `header_tier_name` |\n| Route resolves but requests fail on one connection | A connection can be `is_active: false` while a sibling auth-type works — pass `--auth-type` explicitly when a provider has several connections |\n| Every command answers `Invalid API key — Run mnfst login` under env auth | Not always an auth problem: a wrong `MANIFEST_URL` (dead or different install) answers exactly like a bad key. Run `mnfst doctor` — it checks config, host, credential, connections and agents in that order, so it separates \"host is dead\" from \"key is wrong for this host\" — and never tells you to re-login on an env key |\n| Need a different reasoning effort, temperature, or other model param without editing the app | `mnfst routing params get X [--tier t] [--model m]` lists what the routed model accepts and what is saved; `mnfst routing params set X [--tier t] [--model m] --set reasoning.effort=high [--unset temperature]` saves it. Saved params **override** what the app sends, per tier and per model (a fallback keeps its own), but only on requests Manifest routes: `model: \"auto\"` or a matching custom-tier header. A request naming a concrete model skips them |\n| `agent configure` / `routing custom create` reject a model as undiscovered | Both validate against the agent's *discovered* models, and a connection with `cached_model_count: 0` contributes none: run `mnfst provider refresh` (stale or empty catalog), or pass `--force` — the backend still routes uncatalogued models through provider-qualified passthrough |\n\n## Common mistakes\n\n- Handing an app a raw provider key — it loses fallbacks, cost attribution, healing, and the log. Create an agent and use `agent env`.\n- Verifying a route with hand-rolled `curl` — fake-200s read as success. Use `routing test`.\n- Slurping the whole request log — pagination is one page per call by design; loop on `next_cursor`.\n\nAlso on the surface when needed: `mnfst doctor` (first stop when anything is wrong), `agent setup <name> [--reveal]` (setup block anytime; key masked unless revealed), `agent key path|show`, `agent provider enable|disable <agent> <provider>`, `agent rotate-key` (revocation is instant), `mnfst models <agent> --cost --capabilities`.\n\n<!-- File: packages/backend/src/routing/autofix/contract/phoenix-openapi.yaml (Tokens: ~5108 | Category: anthropic-skill) -->\nopenapi: 3.1.0\ninfo:\n  title: Phoenix MVP — request-healing API\n  version: 0.0.1\n  description: >\n    Advisory request-healing for LLM providers. Submit a failed request plus the\n    provider error to get a corrected request body, then report whether the retry\n    worked. See README.md for the full loop and data model.\nservers:\n  - url: http://127.0.0.1:3000\n    description: Local development\n  - url: https://your-app.up.railway.app\n    description: Production — set to your Railway deployment URL\n\nsecurity:\n  - ApiKeyAuth: []\n\npaths:\n  /api/heal:\n    post:\n      summary: Resolve a patch for a failed request\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      description: >\n        Fingerprints the error and returns a healed request body if a\n        deterministic patch exists. Only request-side errors are healable.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/HealRequest'\n      responses:\n        '200':\n          description: A heal decision (patched, pending confirmation, still resolving, or no patch).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HealResult'\n        '400':\n          description: Malformed body (failed schema validation).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ZodError'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '422':\n          description: Error is not request-side (5xx / auth / rate-limit); nothing to heal.\n\n  /api/heal/observe:\n    post:\n      summary: Record failed requests as diagnostic evidence\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ObservationBatch'\n      responses:\n        '200':\n          description: Observation batch accepted.\n        '401':\n          description: Missing or invalid static key or instance credential.\n\n  /api/heal-attempts/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read a heal attempt and its parent issue\n      responses:\n        '200':\n          description: The heal attempt.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HealAttemptView'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n    patch:\n      summary: Report the outcome of a retried request\n      security:\n        - ApiKeyAuth: []\n        - ManifestInstanceAuth: []\n      parameters:\n        - $ref: '#/components/parameters/ManifestVersion'\n        - $ref: '#/components/parameters/ManifestHarness'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/HealOutcome'\n      responses:\n        '200':\n          description: Recorded outcome (idempotent).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OutcomeResult'\n        '400':\n          description: Malformed body, or error omitted when retryStatusCode >= 400.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ZodError'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: No such heal attempt.\n\n  /api/issues/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read an issue and its resolved patch\n      responses:\n        '200':\n          description: The issue.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/IssueView'\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n\n  /api/issues:\n    get:\n      summary: List issues (most-recently-updated first)\n      parameters:\n        - name: patchId\n          in: query\n          schema: { type: string, format: uuid }\n          description: Only issues fixed by this patch.\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Issues.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/IssueListItem' }\n        '400':\n          description: Malformed query.\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/heal-attempts:\n    get:\n      summary: List heal attempts (most-recent first)\n      parameters:\n        - name: issueId\n          in: query\n          schema: { type: string, format: uuid }\n        - name: patchId\n          in: query\n          schema: { type: string, format: uuid }\n        - name: traceId\n          in: query\n          schema: { type: string }\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Heal attempts.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/AttemptListItem' }\n        '400':\n          description: Malformed query.\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/patches:\n    get:\n      summary: List patches with usage counts (most-recent first)\n      parameters:\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Patches.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/PatchListItem' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/patches/{id}:\n    parameters:\n      - $ref: '#/components/parameters/Id'\n    get:\n      summary: Read a patch and the issues it fixes\n      responses:\n        '200':\n          description: The patch.\n          content:\n            application/json:\n              schema: { $ref: '#/components/schemas/PatchDetail' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: Unknown or non-UUID id.\n\n  /api/traces:\n    get:\n      summary: List traces with per-trace outcome counts (most-recent first)\n      parameters:\n        - $ref: '#/components/parameters/Limit'\n        - $ref: '#/components/parameters/Offset'\n      responses:\n        '200':\n          description: Traces.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { $ref: '#/components/schemas/TraceListItem' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n\n  /api/traces/{traceId}:\n    parameters:\n      - name: traceId\n        in: path\n        required: true\n        schema: { type: string }\n    get:\n      summary: A trace's heal attempts, oldest-first\n      responses:\n        '200':\n          description: The trace timeline.\n          content:\n            application/json:\n              schema: { $ref: '#/components/schemas/TraceDetail' }\n        '401':\n          description: Missing or invalid API key (when API_KEY is enforced).\n        '404':\n          description: No such trace.\n\n  /api/health:\n    get:\n      summary: Liveness probe (public — no API key)\n      security: []\n      responses:\n        '200':\n          description: Process is up.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  status: { type: string, example: ok }\n                  uptimeSeconds: { type: integer }\n\n  /api/ready:\n    get:\n      summary: Readiness probe — process up and database reachable (public)\n      security: []\n      responses:\n        '200':\n          description: Ready.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  status: { type: string, example: ready }\n        '503':\n          description: Database not ready.\n\ncomponents:\n  securitySchemes:\n    ApiKeyAuth:\n      type: apiKey\n      in: header\n      name: x-api-key\n      description: >\n        Static API key. Send as `x-api-key: <key>` (an `Authorization: Bearer\n        <key>` header is also accepted). Enforced in deployed environments where\n        the API_KEY env var is set; disabled locally when it is unset.\n    ManifestInstanceAuth:\n      type: apiKey\n      in: header\n      name: X-Manifest-Instance\n      description: >\n        Anonymous install identifier announced by a self-hosted Manifest. This\n        is an identifier, not a credential: it is not secret and no registration\n        handshake issues it. Phoenix creates the instance row the first time it\n        sees an id. Accepted in place of ApiKeyAuth on the gateway write\n        operations.\n\n  parameters:\n    Id:\n      name: id\n      in: path\n      required: true\n      schema:\n        type: string\n        format: uuid\n    Limit:\n      name: limit\n      in: query\n      description: Page size (default 50, max 200).\n      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }\n    Offset:\n      name: offset\n      in: query\n      description: Rows to skip for pagination.\n      schema: { type: integer, minimum: 0, default: 0 }\n    ManifestVersion:\n      name: X-Manifest-Version\n      in: header\n      required: false\n      description: Running Manifest release version. Invalid values become `unknown` in analytics.\n      schema:\n        type: string\n        maxLength: 32\n        pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'\n    ManifestHarness:\n      name: X-Manifest-Harness\n      in: header\n      required: false\n      description: Agent platform slug. Missing or unknown values become `other` in analytics.\n      schema:\n        type: string\n        enum:\n          [\n            openclaw,\n            hermes,\n            nanobot,\n            craft,\n            n8n,\n            claude-code,\n            opencode,\n            codex,\n            openai-sdk,\n            anthropic-sdk,\n            vercel-ai-sdk,\n            langchain,\n            curl,\n            other,\n          ]\n  schemas:\n    InstanceRegistration:\n      type: object\n      required: [version, schema_version]\n      properties:\n        version:\n          type: string\n          maxLength: 32\n          pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'\n        schema_version:\n          type: integer\n          const: 1\n      additionalProperties: true\n\n    InstanceCredential:\n      type: object\n      required: [instance_id, secret]\n      properties:\n        instance_id: { type: string, format: uuid }\n        secret:\n          type: string\n          description: Opaque 256-bit base64url bearer token. Returned only once.\n      additionalProperties: false\n\n    ObservationBatch:\n      type: object\n      required: [observations]\n      properties:\n        observations:\n          type: array\n          minItems: 1\n          maxItems: 200\n          items: { $ref: '#/components/schemas/HealRequest' }\n\n    ProviderError:\n      type: object\n      required: [message]\n      properties:\n        message: { type: string }\n        type: { type: [string, 'null'] }\n        param: { type: [string, 'null'] }\n        code: { type: [string, 'null'] }\n\n    Operation:\n      type: object\n      description: A deterministic transform applied to the request body.\n      required: [type]\n      properties:\n        type: { type: string }\n      additionalProperties: true\n      example: { type: rename_param, from: max_tokens, to: max_output_tokens }\n\n    HealRequest:\n      type: object\n      required: [traceId, provider, api, request, response]\n      properties:\n        traceId:\n          type: string\n          minLength: 1\n          description: >\n            Caller-supplied id of the logical request being healed. Send the\n            same value on every heal of that request's retry chain (not the\n            provider's per-call request id) — it groups the attempts into one\n            trace.\n        tenantId:\n          type: string\n          minLength: 1\n          description: >\n            Opaque id of the gateway tenant this request belongs to (Manifest's\n            Tenant.id). Optional on the wire; recorded so a failure can be\n            attributed to a tenant. Manifest always sends it on the proxy path.\n        provider: { type: string, minLength: 1, example: openai }\n        model:\n          type: string\n          minLength: 1\n          maxLength: 1024\n          description: >\n            Logical provider model identity. Native transports may encode it in\n            the URL rather than in the JSON request body.\n        authType:\n          type: string\n          enum: [api_key, subscription, local]\n          description: Provider credential route. Optional during the Manifest rollout.\n        api:\n          type: string\n          enum: [responses, chat_completions, messages]\n        url: { type: string, format: uri }\n        request:\n          type: object\n          description: The original outgoing request body, verbatim.\n          additionalProperties: true\n        response:\n          type: object\n          required: [statusCode, error]\n          properties:\n            statusCode: { type: integer, example: 400 }\n            error: { $ref: '#/components/schemas/ProviderError' }\n        providerExchange:\n          type: object\n          description: Final provider-native transport exchange, separate from the patch input.\n          required: [format, request, response]\n          properties:\n            format:\n              type: string\n              enum:\n                - openai_chat_completions\n                - openai_responses\n                - anthropic_messages\n                - google_generate_content\n                - google_code_assist\n            url: { type: string, format: uri }\n            request:\n              type: object\n              required: [body]\n              properties:\n                body: { type: object, additionalProperties: true }\n                redactedFields: { type: array, items: { type: string } }\n            response:\n              type: object\n              required: [statusCode, body]\n              properties:\n                statusCode: { type: integer }\n                body: {}\n        responseTimeMs: { type: integer, minimum: 0 }\n        responseSizeBytes: { type: integer, minimum: 0 }\n\n    HealResult:\n      type: object\n      required: [status, issueId]\n      properties:\n        status:\n          type: string\n          enum: [patched, unverified, resolving, no_patch]\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        healAttemptId: { type: string, format: uuid }\n        operations:\n          type: array\n          items: { $ref: '#/components/schemas/Operation' }\n        healedBody:\n          type: object\n          additionalProperties: true\n        explanation:\n          type: object\n          description: >\n            Human-readable \"why\" for a served heal (present on \"patched\" /\n            \"unverified\"). operations[].detail is always deterministic (built from\n            the operations); summary prefers an authored rationale (source \"agent\" /\n            \"operator\") and falls back to the deterministic composition (source\n            \"deterministic\").\n          required: [summary, operations, source]\n          properties:\n            summary: { type: string }\n            operations:\n              type: array\n              items:\n                type: object\n                required: [type, detail]\n                properties:\n                  type: { type: string }\n                  detail: { type: string }\n            source:\n              type: string\n              enum: [deterministic, agent, operator]\n        retryAfterMs:\n          type: integer\n          description: Present only when status is \"resolving\".\n\n    HealOutcome:\n      type: object\n      required: [retryStatusCode]\n      description: error is required when retryStatusCode >= 400.\n      properties:\n        retryStatusCode: { type: integer, example: 200 }\n        error: { $ref: '#/components/schemas/ProviderError' }\n\n    OutcomeResult:\n      type: object\n      required: [healAttemptId, status, issueStatus]\n      properties:\n        healAttemptId: { type: string, format: uuid }\n        status:\n          type: string\n          enum: [succeeded, failed, expired]\n        issueStatus: { type: string }\n\n    IssueView:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        fingerprint: { type: string }\n        provider: { type: string }\n        api: { type: string }\n        model: { type: [string, 'null'] }\n        error:\n          type: object\n          properties:\n            code: { type: [string, 'null'] }\n            type: { type: [string, 'null'] }\n            param: { type: [string, 'null'] }\n        status:\n          type: string\n          enum: [resolving, unverified, verified, ineffective, no_fix_found]\n        occurrenceCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n        patch:\n          type: [object, 'null']\n          properties:\n            id: { type: string, format: uuid }\n            operations:\n              type: array\n              items: { $ref: '#/components/schemas/Operation' }\n\n    HealAttemptView:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        traceId: { type: string }\n        status:\n          type: string\n          enum: [pending, succeeded, failed, expired]\n        retryStatusCode: { type: [integer, 'null'] }\n        retryErrorFingerprint: { type: [string, 'null'] }\n        confirmedAt: { type: [string, 'null'], format: date-time }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n        issue:\n          type: [object, 'null']\n          properties:\n            id: { type: string, format: uuid }\n            status: { type: string }\n            provider: { type: string }\n            api: { type: string }\n            model: { type: [string, 'null'] }\n            fingerprint: { type: string }\n\n    IssueListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        fingerprint: { type: string }\n        provider: { type: string }\n        api: { type: string }\n        model: { type: [string, 'null'] }\n        errorCode: { type: [string, 'null'] }\n        errorType: { type: [string, 'null'] }\n        errorParam: { type: [string, 'null'] }\n        status:\n          type: string\n          enum: [resolving, unverified, verified, ineffective, no_fix_found]\n        patchId: { type: [string, 'null'], format: uuid }\n        occurrenceCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    AttemptListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        issueId: { type: string, format: uuid }\n        patchId: { type: string, format: uuid }\n        traceId: { type: string }\n        status:\n          type: string\n          enum: [pending, succeeded, failed, expired]\n        retryStatusCode: { type: [integer, 'null'] }\n        retryErrorFingerprint: { type: [string, 'null'] }\n        confirmedAt: { type: [string, 'null'], format: date-time }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    PatchListItem:\n      type: object\n      properties:\n        id: { type: string, format: uuid }\n        operations:\n          type: array\n          items: { $ref: '#/components/schemas/Operation' }\n        operationsHash: { type: string }\n        issueCount: { type: integer }\n        attemptCount: { type: integer }\n        createdAt: { type: string, format: date-time }\n        updatedAt: { type: string, format: date-time }\n\n    PatchDetail:\n      allOf:\n        - $ref: '#/components/schemas/PatchListItem'\n        - type: object\n          properties:\n            issues:\n              type: array\n              items: { $ref: '#/components/schemas/IssueListItem' }\n\n    TraceListItem:\n      type: object\n      properties:\n        traceId: { type: string }\n        attemptCount: { type: integer }\n        pendingCount: { type: integer }\n        succeededCount: { type: integer }\n        failedCount: { type: integer }\n        firstAt: { type: string, format: date-time }\n        lastAt: { type: string, format: date-time }\n\n    TraceAttempt:\n      allOf:\n        - $ref: '#/components/schemas/AttemptListItem'\n        - type: object\n          properties:\n            issue:\n              type: object\n              properties:\n                id: { type: string, format: uuid }\n                status: { type: string }\n                provider: { type: string }\n                api: { type: string }\n                model: { type: [string, 'null'] }\n                errorCode: { type: [string, 'null'] }\n                errorType: { type: [string, 'null'] }\n                errorParam: { type: [string, 'null'] }\n            patch:\n              type: object\n              properties:\n                id: { type: string, format: uuid }\n                operations:\n                  type: array\n                  items: { $ref: '#/components/schemas/Operation' }\n\n    TraceDetail:\n      type: object\n      properties:\n        traceId: { type: string }\n        attempts:\n          type: array\n          items: { $ref: '#/components/schemas/TraceAttempt' }\n\n    ZodError:\n      type: object\n      description: Flattened zod validation error.\n      properties:\n        formErrors:\n          type: array\n          items: { type: string }\n        fieldErrors:\n          type: object\n          additionalProperties:\n            type: array\n            items: { type: string }\n\n<!-- File: packages/cli/README.md (Tokens: ~905 | Category: plugin-manifest) -->\n# Manifest CLI\n\n`mnfst` manages a Manifest gateway from your terminal. Use it to create agents, connect AI providers, configure model routing, and inspect requests. The dashboard calls agents **Harnesses**; CLI commands use `agent`.\n\nThe npm package is `mnfst-gateway-cli`. The command it installs is `mnfst`. Node.js 20 or later is required.\n\n## Install\n\n```bash\nnpm install -g mnfst-gateway-cli\nmnfst --help\n```\n\nTo run it without a global install:\n\n```bash\nnpx mnfst-gateway-cli --help\n```\n\n## Quick start\n\nSign in to the hosted Manifest gateway, then create an agent and connect a provider:\n\n```bash\nmnfst login\nmnfst agent create --name my-app --platform openai-sdk\nmnfst provider connect openai --agent my-app --credential-env OPENAI_API_KEY\nmnfst models my-app --provider openai\nmnfst agent configure my-app --models gpt-4o-mini --provider openai\nmnfst routing status my-app\nmnfst agent setup my-app\n```\n\nSet `OPENAI_API_KEY` in your environment before connecting the provider. Choose a model shown by `mnfst models` if `gpt-4o-mini` is unavailable to your account. `mnfst agent setup` returns wiring instructions with the agent key masked.\n\nFor a self-hosted gateway, pass its URL when you sign in:\n\n```bash\nmnfst login --url https://gateway.example.com\n```\n\nThe CLI uses that gateway for later commands. You can select another gateway with `--url` or `MANIFEST_URL`.\n\n## Useful commands\n\n| Task                           | Command                                        |\n| ------------------------------ | ---------------------------------------------- |\n| Check your connection          | `mnfst doctor`                                 |\n| List available agent platforms | `mnfst agent platforms`                        |\n| List connectable providers     | `mnfst provider catalog`                       |\n| Review an agent's routing      | `mnfst routing status my-app`                  |\n| Read or set model params       | `mnfst routing params get my-app`              |\n| Read recent requests           | `mnfst requests get --agent my-app --range 7d` |\n| See all commands and options   | `mnfst --help`                                 |\n\nMost commands write JSON to stdout for scripts and coding agents. `mnfst agent env` and `mnfst skill show` write text. `mnfst routing test` sends a real provider request, which may incur provider charges. Destructive commands require `--yes`.\n\n## Credentials\n\nBrowser login is the default. For a script, provide a management credential through standard input or an environment variable:\n\n```bash\nprintf '%s' \"$MY_KEY\" | mnfst login --token-stdin\n# or\nmnfst login --token-env MY_KEY\n```\n\nThe CLI stores login credentials for each gateway host in `~/.config/manifest/config.json` with file mode `0600`. A stored credential is only sent to the host where it was saved. `MANIFEST_API_KEY` overrides the stored credential for the selected host.\n\nAgent keys are separate from management credentials. The CLI stores a newly created agent key in its local keystore and does not print it in `mnfst agent setup` unless you pass `--reveal`. Use `mnfst agent env my-app` to get the key and URL for your application; its output contains the key, so handle it as a secret.\n\n## Usage telemetry\n\nThe CLI records usage telemetry by default. It batches events locally and normally sends one request per install per day. The payload contains a random, persistent install ID; CLI version; operating system; Cloud or self-hosted target; and each command's name, result, duration, and minute-level time. It may include the detected coding-agent runtime. It does not include command arguments, agent or provider names, gateway URLs, prompts, or credentials.\n\nSet `MANIFEST_TELEMETRY_DISABLED=1` to opt out. Telemetry files are stored beside the CLI config. You can set `MANIFEST_CLI_TELEMETRY_ENDPOINT` to send usage data to your own endpoint.\n\n## Documentation\n\nSee the [CLI documentation](https://manifest.build/llm-gateway/docs/cli/) for more examples and the [GitHub repository](https://github.com/mnfst/llm-gateway) for source and issues.\n\n<!-- File: packages/manifest/README.md (Tokens: ~366 | Category: plugin-manifest) -->\n# manifest\n\nThis package exists to hold the canonical **Manifest release version**.\n\nIt contains no code. The real backend and frontend live in [`../backend`](../backend) and [`../frontend`](../frontend), and shared types in [`../shared`](../shared). The Docker image at [`manifestdotbuild/manifest`](https://hub.docker.com/r/manifestdotbuild/manifest) bundles the built backend + frontend.\n\n## What it's for\n\n- **Changesets target**: `.changeset/config.json` ignores `manifest-backend`, `manifest-frontend`, and `manifest-shared`, so when contributors run `npx changeset` the only selectable package is this one. Always target `manifest` regardless of which files you actually changed — this is the single source of truth for \"the version of Manifest as a whole\".\n- **Docker release version**: `.github/workflows/docker.yml` reads the `version` field from this package's `package.json` via `jq` and uses it to tag the image (`manifestdotbuild/manifest:{version}` + derived `{major}.{minor}` / `{major}` / `sha-<short>` tags).\n- **Changelog**: `CHANGELOG.md` here (auto-generated by changesets) is the unified Manifest release history.\n\n## Adding a changeset\n\n```bash\nnpx changeset\n# → select \"manifest\"\n# → choose patch / minor / major\n# → write a one-line summary\n```\n\nCommit the generated file. On merge to `main`, the release workflow opens a `chore: version packages` PR bumping `version` and appending to `CHANGELOG.md`. Merge that PR to land the version bump, then manually run the Docker workflow (`Actions → Docker → Run workflow` with a blank input) to publish.\n\n<!-- File: .cursorrules (manifest-yaml-cursorrules-prompt-file.mdc) (Tokens: ~700 | Category: cursor-rule) -->\n---\ndescription: \"Cursor rules for manifest development with YAML integration.\"\nglobs: **/*\nalwaysApply: false\n---\n**Prompt for Expert Manifest Developer**\n\n**You are an assistant for app creation. You are going to use the backend Manifest. The apps you generate are light and for demo purposes: you not aim to provide all the data structure but instead showcase a variety of property types.**\n\n**Code structure**\nWhen asked to create a backend, execute the following actions:\n\n1. Install the `manifest` npm package\n2. Add the following scripts to `pacakge.json`: \"manifest\": \"node node_modules/manifest/scripts/watch/watch.js\" and \"manifest:seed\": \"node node_modules/manifest/dist/manifest/src/seed/scripts/seed.js\"\n3. Create the `manifest/backend.yml` file and add the manifest code to it.\n4. Add the `redhat.vscode-yaml` as recommendation in `.vscode/extensions.json`\n5. Configure `yaml.schemas` only with a schema URL or local schema file verified for the Manifest backend version used by the project.\n\n**Backend file**\nOn the `manifest/backend.yml`, follow those rules:\n- Strictly follow the Manifest JSON Schema shipped with the project or verified from the current Manifest backend documentation.\n- Start by addind a quick name to the app\n- Limit to 2 or 3 entities maximum\n- Limit to 4 properties maximum per entity\n- Try to showcase different property types\n- Only use validation properties once or twice\n- No entity should be called admin\n- Do not use authenticable entities\n- Add an emoji after each entity name, but do not use the emoji it on relationships references\n- Add a linebreak before each entity object\n- Each entity only appears once. Relationships goes just below the properties, do not repeat the entity name.\n- Do not use special characters.\n. Do not use middlewares, endpoints or hooks.\n- Use YAML abbreviated form for objects, with spaces. Example: { name: issueDate, type: date }\n- Do not add relationships to single entities\n- For relationships, use the short form. Ex: ' belongsTo:\n      - Author'\n- Add policies. Most projects only have \"read\" public policies. Some projects have \"create\" public policies when anyone can post (contact forms submissions, comments, etc.)\n- If using the \"choice\" property type, use \"options.values\" property to list choices. Example:  `{ name: type, type: choice, options: { values: [\"Fire\", \"Water\", \"Grass\"] } }`\n- Do not add \"seedCount\" and \"mainProp\" to entities\n\n**Documentation**\nRefer to the Manifest backend documentation that matches the project's installed version.\n\n**Example**\nThis is an example of the content of a `backend.yml` file:\nname: My pet app 🐾\nentities:\n  Owner:\n    properties:\n      - name\n      - { name: birthdate, type: date }\n\n  Cat:\n    properties:\n      - name\n      - { name: age, type: number }\n      - { name: birthdate, type: date }\n    belongsTo:\n      - Owner\n\n  Homepage:\n    nameSingular: Home content\n    single: true\n    properties:\n      - title\n      - { name: description, type: richText }\n      - { name: cover, type: image }\n</yakaai_skills>"}