acdp 0.14.1

Rust client library for the Agent Context Distribution Protocol (ACDP v0.1.0 + v0.2.0 Trust & Hardening)
Documentation
# Language Bindings

The crate ships three SDKs that reuse the Rust crypto core:

- **`bindings/acdp-py`** — Python, via [PyO3]https://pyo3.rs / maturin
  (published to PyPI as `acdp`).
- **`bindings/acdp-node`** — Node.js, via [NAPI-rs]https://napi.rs
  (published to npm as `@agentcontextdistributionprotocol/acdp`).
- **`bindings/acdp-wasm`** — a browser/edge WebAssembly **verifier core**, via
  `wasm-bindgen` / wasm-pack (published to npm as
  `@agentcontextdistributionprotocol/acdp-wasm`). Verification-only — no
  producer/signing surface.

All implement the same protocol primitives as the Rust crate, so a context
signed in Python verifies in Node, in the browser, and in Rust. The protocol
contract they implement is the same RFC set (0001 through 0015) — see
[RFC-ACDP-0001](https://github.com/agentcontextdistributionprotocol/agentcontextdistributionprotocol/blob/main/rfcs/RFC-ACDP-0001-core.md).

## Design: crypto in Rust, HTTP in the host

The single most important design decision: **the bindings never make network
calls.** They expose the deterministic, security-critical operations — building,
hashing, signing, verifying — and leave transport to the host language's HTTP
stack (`httpx`, `fetch`, …).

This is why both bindings depend on `acdp` with `default-features = false`:
`reqwest` / `tokio` / `rustls` never enter the Python wheel or the `.node`
binary. They only need the pure-types/crypto core (see
[Architecture → feature gating](architecture.md#feature-gating)).

### JSON across the FFI boundary

Every binding method accepts and returns **JSON strings** — the same HTTP
request/response bodies you'd send on the wire. No Rust types cross the
boundary, so the API stays small and stable across language updates.

```
host language  ──(JSON string)──►  binding (Rust crypto)  ──(JSON string)──►  host language
     │                                                                              │
     └──────────────────── HTTP (httpx / fetch) ──────────────────────────────────┘
```

### Key handling

`AcdpProducer` stores a **32-byte seed**, not a live `SigningKey`. `SigningKey`
is `ZeroizeOnDrop` and not `Clone`, so the binding rebuilds it from the seed for
each call. The seams that make this work are `SigningKey::seed_bytes()` and
`SigningKey::sign_string()` in `crates/acdp-crypto/src/sign.rs` — they exist
specifically to support the binding surface.

## Python (`acdp-py`)

```bash
cd bindings/acdp-py && maturin develop      # or: make sdk-py
```

```python
import json, httpx
from acdp import AcdpProducer, AcdpVerifier

# Build + sign — returns a JSON publish request
producer = AcdpProducer.generate("did:web:agents.example.com:my-agent",
                                 "did:web:agents.example.com:my-agent#key-1")
req = producer.build_publish_request(title="Q1 snapshot", context_type="data_snapshot")

# Transport is yours:
httpx.post("https://registry.example.com/contexts",
           content=req, headers={"Content-Type": "application/acdp+json"})

# Verify a retrieved body (raises on mismatch)
AcdpVerifier.verify_content_hash(body_json, stored_hash)
AcdpVerifier.verify_signature(pub_key_b64, sig_b64, content_hash)

# Bind the served identity to the one you requested (RFC-ACDP-0006 §4.1
# step 7, NORMATIVE). `ctx_id` is registry-assigned and in the §5.7
# exclusion set, so neither content_hash nor the signature covers it —
# without this call a registry could serve any other validly-signed body
# from the same producer under the URL you asked for.
AcdpVerifier.verify_ctx_id_binding(body_json=body_json, expected_ctx_id=requested_ctx_id)
```

All `AcdpVerifier` methods that return a plain bool follow the same
convention: they return `True`/`true` on success and raise/throw on
failure — never `False`/`false`. Writing `if AcdpVerifier.verify_...(...)`
guards a branch that can't be reached.

## Node.js (`acdp-node`)

```bash
cd bindings/acdp-node && npm run build:debug   # or: make sdk-node
```

```js
const { AcdpProducer, AcdpVerifier } = require('@agentcontextdistributionprotocol/acdp');

const producer = AcdpProducer.generate(
  'did:web:agents.example.com:my-agent',
  'did:web:agents.example.com:my-agent#key-1');
const req = producer.buildPublishRequest({ title: 'Q1 snapshot', contextType: 'data_snapshot' });

await fetch('https://registry.example.com/contexts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/acdp+json' },
  body: req,
});

AcdpVerifier.verifyContentHash(bodyJson, storedHash);  // throws on mismatch
AcdpVerifier.verifySignature(pubKeyB64, sigB64, contentHash);

// Bind the served identity to the one you requested (RFC-ACDP-0006 §4.1
// step 7, NORMATIVE) — argument order is (bodyJson, expectedCtxId): the
// body carries the *served* ctx_id, the second argument is what you
// requested. ctx_id is registry-assigned and outside content_hash /
// signature coverage, so this explicit check is the only binding
// available on the receipt-less path.
AcdpVerifier.verifyCtxIdBinding(bodyJson, requestedCtxId);  // throws on mismatch
```

The Node API is the same surface in camelCase.

## Verifying a registry receipt

`verify_receipt` / `verifyReceipt` (RFC-ACDP-0010 §8) checks a registry
receipt against the body it accompanies. `body_json` is a **required**
second argument (not optional) — it binds the receipt's `lineage_id`,
`origin_registry`, and `created_at` to the served body's own fields (§8
step 3), the sibling check to `verify_ctx_id_binding` on the
receipt-bearing path.

```python
# Python
AcdpVerifier.verify_receipt(
    receipt_json,            # the `registry_receipt` object, as received
    body_json,                # the accompanying `body`, from the same retrieval
    registry_public_key_b64,  # resolved via AcdpDid.web_to_url + httpx
    expected_ctx_id,          # the ctx_id you actually requested
    recomputed_body_hash,     # YOUR OWN verify_content_hash result — never
                               # the body's echoed content_hash field
    producer_key_fingerprint, # fingerprint of the resolved producer key
)
```

```js
// Node.js
AcdpVerifier.verifyReceipt(
  receiptJson,
  bodyJson,
  registryPublicKeyB64,
  expectedCtxId,
  recomputedBodyHash,
  producerKeyFingerprint,
);
```

Two checks stay the HOST's obligation on every binding, because neither
needs the body — they need things this binding never sees:

1. **Serving-authority binding** — `receipt.registry_did` must equal
   `"did:web:" + <authority>` for the authority the response was
   *actually fetched from*, compared against your HTTP client's request
   URL, not any field inside the response.
2. **Recompute, don't trust, the body hash** — `recomputed_body_hash`
   must be a hash *you* independently recomputed (run
   `verify_content_hash` on the body first), never the body's echoed
   `content_hash` field taken on faith.

## WebAssembly (`acdp-wasm`)

A verification-only core for browsers and edge runtimes. It exposes the
consumer-side checks (`verifyContentHash`, `verifySignatureEd25519`,
`verifyCtxIdBinding`, `verifyBodyOffline`, receipt/log/lifecycle/witness
verification) but no producer/signing surface — signing keys should not
live in a browser.

```bash
cd bindings/acdp-wasm && wasm-pack build --target web
```

```js
import init, { verifyContentHash, verifyCtxIdBinding, verifyReceipt } from '@agentcontextdistributionprotocol/acdp-wasm';
await init();
const verdict = JSON.parse(verifyContentHash(bodyJson, storedHash));

// Bind the served ctx_id to the one requested (RFC-ACDP-0006 §4.1 step 7).
// Like verifyContentHash, a malformed *served* ctx_id is
// reported as a `{"valid": false, ...}` verdict, not a throw — only a
// malformed `expectedCtxId` argument throws.
const binding = JSON.parse(verifyCtxIdBinding(bodyJson, requestedCtxId));
if (binding.valid) { /* served ctx_id matches what was requested */ }

// verifyReceipt takes bodyJson as its required 2nd argument (RFC-ACDP-0010
// §8 step 3 body binding). A malformed bodyJson throws (host input); a
// body/receipt mismatch is a `{"valid": false, ...}` verdict, like any
// other cross-check failure.
const rcpt = JSON.parse(verifyReceipt(
  receiptJson, bodyJson, registryPublicKeyB64,
  expectedCtxId, recomputedBodyHash, producerKeyFingerprint,
));
```

See `bindings/acdp-wasm/README.md` for the full exported surface.

## Golden-vector parity

Both binding test suites pin the **same** constants from the `sig-001` golden
vector:

```
content_hash    = "sha256:f170150d…"
signature.value = "ErkbV+FU…"
```

The `bindings/interop/` suite cross-builds the identical request in *both*
bindings and asserts byte equality. If any of those constants drift, the
protocol is broken — that's the tripwire.

```bash
cd bindings/interop && pytest        # or: make interop
```

## Build details

Each binding is a **standalone Cargo package** (its own `[workspace]` table)
that references the parent crate via `path = "../.."`. They are **not** part of
`cargo test` on the root crate — build each independently with maturin / napi /
wasm-pack. The top-level `Makefile` wraps the common targets: `make sdk-py`,
`make sdk-node`, `make interop`.