Files
ietf-draft-analyzer/demo/act-ect-mcp/README.md
Christian Nennemann 9a0dc899a8 feat: add ACT+ECT over MCP demo with LangGraph agent
End-to-end PoC demonstrating Agent Context Token authorization and
Execution Context Token accountability over MCP tool calls, using a
LangGraph agent with ES256-signed JWT tokens and DAG verification.
2026-04-12 12:43:22 +00:00

160 lines
6.5 KiB
Markdown

# ACT + ECT + MCP + LangGraph — end-to-end PoC
A working demonstration of `draft-nennemann-act-01` (Agent Context Token)
and `draft-nennemann-wimse-ect-01` (Execution Context Token) in a realistic
agent stack:
* a **LangGraph** ReAct agent driven by a local **Ollama** LLM;
* talking over **MCP** streamable-HTTP to a FastMCP server;
* every request carries an **ACT** mandate, a per-call **ECT**, and an
**RFC 9421** HTTP signature with the `wimse-aud` parameter from
`draft-ietf-wimse-http-signature-03`;
* the server rejects any request where ACT / ECT / HTTP-signature /
capability / body-hash binding fails;
* a verifier CLI replays the run's ledger, re-runs the two refimpls, and
prints the resulting DAG.
## Why this exists
The two drafts (ACT and ECT) claim to fit together — ACT giving the
lifecycle (mandate → execution record) and ECT giving the per-call
execution context on the wire. This PoC proves the claim end to end:
the same refimpls that ship in `workspace/packages/{act,ect}/` are the
only crypto/verification layer used here. There is no token forgery
shortcut.
## Requirements
* `uv` ([install](https://docs.astral.sh/uv/))
* Local Ollama with a chat model pulled (`qwen3:8b` by default)
* Python 3.11+ (uv will fetch one if missing)
## Run the demo
```bash
./demo.sh
```
This script:
1. Syncs deps (uv installs the sibling `ietf-act` and `ietf-ect`
packages in editable mode, plus `mcp`, `langgraph`,
`langchain-ollama`, `langchain-mcp-adapters`, `fastapi`, …).
2. Launches the MCP server on `127.0.0.1:8765`.
3. Runs the agent (`poc-agent`) against it with a canned research task.
4. Runs the verifier (`poc-verify`) over the ledger the agent emitted.
5. Shows the last five server-audit entries.
Expected tail of output:
```
mandate verified jti=64f5ec87
ects verified n=7 (tool-calls=2, session=5)
record verified jti=64f5ec87 status=completed
ect-dag wellformed every pred is the mandate or a prior ECT
Run
===
mandate 64f5ec87 task='Search for quantum entanglement, …'
iss=user sub=agent aud=mcp-server
cap=['mcp.session.initialize', 'mcp.session.list_tools', 'mcp.session.other', 'mcp.search', 'mcp.summarize']
Tool-call ECT DAG:
ect 73af4cd3 exec_act=mcp.search pred=['64f5ec87']
ect 0e3ffa01 exec_act=mcp.summarize pred=['64f5ec87', '73af4cd3']
ACT Phase 2 record:
jti=64f5ec87 exec_act=mcp.summarize
status=completed pred=[]
inp_hash=… out_hash=…
```
The mandate jti and the record jti are identical — this is ACT §3.2:
the Phase 2 token records the same task it started as. The tool-call
ECT DAG captures the per-HTTP-request ordering.
## Architecture
```
user ──(mints ACT mandate)──► agent
│ create_react_agent(ChatOllama, tools)
┌──────────────────────────────┐
│ LangGraph ReAct loop │
│ LLM decides tools to call │
│ │
│ langchain-mcp-adapters │
│ ↑ streamable_http session │
└──────────┬───────────────────┘
httpx.AsyncClient with event hooks
┌───────────▼─────────────┐
│ on_request: │
│ - mint ECT(inp_hash) │
│ - RFC 9421 sign │
│ - attach Authorization │
│ + Wimse-ECT + sig │
└───────────┬──────────────┘
POST /mcp
┌───────────▼──────────────┐
│ FastMCP streamable-http │
│ + ActEctAuthMiddleware │
│ verifies: │
│ ACT mandate │
│ ECT │
│ HTTP-signature │
│ inp_hash == body │
│ exec_act in cap │
│ ECT.iss == sub │
│ then dispatches tool │
└──────────────────────────┘
```
## Files
* `src/poc/keys.py` — ES256 keys for the three PoC identities
(`user`, `agent`, `mcp-server`).
* `src/poc/tokens.py` — thin wrappers around `ietf-act` and `ietf-ect`
that fix the PoC's shape.
* `src/poc/http_sig.py` — minimal RFC 9421 signer/verifier covering
`@method`, `@target-uri`, `content-digest`, `wimse-ect`, with the
`wimse-aud` metadata parameter from http-signature-03.
* `src/poc/server.py` — FastMCP server with ACT + ECT + signature
middleware. Writes `keys/server-audit.jsonl`.
* `src/poc/agent.py` — LangGraph + Ollama agent. Writes
`keys/ledger.jsonl` — one mandate, N ECTs, one final ACT record.
* `src/poc/verify_cli.py` — ledger verifier, prints the DAG.
* `tests/` — pytest suite (see below).
## Non-goals
* No real LLM API costs (Ollama is local).
* No distributed SCITT anchoring — server audit log is a plain JSONL.
* No Go-side client in this PoC; Python ↔ Python. Go refimpl lives in
`workspace/drafts/ietf-wimse-ect/refimpl/go-lang/`.
* The PoC uses a single mandate per run and a single Phase 2 record
(ACT §3.2 jti preservation). If you want a multi-record DAG of ACT
tasks, you'd need to exercise the delegation machinery
(`act.delegation.create_delegated_mandate`) — this PoC does not, to
keep the wire story tight.
## Tests
```bash
uv run pytest
```
Covers:
* Minting + round-trip verification of each token type.
* HTTP-signature round-trip.
* Middleware rejection paths (missing headers, wrong audience, stolen
ECT on a mutated body).
* End-to-end: launches an in-process server via httpx's ASGI transport
and runs the agent's token-injection hooks over it (no Ollama
required — uses a fake LLM).