agent-toolprint 0.1.0

Double-signed receipts for AI-agent tool invocations — DSSE + JCS + Ed25519, verifiable offline (Rust port of @p-vbordei/agent-toolprint)
Documentation
# Architecture

## Goal

Port the [`agent-toolprint` v0.1 SPEC](../SPEC.md) to idiomatic Rust while staying byte-deterministic with the [TypeScript reference](https://github.com/p-vbordei/agent-toolprint). Same wire format, same conformance vectors, same SHA-256 over canonical payload bytes.

## Module map

Eight small modules under `src/`, each mirrors one TS file.

| Rust | TypeScript | Responsibility |
|---|---|---|
| `lib.rs` | `index.ts` | Public re-exports. |
| `types.rs` | `types.ts` | Strict `validate_receipt` / `validate_envelope` over `serde_json::Value`. |
| `canonical.rs` | `canonical.ts` | RFC 8785 JCS via `serde_jcs` + SHA-256 helper. |
| `did_key.rs` | `did-key.ts` | Multicodec `0xed01` encode/decode + bundled async `Resolver`. |
| `envelope.rs` | `envelope.ts` | DSSE Pre-Authentication Encoding + envelope shape. |
| `sign.rs` | `sign.ts` | `sign_agent`, `countersign_tool` over `ed25519-dalek`. |
| `verify.rs` | `verify.ts` | The five SPEC §4 checks; pluggable `Resolver`; optional plaintext re-hash. |
| `chain.rs` | `chain.ts` | `chain(parent_env, child_env)` returns the link check. |
| `error.rs` || `thiserror`-based `Error` for the surface above. |

## Dependency choices

- **`ed25519-dalek`** for Ed25519. Audited, widely deployed.
- **`serde_jcs`** for RFC 8785 — but see "JCS precision" below for one quirk we work around.
- **`serde_json`** with the `preserve_order` feature so error messages reproduce the input field order, useful for debugging non-canonical payload negatives.
- **`bs58`** for `did:key` z-prefix multibase (base58btc).
- **`async-trait`** so consumers can plug in network resolvers; `DidKeyResolver` is the bundled offline impl.

## Byte-determinism invariants

Three byte equalities must hold across ports:

1. **Canonical payload bytes**`canonical(&receipt)` is RFC 8785: sorted keys, no whitespace, ECMA-262 number formatting. Verified by `clauses_present`/`all_vectors_pass` against the SHA published in each C1 vector.
2. **DSSE PAE bytes**`DSSEv1 <len(type)> <type> <len(body)> <body>`; the `<body>` is the canonical payload.
3. **Signature bytes** — Ed25519 over the PAE, base64-encoded with standard alphabet and padding. Seeded keys (`agent_sk_seed` / `tool_sk_seed`) keep these byte-identical across ports.

### JCS precision — `normalize_numbers` defensively included

RFC 8785 demands ECMA-262 `ToString(f64)` formatting for all JSON numbers. `serde_jcs` 0.1 preserves `u64`/`i64` past 2^53, which would silently diverge from the TS reference if a future schema added a `timestamp_ns` or counter field. `canonical.rs::normalize_numbers` walks the value and coerces any integer above the safe range to `f64` before handing to `serde_jcs`. v0.1 receipts carry no such fields, but the guard keeps byte-equality stable for v0.2.

## Testing strategy

`tests/conformance.rs` has three `#[tokio::test]` functions:

- `clauses_present` — discover every JSON under `vectors/` and assert all four clauses (C1–C4) are represented.
- `all_vectors_pass` — drive the 15 vectors through the implementation:
  - **C1** (3) — valid receipts canonicalize to the published SHA and round-trip through `verify`.
  - **C2** (7) — payload byte-flip, agent sig flip, tool sig flip, swap-sigs, agent keyid mismatch, tool keyid mismatch, non-canonical payload. All must fail `verify`.
  - **C3** (2) — agent-only and tool-only single-sig envelopes rejected.
  - **C4** (3) — `chain(parent, child)` returns the expected boolean for linked / wrong-parent / missing-parent.
- `end_to_end_roundtrip` — generate a real keypair, build a receipt with `Uuid::new_v4`, double-sign, verify with `plaintext` re-hash.

Vectors live under `vectors/`. Adding a new one is dropping a JSON file and running `cargo test` — `walk` discovers it.