v0.4.0 — cap envelope inline, three-agent delegation works over the wire
Summary
Closes #10. The Macaroons-style attenuation that PACT advertised in v0.1 finally works end-to-end across the wire. A→B→C delegation: A issues to B, B attenuates and grants to C, C presents to A. A verifies the chain.
What changed
`PACTMessage` gains an optional `cap_envelope: dict | None` field. When the receiver doesn't have the cap_id locally and `cap_envelope` is present, the receiver:
- Verifies the envelope's `cap_id` matches `msg.cap_id`
- Verifies the cap was issued by us (root issuer check)
- Gathers required pubkeys from peer cache (delegators in chain + holder)
- Calls `verify_capability` with full `known_keys` (issue #8 fail-closed applies)
- Caches the cap locally for future use
`build_req` accepts an optional `cap_envelope` parameter. Passing the full `cap.to_dict()` is the one-line client-side change.
Behavior change
Stricter: `cap_id` claimed but neither in local store nor provided as envelope = explicit `cap_unknown` error.
Pre-v0.4.0: silently fell through to action-name dispatch without auth (a residual bypass path that combined with the v0.1 auto-grant problem).
Tests
| v0.3.1 | v0.4.0 | |
|---|---|---|
| Tests passing | 131 | 135 |
| Documented xfails | 0 | 0 |
| CI | 3.11 / 3.12 / 3.13 green | 3.11 / 3.12 / 3.13 green |
4 new tests in `tests/integration/test_cap_envelope.py`:
- `test_three_agent_delegation_over_the_wire` — the killer scenario
- `test_unknown_cap_without_envelope_rejected` — closes the silent fallthrough
- `test_envelope_with_mismatched_cap_id_rejected` — anti-confusion
- `test_envelope_with_wrong_issuer_rejected` — anti-impersonation
Stats arc since the v0.1.3 case study
| v0.1.3 | v0.4.0 | |
|---|---|---|
| Tests passing | 118 + 1 xfail | 135 |
| Critical auth bypasses | 3 | 0 |
| Durability gaps | 1 | 0 |
| Wire-level delegation | broken | works |
| Documented xfails | 1 | 0 |
Install
```bash
pip install --upgrade pact-passport
```
What's left
- #11 streaming RES_CHUNK design — deferred. Not a bug; a feature, ~16 hours of protocol-level work for v0.5+.
Every other open issue is closed.