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.
160 lines
6.5 KiB
Markdown
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).
|