diff --git a/README.md b/README.md
index 36bfd49..f67a144 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,16 @@
*Damage photos, a scanned policy, and adjuster notes go in.
An evidence-backed recommendation — with its reasoning, precedents, and audit trail — comes out.*
+
**The AI gathers and reasons. A human decides.**
-[]([Evincta](https://evincta.vercel.app))
+[](https://evincta.vercel.app)
+
+[](.github/workflows/eval.yml)
-[](.github/workflows/eval.yml)
+[](.github/workflows/ci.yml)
@@ -31,15 +34,25 @@ An evidence-backed recommendation — with its reasoning, precedents, and audit
---
> [!NOTE]
-> **Live demo:** [https://evincta.vercel.app](https://evincta.vercel.app) → click **“Enter as demo adjuster.”**
-> The API runs on a free-tier container that sleeps when idle, so the **first load may take ~30s to wake** — after that it's fast. Browsing and deciding claims costs nothing (recommendations are pre-computed and cached).
+> **Live demo:** [evincta.vercel.app](https://evincta.vercel.app) → click **“Enter as demo adjuster.”**
+> The API runs on a free-tier container that sleeps when idle, so the **first load may take ~30s to wake** — after that it's fast. Browsing and deciding claims costs nothing; recommendations are pre-computed and cached.
-
-

+

-*The adjuster's review console: queue, recommendation with cited precedents, evidence, and the approve / override gate.*
+
The adjuster's review console: queue, recommendation with cited precedents, evidence, and the approve / override gate.
+
+
+
+---
+
+
+
+**A production-grade AI system, not a notebook demo.**
+Vision · retrieval · tool-using agents · human-in-the-loop · held-out eval · deployed · hardened — end to end.
+
+`traced` · `evaluated` · `guarded` · `cost-controlled`
@@ -48,6 +61,7 @@ An evidence-backed recommendation — with its reasoning, precedents, and audit
## Table of contents
- [Why this project](#why-this-project)
+- [At a glance](#at-a-glance)
- [What it does](#what-it-does)
- [Why it's technically interesting](#why-its-technically-interesting)
- [Architecture](#architecture)
@@ -55,6 +69,7 @@ An evidence-backed recommendation — with its reasoning, precedents, and audit
- [Evaluation](#evaluation)
- [What I actually debugged](#what-i-actually-debugged)
- [Production hardening](#production-hardening)
+- [Engineering standard](#engineering-standard)
- [Scope & boundaries](#scope--boundaries)
- [Run it locally](#run-it-locally)
- [Tech stack](#tech-stack)
@@ -69,7 +84,21 @@ Auto-claims triage is **20–30 minutes of multimodal context-gathering per clai
Evincta is that system. It's deliberately built as a **decision-support engine, not a classifier**: it never auto-approves a payout. It assembles the evidence, reasons over it with a panel of specialised agents, and hands a human adjuster a recommendation they can accept or override — with every step traced, evaluated, and audited.
-It's the flagship of a four-project portfolio, all built to one production standard: **traced · evaluated · guarded · cost-controlled.**
+It is the flagship of a four-project portfolio, all built to one standard: **traced · evaluated · guarded · cost-controlled.**
+
+---
+
+## At a glance
+
+| | |
+|---|---|
+| **Domain** | Auto / vehicle-damage insurance claims |
+| **Input** | Damage photos · scanned policy · free-text adjuster notes |
+| **Output** | `approve / investigate / deny` · payout range · fraud-risk · policy basis · cited precedents · rationale |
+| **Core pattern** | Deterministic tools decide facts; a single constrained LLM synthesises the decision |
+| **Safety model** | Structural human-in-the-loop gate + hash-chained audit + graceful degradation to human review |
+| **Proof** | 18 held-out claims · asymmetric scoring · **CI gate that fails the build on regression** |
+| **Live** | Deployed on Vercel + Render (free tier), secured and rate-limited |
---
@@ -89,16 +118,16 @@ The output isn't a label — it's a **structured recommendation**: `approve / in
## Why it's technically interesting
-This is the part most "AI projects" skip. Evincta is built like production software, and each choice below is a deliberate one with a trade-off behind it:
+This is the part most "AI projects" skip. Evincta is built like production software, and each choice below is deliberate, with a trade-off behind it:
| | |
|---|---|
-| 🧩 **Real MCP, not a metaphor** | Four actual [Model Context Protocol](https://modelcontextprotocol.io) servers (`policy · cost · fraud · precedent`) behind one host, launched as subprocesses and reused across requests. The agents call tools over a real protocol — the same primitive that lets these capabilities be swapped, versioned, or served remotely. |
+| 🧩 **Real MCP, not a metaphor** | Four actual [Model Context Protocol](https://modelcontextprotocol.io) servers (`policy · cost · fraud · precedent`) behind one host, launched as subprocesses and reused across requests. The agents call tools over a real protocol — the primitive that lets these capabilities be swapped, versioned, or served remotely. |
| ⚖️ **Hybrid agents — trust placed deliberately** | The specialists are **deterministic tool-callers** (coverage, cost, fraud, precedent) — no LLM guessing where math and policy lookups belong. Exactly **one** LLM does the hard part: **synthesising** the evidence into a decision, constrained to a typed schema. LLMs synthesise; tools decide facts. |
| 🚦 **Human-in-the-loop, enforced structurally** | The graph *cannot* commit a decision. It runs to an `interrupt`, persists state via a checkpointer, and waits. The decision only exists when a human POSTs approve/override — which resumes the graph and writes the audit. It's not a convention; the machine can't skip it. |
| 📊 **A held-out eval that gates CI** | 18 claims are held out and **never indexed**. Every push runs the full pipeline against ground truth with **asymmetric scoring** (approving a should-deny claim is penalised far more than a cautious flag) and **fails the build on regression**. |
| 🔒 **Tamper-evident audit** | Every decision appends to a **hash-chained** JSONL log — each entry carries the previous entry's hash, so the record can't be silently edited after the fact. |
-| 🛡️ **Hardened, honestly** | Retry + circuit breaker with **graceful degradation to human review** on failure, rate limiting, a daily spend cap, path-traversal and schema validation, structured logs, and dependency health checks. Infra-scale items (Postgres, Redis, alerting) are documented as roadmap, not faked. |
+| 🛡️ **Hardened and tested** | Retry + circuit breaker with **graceful degradation to human review**, rate limiting, a daily spend cap, path-traversal and schema validation, structured logs, and dependency health checks — each **verified by tests that run in CI**. Infra-scale items are documented as roadmap, not faked. |
---
@@ -125,11 +154,11 @@ This is the part most "AI projects" skip. Evincta is built like production softw
Langfuse tracing (every call, every cost) · held-out eval + CI gate (GitHub Actions) │
```
-Each layer has an [Architecture Decision Record](docs/adr/) explaining *why* it's built that way — the MCP transport choice, the agent topology, the eval design, the deploy/security posture, and the hardening line. The [Langfuse trace](docs/media/trace.png) below shows one real end-to-end run — coverage → cost → precedent → fraud → recommendation — in a single 26s span:
+Every layer has an [Architecture Decision Record](docs/adr/) explaining *why* it's built that way — MCP transport, agent topology, eval design, deploy/security posture, and the hardening line. The [Langfuse trace](docs/media/trace.png) below shows one real end-to-end run — coverage → cost → precedent → fraud → recommendation — in a single 26s span:
-
-

+

+
One claim, fully traced — every model call and its cost, observable end to end.
---
@@ -142,7 +171,7 @@ Most "multi-agent" systems hand every step to an LLM and hope. Evincta doesn't.
- **Coverage** — is this incident covered? (policy exclusions, limits, deductible) → a hard fact.
- **Cost** — estimated repair range for this severity and damaged parts → a lookup, not a guess.
-- **Fraud** — deterministic signals (pre-existing damage flags, inconsistencies) → rules, not vibes.
+- **Fraud** — deterministic signals (pre-existing damage, inconsistencies) → rules, not vibes.
- **Precedent** — the *k* most similar past claims by multimodal similarity → retrieval.
Then **exactly one LLM call** — the **synthesiser** — takes those facts and produces the decision, forced into a typed schema (`Decision` and `FraudRisk` enums, a payout range, cited precedent IDs, a rationale). Its prompt applies rules **in priority order**: coverage is an absolute deny-gate; then fraud escalates to investigate; then below-deductible denies; else approve.
@@ -171,7 +200,7 @@ Evincta is scored end-to-end on **18 held-out claims** — never indexed, never
-Fraud recall is reported separately and is **not gated**, deliberately. The synthetic frauds in the eval set are *image-reuse* type, whose only signal is image/incident inconsistency — which is indistinguishable, at the data level, from the noise in synthetically-paired images. Reliable detection needs **perceptual image hashing across claims**, which is roadmapped, not implemented. Gating a two-sample metric would be dishonest.
+Fraud recall is reported separately and is **not gated**, deliberately. The synthetic frauds in the eval set are *image-reuse* type, whose only signal is image/incident inconsistency — indistinguishable, at the data level, from the noise in synthetically-paired images. Reliable detection needs **perceptual image hashing across claims**, which is roadmapped, not implemented. Gating a two-sample metric would be dishonest.
Two things make this an acceptable, honest position rather than a gap: the **human approval gate** means a missed fraud flag is never an automatic payout, and the limitation is named explicitly rather than hidden behind a flattering number. *Knowing what real fraud detection requires — and scoping it deliberately — is the point.*
@@ -195,20 +224,39 @@ The eval didn't just produce a number — it **caught real bugs**, and tracing t
> The throughline — bugs #3 and #4 are the *same lesson twice*: **the LLM overriding a deterministic tool.** The fix is consistent — constrain outputs to schemas, and make the prompt treat tool results as authoritative gates. That's the central discipline of building an LLM decision system, learned by hitting it, not reading it.
-The git history preserves this as separate `fix:` commits with before/after numbers in the commit bodies — the debugging arc is legible in the log.
+The git history preserves this as separate `fix:` commits with before/after numbers in the bodies — the debugging arc is legible in the log.
---
## Production hardening
-Implemented (application-level, runs on free tier):
+The live system is hardened across five areas. Application-level hardening is **implemented and verified by tests that run in CI**; infrastructure-scale items are documented as precise roadmap, not faked — the line between the two is itself a deliberate engineering decision ([ADR 0008](docs/adr/0008-hardening.md)).
-- **Resilience** — retry + timeout + backoff on every external call; a **circuit breaker**; and **graceful degradation** — a dependency failure routes the claim to *human review*, never a wrong automated decision.
-- **Cost & abuse** — per-route **rate limiting**, a **daily spend cap** (a circuit breaker for the API bill), and request-size limits.
-- **Robustness** — **path-traversal** input validation, schema-validated model output, and prompt-injection *defense-in-depth* (delimited untrusted input · constrained output enum · human gate).
-- **Observability** — **structured JSON logging** and liveness + **dependency-checking readiness** probes.
+| Area | Implemented | Verified by |
+|---|---|---|
+| **Resilience** | Retry + timeout + backoff on every external call; circuit breaker; **graceful degradation** — a dependency failure routes the claim to *human review*, never a wrong automated decision | Unit tests (breaker opens/half-opens; forced failure → `investigate`) |
+| **Cost & abuse** | Per-route **rate limiting**; a **daily spend cap** (a circuit breaker for the API bill); request-size limits | `429` on limit breach; cap refuses spend over ceiling; `413` on oversized body |
+| **Robustness** | **Path-traversal** input validation; schema-validated model output; prompt-injection *defense-in-depth* (delimited untrusted input · constrained output enum · human gate) | `422` on traversal input; malformed output raises |
+| **Observability** | **Structured JSON logging** (no secrets/PII); liveness + **dependency-checking readiness** probes | `/health/ready` reports per-dependency status |
+| **Concurrency** | Shared-host tool calls serialised under a lock | Documented Postgres/Redis seam for multi-instance scale |
-Documented as roadmap (needs infrastructure beyond the free demo): Postgres for the claim index + LangGraph checkpointer and Redis for the limiter/spend counter (multi-instance scale); real-time alerting on top of the existing Langfuse tracing. Prompt injection is *mitigated, not claimed solved.* Rationale in [ADR 0008](docs/adr/0008-hardening.md).
+> **The safest failure mode for a decision system:** on any partial failure, Evincta escalates to a human rather than deciding on incomplete evidence. A crash is bad; a confident wrong decision on missing data is worse. The circuit breaker degrades *into the human gate* — reusing the safety mechanism the whole system is built around.
+
+**Roadmap (needs infrastructure beyond the free demo):** Postgres for the claim index + LangGraph checkpointer and Redis for the limiter/spend counter (multi-instance scale); real-time alerting on top of the existing Langfuse tracing. Prompt injection is *mitigated via defense-in-depth, not claimed solved.*
+
+---
+
+## Engineering standard
+
+- **Traceable** — every model call and its cost is observable in Langfuse; one full trace is shown above.
+- **Evaluated** — a held-out set, asymmetric scoring aligned to real risk, and a CI gate that blocks regressions.
+- **Guarded** — resilience, rate limits, spend caps, and input/output validation, each with a test.
+- **Cost-controlled** — token spend is estimated per call and hard-capped; the demo is cached to cost nothing to view.
+- **Decision-recorded** — eight [ADRs](docs/adr/) capture *why* each layer is built the way it is.
+- **Legible history** — small, focused commits; the debugging arc is readable in the log.
+- **Honest** — limitations (fraud recall, single-instance state, demo auth) are stated plainly, with a documented path forward for each.
+
+*The through-line: production-minded AI engineering — building systems that are observable, measurable, safe to fail, and honest about their edges.*
---
@@ -249,11 +297,12 @@ Or the whole API in one container:
docker build -t evincta-api . && docker run --rm -p 8000:8000 --env-file .env evincta-api
```
-Run the eval and the gate:
+Run the eval, the gate, and the hardening tests:
```bash
python -m eval.run_eval # scores the 18 held-out claims end-to-end
python -m eval.gate # exit 0 if every gated metric passes
+pytest tests/ -v # eval + hardening tests (breaker, degradation, validation)
```
---
@@ -270,7 +319,8 @@ python -m eval.gate # exit 0 if every gated metric passes
| **API** | FastAPI (async · lifespan-managed MCP host) |
| **Frontend** | React + TypeScript + Tailwind |
| **Observability** | Langfuse (per-call tracing + cost) |
-| **Eval / CI** | Custom harness + GitHub Actions gate |
+| **Resilience** | tenacity (retry/backoff) · custom circuit breaker · slowapi (rate limit) |
+| **Eval / CI** | Custom harness · pytest · GitHub Actions gate |
| **Deploy** | Docker → Render (API, free) · Vercel (frontend) |
| **Language / CI** | Python 3.12 · GitHub Actions |
@@ -284,9 +334,9 @@ python -m eval.gate # exit 0 if every gated metric passes
- [x] **Phase 4** — Multi-agent recommender + human gate + audit
- [x] **Phase 5** — Held-out eval harness + asymmetric scoring + CI gate
- [x] **Phase 6** — FastAPI + React console · deploy · security · production hardening
-- [x] **Next** — perceptual-hash fraud detection · Postgres/Redis for horizontal scale · real-time alerting · SSO
+- [ ] **Next** — perceptual-hash fraud detection · Postgres + Redis for horizontal scale · real-time alerting · SSO
-**Known limitations (stated plainly):** fraud recall is limited without perceptual hashing; the demo runs single-instance (SQLite + JSON index); latency is ~15s/claim (sequential vision + synthesis calls); auth is demo-grade, SSO-ready at the same seam.
+**Known limitations (stated plainly):** fraud recall is limited without perceptual hashing; the demo runs single-instance (SQLite + JSON index); latency is ~15s/claim (sequential vision + synthesis calls); auth is demo-grade, SSO-ready at the same seam. None are hidden — each has a documented path forward.
---
@@ -303,14 +353,19 @@ evincta/
├─ api/ # FastAPI: lifecycle endpoints + human gate + auth
├─ web/ # React + Tailwind review console
├─ eval/ # ⑤ held-out eval · asymmetric metrics · thresholds · CI gate
+├─ tests/ # eval + hardening tests (run in CI)
├─ docs/adr/ # architecture decision records (0001–0008)
-└─ .github/workflows/ # CI + the eval gate
+└─ .github/workflows/ # CI · eval gate · hardening tests
```
+---
+
-**Built by [Atti Rehman](https://github.com/AttiR).** Traced, evaluated, guarded, cost-controlled.
+**Built by [Atti Rehman](https://github.com/AttiR)**
+
+Traced · evaluated · guarded · cost-controlled.
*The AI gathers and reasons. A human decides.*
diff --git a/docs/adr/0008-hardening.md b/docs/adr/0008-hardening.md
new file mode 100644
index 0000000..edbe69a
--- /dev/null
+++ b/docs/adr/0008-hardening.md
@@ -0,0 +1,83 @@
+# ADR 0008 — Production hardening
+
+## Status
+Accepted
+
+## Context
+Evincta is deployed and functional (ADR 0007), but "deployed" is not
+"hardened." A system that handles claim decisions and calls paid external
+models needs to survive dependency failures, refuse to drain its budget,
+reject hostile input, be observable when it breaks, and behave under
+concurrency. The constraint: the demo runs on free tier, so hardening
+that requires paid infrastructure cannot be genuinely demonstrated.
+
+## Decision
+
+Harden across five areas, drawing a deliberate line between what is
+implemented in application code and what is documented as roadmap.
+
+**Implemented (application-level, runs and is tested on free tier):**
+
+- **Resilience.** Retry + timeout + backoff on every external call
+ (Anthropic SDK retries configured; Voyage/Pinecone wrapped with
+ tenacity, exponential backoff + jitter). A circuit breaker trips on
+ sustained failure and fails fast during a cooldown. On failure the
+ claim **degrades to human review** ("investigate") rather than deciding
+ on incomplete evidence — the safest failure mode for a decision system,
+ reusing the Phase-4 human gate as the safety net.
+- **Cost & abuse.** Per-route rate limiting (slowapi), scaled to cost —
+ the paid submit route capped tight, cheap reads looser. A daily spend
+ cap estimates token cost per model call and refuses further calls once
+ a ceiling is exceeded (a circuit breaker for the bill). A request-body
+ size limit caps a cost/DoS vector.
+- **Robustness.** Path-traversal validation on the claim-directory input;
+ model output validated against the Pydantic recommendation schema;
+ prompt-injection defense-in-depth — untrusted claim notes are delimited
+ and the system instructed to treat them as data, the output space is
+ constrained to enums, and the human gate backstops any hijack.
+- **Observability.** Structured JSON logging (structlog) with event
+ fields, excluding secrets and raw claim content; a liveness probe
+ (/health) and a readiness probe (/health/ready) that checks dependency
+ reachability (MCP host, Pinecone).
+- **Concurrency.** Tool calls through the single shared MCP host are
+ serialised under a lock so concurrent requests do not interleave on the
+ same stdio session.
+
+Each of the above is verified by a test that runs in CI (breaker opens/
+half-opens; forced failure degrades to investigate; 429 on rate-limit
+breach; 413 on oversized body; 422 on traversal input; /health/ready
+reports per-dependency status).
+
+**Documented as roadmap (needs infrastructure beyond the free demo):**
+
+- Postgres for the claim index (`api/store.py`) and the LangGraph
+ checkpointer (swap SqliteSaver → a Postgres saver), and Redis for the
+ rate limiter and spend counter — required for multi-instance horizontal
+ scale. Named to the component level so the migration is concrete.
+- Real-time alerting on top of the existing Langfuse tracing (tracing is
+ post-hoc; alerting needs a paging service).
+- Perceptual-hash fraud detection (also tracked in the eval roadmap).
+
+## Consequences
++ A dependency failure never causes a crash or a wrong automated decision;
+ it escalates to a human.
++ Abuse and runaway loops are bounded (rate limit + spend cap + size cap).
++ Untrusted input is validated; a model hijack is constrained and gated.
++ The system is observable at the event level and reports dependency health.
++ Hardening claims in the README are backed by CI tests, not assertions.
+- Rate limiter and spend counter are in-process (correct for one free
+ container); multi-instance correctness requires the documented Redis move.
+- Prompt injection is mitigated via defense-in-depth, not solved — stated
+ honestly rather than overclaimed.
+- Latency (~15s/claim) is inherent to sequential vision + synthesis calls;
+ not addressed here.
+
+## Alternatives rejected
+- Half-implementing Postgres/Redis to claim "scale": would not run on the
+ free demo and proves less than documenting the exact seam.
+- Blanket rate limits (one number for all routes): ignores that cost
+ differs sharply between paid writes and cheap reads.
+- Blocking on graceful degradation being "perfect": escalating to the
+ human gate is a correct, simple degrade path and reuses existing safety.
+- Claiming prompt injection is solved: dishonest; defense-in-depth +
+ honest scoping is the defensible position.
diff --git a/services/agents/tools.py b/services/agents/tools.py
index 2bec372..4010890 100644
--- a/services/agents/tools.py
+++ b/services/agents/tools.py
@@ -1,11 +1,17 @@
import asyncio
import json
+import threading
from services.mcp.host import Host
# set by the API at startup; None when running scripts/eval
_SHARED_HOST = None
_SHARED_LOOP = None
+# the shared MCP host is one connection used by many requests.
+# serialize tool calls through it with a lock so concurrent requests
+# don't interleave on the same stdio session:
+_host_lock = threading.Lock()
+
def set_shared_host(host, loop=None):
"""Register a long-lived MCP host (and the event loop that owns it).
@@ -28,10 +34,10 @@ async def _call_async(host, tool_name, args):
def call_tool(tool_name: str, args: dict) -> dict:
if _SHARED_HOST is not None and _SHARED_LOOP is not None:
- # API path: run on the host's own loop, from this worker thread.
- fut = asyncio.run_coroutine_threadsafe(
- _call_async(_SHARED_HOST, tool_name, args), _SHARED_LOOP)
- return fut.result()
+ with _host_lock: # one tool call at a time per host
+ fut = asyncio.run_coroutine_threadsafe(
+ _call_async(_SHARED_HOST, tool_name, args), _SHARED_LOOP)
+ return fut.result()
# script/eval path: spin up a throwaway host (as before)
async def _go():