Architecture

This page explains how the crate is organized internally and why. The protocol-level rationale for the three-layer split lives in RFC-ACDP-0001 §5 (identifiers, canonicalization, hashing, signatures) and RFC-ACDP-0002 (the body structure). This page maps that structure onto Rust modules and explains the one rule you must never break: a field's layer determines whether it is hashed, signed, or mutable.

The three layers

PublishRequest                               crates/acdp-types/src/publish.rs

  ├── Body                  ← immutable, JCS-canonicalized   crates/acdp-types/src/body.rs
  │     │
  │     └── ProducerContent ← Body minus the §5.7 exclusion set
  │           │                (producer-controlled fields only)
  │           ├── content_hash  = sha256(JCS(ProducerContent))    crates/acdp-crypto/src/hash.rs
  │           └── signature     = Ed25519( ASCII "sha256:<hex>" ) crates/acdp-crypto/src/sign.rs
  │                                            ▲
  │                                            └─ ⚠️ the ASCII string, NOT the 32-byte digest

  ├── content_hash          ← echoed in the request for transport
  └── signature             ← the producer's Ed25519 signature

FullContext = Body + RegistryState                     ← retrieval shape

                     └── status, receipt, … (mutable/registry-derived)  crates/acdp-types/src/body.rs

Three operations are protocol-critical and the crate implements them exactly:

OperationSpecImplementation
JCS canonicalizationRFC 8785crates/acdp-jcs/src/lib.rsin-house, handles -0.0
content_hashRFC-ACDP-0001 §5.7crates/acdp-crypto/src/hash.rssha256(JCS(ProducerContent))
Ed25519 / P-256 sign/verifyRFC-ACDP-0001 §5.8/§5.11crates/acdp-crypto/src/{sign,verify}.rs

Three things that trip people up

  1. The signature preimage is the ASCII string "sha256:<hex>" — the 71-byte text — not the raw 32-byte digest. This is the single most common implementation mistake. See crates/acdp-crypto/src/sign.rs.
  2. Option::is_none fields are skipped, not emitted as null. Emitting null for an unset field changes the JCS bytes and therefore the content_hash. This is load-bearing for the sig-001 golden vector.
  3. Body and RegistryState are split deliberately. status is not a body field — it's registry-derived and lives in RegistryState. Merging them would let mutable state into the signed preimage.

Workspace crate map

The umbrella acdp crate (src/lib.rs) is a thin facade that re-exports a fine-grained set of crates under crates/, preserving the historical acdp::{types, crypto, did, validation, verify, producer, client, registry, …} paths. The crates form a strict bottom→top dependency DAG:

CrateRole
acdp-primitivesLeaf types (AgentDid, CtxId, …), AcdpError, ACDP_VERSION, limits/time/serde helpers.
acdp-jcsRFC 8785 JCS — in-house, handles -0.0.
acdp-safe-httpSsrfPolicy, the HTTPS guard, and SafeDnsResolver (the DNS-time IP filter).
acdp-didWebResolver for did:web (LRU-cached, SSRF-gated) and offline did:key resolution (Ed25519 + P-256).
acdp-cryptohash (content_hash + lineage_id), sign/verify (Ed25519 + ECDSA-P256), fingerprint, Merkle.
acdp-typesWire types: body, publish, search, data_ref, capabilities, receipt, lifecycle, log, cosignature, revocation, primitives. Body/RegistryState are kept apart; Status/ContextType/Visibility are open enums.
acdp-validationOne-stop schema validator: validate_publish_request, validate_body, validate_data_ref, validate_metadata, compute_embedded_hash.
acdp-verifyHigh-level verification: resolver-backed Verifier (RFC-ACDP-0001 §5.11) plus offline did:key body/request/lifecycle verification.
acdp-producerProducer + RequestBuilder. Enforces v1-vs-v2+ rules, ms-truncates timestamps, validates, computes content_hash, then signs.
acdp-client (feature client)RegistryClient, VerifiedContext, VerificationPolicy/Report, CrossRegistryResolver. Implements the acdp-consumer profile.
acdp-server (feature server)RegistryServer, PublishValidator, InMemoryStore. Building blocks for separate acdp-registry-* crates.
acdp-cliThe acdp binary (cargo run -p acdp-cli). Uses std::env::args directly — no clap by design.

did→crypto and validation→producer are test-only (dev-dependency) cycles; acdp-verify also takes acdp-producer as a dev-dependency to build signed fixtures.

Feature gating

Features on the umbrella crate add layers outward from a pure core:

              ┌─────────────────────────────────────────────┐
 (own crate)  │ acdp-cli  (the `acdp` binary)               │
              ├─────────────────────────────────────────────┤
 client ───►  │ client::{RegistryClient, VerifiedContext,   │
              │   CrossRegistryResolver}  ·  did::WebResolver│
              ├─────────────────────────────────────────────┤
 server ───►  │ registry::{RegistryServer, PublishValidator,│
              │   InMemoryStore}                             │
              ├─────────────────────────────────────────────┤
  core   ───► │ types · crypto · validation · verify ·      │  ← always present,
 (no feat)    │ producer · error · profile · safe_http · did │    no HTTP stack
              └─────────────────────────────────────────────┘
  • client and server are independent. A consumer pulls client; a registry pulls server. They don't require each other.
  • The core has no async runtime and no HTTP. reqwest/tokio/rustls only arrive with client. Offline did:key verification and the receipt types work with --no-default-features. This is what lets the bindings ship a thin wheel/.node/.wasm — crypto in Rust, HTTP in the host language.

Where the work happens

Most behavior changes land in crates/acdp-validation/src/lib.rs (~44 KB). It is the single source of structural truth: the builder calls it before signing, the server feature calls it during publish, and VerifiedContext calls it during retrieval. If you're changing what counts as a valid context, that's the file.

The crypto layer is intentionally small and rarely changes — and when it does, it must keep passing the sig-001 / can-001 golden vectors (see Conformance & testing).

Design rules (repo conventions)

These are enforced and will fail CI or review if broken:

  • No unsafeunsafe_code = "forbid" at the crate root.
  • JCS is in-house — do not swap crates/acdp-jcs/src/lib.rs for an external crate; the -0.0 handling is pinned by proptest_jcs.rs and can-001.
  • No clap in the CLI — manual arg parsing keeps the dep graph identical to the library.
  • Option::is_none skip-serialization is load-bearing — never emit null for unset fields.
  • Producer timestamps are ms-truncated (time::trunc_ms) per RFC-ACDP-0001 §5.3.
  • Conventional Commitsrelease-plz derives versions from feat:/fix:/docs:/… prefixes.