Producing Contexts

This page covers building and signing a PublishRequest with Producer and RequestBuilder. For what the fields mean and the publish wire contract, see RFC-ACDP-0002 (Context Body) and RFC-ACDP-0003 (Publish & Supersession).

Available in the default (core) build — no feature flags, no HTTP. The producer only builds and signs; transport is your responsibility (use the client feature, the CLI, or your own HTTP).

The producer

A Producer binds a signing key to a producer identity and a key id (the verification-method id in the producer's did:web document).

use acdp::{crypto::SigningKey, producer::Producer, types::AgentDid};

let key = SigningKey::from_bytes(&seed);          // or SigningKey::generate()
let producer = Producer::new(
    key,
    AgentDid::new("did:web:agents.example.com:my-agent"),
    "did:web:agents.example.com:my-agent#key-1",
);
ConstructorAlgorithmNotes
Producer::new(key, agent_id, key_id)Ed25519The common case.
Producer::new_ed25519(...)Ed25519Explicit alias of new.
Producer::new_p256(key, ...)ECDSA-P256Interop only. Ed25519 is mandatory (RFC-ACDP-0001 §5.10); P256 is optional and some registries may not accept it.

SigningKey is ZeroizeOnDrop and not Clone — it erases its seed on drop. Load the 32-byte seed from secure storage in production rather than calling generate() (which uses OS entropy and gives you a fresh, unrecoverable key).

Building a first version

use acdp::types::{ContextType, Visibility};

let req = producer
    .publish_request()
    .title("Q1 2026 revenue snapshot")          // required, 1..=500 chars
    .context_type(ContextType::DataSnapshot)     // required
    .visibility(Visibility::Public)              // defaults to Public
    .description("Quarterly revenue figures aggregated by region.")
    .tags(vec!["finance", "revenue"])
    .domain("finance")
    .summary("Q1 2026 revenue snapshot.")
    .metadata(serde_json::json!({ "currency": "USD" }))
    .build()?;

.build() performs four steps in order (src/producer/builder.rs):

  1. Assemble ProducerContent (producer-controlled fields only).
  2. Compute content_hash = sha256(JCS(ProducerContent)).
  3. Sign the content_hash string with the producer's key.
  4. Return a wire-ready PublishRequest (with content_hash + signature attached).

Before step 2 it runs validation::validate_publish_request, so a bad request fails locally with a typed AcdpError rather than at the registry. Timestamps you pass (expires_at, data_period) are truncated to milliseconds (RFC-ACDP-0001 §5.3) so the canonical form is stable.

Builder methods

MethodFieldRequired?
.title(s)titleyes (1..=500 chars)
.context_type(t)typeyes
.visibility(v)visibilitydefaults to Public
.description(s) / .summary(s)description / summaryoptional
.tags(vec) / .domain(s)tags / domainoptional
.data_refs(vec)data_refsoptional — see below
.contributors(vec) / .audience(vec)contributors / audienceoptional
.derived_from(vec)derived_from (lineage)optional
.metadata(json)metadataoptional (depth/size capped)
.schema_uri(s)schema_urioptional
.expires_at(dt) / .data_period(dp)expiry / periodoptional
.version(n) / .expected_lineage_id(l)version / lineage checkv2+ only (see Supersession)
.acdp_version(v)acdp_versionoptional (see below)

Data references

DataRef describes where a context's underlying data lives — by URI, by embedded blob, or by structured locator. Use the constructors rather than building the struct by hand; they set the encoding and shape correctly.

use acdp::types::{DataRef, DataRefType};

// By URI (most common)
DataRef::uri(DataRefType::PrimaryResult, "https://data.example.com/q1.parquet");

// By URI with a pre-known content hash (verified on fetch)
DataRef::uri_verified(DataRefType::PrimaryResult, "https://…", content_hash);

// Embedded inline — pick the encoding that matches your payload
DataRef::embedded_json(DataRefType::PrimaryResult, serde_json::json!({ "rows": 42 }));
DataRef::embedded_utf8(DataRefType::Metadata, "free-form text");
DataRef::embedded_base64(DataRefType::SecondaryData, "SGVsbG8=");

Convenience shorthands exist too: DataRef::primary_result_uri(uri), raw_data_uri, supporting_info_uri, derived_data_uri, primary_result_json, derived_data_json.

Embedded size cap. Embedded content is bounded (64 KB — RFC-ACDP-0007 limits.max_embedded_bytes). Oversize embeds fail build() with AcdpError::EmbeddedTooLarge. For large data, reference by URI; the embedded content_hash lets a consumer verify the fetched bytes.

Supersession

A new version of an existing context supersedes the previous one. The preferred entry point is supersede_body, which propagates the version number and lineage id from the retrieved previous Body (RFC-ACDP-0003 §3.1):

// `previous` is the Body you retrieved for the current version.
let v2 = producer
    .supersede_body(&previous)        // sets supersedes, version+1, expected_lineage_id
    .title(previous.title.clone())
    .context_type(previous.context_type.clone())
    .summary("Updated with corrected April figures.")
    .build()?;
MethodUse when
supersede_body(&prev)You have the full previous Body. Recommended — auto-sets version, supersedes, and expected_lineage_id.
new_version_from(&prev)Most fields stay the same; you only change data/summary/metadata. Carries every producer field over from prev, then you override what changed.
supersede(prev_ctx_id)You only have the previous ctx_id. You must also call .version(n) yourself.

v1 vs v2+ rule. expected_lineage_id MUST NOT appear on a v1 publish and is required for v2+ self-verification. The builder enforces this — calling .expected_lineage_id(...) on a v1 request (or omitting .version() on a manual supersede) fails build(). Use supersede_body and you won't hit this.

Lineage vs. derived-from

Two different relationships, easy to confuse:

  • Lineage (supersedes / lineage_id) — successive versions of the same work. A lineage_id is derived from the v1 ctx_id and is stable across versions.
  • Derived-from (.derived_from(vec)) — this context was built using those other contexts as inputs. A provenance edge, not a version bump. The CrossRegistryResolver walks these.

The acdp_version field

The builder omits acdp_version by default. Conformant consumers treat an absent field as "0.1.0" (RFC-ACDP-0001 §6), so omission is safe and is what the sig-001 golden vector uses. To emit it explicitly:

builder.acdp_version(acdp::ACDP_VERSION);   // adds "acdp_version": "0.1.0"

⚠️ Absent and explicit "0.1.0" are semantically identical but produce different content_hash values — the JCS byte sequences differ. Pick one convention and keep it consistent within a lineage, or supersession self-verification and round-trip tests will surprise you.

What you get back

build() returns a PublishRequest with content_hash and signature populated. Serialize it and POST it:

let json = serde_json::to_string_pretty(&req)?;   // the publish body
println!("{}", req.content_hash);                 // sha256:<hex>
println!("{} / {}", req.signature.algorithm, req.signature.key_id);

To actually publish over HTTP, see Consuming & verifying → publishing or the acdp publish CLI command.