asbuilt
by aniJani
README.md
# asbuilt
**Your code is the plan. Your backend is the as-built. asbuilt checks them against each other.**
In July 2025, Replit's coding agent deleted a production database for ~1,200
executives during a code freeze. In April 2026, a Cursor agent wiped a startup's
production database and its backups with a single API call. Both incidents share
one root cause: **coding agents act on what they *assume* the backend looks like,
because they cannot see what it *actually* looks like.**
asbuilt gives agents (and you) that missing sense — read-only, by construction.
## What it does
asbuilt parses your repo, extracts every assumption the code makes about its
backend — ORM tables, Prisma schemas, raw SQL strings, Supabase calls, storage
buckets, deployed functions, auth providers, env vars — and verifies each one
against the **live** backend, with `file:line` provenance:
```
$ asbuilt check prod
target: prod risk: prod
❌ fail table_exists(table=invoices) [src/db.ts:10]
✅ pass table_exists(table=orders) [src/db.ts:6]
⏭️ skipped dynamic argument [src/db.ts:14]
note: RLS intent is not derived from code; verify explicitly with rls_enabled / policy_exists assertions.
note: 2 app-level env references were not checked — no connector exists for the application host.
summary: 1 pass, 1 fail, 0 unsupported, 1 skipped
```
<!-- demo.gif goes here -->
In construction, the *as-built drawings* record what was actually built, as
opposed to what the plans intended. Your code is the plan; your live backend is
the as-built. Divergence between them is exactly where "the agent said done but
production disagrees" lives.
## Install
Not on PyPI yet — install straight from source. As an MCP server for your
coding agent (Claude Code shown; any MCP client works):
```bash
claude mcp add asbuilt -- uvx --from git+https://github.com/aniJani/asbuilt asbuilt
```
Or classic:
```bash
pip install git+https://github.com/aniJani/asbuilt
claude mcp add asbuilt -- asbuilt
```
Once published to PyPI, these will work too:
```bash
claude mcp add asbuilt -- uvx asbuilt
# or
pip install asbuilt
claude mcp add asbuilt -- asbuilt
```
## Try it without a backend
No database, no MCP client, no credentials required — the built-in `fixture`
connector serves a recorded state file instead of a live backend, so you can
see the whole loop in three files.
```bash
mkdir -p demo/.asbuilt demo/src
```
`demo/.asbuilt/targets.json` — a target backed by a local JSON file instead of
a real connection:
```json
{"targets": {"demo": {"connector": "fixture", "statePath": "state.json", "risk": "dev"}}}
```
`demo/state.json` — the "live" backend, recorded as a state doc (this is the
same shape a real connector like `postgres` or `supabase` returns):
```json
{"connector": "fixture", "target": "demo", "sections": {"schema": {"hash": "x", "tables": [{"name": "orders", "columns": [], "indexes": [], "foreignKeys": []}]}}}
```
`demo/src/db.ts` — a source file that assumes two tables, only one of which
exists in `state.json`:
```ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(process.env.URL, process.env.KEY)
export const listOrders = () => supabase.from('orders').select('*')
export const listInvoices = () => supabase.from('invoices').select('*')
```
Then run it:
```bash
cd demo && asbuilt check demo
```
```
target: demo risk: dev
❌ fail table_exists(table=invoices) [src/db.ts:4]
✅ pass table_exists(table=orders) [src/db.ts:3]
note: RLS intent is not derived from code; verify explicitly with rls_enabled / policy_exists assertions.
note: 2 app-level env references were not checked — no connector exists for the application host.
summary: 1 pass, 1 fail, 0 unsupported, 0 skipped
```
`asbuilt check` reads `.asbuilt/targets.json` from the **current directory**
(or `$ASBUILT_PROJECT_DIR` if set) — that's where target *and* state-file
resolution happen. `--repo <path>` only changes which directory gets scanned
for code assumptions; it does not relocate where targets are read from.
## Configure a target
`.asbuilt/targets.json` in your repo (env-var **names** only — secrets stay in
your environment):
```jsonc
{ "targets": {
"dev": { "connector": "supabase", "projectRef": "abc123",
"tokenEnv": "SUPABASE_ACCESS_TOKEN", "risk": "dev" },
"prod": { "connector": "postgres", "urlEnv": "PROD_PG_URL_RO", "risk": "prod" } } }
```
Connectors: **Postgres** (anything speaking pg: RDS, Neon, Supabase, Azure),
**Supabase** (schema/RLS + auth/storage/functions/secrets), **Firebase**
(Firestore collections & indexes, security rules, auth, storage, functions —
see [docs/firebase-setup.md](docs/firebase-setup.md)).
## The tools
| Tool | What the agent gets |
|---|---|
| `deployment_check` | the flagship: code assumptions vs. live state, with `file:line` |
| `infra_state` | live backend state, normalized |
| `infra_verify` | assert specifics: `table_exists`, `rls_enabled`, `bucket_exists`, … |
| `infra_impact` | blast-radius before a destructive op ("dropping `users` breaks 3 FKs") |
| `infra_snapshot` | content-addressed, redacted state snapshots |
| `infra_drift` | structural diff between any two states (or live) |
Every response carries the target's **risk tier** (`prod`/`staging`/`dev`) — the
signal that was missing in both incidents above.
## Honest by design
- **Read-only by construction.** The connector protocol has no write method.
Postgres opens `default_transaction_read_only=on`. The Firebase connector
probes its own credential and **refuses to run** if it holds write permissions.
- **Unknown is never a pass.** Assertions the connector can't evaluate return
`unsupported`; dynamic code references are `skipped` and listed — "checked 12,
skipped 3" can never be misread as all-clear.
- **Secrets never enter captured state.** Values are hashed at capture time.
## Benchmark
`benchmark/` measures the failure mode this exists to prevent: how often agents
are **confidently wrong** about live infra, with vs. without verification.
15 seeded cases exercise all four MCP tools (`deployment_check`, `infra_verify`,
`infra_impact`, `infra_drift`) against the fixture connector, graded by
deterministic accept/reject regex pairs — no LLM judge. `python -m
benchmark.runner` runs fully offline (no `ANTHROPIC_API_KEY` needed) and
doubles as the connector regression harness, printing each case's
tool-derived answer against its known-correct truth.
The agent-alone-vs-agent-with-tools model comparison (`_run_comparison` in
`benchmark/runner.py`) requires `ANTHROPIC_API_KEY` and hasn't been run live
yet — there is no published number for how much verification actually moves
agent accuracy. That comparison is implemented and offline-testable, but the
"agents are less confidently wrong with asbuilt than without it" claim is
still a hypothesis, not a result.
## License
MIT
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues