# 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?
