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
40 changes: 40 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# `docs/` — project documentation

> Everything that is not source code, organised so judges, contributors, and future-you can find what they need in one click.

| File | Audience | What it covers |
|---|---|---|
| [`submission.md`](./submission.md) | ETHGlobal judges | One-pager: track, sponsors used, on-chain proof table, decision-engine summary, run instructions, test coverage |
| [`demo-script.md`](./demo-script.md) | Recording team | Verbatim 7-beat walkthrough for the demo video |
| [`architecture.md`](./architecture.md) | Maintainers + reviewers | System design, 8-step decision flow, post-hackathon Rust + Solidity roadmap |
| [`deploy.md`](./deploy.md) | Anyone hosting it | $0 deploy walkthrough on Render + Cloudflare Pages, including CORS + keepalive config |
| [`product-idea.md`](./product-idea.md) | Future-you | Original product framing notes from the planning session |
| [`sponsors/`](./sponsors) | Maintainers + sponsors | Per-sponsor research notes — see [`sponsors/README.md`](./sponsors/README.md) |

## Reading order

```mermaid
flowchart LR
A[../README.md<br/>quick orientation] --> B[submission.md<br/>judge one-pager]
B --> C[architecture.md<br/>system design]
C --> D[sponsors/<br/>sponsor research]
D --> E[demo-script.md<br/>recording walkthrough]
E --> F[deploy.md<br/>$0 hosting]
```

## Conventions

- All docs are plain Markdown with optional Mermaid blocks.
- Every doc that references a file uses a relative link so GitHub renders them and offline editors can follow them.
- No emojis anywhere — the same project-wide rule the source code follows. CI scans these files too.
- Live on-chain proof (rootHash, storage tx, block, gas) is duplicated in [`../README.md`](../README.md) and [`submission.md`](./submission.md) so judges can verify before clicking through.

## Pointers

| | |
|---|---|
| Project root | [`../README.md`](../README.md) |
| Source | [`../src/`](../src) |
| Tests | [`../tests/`](../tests) |
| Frontend | [`../web/`](../web) |
| Conventions for AI agents | [`../AGENTS.md`](../AGENTS.md) |
35 changes: 35 additions & 0 deletions docs/sponsors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# `docs/sponsors/` — sponsor research notes

> Per-sponsor research and integration notes captured during planning. Each file is the working spec the adapter was built against, kept after the build so the integration story is reproducible.

| File | Sponsor | What it covers | Adapter |
|---|---|---|---|
| [`0g.md`](./0g.md) | 0G Labs | Storage SDK shape, Galileo testnet config, Indexer.upload semantics, faucet path, mainnet vs testnet contracts | [`../../src/memory/zeroGStore.ts`](../../src/memory/zeroGStore.ts) |
| [`keeperhub.md`](./keeperhub.md) | KeeperHub | API key types, workflow execute endpoint, response shape variance, auth patterns | [`../../src/playbooks/keeperhub.ts`](../../src/playbooks/keeperhub.ts) |
| [`gensyn-axl.md`](./gensyn-axl.md) | Gensyn | AXL local HTTP bridge model, MCP / A2A support, NAT-friendly mesh, bootstrap-node requirements | [`../../src/transport/axlGossip.ts`](../../src/transport/axlGossip.ts) |

## Prize matrix

| Sponsor | Prize pool | Status |
|---|---|---|
| 0G | `$15,000` | Adapter shipped, live anchor verified on Galileo testnet |
| KeeperHub | `$5,000` | Adapter shipped, real workflow fired against live API |
| Gensyn AXL | `$5,000` | Adapter shipped (decision gossip transport), MCP-style publish endpoint |
| **Total addressable** | **`$25,000`** | All three adapters real, tested, env-gated, soft-failure |

## How to add a new sponsor

1. Add a research note `<sponsor>.md` in this folder — what it does, what its API looks like, what shape it expects.
2. Pick the right trait seam: `Store`, `Simulator`, `PlaybookRunner`, `NotificationChannel`, or `GossipTransport`.
3. Implement the real adapter in `src/<area>/`, mirroring an existing one (`KeeperHubRunner` for HTTP-based, `ZeroGStore` for SDK-based).
4. Wire it into [`../../src/risk-gate/server.ts`](../../src/risk-gate/server.ts), env-gated so a missing key falls back to the in-memory or no-op impl.
5. Add tests under `../../tests/<adapter>.test.ts` — happy path + soft-failure + adversarial bodies.
6. Update [`../../README.md`](../../README.md) sponsor section + this index.

## Pointers

| | |
|---|---|
| Parent | [`../README.md`](../README.md) |
| Project root | [`../../README.md`](../../README.md) |
| Wiring conventions | [`../../.claude/skills/sponsor-wiring/SKILL.md`](../../.claude/skills/sponsor-wiring/SKILL.md) |
35 changes: 35 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# `scripts/` — operator helpers

> Small bash scripts used during development. Both honour `.env.local` so credentials never live on the command line.

| File | What it does |
|---|---|
| [`dev.sh`](./dev.sh) | Boots the API on `:8787` (Bun watch mode) and the Astro frontend on `:4321` in parallel. Kills both on Ctrl-C. Wired to `bun run dev` |
| [`kh.sh`](./kh.sh) | Bash wrapper for the KeeperHub REST API — reads `KEEPERHUB_API_KEY` and `KEEPERHUB_API_URL` from `.env.local`. Subcommands: `list`, `get <id>`, `run <id>`, `status <runId>`, `ping` |

## `kh.sh` examples

```sh
./scripts/kh.sh ping # auth check
./scripts/kh.sh list # workflows in the org
./scripts/kh.sh get 8c12ujo1ax7b93w21updd
./scripts/kh.sh run 8c12ujo1ax7b93w21updd '{"owner":"0xtreasury"}'
./scripts/kh.sh status <runId>
```

The wrapper exists so workflow ids can be discovered and exercised without rerunning the full server, and so the org-key vs user-key distinction is centralised in one place rather than repeated in shell history.

## Conventions

- Every script reads `.env.local` if it exists; never echoes secrets.
- Failures `exit 1` with a one-line reason; success is silent or prints structured output.
- No `set -e` shortcuts that silently mask real failures — explicit checks for empty env vars and HTTP non-2xx.

## Pointers

| | |
|---|---|
| Parent | [`../README.md`](../README.md) |
| KeeperHub adapter | [`../src/playbooks/keeperhub.ts`](../src/playbooks/keeperhub.ts) |
| Sponsor research | [`../docs/sponsors/keeperhub.md`](../docs/sponsors/keeperhub.md) |
| Env template | [`../.env.example`](../.env.example) |
46 changes: 46 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# `src/` — ChainShield server

> Fastify HTTP API that evaluates transaction intents against a deterministic policy engine, anchors every decision on 0G Storage, fires KeeperHub remediation on `BLOCK`, and gossips the verdict over the Gensyn AXL mesh.

The whole server is plain TypeScript on Bun. Five trait-shaped seams (`Store`, `Simulator`, `PlaybookRunner`, `NotificationChannel`, `GossipTransport`) keep every external dependency replaceable — tests run against in-memory fakes; production wires real adapters from env.

| Folder | What it is |
|---|---|
| [`core/`](./core) | Types, Zod schemas, `PolicyService`, `DecisionEngine`, EVM selector helpers |
| [`memory/`](./memory) | `Store` interface, `InMemoryStore`, `ZeroGStore` (0G anchor adapter) |
| [`simulator/`](./simulator) | `Simulator` interface + `HeuristicSimulator` (ERC-20 calldata decode + balance projection) |
| [`playbooks/`](./playbooks) | `PlaybookRunner` interface, `KeeperHubRunner`, `WebhookChannel`, `CollectorChannel` |
| [`transport/`](./transport) | `GossipTransport` interface, `AxlGossipTransport`, `NoopGossip` |
| [`risk-gate/`](./risk-gate) | Fastify `app.ts` + `server.ts` composition root |
| [`cli/`](./cli) | `demo.ts` — four canonical scenes against the live API |

## Request flow

```mermaid
flowchart LR
Caller[Wallet / treasury client] -->|POST /evaluate| App
App[risk-gate/app.ts<br/>Fastify route] --> Engine
Engine[core/engine.ts<br/>5-rule ladder] --> Sim[simulator/<br/>HeuristicSimulator]
Engine --> Store[memory/<br/>ZeroGStore]
Engine -->|BLOCK| Run[playbooks/<br/>KeeperHubRunner]
Engine -->|BLOCK| Gossip[transport/<br/>AxlGossipTransport]
Engine -->|BLOCK| Notify[playbooks/<br/>WebhookChannel]
```

## Hard rules

1. Verdict ladder is monotonic: `BLOCK > REQUIRE_HUMAN_CONFIRMATION > ALLOW`. A rule may only escalate, never de-escalate.
2. `forbiddenSelectors` is checked before any other rule and short-circuits to `BLOCK` with risk 95.
3. Defensive guards (`invalidIntentValue`, `invalidApprovalCap`) escalate to `REQUIRE_HUMAN_CONFIRMATION` (risk 70) instead of throwing.
4. Every decision is persisted via `Store.appendDecision` exactly once.
5. `reasons[]` is human-readable English; `rulesMatched[]` is machine-readable rule keys. Keep them in sync.
6. Sponsor adapters fail soft — a Galileo hiccup or KeeperHub outage never returns 5xx.

## Related

| | |
|---|---|
| Tests | [`../tests/`](../tests) — 109 specs across 13 files |
| Composition root | [`risk-gate/server.ts`](./risk-gate/server.ts) |
| Frontend | [`../web/`](../web) — Astro 6, separate Bun workspace |
| Conventions | [`../AGENTS.md`](../AGENTS.md) |
36 changes: 36 additions & 0 deletions src/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# `src/cli/` — demo runner

> A four-scene CLI that drives the live API end-to-end. Used for the recording and as a smoke test before submission.

| File | Role |
|---|---|
| [`demo.ts`](./demo.ts) | Boots a fresh policy via `POST /policies`, then runs four `POST /evaluate` calls covering every verdict path. Exits `0` if the verdict and reasons match the expected shape, `1` otherwise. |

## Scenes

| # | Intent | Expected verdict | Rule that fires |
|---|---|---|---|
| 1 | Small transfer to allowlisted vault | `ALLOW` | (all rules pass) |
| 2 | Transfer above the per-tx ETH cap | `BLOCK` | `maxTransferEth` |
| 3 | `approve(spender, MAX_UINT256)` | `BLOCK` | `forbiddenSelectors` (or `approvalCapByToken`) |
| 4 | Transfer to an off-allowlist destination | `REQUIRE_HUMAN_CONFIRMATION` | `allowedDestinations` |

## How to run

```sh
# 1. boot the API (in another terminal)
bun run dev

# 2. drive the four scenes
bun run demo
```

The demo uses `INTERNAL_API_BASE` if set, otherwise defaults to `http://127.0.0.1:8787`. Output prints the verdict, risk score, matched rules, and the `anchor.rootHash` if 0G is wired.

## Pointers

| | |
|---|---|
| Parent | [`../README.md`](../README.md) |
| API surface | [`../risk-gate/app.ts`](../risk-gate/app.ts) |
| Recording walkthrough | [`../../docs/demo-script.md`](../../docs/demo-script.md) |
61 changes: 61 additions & 0 deletions src/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# `src/core/` — types, schemas, engine

> The heart of ChainShield. Pure TypeScript with no I/O — every external dependency is injected as a trait so the engine stays unit-testable in microseconds.

| File | Role |
|---|---|
| [`types.ts`](./types.ts) | All exported types: `TxIntent`, `Policy`, `Decision`, `Verdict`, `SimulationResult`, `BalanceDelta`, `ApprovalDelta`, `PlaybookRun`, `LlmReasoning` |
| [`schemas.ts`](./schemas.ts) | Zod schemas at the API boundary — `policyInputSchema`, `evaluateRequestSchema`. Parsed once at the edge; plain TS types flow through internals |
| [`policyService.ts`](./policyService.ts) | Policy CRUD with version bumping. Zod-validates input, persists to `Store`, returns the canonical record |
| [`engine.ts`](./engine.ts) | `DecisionEngine` — the 5-rule ladder + simulator integration + remediation dispatch |
| [`selectors.ts`](./selectors.ts) | EVM 4-byte selector helpers (`selectorOf`, `decodeUint256`) + curated selector constants (`ERC20_APPROVE`, `ERC20_TRANSFER`, …) |

## Decision ladder

```mermaid
flowchart TD
A[Intent in] --> B{forbiddenSelectors?}
B -->|yes| Z[BLOCK risk 95]
B -->|no| C{maxTransferEth?}
C -->|over| Z
C -->|under| D{maxDailyOutflowEth?}
D -->|over| Z
D -->|under| E{allowedDestinations?}
E -->|off-list| F[REQUIRE_HUMAN_CONFIRMATION risk 60]
E -->|on-list| G{approvalCapByToken?}
G -->|over| Z
G -->|under| H{simulator revert?}
F --> H
H -->|revert| I[REQUIRE_HUMAN_CONFIRMATION risk 70]
H -->|success| J[ALLOW]
J --> K[persist + anchor]
I --> K
Z --> L[persist + anchor + remediation]
```

## Invariants (do not break)

1. The verdict ladder is **monotonic** — rules can only escalate.
2. `forbiddenSelectors` short-circuits before any other rule.
3. `maxTransferEth` and `approvalCapByToken` produce `BLOCK` with risk ≥ 90.
4. `maxDailyOutflowEth` reads the timeline; only non-blocked rows count toward the rolling sum.
5. `allowedDestinations` downgrades `ALLOW` to `REQUIRE_HUMAN_CONFIRMATION` (risk 60). It cannot promote to `BLOCK` on its own.
6. Defensive guards escalate to `REQUIRE_HUMAN_CONFIRMATION` instead of throwing.
7. `reasons[]` and `rulesMatched[]` stay in sync.

## Tests

| | |
|---|---|
| 5-rule ladder + guards | [`../../tests/engine.test.ts`](../../tests/engine.test.ts) |
| Simulator integration + revert escalation | [`../../tests/engineSimulation.test.ts`](../../tests/engineSimulation.test.ts) |
| Playbook + notifications + AXL gossip on `BLOCK` | [`../../tests/engineRemediation.test.ts`](../../tests/engineRemediation.test.ts) |
| Policy CRUD + version bumping | [`../../tests/policyService.test.ts`](../../tests/policyService.test.ts) |

## Pointers

| | |
|---|---|
| Parent | [`../README.md`](../README.md) |
| Wired by | [`../risk-gate/server.ts`](../risk-gate/server.ts) |
| Selector reference | [`../../.claude/skills/selector-decode/SKILL.md`](../../.claude/skills/selector-decode/SKILL.md) |
50 changes: 50 additions & 0 deletions src/memory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `src/memory/` — Store layer

> Pluggable persistence behind a single trait. Tests use the in-memory impl; production wires the 0G anchor adapter.

| File | Role |
|---|---|
| [`store.ts`](./store.ts) | The `Store` interface (`putPolicy`, `getPolicy`, `listPolicies`, `appendDecision`, `listDecisions`, optional `getAnchor`) and the `AnchorRecord` type |
| [`memoryStore.ts`](./memoryStore.ts) | `InMemoryStore` — pure in-process maps + arrays. Used in tests and as the fallback when `ZERO_G_PRIVATE_KEY` is unset |
| [`zeroGStore.ts`](./zeroGStore.ts) | `ZeroGStore` — uploads each policy and decision JSON to 0G Galileo via `@0gfoundation/0g-storage-ts-sdk`, caches the local copy, exposes anchor `(rootHash, txHash)` per id |

## Anchor flow

```mermaid
flowchart LR
PS[PolicyService.put] --> ZG[ZeroGStore.putPolicy]
ZG -->|in-memory write returns ~50ms| Caller
ZG -.->|background| Sched[scheduleAnchor]
Sched --> Try[tryAnchor]
Try --> SDK["Indexer.upload(MemData(json))"]
SDK --> Galileo[(0G Galileo<br/>storage indexer)]
Galileo -->|"&#123;rootHash, txHash&#125;"| Anchors[(anchors map)]
Anchors --> Get[getAnchor by id]
```

## Soft-failure contract

- `Indexer.upload` returns either an error-tuple `[result, Error|null]` or throws — `tryAnchor` handles both paths.
- Two response shapes: single `{ rootHash, txHash, txSeq }` and fragmented `{ rootHashes[], txHashes[], txSeqs[] }` (>4 GB). `tryAnchor` normalises both.
- Failure is logged (`logger.warn`) and the anchor map simply lacks an entry — the API still serves the response with `anchor: null`.
- Background uploads never block the API hot path; tests use the optional `waitForAnchor(id)` helper to settle pending uploads deterministically.

## Tests

| | |
|---|---|
| Anchor on write, soft-failure, empty-result handling | [`../../tests/zeroGStore.test.ts`](../../tests/zeroGStore.test.ts) |
| Anchor surfacing on policy + decision API responses (real `ZeroGStore` + `buildApp` e2e) | [`../../tests/apiAnchor.test.ts`](../../tests/apiAnchor.test.ts) |
| Multi-tenant decision isolation by client id | [`../../tests/clientIsolation.test.ts`](../../tests/clientIsolation.test.ts) |

## Live proof

A real policy was anchored on Galileo testnet — see the rootHash + storage tx table at the top of [`../../README.md`](../../README.md).

## Pointers

| | |
|---|---|
| Parent | [`../README.md`](../README.md) |
| Wired by | [`../risk-gate/server.ts`](../risk-gate/server.ts) — picks `ZeroGStore` when `ZERO_G_PRIVATE_KEY` is set |
| Sponsor research | [`../../docs/sponsors/0g.md`](../../docs/sponsors/0g.md) |
51 changes: 51 additions & 0 deletions src/playbooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# `src/playbooks/` — remediation + notifications

> When a verdict is `BLOCK`, ChainShield does more than say "no" — it fires an automated remediation playbook and pages every configured notification channel. Both layers are pluggable behind small interfaces.

| File | Role |
|---|---|
| [`runner.ts`](./runner.ts) | The `PlaybookRunner` and `NotificationChannel` interfaces, plus `MockRunner` for tests |
| [`keeperhub.ts`](./keeperhub.ts) | `KeeperHubRunner` — fires KeeperHub workflows via `POST /api/workflow/:id/execute` with bearer auth, scrubs HTML error pages |
| [`notifier.ts`](./notifier.ts) | `WebhookChannel` (Discord-shaped embed by default, custom shapes via `contentTemplate`) and `CollectorChannel` (test-only in-memory recorder) |

## Remediation flow

```mermaid
flowchart LR
Engine[DecisionEngine.handleRemediation] --> R{remediation.onBlock?}
R -->|empty| N
R -->|ids[]| RR[KeeperHubRunner.run id 0]
RR -->|ok| Decision[playbookTriggered set]
RR -->|throws| Next[try id 1, id 2, ...]
Next -->|all fail| Decision2[reasons append failure]
Decision --> N{notifyChannels?}
Decision2 --> N
N -->|empty| Done
N -->|name list| CH[for each registered channel<br/>WebhookChannel.notify]
CH --> Done[return decision]
```

## Hard rules

- A failing playbook **never** affects the verdict or persistence — failures are pushed into `decision.reasons` (truncated to 256 chars) and the engine continues.
- Notification failures are silently swallowed — paging cannot down-convert a `BLOCK` to anything else.
- `KeeperHubRunner` returns the first response field it finds in priority `runId → id → executionId` and falls back to `"unknown"` so the JSON shape stays stable.
- HTML error bodies (auth-failure pages, 404s) are detected by `summarizeErrorBody` and replaced with `(html error page)` before they reach `decision.reasons` or the UI.

## Tests

| | |
|---|---|
| `MockRunner` invocation tracking + forced failures | [`../../tests/playbooks.test.ts`](../../tests/playbooks.test.ts) |
| `KeeperHubRunner` happy path, response-shape fallback, HTML scrub, body truncation, URL encoding | same file |
| `WebhookChannel` Discord embed shape + custom template + non-2xx error | same file |
| Engine integration: trigger on `BLOCK`, fall-through on failure, notification fan-out | [`../../tests/engineRemediation.test.ts`](../../tests/engineRemediation.test.ts) |

## Pointers

| | |
|---|---|
| Parent | [`../README.md`](../README.md) |
| Wired by | [`../risk-gate/server.ts`](../risk-gate/server.ts) — picks `KeeperHubRunner` when `KEEPERHUB_API_KEY` is set |
| Helper script | [`../../scripts/kh.sh`](../../scripts/kh.sh) — `list / get / run / status / ping` against the live KeeperHub API |
| Sponsor research | [`../../docs/sponsors/keeperhub.md`](../../docs/sponsors/keeperhub.md) |
Loading