feat: interop test package + session handoff doc

Cross-spec interop validation between ietf-act and ietf-ect:
- new packages/interop/ sibling package (ietf-act-ect-interop)
- 32 tests pass: shared claims, algorithm matrix, DAG structure,
  divergence handling, anti-goals
- documents ES256 raw signature wire-compatibility
- documents airtight typ separation (act+jwt vs exec+jwt)

Hazards surfaced:
- ACTLedger.append() silently accepts ECT Payload via duck-typing
  (both have .jti) — documented in interop README as a production
  hazard requiring external isinstance checks

Session handoff:
- SESSION-2026-04-12.md — snapshot of decisions, artifacts, open
  actions, and next-session starting points

Also: session-end commit of hash-format fix propagation to
packages/ect/ (the fix was applied to the old refimpl location
but did not propagate through the parallel package-move agent).
This commit is contained in:
2026-04-12 07:39:41 +02:00
parent 3a139dfc7e
commit 37859beef6
10 changed files with 1053 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
# ietf-act-ect-interop
Cross-spec interop tests between the `ietf-act` (Agent Context Token,
draft-nennemann-act-01) and `ietf-ect` (Execution Context Tokens,
draft-nennemann-wimse-ect-01) Python reference implementations.
The purpose of this package is not to ship runtime code — it is to
**document empirically** which shared claims, algorithms, and
structures round-trip cleanly between the two refimpls, so implementers
building bridges or shared tooling know what they can rely on.
## Compatibility matrix
Observed as of the commit that produced these 32 passing tests:
| Layer | Direction | Status | Evidence |
|---|---|---|---|
| ES256 raw JWS signature | ACT ↔ ECT | Compatible | `test_es256_primitive_is_wire_compatible_at_raw_sig_level` — ACT's `crypto.verify` accepts an ECT-signed compact's r\|\|s bytes |
| EdDSA signature | ACT → ECT | Incompatible | ECT verify refuses EdDSA at the alg gate (ES256-only) |
| `typ` header | ACT ↔ ECT | Strictly separated (by design) | `act+jwt` vs `exec+jwt`; each verifier rejects the other |
| `jti` format | Shared | Compatible | Same UUID string accepted by both |
| `wid` | Shared | Compatible | Preserved on both sides |
| `iat` / `exp` (NumericDate) | Shared | Compatible | Integer seconds on both |
| `aud` (string form) | ACT → ECT | Compatible (lossy round-trip) | ACT stores `str \| list[str]`; ECT coerces to `list[str]` via `_audience_deserialize` |
| `exec_act` | Shared | Compatible | ACT ABNF-legal values pass through ECT unchanged |
| `pred` array | Shared | Compatible | Same topology, same wire shape |
| `inp_hash` / `out_hash` | Shared | Compatible **now** | Both specs use plain base64url (ECT was aligned — the prefixed `sha-256:` form is now rejected by ECT) |
| `cap``exec_act` coupling | ACT-only | Divergent | ACT verifier raises `ACTCapabilityError`; ECT does not enforce |
| `status` claim | ACT-only | Divergent | Required in ACT Phase 2; absent in ECT |
| `sub`, `task`, `iss` required | ACT-only | Divergent | ECT `Payload.from_claims` silently drops them |
| `ect_ext`, `inp_classification` | ECT-only | Divergent | ACT `ACTRecord.from_claims` silently drops them |
| DAG cross-resolution | Separate stores | **Not supported** | `ECTStore` is keyed on `Payload`; `ACTLedger` on `ACTRecord`; no refimpl bridges the two |
### Hazard flag (found while writing these tests)
`ACTLedger.append()` does **not** perform a runtime `isinstance` check
— it relies on duck typing. An ECT `Payload` object has a `.jti`
attribute, and will therefore be **silently accepted** by the ACT
ledger. This is an implementation hazard, not a spec-level guarantee:
production bridges must enforce explicit type checks outside both
refimpls. Pinned in `test_act_ledger_does_not_type_check_ect_payload`.
## Do / Do not
**Do**
- Reuse ES256 (P-256) key material across ACT and ECT deployments — the signing primitive is byte-identical.
- Treat `jti`, `wid`, `pred`, `exec_act` as semantically aligned when building cross-type audit views.
- Rely on `inp_hash` / `out_hash` being byte-portable between refimpls today.
**Do not**
- Feed an ACT compact token to an ECT verifier or vice versa. The `typ` gates are deliberate and permanent.
- Forge one token type as the other. This is a first-class anti-goal.
- Expect ECT to enforce ACT's `cap` / `exec_act` coupling. Authorization stays in ACT.
- Use EdDSA-signed ACT tokens in an ECT-only deployment. ECT is ES256-only.
- Cross-insert objects between `ACTLedger` and `ECTStore` / `MemoryLedger`. Python's duck typing will let some of these through; that's a bug waiting to happen.
## Install and run
From the workspace root:
```bash
pip install -e packages/act
pip install -e packages/ect
pip install -e packages/interop
cd packages/interop
python -m pytest tests/ -v
```
Expected: **32 passed**.
## Test file map
| File | Focus |
|---|---|
| `tests/test_shared_claims.py` | `pred`, `jti`/`wid`, hash format, `exec_act` string shape |
| `tests/test_algorithm_matrix.py` | ES256 ↔ EdDSA × verifier compatibility |
| `tests/test_dag_structure.py` | `pred`-array topology equivalence; store separation |
| `tests/test_divergence.py` | claims each parser ignores; `typ` separation; `cap` coupling; `status` requirement |
| `tests/test_anti_goals.py` | cross-type forgery rejection; cross-type store hazard |
## Open questions for spec editors
- Should ECT optionally accept Ed25519? Today it is ES256-only.
- Should the refimpls enforce type-level rejection of cross-type objects passed into their stores/ledgers, or is that outside scope?