criticalSQL injection in logincrapi-sqli-001sqliservices/identity/login.js:42
service_policy · full✓ code fix drafted
Details
Description: The email field is concatenated straight into the auth query.
Exploit: email=" OR 1=1 -- lets any password through and dumps the users table.
Code cure: Fix sql injection in login
highBOLA on vehicle locationcrapi-bola-002broken_object_authzservices/identity/vehicle.js:88
api_schema · full✓ code fix drafted
Details
Description: Any authenticated user can read another user's vehicle GPS by id.
Exploit: Swap {id} to another user's vehicle uuid; server returns their live location.
Code cure: Fix bola on vehicle location
highMass assignment on profilecrapi-mass-003mass_assignmentservices/identity/dashboard.js:61
waf · partial✓ code fix drafted
Details
Description: The update handler binds the whole body, so `role` and `credit` are writable.
Exploit: POST {"role":"admin","available_credit":9999} — privilege + balance escalation.
Code cure: Fix mass assignment on profile
highJWT + card data in responsecrapi-tokenleak-006sensitive_dataservices/workshop/mechanic.js:120
waf_data_guard · partial✓ code fix drafted
Details
Description: The receipt payload echoes the full PAN and a signed service token.
Exploit: GET a receipt; response body contains the 16-digit card number in cleartext.
Code cure: Fix jwt + card data in response
mediumNo rate limit on OTP verifycrapi-bruteforce-004rate_abuseservices/identity/otp.js:30
rate_limit · full✓ code fix drafted
Details
Description: The 4-digit OTP endpoint has no throttle — brute-forceable in minutes.
Exploit: Fire all 10k OTPs; no lockout, no delay.
Code cure: Fix no rate limit on otp verify
mediumUsername enumeration on signupcrapi-userenum-005broken_authservices/identity/signup.js:25
no band-aid — code cure only✓ code fix drafted
Residual risk: no positive-security band-aid fits; ships as code-only.
Details
Description: Distinct errors for taken vs free emails leak which accounts exist.
Exploit: Diff the 'already registered' vs 'ok' responses to enumerate users.
Code cure: Fix username enumeration on signup
+
Findings & band-aid coverage
criticalSQL injection in logincrapi-sqli-001sqliservices/identity/login.js:42
service_policy · full✓ code fix drafted
Details
Description: The email field is concatenated straight into the auth query.
Exploit: email=" OR 1=1 -- lets any password through and dumps the users table.
Code cure: Fix sql injection in login
highBOLA on vehicle locationcrapi-bola-002broken_object_authzservices/identity/vehicle.js:88
api_schema · full✓ code fix drafted
Details
Description: Any authenticated user can read another user's vehicle GPS by id.
Exploit: Swap {id} to another user's vehicle uuid; server returns their live location.
Code cure: Fix bola on vehicle location
highMass assignment on profilecrapi-mass-003mass_assignmentservices/identity/dashboard.js:61
waf · partial✓ code fix drafted
Details
Description: The update handler binds the whole body, so `role` and `credit` are writable.
Exploit: POST {"role":"admin","available_credit":9999} — privilege + balance escalation.
Code cure: Fix mass assignment on profile
highJWT + card data in responsecrapi-tokenleak-006sensitive_dataservices/workshop/mechanic.js:120
waf_data_guard · partial✓ code fix drafted
Details
Description: The receipt payload echoes the full PAN and a signed service token.
Exploit: GET a receipt; response body contains the 16-digit card number in cleartext.
Code cure: Fix jwt + card data in response
mediumNo rate limit on OTP verifycrapi-bruteforce-004rate_abuseservices/identity/otp.js:30
rate_limit · full✓ code fix drafted
Details
Description: The 4-digit OTP endpoint has no throttle — brute-forceable in minutes.
Exploit: Fire all 10k OTPs; no lockout, no delay.
Code cure: Fix no rate limit on otp verify
mediumUsername enumeration on signupcrapi-userenum-005broken_authservices/identity/signup.js:25
no band-aid — code cure only✓ code fix drafted
Residual risk: no positive-security band-aid fits; ships as code-only.
Details
Description: Distinct errors for taken vs free emails leak which accounts exist.
Exploit: Diff the 'already registered' vs 'ok' responses to enumerate users.
Code cure: Fix username enumeration on signup
Candidates the verify agent did not confirm
3 of 9 candidates. The discover agent proposed these; the adversarial verify step could not confirm them, so they carry no band-aid and are not counted anywhere else on this page. They are shown because "considered and rejected" is a result worth seeing — not because they are findings.
criticalRemote code execution via eval in report renderercrapi-eval-007command_injectionservices/community/render.js:118
Details
Description: A template string reaches eval() in the report renderer.
Exploit: Inject a template expression that closes the string and calls process.exit.
Description: A database password appears as a literal in the source.
Exploit: Read the credential from the repository and connect directly.
mediumNo rate limit on password resetcrapi-nolimit-009rate_abuseservices/identity/reset.js:47
Details
Description: The password-reset endpoint has no throttle in application code.
Exploit: Loop reset requests to enumerate accounts or exhaust the mail quota.
Generated XC band-aid policies
api_schemacrapi-lab-apidef
rate_limitotp-throttle
service_policydeny-login-sqli
wafcrapi-lab-waf
waf_data_guardmask-pan
-
Band-aid impact exploit before → after (live validation)
control
policy
exploit before
exploit after
legit
result
when
service_policy self-healed ×2
deny-login-sqli
200 allowed
403 blocked
ok
PASS
2026-08-05T13:06:09
apply_timing self-healed ×2
200 allowed
403 blocked
ok
PASS
2026-08-05T13:06:13
api_schema
crapi-lab-apidef
200 allowed
403 blocked
ok
PASS
2026-08-05T13:07:09
apply_timing
200 allowed
403 blocked
ok
PASS
2026-08-05T13:07:13
waf
crapi-lab-waf
200 allowed
403 blocked
ok
fail
2026-08-05T13:07:50
apply_timing
200 allowed
403 blocked
ok
PASS
2026-08-05T13:07:54
rate_limit
5/MINUTE
burst 30 allowed
25/30 rate-limited (429)
—
PASS
2026-08-05T13:08:19
Blast radius what each band-aid would block in recorded traffic — 500 of 500 recorded request(s) replayed through crapi-lab · window 2026-07-20T09:00:00Z..2026-07-20T10:00:00Z
policy
evaluated
would block
rate
verdict
top blocked paths
caveats
deny-login-sqli
500
3
0.6%
within threshold
/identity/api/auth/login ×3
—
+
Band-aid impact exploit before → after (live validation)
control
policy
exploit before
exploit after
legit
result
when
service_policy self-healed ×2
deny-login-sqli
200 allowed
403 blocked
ok
PASS
2026-08-05T16:05:09
apply_timing self-healed ×2
200 allowed
403 blocked
ok
PASS
2026-08-05T16:05:13
api_schema
crapi-lab-apidef
200 allowed
403 blocked
ok
PASS
2026-08-05T16:06:09
apply_timing
200 allowed
403 blocked
ok
PASS
2026-08-05T16:06:13
waf
crapi-lab-waf
200 allowed
403 blocked
ok
fail
2026-08-05T16:06:50
apply_timing
200 allowed
403 blocked
ok
PASS
2026-08-05T16:06:54
rate_limit
5/MINUTE
burst 30 allowed
25/30 rate-limited (429)
—
PASS
2026-08-05T16:07:19
Blast radius what each band-aid would block in recorded traffic — 500 of 500 recorded request(s) replayed through crapi-lab · window 2026-07-20T09:00:00Z..2026-07-20T10:00:00Z
policy
evaluated
would block
rate
verdict
top blocked paths
caveats
deny-login-sqli
500
3
0.6%
within threshold
/identity/api/auth/login ×3
—
Remediation ledger found → mitigated → remediated → retired
diff --git a/demo/out/run.json b/demo/out/run.json
index 02fd888..c1f0c1f 100644
--- a/demo/out/run.json
+++ b/demo/out/run.json
@@ -1,6 +1,6 @@
{
- "run_id": "cfc003c6cfb6",
- "created": "2026-08-05T13:15:09.728200+00:00",
+ "run_id": "7bc42a95f23d",
+ "created": "2026-08-05T16:14:09.354173+00:00",
"repo": "/src/crapi",
"config_path": "config/agents.yaml",
"models": {
diff --git a/demo/out/simulation.json b/demo/out/simulation.json
index e98faf6..ae8f738 100644
--- a/demo/out/simulation.json
+++ b/demo/out/simulation.json
@@ -1,5 +1,5 @@
{
- "ts": "2026-08-05T13:15:09.733157+00:00",
+ "ts": "2026-08-05T16:14:09.360187+00:00",
"lb": "crapi-lab",
"source": "xc:crapi-lab",
"records": 500,
diff --git a/docs/DEMO.md b/docs/DEMO.md
index c7450c2..2fc87dd 100644
--- a/docs/DEMO.md
+++ b/docs/DEMO.md
@@ -1,96 +1,231 @@
-# DEMO — virtual-patch-copilot in five minutes
+# DEMO — the whole product, from the console
-Two ways to run it. **Path A** needs no cloud and no API keys — it tells the whole story from a
-curated dataset, ideal for a laptop walkthrough or a screen recording. **Path B** is the live
-end-to-end: a real app behind F5 Distributed Cloud (XC), a real exploit blocked in minutes.
+Everything here is driven from the **ops console** in a browser. No terminal is needed once the
+server is up. CLI equivalents are given where you might want them, but the demo is the GUI.
The one-line story to keep in mind:
-> A frontier model finds exploitable vulns. The code fix needs 20–30 days of change control.
-> The copilot puts an **XC band-aid in front of the app in minutes** — exploit blocked, legit
-> traffic untouched — and opens the **code-fix PR (the cure)** in the same pass. When the cure
-> ships, it **retires** the band-aid. Every step is gated, validated, and reversible.
+> A frontier model finds exploitable vulns. The code fix needs 20–30 days of change control. The
+> copilot puts an **F5 band-aid in front of the app in minutes** — exploit blocked, legit traffic
+> untouched — and opens the **code-fix PR (the cure)** in the same pass. When the cure ships, it
+> **retires** the band-aid. Every step is gated, validated, reversible and recorded.
+
+There are three demos, in increasing order of what they need. **Pick one and finish it** — they
+tell the same story at different levels of proof, and running half of each is worse than running
+one whole.
+
+| | needs | proves | wall clock |
+|---|---|---|---|
+| **A — Offline** | nothing | the whole arc, from a curated dataset | 5 min |
+| **B — Live on F5 XC** | XC tenant + a model key | a real exploit blocked at a real edge | 12 min |
+| **C — Declarative WAF** | B, plus a BIG-IP | the same policy, emitted for someone else's WAF | +6 min |
---
-## Path A — the offline walkthrough (no XC, no keys)
+## The console at a glance
-```bash
-pip install -e ".[console]"
-python3 demo/build_demo_out.py # writes a curated demo/out (crAPI-flavoured)
-VPCOPILOT_OUT=demo/out vpcopilot console # http://127.0.0.1:8787
```
+1 Scan → 2 Review → 3 Simulate → 4 Mitigate → 5 Cure → 6 Retire → 7 Benchmark ⚙ Setup
+```
+
+The seven numbered steps are the arc, left to right. **⚙ Setup** holds credentials, the XC and
+BIG-IP status panels, the audit sink and the per-agent model table. A **Run settings** bar
+(`dry-run · rollback · LB · refine×3`) appears on the four steps that can change something —
+Simulate, Mitigate, Cure, Retire — and never on the ones that cannot.
+
+The **hero band** across the top is the headline: exploitable vulns → mitigated live → time to
+mitigate, against your change-control window.
+
+---
-Walk the steps top to bottom — the whole arc is already in the data:
-
-1. **② Review → hero band.** "6 exploitable vulns → mitigated live in ~30s, vs a 25-day change
- window." Five XC control families are in play (service_policy, api_schema, waf, rate_limit,
- waf_data_guard). One finding ships code-only (no band-aid fits) — honesty, not theatre.
-2. **② Review → findings.** Click any row to inspect the exploit, the vulnerable code, the
- generated band-aid, and the code cure. Note the SQLi row.
-3. **② Review → Open HTML report ↗** (one click, right there in the step — no hunting in Setup).
- The same story as a shareable, self-contained `report.html`: hero, severity/coverage bars,
- model-independence, and the band-aid impact table where the SQLi service policy shows
- **self-healed ×2** — the refiner's first policy didn't block, it diagnosed and retried until the
- exploit actually returned 403 — and rate-limit shows the behavioral proof (25/30 requests 429'd).
- **Download** grabs a stamped copy. It is rebuilt from the current out dir every time you open
- it, so it is never a stale file.
-4. **⑥ Retire → ledger.** The four-state track: `found → mitigated → remediated → retired`.
- `crapi-sqli-001` is walked all the way to **retired** — its cure PR merged, so the band-aid was
- detached.
-5. **⑥ Retire → audit trail.** *Every change made to a load balancer*, one row each: when (UTC) ·
- action · justified by (the finding + severity) · control (+ the XC object) · load balancer
- (+ namespace) · outcome (with the `200 allowed → 403 blocked` proof and a self-heal ×N badge) ·
- by. Filter it, expand `▸` for the raw JSON, then **Export evidence bundle (.zip)** — the
- normalized `audit.csv`, the raw `audit.log`, the exact XC configs pushed, the pre-change LB
- snapshots, and a manifest that SHA-256s every member. **All runs** does the same for every run
- dir on disk. The curated log is hand-built, so a couple of rows carry no finding or actor; a
- live apply (Path B) stamps both on every record. Detail: [AUDIT.md](AUDIT.md).
-
-The report also lives at `demo/out/report.html` — open it directly with no server. The same bundle
-is available from the CLI:
+## Demo A — offline (no cloud, no keys)
```bash
-vpcopilot export --out demo/out # -> demo/out/audit-bundle.zip
+pip install -e ".[console]"
+python3 demo/build_demo_out.py # writes a curated demo/out (crAPI-flavoured)
+VPCOPILOT_OUT=demo/out vpcopilot console # http://127.0.0.1:8787
```
+Nine candidates, six verified — the whole arc already in the data. Walk the steps left to right.
+
+### ② Review — what was found, and what was not
+
+The chip row is the funnel: **candidates: 9 → verified: 6 → band-aids: 5 → code-fix PRs: 6**.
+
+The gap between 9 and 6 is the point. An adversarial verify step refuted three candidates — an
+`eval()` sink that is not reachable, a "hardcoded password" that is a test fixture, a missing rate
+limit that already exists upstream — and they are **not** silently dropped. In the HTML report they
+render below the fold in their own section, labelled *"Candidates the verify agent did not
+confirm"*, excluded from every count and every chart on the page.
+
+Say this out loud: *the tool argues with itself, and shows you the argument.*
+
+Click any finding row to expand the exploit, the vulnerable code, the generated policy and the
+code cure.
+
+Each row carries its **CWE and OWASP API category**, with the tier spelled out — `CWE-89 · mapped`
+versus `CWE-22 · advisory`. That distinction is the honesty: `advisory` means the OSV record named
+the weakness and we are quoting it; `mapped` means the copilot classified it. Two findings carry
+**no CWE at all**, and that is the correct answer — `CWE-840` is *prohibited* by MITRE for mapping
+to real vulnerabilities, and username enumeration is not "weak authentication". A blank you can
+explain beats a guess.
+
+### ② Review → **Open HTML report ↗**
+
+The shareable artifact, rebuilt from the run directory every time you open it, so it is never
+stale. **Download** takes a stamped copy.
+
+Point at three things:
+
+1. **Findings by OWASP API Top 10** — and the `(no category)` bar. Injection was *removed* from the
+ 2023 API list, so `sqli` carries a CWE and no category. The chart accounts for every finding
+ rather than quietly charting a subset.
+2. **The blast-radius table** — a verdict per policy, with a caveats column. A replay that could
+ not be measured says *not measured*, not a green 0.0%.
+3. **Self-healed ×2** on the SQLi policy — the first generated policy did not block; the refiner
+ diagnosed it and retried until the exploit actually returned 403.
+
+### ③ Simulate — the blast radius, before anything is applied
+
+The step people do not expect, and the one that lands with security teams. The safety spine proves
+a band-aid blocks *the finding's exploit* and passes *one* legit request. It says nothing about the
+other million requests a day, and that gap is why controls sit in monitor mode.
+
+Simulate replays a recorded traffic sample against each candidate policy through a **spare** load
+balancer and reports what each one *would* block. Nothing is applied; the spare LB is snapshotted
+and restored.
+
+### ⑥ Retire — the ledger and the audit trail
+
+The four-state track: `found → mitigated → remediated → retired`. `crapi-sqli-001` is walked all
+the way — its cure PR merged, so the band-aid was detached.
+
+Below it, **every change ever made to a load balancer**, one row each: when (UTC) · action ·
+justified by (finding + severity) · control · load balancer + namespace · outcome (with the
+`200 allowed → 403 blocked` proof) · by whom. Expand `▸` for the raw JSON.
+
+**Export evidence bundle (.zip)** — the normalized `audit.csv`, the raw `audit.log`, the exact
+configs pushed, the pre-change LB snapshots, and a manifest that SHA-256s every member. **All
+runs** does the same across every run directory on disk.
+
+Two things worth saying while it downloads:
+
+- Dry runs are deliberately **absent** from the trail. Nothing changed, so there is nothing to
+ answer for.
+- Overriding a protected load balancer writes its **own** audit event, so crossing a rail is
+ visible to anyone scanning the trail rather than buried in a field of an ordinary record.
+
+### ⑦ Benchmark
+
+Compare models per agent. The point is that *nothing here is Anthropic-specific* — every agent's
+model is set in `config/agents.yaml`, and ⚙ Setup shows the current assignment for all eight.
+
---
-## Path B — live, behind XC (the real thing)
+## Demo B — live on F5 Distributed Cloud
-Prereqs in `.env`: `XC_API_URL`, `XC_API_TOKEN`, `XC_NAMESPACE`, a model key (e.g.
-`ANTHROPIC_API_KEY`), and a `GITHUB_TOKEN` for PRs. Optional: `XC_DASHBOARD_URL` for the
-"XC security dashboard ↗" deep link; `CHANGE_CONTROL_DAYS` to match the customer's number.
+**⚙ Setup** first. Fill in `XC_API_URL`, `XC_API_TOKEN`, `XC_NAMESPACE` and a model key, then
+**Save to .env**. A value already supplied through the environment shows as **(set in
+environment)** — saving `.env` will not override it until the console restarts, and the page says
+so rather than letting you believe a change took effect.
-```bash
-vpcopilot console # http://127.0.0.1:8787
-```
+Then `vpcopilot console`, and pick a lab load balancer in **Run settings**.
+
+### ① Scan — four kinds of input
-1. **① Scan** a vulnerable app repo (VAmPI / crAPI / Nimbus). Watch discover → verify → triage →
- generate → remediate stream live. The log box is scrollable and holds the **whole** transcript —
- scroll up mid-scan to re-read the discover output and it stays put; a **↓ follow** chip and a
- line counter appear until you scroll back to the bottom. Long scans no longer push the page down.
-2. **② Review** the findings, and hit **Open HTML report ↗** if someone wants the artifact now.
-3. **④ Mitigate.** With `dry-run` OFF and `keep live` ON (**Run settings** — the collapsible bar at the top of the Mitigate step), click
- **Mitigate service_policy ▶** on a finding. The refiner streams in the row: attach → validate →
- (refine → retry)* → **before 200 through → after 403 BLOCKED · legit ok**, with a *self-healed in
- N attempts* badge if it took more than one try. It never claims success unless the live exploit
- is actually blocked.
-4. **XC security dashboard ↗** (hero band) — jump to the native WAF/API-Security telemetry to show
- the block landing in XC.
-5. **⑤ Cure → Open PR** on the same finding to draft the real code fix against your repo.
-6. **⑥ Retire** once the cure merges — the band-aid is detached and the finding goes `retired`. The
- loop is closed. Below the ledger, the **audit trail** now has a row per live change: which
- finding justified it, which LB and namespace it touched, whether it stuck, and who ran it
- (`VPCOPILOT_ACTOR`, else the OS user). **Export evidence bundle (.zip)** hands that to whoever
- asks why the load balancer changed — see [AUDIT.md](AUDIT.md).
-
-Dry runs are deliberately *not* in the trail: nothing changed, so there is nothing to answer for.
-The bundle is evidence for a human reviewer, not a compliance certification.
-
-Guardrails hold throughout: protected LBs (`VPCOPILOT_PROTECTED_LBS`, default `nimbus-www`) and
-`nimbus-*` policies refuse mutation unless you explicitly opt in; every apply snapshots first and
-rolls back on failure.
+The step takes any of these, and the last three are the ones people have not seen before:
+
+| Field | What it does |
+|---|---|
+| **Target repo** | source scan — discover → verify → triage → generate → remediate |
+| **…or a security advisory** | a CVE / GHSA / PYSEC id, resolved against OSV. No repo, no credentials |
+| **…or an OpenAPI spec** | scans the *contract*. With a repo, also cross-checks the spec against the code |
+| **…or dependency manifests** | every pinned package resolved against OSV — `requirements.txt`, `package-lock.json`, `pom.xml` |
+
+**Preview (no model calls)** surveys the dependencies without spending a token — a good answer when
+someone asks what a run costs.
+
+The run log is the whole transcript and stays where you scroll it; a **↓ follow** chip appears if
+you scroll up mid-scan.
+
+### ④ Mitigate — the moment that sells it
+
+**Run settings**: `dry-run` OFF, `keep live` ON. Then **Mitigate ▶** on a finding.
+
+The row streams: attach → validate → *(refine → retry)* → **before 200 through → after 403
+BLOCKED · legit ok**. It never reports success unless the live exploit is actually blocked, and
+never reports a block without also confirming a legitimate request still passes.
+
+Seven control families are available — `service_policy`, `waf`, `waf_data_guard`, `api_schema`,
+`rate_limit`, `malicious_user`, `bot_defense` — and triage picks per finding. Some findings get
+**no band-aid at all**: plaintext password storage is a data-at-rest problem the edge never sees,
+and the tool says "code cure only" rather than inventing a control for it.
+
+Then **XC security dashboard ↗** in the hero band, to show the block landing in F5's own telemetry.
+
+### ⑤ Cure → ⑥ Retire
+
+**Open PR** drafts the real fix against your repo. When it merges, **Reconcile & retire proven
+fixes** re-fires the exploit at the app's **origin** — around the band-aid, so a blocked request
+cannot be mistaken for a fixed bug — and only then detaches the control.
+
+If the cure has not merged, the console **refuses and tells you why**, with an explicit *retire
+anyway* button. That is the pattern throughout: warn, explain, let a human override, record the
+override.
+
+---
+
+## Demo C — the declarative WAF (not just F5 XC)
+
+The objection you will hear is *"we are not an F5 shop"*. This answers it.
+
+### ② Review → **Emit**
+
+Pick a target and emit the same finding's policy as a declarative WAF policy for:
+
+- `xc` — F5 Distributed Cloud
+- `bigip-awaf` — BIG-IP Advanced WAF, as an AS3 declaration
+- `nginx-app-protect` — NGINX App Protect
+
+Same finding, same evidence, three vendors' syntax. Where a target genuinely cannot express a
+control, it is listed as **unsupported** rather than emitted as something approximate.
+
+### ⚙ Setup → BIG-IP lab
+
+With an appliance configured, the panel shows the AS3 version, the tenants it can see, and the
+protected ones. `/Common` is refused outright — not overridable, not even on a dry run, because
+previewing the deletion of the appliance's own configuration is previewing an outage.
+
+The emitted policy is validated against the vendor's own published schema and — in the reference
+lab — pushed to a real BIG-IP, where the exploit is fired and the account balance is checked
+afterwards. The assertion is the **balance**, not the status code: BIG-IP's blocking page returns
+HTTP 200, so a status-code assertion would pass on a policy that blocked nothing.
+
+---
+
+## The safety rails, if someone asks
+
+All of these hold on **all three surfaces** — CLI, console and the MCP server:
+
+- **Protected load balancers** refuse mutation unless explicitly overridden, and the name is parsed
+ before the check, so `./nimbus-www` is refused too.
+- **Every apply snapshots first** and rolls back on failure — and the rollback is *verified*, not
+ assumed. A control plane that accepts the restore and applies nothing raises loudly.
+- **The blast-radius gate** blocks promotion of a policy that would block too much real traffic,
+ and honours the threshold you set rather than silently substituting a default.
+- **Secrets never reach an artifact.** Headers, bodies *and query strings* are redacted before a
+ request sample is written, and the redaction is counted, so a sample cannot report itself clean.
+- **Refusing to guess.** "We could not check this" and "this is clean" never render alike, anywhere.
+ An unreadable snapshot is not "no drift". A scan of a path that does not exist is refused, not
+ answered with an empty report.
+
+## For an agent audience: MCP
+
+`vpcopilot mcp` serves the pipeline over stdio as 15 tools — `scan_start`, `scan_status`,
+`scan_result`, `impact`, `patches_list`, `deps`, `drift`, `simulate`, `ledger`, `reconcile`,
+`verify_bundle` and more. Tools that would change a load balancer, open a PR or retire a control
+are **absent** unless the operator started the server with writes enabled. The human gate is not
+something a tool call can satisfy on its own.
---
@@ -99,37 +234,46 @@ rolls back on failure.
- **Band-aids, not cures.** Every mitigated finding also gets a code-fix PR. The band-aid buys the
20–30 days; the PR is the fix. The ledger tracks both to `retired`.
- **Self-healing.** The copilot validates its own policy against the finding's real exploit and
- refines until it works — so it never ships a band-aid that doesn't block.
-- **Model-independent.** Every agent's model is set per-agent in `config/agents.yaml` (Workflow /
- Model independence panels show it) — Claude, OpenAI, Gemini, or local Ollama, no code change.
-- **Reversible + gated.** Snapshot → self-test → attach → validate → keep or rollback. A human
- approves every live change in the console.
-- **Auditable.** Every live change is recorded with the finding that justified it, the LB +
- namespace it touched, whether it stuck, and who ran it — exportable as a .zip with a SHA-256
- manifest ([AUDIT.md](AUDIT.md)). It is evidence for a human reviewer, not a compliance
- certification.
+ refines until it works, so it never ships a band-aid that does not block.
+- **Model-independent.** Eight agents, each with its own model in `config/agents.yaml` — Claude,
+ OpenAI, Gemini or local Ollama, with no code change.
+- **Reversible and gated.** Snapshot → self-test → attach → validate → keep or roll back. A human
+ approves every live change.
+- **Auditable.** Every live change carries the finding that justified it, the LB and namespace it
+ touched, whether it stuck, and who ran it — exportable with a SHA-256 manifest
+ ([AUDIT.md](AUDIT.md)). Evidence for a human reviewer, **not** a compliance certification.
+
+## When something declines mid-demo
+
+Do not treat it as a failure — it is the product working, and in front of a security audience it is
+the most credible thing that can happen. Read the reason aloud.
+
+| You see | Say |
+|---|---|
+| a finding with **no band-aid** | "the edge cannot see this one — it gets a code fix, and it says so" |
+| a finding with **no CWE** | "MITRE prohibits the obvious mapping here; a blank we can explain beats a guess" |
+| **"not measured"** in the blast radius | "that replay could not be measured — it will not pretend it came back clean" |
+| a **refused retire** | "the cure has not merged; the band-aid is the only thing holding this shut" |
+| a **refused protected LB** | "that one is off-limits, and the override would be recorded" |
## Screenshots
-Captured from `demo/out` (Path A) and checked in under [`docs/images/`](images/) — they carry the
-story on their own:
+Checked in under [`docs/images/`](images/):
| Shot | File |
|---|---|
| Scan — the target form and its scrollable run log | [`1-scan.png`](images/1-scan.png) |
-| Review — hero band + findings + the HTML-report buttons | [`2-review.png`](images/2-review.png) |
-| Simulate — blast radius of each candidate before it is applied | [`3-simulate.png`](images/3-simulate.png) |
+| Review — hero band + findings + report buttons | [`2-review.png`](images/2-review.png) |
+| Simulate — blast radius before anything is applied | [`3-simulate.png`](images/3-simulate.png) |
| Mitigate — per-finding live apply | [`4-mitigate.png`](images/4-mitigate.png) |
-| Retire — four-state ledger (`crapi-sqli-001` at *retired*) + the audit trail | [`6-retire.png`](images/6-retire.png) |
-| The shareable HTML report (self-heal ×2 + rate-limit proof) | [`report.png`](images/report.png) |
+| Retire — four-state ledger + audit trail | [`6-retire.png`](images/6-retire.png) |
+| The shareable HTML report | [`report.png`](images/report.png) |
-To regenerate them: rebuild the dataset with `python3 demo/build_demo_out.py`, run
-`VPCOPILOT_OUT=demo/out vpcopilot console`, then capture the `#scan`, `#review`, `#mitigate` and
-`#simulate`, `#mitigate` and `#retire` steps plus `demo/out/report.html` at 1200px wide / 2× device pixel ratio.
+To regenerate: `python3 demo/build_demo_out.py`, then `VPCOPILOT_OUT=demo/out vpcopilot console`,
+and capture each step at 1200px wide / 2× device pixel ratio.
-Point the console at a **credential-free** `.env` when you do (`VPCOPILOT_ENV=…`): with XC creds
+Point the console at a **credential-free** `.env` when you do (`VPCOPILOT_ENV=…`). With XC creds
loaded, the hero band renders a deep link carrying your tenant hostname and namespace, and the
-Mitigate step's drift strip renders the target LB's live control set — both would ship in the image.
-Without creds the drift strip renders nothing at all, which is why it does not appear in
-`4-mitigate.png`. `build_demo_out.py` curates `actor`/`host`/`out_dir` in the fixture for the
-same reason — no real machine identity in a shared dataset.
+BIG-IP panel renders your appliance's tenants — both would ship in the image.
+`build_demo_out.py` curates `actor`/`host`/`out_dir` in the fixture for the same reason: no real
+machine identity in a shared dataset.