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