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
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: ci

on:
push:
branches: [develop, main]
pull_request:

jobs:
contracts:
runs-on: ubuntu-latest
defaults:
run:
working-directory: contracts
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: foundry-rs/foundry-toolchain@v1
- run: forge test -vv

api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: services/api/go.mod
cache-dependency-path: |
services/api/go.sum
packages/plan-schema/go.sum
- name: plan-schema tests
working-directory: packages/plan-schema
run: go test ./...
- name: api tests
working-directory: services/api
run: go test ./...
- name: evals
working-directory: services/api
run: go run ./cmd/evals -dir ../../evals/cases

mobile:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/mobile
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.38.6"
channel: stable
cache: true
- run: flutter pub get
- run: flutter analyze --no-fatal-infos
- run: flutter test
93 changes: 89 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,92 @@
IntentGuard turns a natural-language DeFi intent into a schema-validated multi-step plan, requires per-step human approval, then executes against Foundry mocks on Anvil.
# IntentGuard

Layout: `apps/mobile` (Flutter), `services/api` (Go), `contracts` (Foundry), `packages/plan-schema`, `evals`, `deploy`, `docs`.
Natural-language DeFi intent → schema-validated multi-step plan → per-step human approval → execute against Foundry mocks on Anvil. The LLM (or mock planner) is untrusted; nothing hits the chain without schema, policy, and a human approve.

Planner defaults to `PLANNER_MODE=mock` (no API key). Set `PLANNER_MODE=llm` plus `LLM_API_KEY` for an OpenAI-compatible provider — see `services/api/README.md`.
## Layout

See `services/api/README.md` and `contracts/README.md` to run locally.
```text
apps/mobile/ Flutter client (Bloc, Dio, go_router)
services/api/ Go API (auth, intents, planner, policy, executor)
packages/plan-schema/ Plan JSON Schema v1 + Go validate
contracts/ MockERC20 + MockSwapRouter (Foundry)
evals/ Accept/reject fixtures
deploy/ Docker Compose (API + Postgres + Anvil)
docs/ Architecture, threat model, demo script
```

## Architecture

```mermaid
flowchart LR
user[User]
flutter[FlutterApp]
api[API]
planner[Planner_mock_or_llm]
pg[(Postgres)]
anvil[Anvil]
mocks[MockContracts]

user --> flutter
flutter -->|JWT| api
api --> pg
api --> planner
api --> anvil
anvil --> mocks
```

Details: [`docs/architecture.md`](docs/architecture.md). Threat notes: [`docs/threat-model.md`](docs/threat-model.md).

## Run locally

```bash
cd deploy && docker compose up --build
```

In another shell (Foundry on the host):

```bash
./scripts/deploy-anvil.sh
./scripts/seed-anvil.sh
```

API `:8080`, Anvil `:8545`. Full curl path: [`docs/demo-script.md`](docs/demo-script.md).

Mobile:

```bash
cd apps/mobile
flutter run --dart-define=API_BASE=http://127.0.0.1:8080
```

## Env (API)

| Var | Default / notes |
|-----|-----------------|
| `DATABASE_URL` | required |
| `JWT_SECRET` | required |
| `PLANNER_MODE` | `mock` (CI/compose); `llm` needs `LLM_API_KEY` |
| `LLM_BASE_URL` | `https://api.openai.com/v1` |
| `LLM_MODEL` | `gpt-4o-mini` |
| `CHAIN_RPC_URL` | `http://127.0.0.1:8545` |
| `EXECUTOR_PRIVATE_KEY` | Anvil account #0 — local demo only |
| `DEPLOYMENTS_PATH` | `contracts/deployments/anvil.json` |

See [`services/api/README.md`](services/api/README.md).

## Tests / CI

```bash
cd contracts && forge test
cd services/api && go test ./...
cd services/api && go run ./cmd/evals -dir ../../evals/cases
cd apps/mobile && flutter test
```

GitHub Actions runs the same gates on PRs (`.github/workflows/ci.yml`).

## Design notes

- Plan actions v1: `approve`, `swap`, `transfer` only.
- Policy: spend cap, token/spender/recipient allowlists, max steps, slippage bound.
- Executor ABI-encodes stored step JSON — never raw model hex.
- Mock planner fixtures cover accept + schema/policy reject paths for demos and evals.
76 changes: 76 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Architecture

IntentGuard turns a natural-language intent into a schema-validated multi-step plan, requires per-step human approval, then executes against Foundry mocks on Anvil.

## Context

```mermaid
flowchart LR
user[User]
flutter[FlutterApp]
api[API]
llm[LLMProvider]
pg[(Postgres)]
anvil[AnvilRPC]
contracts[MockContracts]

user --> flutter
flutter -->|HTTPS_JWT| api
api --> pg
api -->|plan_JSON| llm
api -->|eth_call_eth_send| anvil
anvil --> contracts
```

## Plan lifecycle

```mermaid
stateDiagram-v2
[*] --> IntentReceived
IntentReceived --> Planning
Planning --> RejectedSchema: schema_fail
Planning --> RejectedPolicy: policy_fail
Planning --> AwaitingApproval: valid_plan
AwaitingApproval --> StepExecuting: user_approves_step
StepExecuting --> AwaitingApproval: step_ok_more_remain
StepExecuting --> Completed: all_steps_ok
StepExecuting --> Failed: step_revert
AwaitingApproval --> Cancelled: user_rejects
```

## Components

| Area | Role |
|------|------|
| `apps/mobile` | Auth, intent composer, plan review, history, demo balances |
| `services/api` | JWT auth, intents, planner, policy, step approve/execute |
| `packages/plan-schema` | Plan JSON Schema v1 + Go validation |
| `contracts` | `MockERC20`, `MockSwapRouter` on Anvil |
| `evals` | Accept/reject fixtures for mock planner + policy |
| `deploy` | Compose: Postgres + Anvil + API |

## Planner

- Interface: `Planner.Plan(ctx, intentText) → Plan`
- Default: `PLANNER_MODE=mock` (deterministic fixtures, CI-safe)
- Optional: `PLANNER_MODE=llm` with `LLM_API_KEY` (OpenAI-compatible)
- Model output is untrusted: schema validate → policy check → human approve → ABI encode from stored step payload

## Trust boundaries

| Zone | Trust |
|------|-------|
| LLM / mock planner output | Untrusted |
| Flutter client | Untrusted (server authorizes by user + plan id) |
| API + policy + schema | Trusted compute |
| Anvil private key | Local demo only |
| Mock contracts | Trusted fixtures you control |

## Data flow (happy path)

1. Client `POST /intents` with intent text (JWT).
2. Planner returns Plan JSON; API validates schema and policy.
3. Plan persists as `awaiting_approval` with pending steps.
4. Client loads `GET /plans/{id}`; user approves step 0…n in order.
5. Executor ABI-encodes the stored step, sends tx on Anvil, records hash/status.
6. When all steps succeed, plan is `completed`.
106 changes: 106 additions & 0 deletions docs/demo-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Demo script

End-to-end path on the local compose stack. Target: under ~15 minutes with Foundry installed.

## Prerequisites

- Docker
- Foundry (`forge`, `cast`, `anvil` — Anvil also runs in compose)
- `curl` + `jq` (optional but handy)

## 1. Start the stack

```bash
cd deploy
docker compose up --build
```

Wait until API logs show it is listening. Postgres `:5432`, Anvil `:8545`, API `:8080`.

## 2. Deploy and seed mocks

From the repo root (host Foundry talking to compose Anvil):

```bash
./scripts/deploy-anvil.sh
./scripts/seed-anvil.sh
```

Confirm health:

```bash
curl -s localhost:8080/health | jq .
```

## 3. Register and login

```bash
curl -s -X POST localhost:8080/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"demo@wallet.test","password":"password123"}' | jq .

ACCESS=$(curl -s -X POST localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"demo@wallet.test","password":"password123"}' | jq -r .access_token)
```

## 4. Happy path — swap

```bash
RESP=$(curl -s -X POST localhost:8080/intents \
-H "Authorization: Bearer $ACCESS" \
-H 'Content-Type: application/json' \
-d '{"text":"swap 10 USDC"}')
echo "$RESP" | jq .
PLAN_ID=$(echo "$RESP" | jq -r .plan.id)
```

Expect plan status `awaiting_approval` with approve + swap steps.

```bash
curl -s -X POST "localhost:8080/plans/$PLAN_ID/steps/0/approve" \
-H "Authorization: Bearer $ACCESS" | jq .
curl -s -X POST "localhost:8080/plans/$PLAN_ID/steps/1/approve" \
-H "Authorization: Bearer $ACCESS" | jq .
curl -s "localhost:8080/plans/$PLAN_ID" \
-H "Authorization: Bearer $ACCESS" | jq .
```

Expect step statuses `succeeded` and plan `completed`.

## 5. Reject path — policy

```bash
curl -s -X POST localhost:8080/intents \
-H "Authorization: Bearer $ACCESS" \
-H 'Content-Type: application/json' \
-d '{"text":"swap 150 USDC"}' | jq .
```

Expect `rejected_policy` / `amount_over_cap`. No executable approve path.

## 6. Reject path — schema

```bash
curl -s -X POST localhost:8080/intents \
-H "Authorization: Bearer $ACCESS" \
-H 'Content-Type: application/json' \
-d '{"text":"bridge funds somewhere"}' | jq .
```

Expect `rejected_schema`.

## 7. Flutter (optional)

```bash
cd apps/mobile
flutter run --dart-define=API_BASE=http://127.0.0.1:8080
```

Sign in → New intent → use chip `swap 10 USDC` → Approve steps → check History.

## Notes

- Compose sets `PLANNER_MODE=mock` (no LLM key).
- Anvil key defaults are local demo only — never point this stack at a real network.
- Eval fixtures: `cd services/api && go run ./cmd/evals -dir ../../evals/cases`
38 changes: 38 additions & 0 deletions docs/threat-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Threat model (MVP)

Scope: local demo stack (Flutter → Go API → Postgres → Anvil mocks). Not a mainnet custody system.

## Assets

| Asset | Why it matters |
|-------|----------------|
| User JWT session | Access to that user’s intents/plans |
| Plan step payloads | What gets ABI-encoded and sent |
| Executor private key | Signs Anvil txs (demo key only) |
| Policy config | Spend caps, allowlists |

## Threats and controls

| Threat | Control |
|--------|---------|
| Prompt injection → evil calldata | Schema allowlist of actions/fields; executor encodes from stored structured steps, never raw model hex |
| Infinite / unbounded approve | Schema + policy reject (`unlimited`, over-cap amounts) |
| Transfer to attacker address | Recipient allowlist (`bad_recipient`) |
| Silent auto-execution | Per-step human approve required; no agent loop |
| Skip ahead in step order | Executor enforces prior steps succeeded |
| Replay / double-submit step | Step status machine + idempotent succeed short-circuit |
| Planner outage / garbage JSON | Timeout + one retry → `planner_unavailable`; unusable output does not execute |
| Policy bypass via client | Caps and allowlists enforced server-side |
| Key theft from repo/demo | Document Anvil account #0 as local-only; never use on real networks |
| Cross-user plan access | Plans loaded by id scoped to authenticated user |

## Explicit non-goals (out of scope)

- Mainnet, real DEX liquidity, MEV, bridges
- Custodial key management / MPC
- Production LLM cost controls or multi-tenant isolation beyond JWT + Postgres row ownership
- Treating the model as a trusted planner

## Residual risk

A compromised API process or leaked Anvil key can move demo tokens on the local chain. Treat compose credentials and `EXECUTOR_PRIVATE_KEY` as disposable local secrets.
15 changes: 15 additions & 0 deletions evals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Evals

YAML fixtures for mock-planner accept/reject gates (schema + policy).

```bash
cd services/api
go run ./cmd/evals -dir ../../evals/cases
```

| `expect` | Meaning |
|----------|---------|
| `accept` | Schema + policy pass |
| `reject_schema` | Plan fails JSON Schema |
| `reject_policy` | Schema ok; policy codes fail |
| `planner_error` | Mock planner cannot plan |
3 changes: 3 additions & 0 deletions evals/cases/01_swap_10_accept.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id: swap_10_accept
intent: swap 10 USDC
expect: accept
3 changes: 3 additions & 0 deletions evals/cases/02_swap_1_accept.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id: swap_1_accept
intent: swap 1 USDC
expect: accept
Loading
Loading