You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Add the Apache-2.0 LICENSE (the ACP/UCP/AP2 stack Bazaar interoperates
with is all Apache-2.0) and declare it in pyproject with the OSI
classifier.
- Rewrite ARCHITECTURE.md as an engineering map: one signed order walked
end to end function by function, a package-by-package table against the
real module tree, the Trust Fabric internals, the input-authenticity
boundary (server-derived segment, tier-bounded grants, owner-only
cancel, agent revoke, and the documented demo shortcuts), the model
boundary, failure handling, the single state swap point, and the
evidence pipeline.
- README: note the input-authenticity guarantees under the gate diagram,
the console's judge-facing affordances (no-token banner, token-free
compile fallback, live model-outage toggle), pointers to the deeper
docs, and a license/version footer (v0.1.0, P0 — Proof).
Metric strings the consistency test pins are untouched. 96 tests, ruff
clean, console builds.
Claude-Session: https://claude.ai/code/session_01BLrj9TWybDCNw1mWxZgAks
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
One sentence: **the model proposes, deterministic policy verifies, and only then does anything execute** — for every layer that touches money, and for every protocol an AI buyer might speak.
3
+
One sentence, one invariant: **the model proposes, deterministic policy verifies, and only then does anything execute** — for every layer that touches money, and for every protocol an AI buyer might speak.
4
4
5
-
The diagrams live in the [README](README.md)(system, order sequence, compile pipeline, policy gate, protocol fan-out); the threat model in [THREAT_MODEL.md](THREAT_MODEL.md). This document is the layer-by-layer map with the guarantees each layer owns.
5
+
This document is the engineer's map. It names the real modules, walks one signed order through the code function by function, and states the guarantee each layer owns and the boundary each one refuses to cross. The visual diagrams (system, order sequence, compile pipeline, policy gate, protocol fan-out) live in the [README](README.md); the attack-by-attack analysis in [THREAT_MODEL.md](THREAT_MODEL.md).
6
6
7
-
## Layers and their guarantees
7
+
---
8
8
9
-
| Layer | Module | What it guarantees |
9
+
## 1. The shape
10
+
11
+
Four products, one spine (`propose → verify → execute`), and a payments rail that is only ever reached after the gate passes:
Every package is a Python module under `bazaar/`; the merchant console is a Vite/React app under `console/`. Nothing in the money path is a stub with a fake interface — the sandbox and the real Razorpay client implement the *same* narrow `PaymentsClient`, and the offline LLM implements the *same*`complete_json` contract as gpt-4o, so switching a backend never changes the control flow.
20
+
21
+
---
22
+
23
+
## 2. One order, end to end (the path that matters)
24
+
25
+
A tier-2 buyer agent buys 5 kg of basmati. Each arrow is a real call; the function that owns it is named.
26
+
27
+
1.**Discover** — `POST /bazaar/v1/discover` → `gateway/discover.py`. Ranking is a deterministic function of relevance, serviceability, stock, budget fit, readiness and trust. No model is in this loop; catalog prose cannot reorder it.
28
+
2.**Open a session** — `POST /bazaar/v1/sessions` (RFC 9421-signed, tag `agent-browse`). `gateway/auth.py::identify` verifies the signature → a `Caller(keyid, tier)`. The **pricing segment is derived server-side** (`app.py::server_segment`) from facts Bazaar owns — an untrusted caller cannot self-declare `b2b` or `new`.
29
+
3.**Propose** — `seller_agent/agent.py::handle` runs the turn. `propose.py::propose` asks the model for ONE `Proposal {tool, args, rule_id}`; buyer and catalog text are wrapped by `llm/base.py::wrap_untrusted` inside `<data>` blocks. The model may name a tool and a `rule_id` — never a price.
30
+
4.**Verify** — `agent.py` normalises model arg aliases, then the offer/quote maths run in **integer paise** in `seller_agent/offer_engine.py::build_quote`. A proposal that carries its own number is rejected (`rule_not_invented`).
31
+
5.**Grant + mandates** — `POST /bazaar/v1/grants` (tag `agent-pay`) issues a `ScopedPaymentGrant` (`trust/grants.py`), capped at the agent's tier ceiling; the buyer signs AP2-shaped Checkout and Payment mandates (`trust/mandates.py`), closed to this exact quote.
32
+
6.**Complete** — `POST /bazaar/v1/sessions/{id}/complete` + `Idempotency-Key`. The signature is verified **before** any cached payload is served (`app.py::complete`). `gateway/checkout.py::complete_session` reserves stock and the grant's pending amount, then runs the gate.
33
+
7.**The gate** — `trust/policy.py::check_checkout` runs the named checks (kill switch first). Any failure → `422 {reason, checks[]}`, no side effect, session stays retryable.
34
+
8.**Execute** — only now does `razorpay_client/*` create a UPI payment link. `gateway/state.py::handle_webhook_event` accepts `payment.captured`**only** for an in-progress session at the exact amount, converts the grant's pending → used, writes the fairness-ledger row and the audit entry.
35
+
36
+
If the model is down at step 3, the circuit breaker (`llm/resilience.py`) answers steps 3–4 from the deterministic backend through the same tools and the same gate. Steps 5–8 never involve a model at all.
37
+
38
+
---
39
+
40
+
## 3. Package by package
41
+
42
+
| Package | Files | What it owns / guarantees |
10
43
|---|---|---|
11
-
| Data model |`bazaar/schemas`| Integer paise everywhere; `bazaar.india` extension fields (GST, HSN, pincode serviceability, COD, pack units); all merchant/buyer free text is typed as untrusted data |
12
-
| Payments |`bazaar/razorpay_client`| One narrow `PaymentsClient` interface is the only money path. Sandbox client (orders, UPI links, refunds, HMAC webhooks) for offline runs; official-SDK client on **Razorpay test-mode keys** with a standard-link fallback when a fresh account has UPI links disabled; a Reserve-Pay mandate ledger (NPCI OC-228 defaults: ₹10,000 / 90 days) that blocks on grant issue, debits on use, releases on revoke |
13
-
| LLM |`bazaar/llm`| Single `complete_json(task, system, user, schema)` contract; OpenAI (gpt-4o, with gpt-4o-mini routed for catalog work) and Anthropic backends; SQLite call cache; **circuit breaker** — on failure the deterministic offline backend answers through the same tools and gate, failovers audited, fallback answers never cached |
14
-
| Compiler |`bazaar/compiler`| Parsers own price/stock/GST — the model may only name, categorise and enrich; confidence < 0.8 goes to a merchant review queue, never guessed; injected instructions stripped and flagged; one compile exports Bazaar + UCP manifests, ACP feed, Beckn catalog, llms.txt, JSON-LD; agent-readiness score |
15
-
| Seller Agent |`bazaar/seller_agent`| Propose → verify → execute, enforced in code; offers only by merchant-approved `rule_id` (a proposal carrying a value is rejected); multi-item carts ("5 kg rice and 2 kg dal" quotes both lines) on the real model and the deterministic fallback alike; bounded observe loop for real-model tool wobble; EN/HI/Hinglish; per-merchant MCP server over stdio |
16
-
| Trust Fabric |`bazaar/trust`| Ed25519 agent registry with tiers T0–T3; RFC 9421 request signatures (nonce, skew, browse/pay tags); AP2-shaped digest-chained mandates; scoped payment grants; **policy gate of named machine-readable checks** (incl. a human-present threshold above ₹15,000 per the RBI e-mandate framing); hash-chained audit log + Merkle root + replay; fairness ledger + cohort auditor that gates rule publishing; `trust/uap.py` is the seam where NPCI's UAP binding lands |
17
-
| Gateway |`bazaar/gateway`| Discover (deterministic ranking), ACP-shaped session state machine, checkout → Razorpay link → webhook (real payload shapes accepted: entity-wrapped, `payment_link.paid`, link-reference session matching); adapters `/acp``/ucp``/beckn`; global `bazaar-catalog` MCP at `/mcp`; merchant-mutating routes gated by `X-Admin-Token`; CORS restricted; refuses to boot in prod on dev secrets |
18
-
| Console |`console/`| Vite + React + TS + Tailwind v4; six pages, light/dark, keyboard nav; the playground drives the same signed, mandated path an external agent takes |
19
-
| Evidence |`bazaar/simulator`, `bazaar/conformance`, `results/`| 200 tasks with expected outcomes, baseline comparison, false-positive cost sweep; 19-probe hand-written red team **plus** a 190-attack model-generated corpus scored end-to-end; 24-check conformance kit runnable against any live gateway (`python -m bazaar.conformance <url>`); replay CLI; a model-driven buyer; `RESULTS.md` generated, never hand-edited |
44
+
|**schemas**|`models.py`| Every money value is integer paise; the `bazaar.india` extension fields (GST, HSN, pincode serviceability, COD, pack units); all merchant/buyer free text is typed as untrusted data. One source of truth for the whole system. |
45
+
|**compiler**|`ingest · sanitize · normalize · enrich · exports · readiness · evaluate · heldout`| Parsers own price/stock/GST — the model may only name, categorise and enrich. `sanitize.py` strips instruction-like text at compile and flags it. Confidence < 0.8 → merchant review queue, never guessed. One compile emits Bazaar + UCP manifests, ACP feed, Beckn catalog, llms.txt, JSON-LD (`exports.py`) and an agent-readiness score (`readiness.py`). `evaluate.py`/`heldout.py` score field accuracy on the synthetic corpus and on hand-written held-out sheets. |
46
+
|**llm**|`base · fake · openai_client · anthropic_client · cache · resilience`| Single `complete_json(task, system, user, schema)` contract. `resilience.py` (circuit breaker) wraps `cache.py` (SQLite call cache) wraps a backend, delegating unknown attributes through. `fake.py` is deterministic and doubles as the model-down fallback; fallback answers are never cached. `openai_client.py` carries per-model token counters so cost is *measured*, not claimed. |
47
+
|**seller_agent**|`intent · offer_engine · propose · agent · tools · explain · rto · mcp_server`|`propose → verify → execute` enforced in code. `intent.py` is a conservative EN/HI/Hinglish parser (also the model-down fallback). `offer_engine.py` computes every rupee deterministically; offers apply only by merchant-approved `rule_id`; stacked discounts are floored at the subtotal. `agent.py` runs a bounded observe→re-propose loop (max 3 steps) so real-model tool wobble is corrected before the gate. `mcp_server.py` exposes one per-merchant MCP server over stdio. |
48
+
|**trust**|`registry · http_sig · keys · mandates · grants · policy · ledger · audit · fairness_auditor · uap`| The Trust Fabric (§4). Ed25519 registry with tiers T0–T3; RFC 9421 seven-step verify; AP2-shaped digest-chained mandates; scoped, single-use grants with a pending-reservation ledger; the policy gate; a hash-chained audit log with a Merkle root and replay; a fairness ledger + cohort auditor that gates rule publishing. `uap.py` is the seam where NPCI's Unified Agent Protocol binding lands. |
49
+
|**gateway**|`app · auth · discover · sessions · checkout · state · client · playground · catalog_mcp · adapters/{acp,ucp,beckn}`| Discover (deterministic), an ACP-shaped session state machine, checkout → Razorpay link → webhook (real payload shapes accepted). Adapters `/acp``/ucp``/beckn` and a global `bazaar-catalog` MCP at `/mcp`. Merchant-mutating routes gated by `X-Admin-Token`; `BAZAAR_ENV=prod` refuses to boot on dev secrets. |
50
+
|**razorpay_client**|`base · fake · real · reserve_pay`| One narrow `PaymentsClient` is the only money path. `fake.py` is a full in-memory sandbox (orders, UPI links, refunds, HMAC webhooks); `real.py` runs on Razorpay test-mode keys with a standard-link fallback when a fresh account has UPI links disabled; `reserve_pay.py` is the blocked-funds mandate ledger (NPCI OC-228 defaults ₹10,000 / 90 days). |
51
+
|**simulator / synthetic / conformance**|`run · tasks · buyer_agent · model_buyer · redteam · redteam_gen` · `corpus` · `checks`| The evidence pipeline (§8). Generates the 52-merchant corpus and the 200-task suite, runs a scripted buyer and a real tool-calling model buyer, two red teams, the fairness audit, and the 24-check conformance kit — all into `results/`, generated, never hand-edited. |
52
+
|**console**|`App · api · store · pages/{Overview,Catalog,Offers,Sessions,Audit,Playground}`| Vite + React + TS + Tailwind v4. Six pages, light/dark, keyboard nav. The playground drives the *same* signed, mandated path an external agent takes; a "Model down" toggle exercises the circuit breaker live; the compiler falls back to a token-free preview so a judge can try it without credentials. |
53
+
54
+
---
55
+
56
+
## 4. The Trust Fabric in detail
57
+
58
+
This is the product. Everything financial is deterministic and auditable.
59
+
60
+
-**Identity** (`http_sig.py`, `keys.py`, `registry.py`) — every buyer request is an RFC 9421 HTTP Message Signature over Ed25519. `verify_request` runs seven named steps in order (headers → key → timestamp → nonce → tag → base → signature) and fails closed on malformed input. The registry issues `keyid`s and tiers T0–T3; re-registering an existing key is idempotent (no tier reset), and an admin `revoke` cuts a compromised key off immediately — a revoked key's public key is withheld, so its next signature fails at the `key` step.
61
+
-**Mandates** (`mandates.py`) — AP2-shaped Checkout and Payment mandates, `open → closed`, signed over a canonical digest by the buyer key. The gate checks signature, stage, expiry, merchant, quote binding and amount equality.
62
+
-**Grants** (`grants.py`) — merchant-scoped, amount-capped, time-boxed, revocable, single-use, every use evented (`grant.used` / `grant.revoked`). A `pending` reservation is taken at checkout and converted to `used` on capture, so the TOCTOU window between checkout and capture cannot be double-spent. A grant cannot be issued above the agent's own tier ceiling.
63
+
-**The policy gate** (`policy.py::check_checkout`) — a sequence of named, machine-readable checks, kill-switch first, each emitting a `Check(name, passed, detail)` onto the audit trail. The exact count depends on the cart: signature, tier, grant scope, both mandates, stock, pincode, per-order caps, kill switch, COD/RTO, `total ≥ ₹1`, and a human-present threshold (default ₹15,000, per the RBI e-mandate framing). Any failure is a graceful decline with a reason — never a partial financial action.
64
+
-**Fairness** (`ledger.py`, `fairness_auditor.py`) — every applied offer logs `(rule_id, version, segment_predicate, inputs_hash, output)`. The auditor replays cohorts that differ only in irrelevant attributes and blocks a rule set that produces different outputs; publishing a rule set that fails the audit is refused.
65
+
-**Audit** (`audit.py`) — append-only JSONL, SHA-256 hash chain, Merkle root, `replay` endpoint and CLI. Chain-field keys are reserved so a caller cannot corrupt the chain by passing `seq`/`prev`.
66
+
67
+
## 5. Input authenticity — the boundary the gate cannot see
68
+
69
+
A signature proves *who* sent a request; it does not prove that what they *claimed about the buyer* is true. Bazaar derives the facts it can, and documents the ones it cannot yet:
70
+
71
+
-**Pricing segment is derived, not declared** (`app.py::server_segment`) — `b2b` requires a merchant/admin designation; `new` vs `returning` follows the agent's completed-order history; `any` grants nothing extra. Only the merchant's own admin-authenticated console may set a segment directly.
72
+
-**Grant amount is bounded by tier**, not by the caller's ask.
73
+
-**Session ownership** — a signed session may only be driven or cancelled by its owning key; an unsigned (T0) session, which has no owner, may only be cancelled by an admin.
74
+
-**Documented demo shortcuts** (see THREAT_MODEL §"What this does not cover") — the buyer key that signs mandates is agent-asserted until a Login-with-Razorpay / UPI binding lands, and `human_present` above the threshold is a self-asserted flag until it is a buyer-signed or AFA token. These are named honestly rather than hidden, and the mandate layer is isolated (`trust/`) so the production binding is an adapter, not a rewrite.
75
+
76
+
## 6. Where a model runs — and where it is forbidden
Money math, ranking and the gate are pure Python. The model only ever gains new kinds of *proposals*, never new authority — the same guarantee on HTTP and over MCP, because the MCP side-effect tools run the same gate.
88
+
89
+
## 7. Failure handling
90
+
91
+
Designed and tested, not an apology. `llm/resilience.py` skips the primary after 3 consecutive failures and answers from the offline backend (quotes, serviceability, checkout keep working; negotiation degrades to the best pre-approved rule; every failover is audited). Payment failure keeps the session retryable on the same link; a stock race re-quotes; a duplicate `complete` returns the original result via the caller-scoped idempotency cache; a forged webhook fails HMAC; the kill switch refuses new actions instantly. `POST /bazaar/v1/dev/chaos {"model_down": true}` (or the console toggle) demonstrates all of it live.
92
+
93
+
## 8. State & the single swap point
94
+
95
+
All state lives behind the narrow methods of `gateway/state.py::BazaarState` — sessions, grants, nonces, reservations, idempotency, registry, audit. It is in-memory for P0; that one file is the swap point for Postgres/Redis in Phase 1, and no caller reaches around it. This is why "distributed state" is a config change, not a rewrite.
96
+
97
+
## 9. Evidence pipeline
98
+
99
+
`python -m bazaar.simulator.run` regenerates `results/` and nothing else writes it. It runs the 200-task suite on the offline engine and on live gpt-4o, the false-positive sweep (three tighter caps), the 19-probe hand-written red team, the 190-attack model-generated red team, the fairness audit and the conformance kit. `results/RESULTS.md` and `results/gpt4o/RESULTS.md` carry a Provenance section (cache hit/miss, model-failover count, measured ₹/order). A CI test (`tests/test_results_consistency.py`) fails the build if any headline number in the README drifts from the generated JSON.
100
+
101
+
---
20
102
21
103
## Measured (both committed, both generated)
22
104
@@ -25,13 +107,15 @@ The diagrams live in the [README](README.md) (system, order sequence, compile pi
25
107
| 200 buyer tasks | 100% accuracy | 99.0% — both misses were impossible tasks it still refused |
26
108
| wrong orders / wrong declines | 0 / 0 | 0 / 0 |
27
109
| red team · fairness · conformance | 19/19 hand-written + 190/190 generated · 159,840 cohorts clean · 24/24 | same |
110
+
| model cost per completed order | — (no model) |**₹3.37** on gpt-4o (cold-cache probe, `results/gpt4o_costprobe/`) |
28
111
| latency p50 / p95 | 47 / 62 ms (deterministic) | cache hit ≈ offline; a live gpt-4o proposal adds ~1.5–4 s |
29
112
30
-
96 tests, fully offline, green in CI.
113
+
96 tests, fully offline, green in CI. Lint clean (`ruff`), console typechecks (`tsc`) and builds (`vite`).
31
114
32
115
## Honest limitations
33
116
34
-
- The synthetic corpus is a closed loop (the generator writes both the messy CSVs and the truth labels), so offline compiler accuracy is the parsers' ceiling; the live-model row reports the real exact-match numbers.
117
+
- The synthetic corpus is a closed loop (the generator writes both the messy CSVs and the truth labels), so offline compiler accuracy is the parsers' ceiling; the live-model row reports the real exact-match numbers, and three hand-written held-out sheets test the parsers on data nobody tuned for.
35
118
- Beckn `on_*` callbacks are returned inline for P0, not POSTed to the BAP; ONDC certification is a later phase.
36
119
- State is in-memory behind narrow methods (`gateway/state.py` is the single swap point for Postgres/Redis).
120
+
- Buyer-key binding and human-present authentication are demo shortcuts, named in the threat model; the injection sanitiser is regex-first and English-leaning (Phase-1 hardening: NFKC normalisation + a classifier pass).
37
121
- The Anthropic backend is wired but has not produced committed results.
0 commit comments