RFC-ACDP-0007

RFC-ACDP-0007

Agent Context Distribution Protocol (ACDP) — Capabilities & Errors

Document: RFC-ACDP-0007 Version: 0.1.0 Status: Community Standards Track (Final)

This RFC specifies the registry capability declaration document and the standard error envelope used by all ACDP endpoints.


1. Status of This Memo

This document is a Final ACDP specification (acdp/0.1.0). It is stable for the 0.1.0 release; subsequent breaking changes require a new RFC and a version bump per VERSIONING.md.


2. Motivation

Two pieces of information must be discoverable about every registry:

  1. What does it support? — Which signature algorithms, profiles, and limits.
  2. How does it report failures? — A consistent error envelope and code registry, so consumers can react programmatically.

Both must be discoverable without prior bilateral configuration. The well-known capabilities document and the standard error envelope serve those needs respectively.


3. Capabilities Document

GET /.well-known/acdp.json

Returns the registry's capability declaration. Conforms to schemas/json/acdp-capabilities.schema.json.

3.1 Required fields

FieldTypeDescription
acdp_versionstringThe ACDP specification version this registry implements. Form: <major>.<minor>.<patch>.
registry_didstringThe registry's own DID, typically did:web:<hostname>.
supported_signature_algorithmsarray of stringSignature algorithms accepted on publish. MUST contain at least "ed25519".
supported_did_methodsarray of stringDID methods this registry can resolve. MUST be non-empty and MUST include "did:web" (RFC-ACDP-0001 §5.4 mandates did:web for v0.1.0 producers; RFC-ACDP-0001 §5.11 specifies the resolution algorithm).
profilesarray of stringProfile(s) this implementation claims conformance to. Any registry MUST declare at least "acdp-registry-core". See RFC-ACDP-0001 §9.
limits.max_payload_bytesintegerMaximum size of a publish request body in bytes.
limits.max_embedded_bytesintegerMaximum decoded size of any embedded data reference. Fixed at 65536 by the spec.

3.2 Optional fields

FieldTypeDescription
read_authentication_methodsarray of stringRead-authentication methods supported by this registry. At least one MUST be declared if the registry serves any non-public contexts. Defined values: http_signatures, mtls, oauth. See RFC-ACDP-0008 §6.2.
anonymous_public_readsbooleanWhether anonymous (unauthenticated) reads are permitted for public contexts. Default false. See RFC-ACDP-0008 §6.3.
supports_idempotency_keybooleanWhether this registry honors the Idempotency-Key header on POST /contexts. Default false. See RFC-ACDP-0003 §6.
limits.idempotency_key_ttl_secondsintegerHow long this registry retains idempotency-key mappings, in seconds. MUST be present when supports_idempotency_key is true. Range 86400 (24h) to 604800 (7d).

3.3 Forward-compatible additions

The capabilities document is additionalProperties: true to support forward compatibility — future versions of ACDP will add capability flags here as new features become available. Consumers MUST tolerate unknown fields.

Implementer note. The CapabilitiesDocument model MUST be deserialized with unknown-field tolerance enabled. Concrete patterns:

  • Rust (serde): add #[serde(flatten)] pub extensions: serde_json::Map<String, serde_json::Value> (or a typed BTreeMap<String, Value>) to capture unknown keys; do NOT annotate the struct with #[serde(deny_unknown_fields)].
  • Python (pydantic v2): set model_config = ConfigDict(extra="allow") on the capabilities model, OR keep the model loose and operate on dict[str, Any] for unknown keys.
  • Python (dataclasses or attrs): keep an explicit catch-all field (e.g. extensions: dict[str, Any] = field(default_factory=dict)) and route unknown keys into it.
  • TypeScript: no action needed by default — object types are open. Runtime decoders (zod, valibot) MUST use a passthrough or partial-strict mode (e.g. zod's .passthrough()); decoders configured to strip or fail unknown keys MUST NOT be used.
  • Go: unmarshalling into map[string]any or a struct with an Extensions json.RawMessage field both work; do NOT use json.UnmarshalDisallowUnknownFields.

Libraries that throw, panic, or strip unknown fields will break silently the next time ACDP adds a capability flag — for example, when push subscriptions ship in a future version, registries will start advertising supports_push_subscriptions: true, and a strict-decoder consumer will fail to read the document at all. The same forward-compat policy applies to the status field on registry state (RFC-ACDP-0004 §4.1).

3.3.1 Schema openness map (NORMATIVE)

ACDP uses a mix of CLOSED schemas (additionalProperties: false, used for tightly defined wire shapes where unknown fields signal a bug) and OPEN schemas (additionalProperties: true, used where forward compatibility matters). Consumers and registries MUST honor each schema's openness exactly as documented; treating a closed schema as open masks bugs, and treating an open schema as closed breaks forward compatibility.

SchemaOpennessadditionalProperties
acdp-publish-request.schema.jsonClosedfalse
acdp-publish-response.schema.jsonClosedfalse
acdp-search-response.schema.jsonClosedfalse
acdp-error.schema.jsonClosedfalse
acdp-error.schema.json (error.details)Opentrue
acdp-data-ref.schema.json (DataRef root object)Opentrue
acdp-data-ref.schema.json (embedded sub-object)Closedfalse
acdp-data-ref.schema.json (structured location object)Opentrue
acdp-context-body.schema.json (Body for retrieval)Opentrue
acdp-capabilities.schema.json (top level)Opentrue
acdp-capabilities.schema.json (limits sub-object)Closedfalse
acdp-context.schema.json (full retrieval envelope)Opentrue
acdp-registry-state.schema.jsonOpentrue
match_summary (in acdp-common.schema.json)Closedfalse
signature (in acdp-common.schema.json)Closedfalse
data_period (in acdp-common.schema.json)Closedfalse

Conformant consumers MUST reject deserializing a closed-schema object that contains fields not defined in the schema (schema_violation). Conformant consumers MUST NOT reject deserializing an open-schema object that contains unknown fields. The fixtures pin specific instances of both rules:

  • Open (tolerate): caps-006/schema-004 (capabilities document top level), can-008 (unknown field at the body root), can-010 (unknown field inside a data_refs[] entry).
  • Closed (reject): pub-007/schema-002 (publish response — forbid extras like content_hash), schema-001 (search response — forbid results), schema-003 (DataRef embedded sub-object), schema-008 (signature object), schema-009 (data_period object), schema-010 (capabilities limits sub-object).

The table above governs every shape across the schema set; the fixtures are representative, not exhaustive.

3.4 Example

{
  "acdp_version": "0.1.0",
  "registry_did": "did:web:registry.example.com",
  "supported_signature_algorithms": ["ed25519"],
  "supported_did_methods": ["did:web"],
  "read_authentication_methods": ["http_signatures"],
  "anonymous_public_reads": true,
  "supports_idempotency_key": true,
  "profiles": ["acdp-registry-core", "acdp-registry-discovery"],
  "limits": {
    "max_payload_bytes": 1048576,
    "max_embedded_bytes": 65536,
    "idempotency_key_ttl_seconds": 86400
  }
}

3.5 Consumer validation checklist (NORMATIVE)

After fetching /.well-known/acdp.json, consumers and cross-registry resolvers MUST validate the following before relying on the document. Schema validation alone is necessary but not sufficient — the items marked (value) below are not enforceable by the JSON Schema in all toolchains, so implementations MUST verify them in code.

  1. acdp_version matches the semver pattern ^\d+\.\d+\.\d+$.
  2. registry_did is a valid DID. For v0.1.0 registries, registry_did MUST be did:web:<authority>, and <authority> MUST equal the hostname the capabilities document was fetched from. (value, cross-field)
  3. supported_signature_algorithms MUST contain "ed25519".
  4. supported_did_methods MUST contain "did:web".
  5. profiles MUST contain "acdp-registry-core".
  6. limits.max_embedded_bytes MUST equal 65536.
  7. limits.max_payload_bytes MUST be >= 1024.
  8. If supports_idempotency_key is true, limits.idempotency_key_ttl_seconds MUST be present and in the inclusive range 86400..604800 (24h to 7d).
  9. If the registry serves any non-public contexts, read_authentication_methods MUST be non-empty (RFC-ACDP-0008 §6.2). (value, cross-field)

A consumer encountering a capabilities document that fails any of the checks above MUST NOT proceed with the operation that required fetching capabilities (publish, retrieval, cross-registry resolution). Implementations SHOULD surface the failing check to operators so the registry can be corrected. The conformance fixtures caps-001..006 (schemas/conformance/caps-001-valid-minimal.json through caps-006-extra-top-level-field.json) pin representative positive and negative payloads for the checklist.

3.5.1 Implementer note: validate capabilities at server construction time

The conditional and cross-field constraints above (registry_did must bind to the serving authority; limits.max_embedded_bytes is fixed at 65536; limits.idempotency_key_ttl_seconds is REQUIRED when supports_idempotency_key = true and is bounded to [86400, 604800]) are enforceable in code but not by JSON Schema in all toolchains. Registry implementers SHOULD validate the capabilities document they intend to serve at server construction time, not at runtime per request. A server that runs the §3.5 checklist once at startup cannot silently start serving with misconfigured limits, a mismatched registry_did, or a missing idempotency TTL. Recommended pattern (pseudocode, illustrative):

server = RegistryServer.try_new(store, caps, authority)
  # raises a typed configuration error at startup if any of the following hold:
  # - caps.registry_did != "did:web:" + authority
  # - "ed25519" not in caps.supported_signature_algorithms
  # - "did:web" not in caps.supported_did_methods
  # - "acdp-registry-core" not in caps.profiles
  # - caps.limits.max_embedded_bytes != 65536
  # - caps.limits.max_payload_bytes < 1024
  # - caps.supports_idempotency_key == true AND
  #     (caps.limits.idempotency_key_ttl_seconds is None
  #      or not 86400 <= caps.limits.idempotency_key_ttl_seconds <= 604800)
  # - caps serves any non-public visibility AND caps.read_authentication_methods is empty

Concrete idioms:

  • Rust: a RegistryServer::try_new(store, caps, authority) -> Result<Self, ConfigError> constructor that enumerates the checks above and returns a typed error per failure. Library APIs SHOULD prefer this over an infallible RegistryServer::new followed by per-request validation.
  • Python: raise ValueError (or a typed RegistryConfigError) from the registry application factory before binding the listening socket.
  • Go: return a non-nil error from the NewRegistryServer constructor; callers log.Fatal rather than starting http.ListenAndServe.
  • TypeScript: throw from the constructor or factory function before the framework starts listening.

The principle is uniform across languages: a misconfigured registry MUST refuse to start. Per-request validation is a defense-in-depth fallback for configuration that mutates at runtime (rare) and for capabilities documents fetched from external sources (consumer-side §3.5).

3.6 Caching

The capabilities document is moderately stable. Registries SHOULD set:

Cache-Control: public, max-age=3600

Consumers MUST refresh the capabilities document at least daily, and MUST refresh on receipt of any error code that suggests version drift (e.g. unsupported_algorithm for an algorithm the consumer believed was supported).


4. Error Envelope

All error responses use the following structure, conforming to schemas/json/acdp-error.schema.json:

{
  "error": {
    "code": "...",
    "message": "Human-readable description",
    "details": {}
  }
}
FieldTypeRequiredDescription
error.codestringYesA machine-readable error code from §5.
error.messagestringYesHuman-readable description, suitable for logs. MUST NOT be used for automated decisions.
error.detailsobjectNoOptional structured details. Shape is error-code-specific.

Content-Type MUST be application/acdp+json. The HTTP status code is per the table in §5. The error envelope MUST be returned for every failure response from an ACDP endpoint, including 4xx, 5xx, 501 Not Implemented, and 502 Bad Gateway responses. Registries MUST NOT return empty bodies, framework default error pages, or non-application/acdp+json content types on ACDP endpoints. The corresponding fixture is err-001-internal-error.json (illustrating a 500 envelope).


5. Error Code Registry

The full registry is maintained in registries/error-codes.md. The codes defined by v0.1.0:

CodeHTTPMeaningSource
invalid_signature400Signature verification failed.RFC-ACDP-0001 §5.8, RFC-ACDP-0003 §2.1
hash_mismatch400The body's content_hash (over ProducerContent) does not match the canonicalized body.RFC-ACDP-0001 §5.7, RFC-ACDP-0003 §2.1
data_ref_hash_mismatch400A DataRef's bytes do not match the producer-declared data_ref.content_hash. Returned by a registry at publish time when an embedded data_ref.content_hash does not match the decoded embedded.content (RFC-ACDP-0002 §6.6 Check 8). Also the code a consumer SHOULD surface when it fetches an external data_ref.location and the retrieved bytes do not match data_ref.content_hash (RFC-ACDP-0002 §6.5). The body's own content_hash and signature are still valid — the integrity failure is at the data-reference level, not the body level.RFC-ACDP-0002 §6.5, §6.6
schema_violation400Request body or query failed structural validation.RFC-ACDP-0003 §2.1
not_authorized403Agent lacks permission for the operation. Returned for supersession by a different agent_id, and for unauthenticated reads on a registry that does not advertise anonymous_public_reads.RFC-ACDP-0003 §3.1, RFC-ACDP-0008 §6.3
not_found404Resource not found. (Also returned for visibility-restricted contexts to non-audience requesters; see RFC-ACDP-0008 §4.5.)RFC-ACDP-0004 §7
superseded_target400 / 409The supersedes target is invalid. details.reason provides specifics. HTTP 400 for static violations (not_found, lineage_mismatch, cross_registry_supersession_unsupported, lineage_walk_failed); HTTP 409 Conflict for race conditions (already_superseded, version_mismatch).RFC-ACDP-0001 §5.6.1, RFC-ACDP-0003 §2.1 steps 9–10, §3.1
unsupported_algorithm400Signature algorithm not in the registry's supported_signature_algorithms.RFC-ACDP-0001 §5.10, RFC-ACDP-0003 §2.1 step 5
rate_limited429Per-agent rate limit exceeded.RFC-ACDP-0008 §4.3
payload_too_large413Request body exceeds limits.max_payload_bytes.RFC-ACDP-0003 §2.1 step 2
embedded_too_large413An embedded data reference exceeds 64 KB.RFC-ACDP-0002 §6.3, RFC-ACDP-0003 §2.1 step 3
key_resolution_failed400The signing key referenced by signature.key_id could not be resolved due to a permanent condition: the DID document parsed successfully but does not contain the requested key fragment; the fragment is missing from key_id; or the producer DID resolves to a network target forbidden by SSRF policy (RFC-ACDP-0008 §4.8). Producer error; not retryable.RFC-ACDP-0003 §2.1 step 6, RFC-ACDP-0008 §4.8
key_resolution_unreachable502The signing key could not be resolved due to a transient condition (DNS failure, TLS error, HTTP non-2xx, network timeout fetching the DID document). Retryable with backoff.RFC-ACDP-0003 §2.1 step 6
key_not_authorized403The DID portion of signature.key_id does not equal body.agent_id, or the resolved verification method is not in the DID document's assertionMethod array.RFC-ACDP-0003 §2.1 step 6
not_implemented501Endpoint or capability not implemented by this registry. Returned with the standard error envelope. Emitted when the requested endpoint requires a profile this registry does not advertise (e.g., GET /contexts/search on a registry that does not declare acdp-registry-discovery in profiles). All acdp-registry-core endpoints are mandatory and MUST NOT return not_implemented.RFC-ACDP-0001 §9.1, RFC-ACDP-0007 §4
cursor_expired400A previously-issued pagination cursor is no longer valid. Client SHOULD restart pagination.RFC-ACDP-0005 §2.5.4
invalid_cursor400A pagination cursor is malformed or unrecognized.RFC-ACDP-0005 §2.5.4
duplicate_publish409An idempotent publish was retried with conflicting content (same Idempotency-Key, different content_hash).RFC-ACDP-0003 §6.2
cross_registry_resolution_failed502A cross-registry resolution failed (DNS resolution refused, response oversize, timeout, redirect-policy violation, or upstream registry unavailable).RFC-ACDP-0006 §7
internal_error500The registry encountered an unexpected internal condition. The standard error envelope MUST be used; error.message MUST NOT leak stack traces or sensitive context. Retryable.RFC-ACDP-0007 §4

Reserved codes (not in this table or the v0.1.0 wire enum): immutable_field is reserved for a future version's mutation endpoints (retraction, attestation updates — see RFC-ACDP-0009 §2.1). unsupported_embedding_model is reserved for a future version's similarity endpoints (see RFC-ACDP-0009 §2.9). Implementations MUST NOT emit either in v0.1.0 responses.

Distinguishing hash failures. Three failure codes can arise from integrity checks; implementations MUST keep them distinct so consumers can react correctly:

  • hash_mismatch — the body's ProducerContent hash, recomputed by the registry or consumer, does not match body.content_hash. The body's signed content cannot be verified; the body is untrusted.
  • data_ref_hash_mismatch — a DataRef's fetched (external) or decoded (embedded) bytes do not match the producer-declared data_ref.content_hash. The body itself is cryptographically valid; only the referenced data has diverged from what the producer signed. Returning hash_mismatch here would wrongly imply the whole body failed verification; returning invalid_signature would wrongly imply a signature-verification failure.
  • invalid_signaturesignature.value does not verify against the resolved public key. The body's authorship cannot be established.

A registry emits data_ref_hash_mismatch only at publish time, for embedded data (embedded.content_hash mismatch — RFC-ACDP-0002 §6.6 Check 8). For external data_refs[].location, hash verification happens consumer-side after fetch (RFC-ACDP-0002 §6.5); a consumer SHOULD surface the mismatch with the same data_ref_hash_mismatch semantic in its own diagnostics, logs, or API surface.

5.1 Adding a code

New codes are added via the RFC process. Codes MUST be lowercase snake_case. Codes MUST NOT collide with existing entries.

5.2 Information leakage

Registries MUST NOT reveal which specific policy check failed beyond the registered code. The error.message string is informational only and MUST NOT be used in automated decision-making by consumers.

For visibility-restricted contexts, registries MUST return not_found (HTTP 404) — they MUST NOT distinguish "not found" from "not authorized" externally. The internal label visibility_denied MAY be used in registry logs or metrics for auditing purposes but MUST NOT appear in wire responses.

5.3 SDK guidance — data_ref_hash_mismatch vs body hash mismatch

The error-code table and the "Distinguishing hash failures" note above are normative for the wire. This section is implementation guidance for SDK authors who expose a verification API to application code.

SDKs exposing verification APIs MUST distinguish data_ref_hash_mismatch from body-level hash and signature failures:

  • Report data_ref_hash_mismatch when a DataRef's bytes — fetched from an external data_ref.location (RFC-ACDP-0002 §6.5) or decoded from data_ref.embedded (RFC-ACDP-0002 §6.6 Check 8) — do not match the producer-declared data_ref.content_hash.
  • An SDK MUST NOT report this case as invalid_signature. invalid_signature implies the producer's key or signature failed; here the signature is intact.
  • An SDK MUST NOT collapse this case into hash_mismatch. hash_mismatch implies the entire body's ProducerContent hash failed and the body is unverifiable; here the body is fully verifiable.

A data_ref_hash_mismatch indicates the body remains cryptographically valid: the producer signed the hash of the data they intended to reference, but the data at that location has since changed (external case) or was mis-encoded (embedded case). It is an integrity failure at the data layer, not the body layer. A verification result object SHOULD therefore carry the body-level verdict and the data-ref-level verdict as separate fields, so an application can decide — for example — to trust the body's metadata and derived_from lineage while treating one stale DataRef as unusable.

The data-ref-007 fixture pins the embedded case (registry-side, publish time). The data-ref-008 fixture pins the external case (consumer-side, fetch time): the body signature MUST still verify, and the SDK SHOULD surface data_ref_hash_mismatch, not invalid_signature.


6. Security Considerations

See RFC-ACDP-0008 Security. Specific to capabilities and errors:

  • The capabilities document MUST be served over TLS.
  • Registries SHOULD include the same registry_did value across all responses to avoid identity confusion.
  • Error messages MUST NOT echo unsanitized request content (defends against XSS in registry-served clients and injection into log pipelines).
  • Rate-limit responses (rate_limited) SHOULD include Retry-After headers when bounded.

7. References