Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions AGENT_KIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# AGENT_KIT.md — bring your own agent

The counter is not reserved for our buyer. The same HTTP surface the
playground uses is the kit's contract: an outside agent — your harness, a
notebook, a curl loop — speaks plain JSON to one endpoint and clears the
counter like any customer. Every check it passes is visible, every span
lands in the hash-chained ledger, and the reference client in `make kit`
proves the whole walk with no in-repo state.

**Test mode only.** Live keys are refused at construction; the rail runs
Razorpay test mode where keys are set and a loudly-labeled simulation
otherwise. Nothing ever pretends to move real money.

---

## The one endpoint

```http
POST /api/chat
Content-Type: application/json

{ "message": "search earbuds" }
```

| Field | | |
|---|---|---|
| `sessionId` | optional | omit on the first call; the response hands one back. Cart, tier and mandate live server-side under it. |
| `message` | required | the agent's utterance — the protocol is below |
| `adapter` | optional | `naive` (default) · `mcp` · `acp` — the wire format of the turn |
| `tier` | optional | the tier a *new* session starts at: `UNVERIFIED` / `ATTESTED` / `MANDATED` |

The response carries `sessionId`, `cart`, `awaitingMandateApproval`, the
full `events[]` trace of the turn (every tool call, the gate's 10-check
card, the payment, the receipt) and parser-exact `suggestions[]`.

Machine-readable kit: **`GET /api/agent/kit`** — generated from the same
constants the product runs on (`TOOL_SCHEMAS`, `TRUST_TIERS`, rail, brain),
so it cannot drift from the code. This file is its human twin.

## The protocol (messages)

| message | effect |
|---|---|
| `search <query> [under <₹n>]` | catalog search → a `products` event |
| `add <productId> [×n]` | adds to the cart |
| `cart` | echoes the cart with the running total |
| `attest` | raises the trust tier (OTP-bound in production; asserted here) |
| `checkout` | drafts **and signs** a mandate for the cart, then waits |
| `approve` | releases the mandate → gate decides → rail captures |
| `status` | passport: tier, caps, cart, active mandate |
| `attack: <corpus-id>` | red-team: replays an authored attack against the live gate |

Escalation is the session's own act: `attest` walks UNVERIFIED → ATTESTED →
MANDATED (₹500 → ₹5,000 → ₹50,000 caps). Approval is the principal's:
`checkout` drafts an Ed25519-signed mandate over canonical JSON and holds;
`bind_and_pay` refuses without a signed mandate in bounds. **No approval,
no money** — the fuzz corpus pins that.

## Tools (the schemas the buyer agent runs)

The same five schemas serve all three transports (`naive` / `mcp` / `acp`
— protocol-shaped; see `src/lib/customs/adapters/index.ts` and the
ablation for the measured overheads):

- `search_catalog` — `{ query }`
- `get_product` — `{ productId }`
- `add_to_cart` — `{ productId, quantity }`
- `request_mandate` — `{}` (drafts + signs, waits for the principal)
- `bind_and_pay` — `{}` (binds against the mandate and pays)

## The golden path, in curl

```bash
BASE=http://localhost:3000 # or the live deployment

# 1 — open a session and search
curl -s $BASE/api/chat -H 'content-type: application/json' \
-d '{"message":"search earbuds"}'

# 2 — add the first match (copy its id from the products event)
curl -s $BASE/api/chat -H 'content-type: application/json' \
-d '{"sessionId":"ses_…","message":"add bud-pro-earbuds"}'

# 3 — raise the tier (₹500 walk-in cap will not cover ₹4,999)
curl -s $BASE/api/chat -H 'content-type: application/json' \
-d '{"sessionId":"ses_…","message":"attest"}'

# 4 — checkout: mandate comes back signed, pendingApproval: true
curl -s $BASE/api/chat -H 'content-type: application/json' \
-d '{"sessionId":"ses_…","message":"checkout"}'

# 5 — approve: gate checklist ticks, rail captures, receipt lands
curl -s $BASE/api/chat -H 'content-type: application/json' \
-d '{"sessionId":"ses_…","message":"approve"}'

# the ledger remembers — and still verifies
curl -s $BASE/api/health
```

## The proof

```bash
make kit # bun scripts/agent-kit-demo.ts $BASE_URL
```

The reference client imports nothing from `src/` — it is exactly what an
outside agent is. It walks search → add → attest → checkout → approve,
asserts every verdict (gate ALLOW, payment captured, receipt issued, chain
still verifying) and exits non-zero on any deviation. Its output is the
shortest demonstration that the counter is not ours alone.

## Honesty notes

- The buyer agent is still in-house; this kit publishes its surface so
*yours* can drive it. Real MCP-stdio / ACP wire transports are an
interface change, pre-logged in `ARCHITECTURE.md` and `ENGINEERING_LOG.md`.
- The gate is deterministic code — your agent's words become tool calls;
bounds are re-verified server-side at bind time regardless of what the
agent believes it was promised.
- Integer paise end to end. Floats never touch money; the refusal itself
is a fuzz case.
5 changes: 5 additions & 0 deletions JUDGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ regenerated, never promised.
`ok:true` with `rail: razorpay-test, simulated:false` (real test-mode rails),
chain verified, deterministic seed, ephemeral state honestly labeled.
CI keeps the URL in this file on every push.
- The agent kit: the counter's HTTP surface is published for outside agents —
`AGENT_KIT.md` (the contract) · `GET /api/agent/kit` (machine twin,
generated from the running constants) · `make kit` (the proof: a reference
client with no in-repo state walks search → add → attest → checkout →
approve over pure HTTP and asserts every verdict).
- D1-1 payment mechanism: **executed with test keys on 2026-09-01**
(`results/d1_1_spike.json`) — Orders API verified live; server-side
tokenization refused by test mode (path A impossible, receipt in the log);
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help verify triage demo meter ablation fuzz project audit spike-d1-1 test all install
.PHONY: help verify triage demo meter ablation fuzz project audit spike-d1-1 kit test all install

BUN := $(shell command -v bun 2>/dev/null)
RUNNER := $(if $(BUN),bun,npx tsx)
Expand All @@ -16,6 +16,7 @@ help:
@echo " all every harness, then verify"
@echo " demo how to run the product locally"
@echo " spike-d1-1 payment-mechanism spike (needs Razorpay test keys)"
@echo " kit an external agent walks the golden path over pure HTTP"

verify: ## evidence checks — the same ones CI runs on every push
node scripts/verify.mjs
Expand All @@ -26,6 +27,9 @@ triage: ## 60-second self-guided judge tour (prints claims, runs checks, exits 0
spike-d1-1: ## payment-mechanism spike (needs RAZORPAY_KEY_ID + RAZORPAY_KEY_SECRET, test keys only)
node scripts/spike-d1-1.mjs

kit: ## the agent kit walkthrough — a client with no in-repo state clears the counter
bun scripts/agent-kit-demo.ts $${BASE_URL:-http://localhost:3000}

fuzz: ## regenerate results/conformance_matrix.json
$(RUNNER) scripts/fuzz.ts

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ the **Why it exists** view and `PAPER.md` §7.
| `VIDEO_TRANSCRIPT.md` | the 5:00 pitch script (recorded at submission) |
| `CLEANUP.md` | operator runbook: what to delete before pushing to GitHub |
| `DEPLOY.md` | operator runbook: run locally, keep it alive, deploy free, operate |
| `Makefile` | verify / triage / fuzz / ablation / meter / project / audit / test |
| `AGENT_KIT.md` | bring your own agent — the counter's HTTP contract + the `make kit` proof |
| `Makefile` | verify / triage / fuzz / ablation / meter / project / audit / kit / test |
| `scripts/verify.mjs` | repo-evidence checks (CI entry, zero deps) |
| `scripts/triage.mjs` | 60-second self-guided judge tour |
| `scripts/fuzz.ts` | attack corpus harness → `results/conformance_matrix.json` |
Expand All @@ -151,6 +152,7 @@ the **Why it exists** view and `PAPER.md` §7.
| `scripts/audit.ts` | hash-chain walk + tamper control → `results/audit_chain.json` |
| `scripts/ledger-fork.ts` | D5-1 regression: concurrent writers must converge, never fork |
| `scripts/spike-d1-1.mjs` | payment-mechanism spike (needs Razorpay test keys) |
| `scripts/agent-kit-demo.ts` | the agent kit reference client — an outside agent clears the counter over HTTP |
| `results/` | all measured numbers — JSON only, regeneration-only |
| `src/lib/customs/gate/types.ts` | mandate schema + trust-tier policy (the contract) |
| `src/lib/customs/gate/canonical.ts` | canonical JSON + lenient chain stringify |
Expand Down Expand Up @@ -183,7 +185,7 @@ the **Why it exists** view and `PAPER.md` §7.
| `src/components/customs/chat-events.tsx` | the transcript — tool calls, gate checklist, receipts |
| `src/components/customs/theme.tsx` | the desk lamp — footer dark/light toggle, persisted, no-flash |
| `src/components/customs/footer.tsx` | the footer — mark, quiet link columns, the theme toggle (x.ai pattern) |
| `src/app/api/` | route handlers: chat, state, decision, fuzz, webhook, health |
| `src/app/api/` | route handlers: chat, state, decision, fuzz, webhook, health, agent/kit |
| `public/logo.svg` | the gate diamond (badge tile) |
| `public/wordmark-light.svg` | the wordmark, light surfaces (this README) |
| `public/wordmark-dark.svg` | the wordmark, dark surfaces (this README) |
Expand Down
2 changes: 2 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Test mode only; without Razorpay keys the rail is a labeled simulation.
- `make verify` — the exact checks CI runs on every push (zero deps)
- `make test` — fuzz + ablation + audit + ledger-fork (concurrent-writer convergence), exit codes propagate
- `make demo` — run the product (no keys needed)
- `make kit` — an external agent walks the golden path over pure HTTP (the agent-kit proof)

## key files

Expand All @@ -31,6 +32,7 @@ Test mode only; without Razorpay keys the rail is a labeled simulation.
- ENGINEERING_LOG.md — dated incidents; every incident became a test
- CLEANUP.md — what to delete before pushing to GitHub
- DEPLOY.md — the operating runbook: run locally, keep alive, deploy free, operate
- AGENT_KIT.md — bring your own agent: the counter's HTTP contract; /api/agent/kit is its machine twin; `make kit` proves an outside client clears the counter
- src/lib/customs/gate/ — mandate contract, canonical JSON, decision checklist
- src/lib/customs/ledger/ — hash-chained JSONL audit trail (the database)
- src/lib/customs/fuzz/corpus.ts — the 12 authored attacks, expected verdicts
Expand Down
128 changes: 128 additions & 0 deletions scripts/agent-kit-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* agent-kit-demo.ts — the reference client: an agent kit walkthrough that
* is ALSO the interop proof. This script holds no in-repo state and imports
* nothing from src/ — it is exactly what an outside agent is: a process
* speaking plain JSON over HTTP. It walks the golden path (search → add →
* attest → checkout → approve → capture) against a running customs instance,
* asserting each verdict, and exits non-zero if the counter ever deviates.
*
* bun scripts/agent-kit-demo.ts [base-url] # default http://localhost:3000
* make kit # same thing, makefile voice
*
* Test mode only — live keys are refused at construction, and this client
* refuses to talk to anything that doesn't identify itself honestly.
*/

const BASE = (process.argv[2] ?? process.env.BASE_URL ?? "http://localhost:3000").replace(/\/$/, "");

interface ChatEvent {
id: string;
ts: number;
role?: string;
text?: string;
kind?: string;
tool?: string;
products?: { id: string; name: string; pricePaise: number }[];
lines?: { name: string; quantity: number; unitPricePaise: number }[];
totalPaise?: number;
mandate?: { id: string; amountCapPaise: number };
pendingApproval?: boolean;
orderId?: string;
decision?: { kind: string; code: string | null; checks: { label: string; pass: boolean | null }[] };
status?: string;
rail?: string;
simulated?: boolean;
manifestNo?: string;
tier?: string;
}

interface ChatResponse {
ok: boolean;
sessionId: string;
tier: string;
awaitingMandateApproval: boolean;
events: ChatEvent[];
error?: string;
}

const ofKind = (events: ChatEvent[], kind: string) => events.filter((e) => e.kind === kind);

let failures = 0;
function expect(label: string, cond: boolean, detail = "") {
const line = `${cond ? "PASS" : "FAIL"} ${label}${detail ? " — " + detail : ""}`;
console.log(line);
if (!cond) failures += 1;
}

async function chat(sessionId: string | null, message: string): Promise<ChatResponse> {
const res = await fetch(`${BASE}/api/chat`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...(sessionId ? { sessionId } : {}), message }),
});
if (!res.ok) throw new Error(`/api/chat → HTTP ${res.status}`);
return (await res.json()) as ChatResponse;
}

async function main() {
console.log(`── customs · agent kit — an external agent clears the counter ──`);
console.log(`base url: ${BASE}\n`);

// 0 — the desk must identify itself, and its chain must verify
const health = (await (await fetch(`${BASE}/api/health`)).json()) as {
ok: boolean;
chainOk: boolean;
rail: { id: string; simulated: boolean };
brain: string;
};
expect("health answers ok:true", health.ok === true);
expect("ledger chain verifies", health.chainOk === true, `rail=${health.rail?.id} brain=${health.brain}`);

// 1 — search the catalog
let r = await chat(null, "search earbuds");
const sid = r.sessionId;
const products = ofKind(r.events, "products")[0]?.products ?? [];
expect("search returns matches", products.length > 0, `${products.length} matches · session ${sid}`);
const pick = products.find((p) => p.id === "bud-pro-earbuds") ?? products[0];
console.log(` picked: ${pick.id} — ₹${(pick.pricePaise / 100).toLocaleString("en-IN")}\n`);

// 2 — build the cart
r = await chat(sid, `add ${pick.id}`);
const cart = ofKind(r.events, "cart")[0];
expect("cart holds the line", (cart?.lines?.length ?? 0) === 1, `total ₹${((cart?.totalPaise ?? 0) / 100).toLocaleString("en-IN")}`);

// 3 — raise the tier so the cap covers the cart
r = await chat(sid, "attest");
const tiered = ofKind(r.events, "tier").length > 0 || r.tier === "ATTESTED";
expect("tier raised to ATTESTED", tiered, r.tier);

// 4 — checkout: the desk drafts and signs a mandate, then waits
r = await chat(sid, "checkout");
const mandate = ofKind(r.events, "mandate")[0];
expect("mandate drafted and pending approval", r.awaitingMandateApproval === true && !!mandate, mandate?.mandate?.id);

// 5 — approve: the gate decides, the rail captures
r = await chat(sid, "approve");
const gate = ofKind(r.events, "gate")[0];
const payment = ofKind(r.events, "payment")[0];
const receipt = ofKind(r.events, "receipt")[0];
expect("gate decision is ALLOW", gate?.decision?.kind === "ALLOW", `${gate?.decision?.checks?.filter((c) => c.pass).length ?? 0}/10 checks pass`);
expect("payment captured", payment?.status === "captured", `rail=${payment?.rail}${payment?.simulated ? " (simulated)" : ""}`);
expect("receipt issued", !!receipt?.manifestNo, `${receipt?.manifestNo} · order ${receipt?.orderId}`);

// 6 — the chain still verifies after our order landed in it
const after = (await (await fetch(`${BASE}/api/health`)).json()) as { chainOk: boolean; events: number };
expect("ledger chain verifies after capture", after.chainOk === true, `${after.events} events on the chain`);

console.log(
`\n── the outside agent paid, bounded and gated · manifest ${receipt?.manifestNo ?? "?"} · ` +
`${failures === 0 ? "all checks green" : failures + " CHECKS FAILED"} ──`
);
if (failures) process.exit(1);
}

main().catch((err) => {
console.error(`agent kit walkthrough failed: ${err instanceof Error ? err.message : err}`);
console.error(`is the product running at ${BASE}? (bun run dev)`);
process.exit(1);
});
2 changes: 2 additions & 0 deletions scripts/verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ const REQUIRED = [
"scripts/verify.mjs", "scripts/triage.mjs", "scripts/spike-d1-1.mjs",
"scripts/fuzz.ts", "scripts/ablation.ts", "scripts/meter.ts",
"scripts/project.ts", "scripts/audit.ts", "scripts/ledger-fork.ts",
"scripts/agent-kit-demo.ts", "AGENT_KIT.md",
"src/app/page.tsx", "src/app/layout.tsx", "src/app/icon.svg",
"src/app/api/chat/route.ts", "src/app/api/state/route.ts",
"src/app/api/agent/kit/route.ts",
"src/app/api/decision/route.ts", "src/app/api/fuzz/route.ts",
"src/app/api/hook/webhook/route.ts", "src/app/api/health/route.ts",
"src/lib/customs/gate/types.ts", "src/lib/customs/gate/canonical.ts",
Expand Down
Loading
Loading