Files
ietf-draft-analyzer/demo/act-ect-mcp
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
..

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)
  • Local Ollama with a chat model pulled (qwen3:8b by default)
  • Python 3.11+ (uv will fetch one if missing)

Run the demo

./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

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).