Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
Informative note (25/10/2026). Each entry describes a change as it was made, including the successive entries under
[Unreleased], where a later entry can refine or replace an earlier one. An entry is not a guarantee of current behaviour, nor of a package version being available in a registry. For current behaviour, see the README, the documentation, the specifications and the API reference.
Published versions (26/10/2026). npm (13 packages, including @dcp-ai/sdk 2.2.0), Python dcp-ai 2.10.0, Go v2.11.0 and Rust dcp-ai 2.11.0 are published, including the verifier fixes, admission and the experimental opt-in APIs; dcp-jcs-v1 remains the normative default. See Published versions. Entries below retain their development history; [Unreleased] does not mean these features are unpublished. The six patch releases of 28/10/2026 are in their own section, the five releases of 05/11/2026 in theirs, and the six releases of 09/11/2026 in theirs.
[2026-11-09] Six releases¶
Published on 09/11/2026 from main (cdb7099b, #140). The npm tarballs are the files tested before publication, with the same sha512 integrity; the PyPI wheel and sdist and the crate were rebuilt from main by the release workflows and contain the same source files as the tested artifacts (the Python files differ only in metadata: Metadata-Version 2.5, hatchling 1.33.0 and the recomputed RECORD; the crate adds .cargo_vcs_info.json, which names main); the Go module is the tag sdks/go/v2.14.0 on main, with the same files and h1 as the tested module. See Published versions.
| Package | Published | Previous |
|---|---|---|
@dcp-ai/sdk |
2.4.0 | 2.3.0 |
dcp-ai (PyPI) |
2.11.0 | 2.10.2 |
Go sdks/go/v2 |
v2.14.0 | v2.13.0 |
Rust dcp-ai |
2.14.0 | 2.13.0 |
@dcp-ai/wasm |
2.2.0 | 2.1.3 |
@dcp-ai/cli |
2.2.0 | 2.1.1 |
- Added (opt-in). Delegated admission, admission from bytes and mandate acknowledgment in the TypeScript, Python, Go and Rust SDKs, and the raw WASM export
wasm_canonicalize_v2_result; see[Unreleased]. - Acceptance change. Ed25519 P-ED-1 in the four SDKs, the WASM build and
dcp-ai verify: a public key or R that is not a canonical encoding, or is of order dividing 8, is rejected. Keep failing evidence as it is; sign again with the same key when it meets P-ED-1, rotate a non-canonical public key and replace a key of small order. - Fixes. WASM
hashObjectandderiveKidthrow instead of returning an error envelope as a value, andcanonicalizetells an{ error: … }value from an error; Go reads SLH-DSA-192f keys without a panic. - Packaging and dependencies. The
@dcp-ai/wasmbinary is built without the builder's local paths, and a release gate checks the packed files.@dcp-ai/cliincludesLICENSE, requires@dcp-ai/sdk^2.4.0and reads its version frompackage.json. The Rustotlpfeature requiresanyhow1.0.103 andtokio1.53.3 or later (RUSTSEC-2026-0190, RUSTSEC-2026-0386); the default andwasmbuilds select neither. - Unchanged. The wire format, the signatures, the profiles,
dcp-jcs-v1and the experimental opt-indcp-jcs-v2APIs. The npm adapters get no new version:@dcp-ai/express2.1.0 keeps its published files, which include noLICENSE, and a new installation resolves@dcp-ai/sdk2.4.0 through its^2.2.0range. TheLICENSEfile added tointegrations/expressis in the repository only.
[2026-11-05] Auditable releases¶
Published on 05/11/2026 after review as auditable candidates. The npm tarballs are the files tested before publication; the PyPI wheel and sdist and the crate were rebuilt from main by the release workflows and contain the same files as the tested artifacts (the crate's .cargo_vcs_info.json names main); the Go module is the tag sdks/go/v2.13.0 on main. See Published versions.
| Package | Published | Previous |
|---|---|---|
@dcp-ai/sdk |
2.3.0 | 2.2.1 |
dcp-ai (PyPI) |
2.10.2 | 2.10.1 |
Go sdks/go/v2 |
v2.13.0 | v2.11.1 |
Rust dcp-ai |
2.13.0 | 2.11.1 |
@dcp-ai/wasm |
2.1.3 | 2.1.1 |
- Contents. The SDK changes made on
mainafter the published versions (#103, #106, #107, #112 and #116 to #119), described in[Unreleased], in each package README and in the release notes for npm, Python, Go and Rust. - Acceptance changes. Every V2 verifier runs V2-2 schema validation, so schema-invalid bundles accepted before fail with
schema: <pointer> <keyword>diagnostics. Gov2.VerifyPayloadHashaccepts only the prefixed hash thatSignPayloadwrites. The mandate validity helpers follow one contract, and mandate chains reject a root mandate that repeats a domain. WASMdualMerkleRootfollows V2-11; roots computed before stay as recorded (migration). - Unchanged. The wire format, the signatures, the profiles,
dcp-jcs-v1as the normative default and the experimental opt-indcp-jcs-v2APIs. - Packaging. The npm tarballs, the wheel, the sdist and the crate now include the Apache-2.0
LICENSE; the Go module zip already had it. - CLI and adapters.
@dcp-ai/cliand the npm adapters get no new version; npm packages says what was checked against@dcp-ai/sdk2.3.0. - Earlier candidates. These versions supersede the candidates of 30/10/2026 below (Go v2.12.0, Rust 2.12.0,
@dcp-ai/wasm2.1.2), which were never published; those numbers are not reused. - Package READMEs. The README inside each published archive is the one prepared before publication and reads "prepared, not published"; published archives cannot change. The READMEs in this repository describe the published versions.
Prepared, not published: Go v2.12.0, Rust 2.12.0 and @dcp-ai/wasm 2.1.2¶
Prepared in the repository on 30/10/2026 and not published: there is no sdks/go/v2.12.0 tag, and crates.io and npm do not have these versions. At preparation time, Published versions and the installation commands kept naming the registry versions of then (Go v2.11.1, Rust dcp-ai 2.11.1, @dcp-ai/wasm 2.1.1); the current ones are in Published versions. The changes are described in [Unreleased] and in the release notes for Go, Rust and npm.
- Go v2.12.0: the acceptance change of
v2.VerifyPayloadHash(the output ofSignPayloadis accepted and a bare digest is rejected; it checks the hash only) and the additivev2.RawAuditEventV2.SDKVersionis2.12.0. - Rust
dcp-ai2.12.0: the additivev2::types::RawAuditEventV2andRawAuditEventError, and, with thewasmfeature, the correctedwasm_dual_merkle_root. Telemetry reads the package version. @dcp-ai/wasm2.1.2:dualMerkleRootandwasm_dual_merkle_rootcompute the V2-11 dual Merkle root; no JavaScript API is added.- New tests of the experimental candidate (gate 7) change no default.
dcp-jcs-v1, the profiles, the wire format, the dependencies and the cryptography are unchanged. The three packages no longer include the translated READMEs, which are underi18n/.
Note (05/11/2026). These candidates date from 30/10/2026 and were never published. Later changes on
mainare not part of them: V2-2 schema validation in every V2 verifier (#116) and the mandate chain checks (#117 to #119). The releases of 05/11/2026 contain those changes and supersede these; the manifests onmaincarry their version numbers, and 2.12.0 and 2.1.2 are not reused. See the audit dossier.
[2026-10-28] SHA3 patch releases¶
Published on 28/10/2026: @dcp-ai/sdk 2.2.1, @dcp-ai/wasm 2.1.1 and @dcp-ai/cli 2.1.1 on npm, dcp-ai 2.10.1 on PyPI, the Go module v2.11.1 (tag sdks/go/v2.11.1) and Rust dcp-ai 2.11.1 on crates.io. The other ten npm packages are unchanged. See Published versions and the release notes for npm, Python, Go and Rust.
Secondary SHA3-256 verification¶
- Acceptance change: the secondary SHA3-256 fields are verified when present (developed on 27/10/2026).
The TypeScript, Python, Go and Rust V2 verifiers, and the WASM build from the Rust SDK, now check
audit_merkle_root_secondary(the SHA3-256 tree built like the primary one;sha3-256:followed by 64 zeros for an empty trail), eachprev_hash_secondary("GENESIS"on the first entry, then the SHA3-256 of the entry before it as carried) and eachintent_hash_secondary(the SHA3-256 of the intent payload), asspec/VERIFICATION.mdV2-11 and its erratum of 27/10/2026 state. Absent fields are not required, so a verified bundle may still lack a complete secondary chain; a present field that isnull, not a string or wrong is rejected. Admission and the experimental candidate routes reuse these checks; no API, result field, canonicalization orhash_alghandling changes. Theprev_hash_secondaryschema pattern also accepts"GENESIS", which OpenClaw writes on the first entry. Shared vectors:tests/interop/v2/secondary_sha3_vectors.json. Earlier versions do not check these fields. The published adapters@dcp-ai/express,@dcp-ai/openclaw,@dcp-ai/anthropic-mcpand@dcp-ai/google-a2a2.1.0 were not republished: they depend on@dcp-ai/sdk^2.2.0and get the secondary checks once your lockfile resolves@dcp-ai/sdk2.2.1 or later.
Package metadata¶
@dcp-ai/cli2.1.1 requires@dcp-ai/sdk^2.2.1, anddcp-ai --versionreports2.1.1instead of the stale1.0.0.- Telemetry reports the new versions: Go
SDKVersion2.11.1; the TypeScript, Python and Rust SDKs read their package version.
[Unreleased]¶
Security¶
-
P-ED-1 in the reference verifier, the CLI and the playground (local source, not in a published version; no version bump).
verifyObjectintools/crypto.jsnow checks P-ED-1 before TweetNaCl, as the SDKs do. Through it, so do the reference V1 path oflib/verify.jsandbin/dcp.js, the transparency-log gossip and the revocation service's Ed25519 check.dcp-ai verifyon V1 files and the playground'sverifyDetachedcheck it too. Each uses one of three identical copies of a byte helper:tools/,cli/lib/andplayground/js/core/ed25519-ped1.js. Acceptance change: a key or R that is not a canonical encoding, or is of order dividing 8, was accepted before and now returnsfalse; the CLI prints INVALID and exits 1. Errors and their order do not change. Three earlier tests changed only their expectations for the identity key. The published@dcp-ai/clidoes not include this, nor does any published@dcp-ai/sdk(2.3.0 and earlier), so the copies installed from them do not either. -
Ed25519 acceptance criterion P-ED-1 (local source of the TypeScript, Python, Go and Rust SDKs, not in a published version; no version bump). V1
verifyObjectand its counterparts, and the default V2 Ed25519 provider, now also reject a signature whose public key or R is not a canonical encoding, or is one of the 8 points of order dividing 8 (the identity included), besides S >= L. The libraries still ensure valid points and [S]B = R + [k]A with k reduced mod L, without cofactor (for R, some by comparing the canonical encoding of [S]B − [k]A with the received bytes); the new checks compare bytes before them. Points with a torsion component that are not of small order are still accepted. This is an intentional change of acceptance: TypeScript, Go and Rust accepted some of these signatures before, and the official PyNaCl build already rejected them. A failed check returnsfalse; inputs that raised an error still raise the same one. Signers, formats, contexts,dcp-jcs-v1, schemas and APIs do not change, andpq_onlyverifies no component it did not verify before. Migration: keep failing evidence as it is, never re-encode or reduce signed bytes; sign again with the same key when the key meets P-ED-1, rotate a non-canonical public key, and replace a key of small order. Not yet applied by the reference V1 verifier, the V1 CLI copy or the playground (S < L only); the reference V2 verifier, server and CLI change only when the SDK is rebuilt and reinstalled; WASM is compiled from the same Rust code but not run. Spec:spec/profiles/crypto/README.md; vectors:tests/interop/v2/ed25519_ped1_vectors.json. -
DCP-05 to DCP-09 demo endpoints off by default, operator-only when enabled (source on
main, not in a published version;server/). The reference server's 31 schema-only endpoints for lifecycle, succession, disputes, rights and delegation records needed no token. Every request under their twelve path families now gets 404 unless the server starts withDCP_DEMO_ENDPOINTS=dcp-05-09, which needs anOPERATOR_TOKENof at least 32 RFC 6750 b64token characters (an invalid setting stops the server); enabled, each request, GET included, needs exactlyAuthorization: Bearer <OPERATOR_TOKEN>, checked before the body is read. The endpoints still verify no signature and establish no principal authority. Decommissioning no longer changes a registered passport's status. The capabilities report the DCP-05 to DCP-09 features asfalseand describe the demo infeatures.dcp05_09_demo_endpointsanddemo_endpoints. Migration: setDCP_DEMO_ENDPOINTS=dcp-05-09and a longOPERATOR_TOKEN, and send the bearer token with every request. -
Simulated HSM provider removed (source on
main, not in a published version).HsmCryptoProviderandHsmProviderFactory(sdks/typescript/src/providers/hsm-provider.ts) had no hardware backend:initialize()reported an authenticated session without loading anything, andverifyaccepted any signature of the right length. They were not exported from the package entry point or registered; code that imported the file directly no longer can, and the public API of@dcp-ai/sdkdoes not change.dcp audit gapscounted Gap #11 closed because the file existed; it now reports it as open whatever files exist. The reference server's/.well-known/dcp-capabilities.jsonadvertiseshsm_provider: false. -
Transparency log: split views are told apart from independent logs (source on
main, not in a published version;services/transparency-log/). Gossip compared every pinned peer's tree head with the local tree, so two independent logs that pin each other alerted as soon as they had the same size and different entries. It kept no history of a peer's heads to compare one view of it with another; a log showing two roots for one size could only trip that comparison by accident. - What changes: a log is identified by its pinned key and exact
log_id, and each log keeps, per witnessed log, the first head of each size. An alert means that one log signed two roots for one size, seen by polling, throughPOST /gossip/exchange, or through two aliases of it. Heads of different logs are never compared, and the local tree only with this log's own heads. History is bounded (256 sizes per log, 8 logs per pinned key), dropped when no alias pins the key any more, and lost on a restart; a poll whose alias changed while it waited is discarded. - Alerts: new fields
peer_public_key,observed_root,sourceandevidence(both signed heads, the signed fields andsignatureonly).local_rootis kept as a deprecated name ofobserved_root. Deduplication is by log, size and pair of roots, among the 100 alerts kept.api/openapi.yamldocuments the alert and the evidence, types the roots as[string, "null"], and now describes the tree head signature as Ed25519 andpublic_keyas the base64 Ed25519 key. - Unchanged: the tree head format and signature, the endpoints and their status codes, the pinned keys, the 16 KB and timeout limits, non-overlapping polls, the environment variables, the SDKs, profiles and canonicalization. There is still no replication between logs and no consistency proof.
- Acceptance change: a domain repeated in a root mandate (source on
main, not in a published version). The chain verifiers (verifyMandateChain,verify_mandate_chain,VerifyMandateChain) now apply the last sentence of DCP-09 §3.5.2 rule 5, a domain that appears twice in one mandate is not comparable, to every mandate of the chain, the root included. Domains are compared as the exact strings the JSON decodes to, with no case folding, trimming or Unicode normalization, as before. - What changes: a chain made of a root mandate alone whose
authority_scoperepeats a domain, with the same or with different actions, was accepted by the four SDKs; it is now rejected. Acceptance only narrows. - Diagnostics: a repeated domain is now reported among the errors of that mandate's own fields, once per domain,
as
mandate[i]: authority_scope domain "…" is duplicated. A root that repeats a domain and has sub-mandates was already rejected, atmandate[1]with… is duplicated in the parent; it is now reported atmandate[0], and that message is no longer produced. A sub-mandate that repeats a domain was already rejected with the same message, which now comes before its link, attenuation and signature checks, so their errors are no longer listed with it. These texts and their order are the SDKs' diagnostics, not a protocol requirement. - Unchanged: the wire format, canonicalization, signatures and contexts, the hybrid policy, trust, attenuation between a mandate and its parent, revocation, the tier floor, the validity helpers and the chain times. WASM exports no chain function.
- Shared vectors:
tests/interop/v2/mandate_domain_vectors.json, 23 signed chains (8 to accept, 15 to reject), run by the four SDK suites. The verdicts follow rule 5; the exact error lists are a regression of these SDKs. - Acceptance change: mandate chain times (source on
main, not in a published version). The chain verifiers (verifyMandateChain,verify_mandate_chain,VerifyMandateChain) now read every time the same way. Validity windows, their nesting in the parent's window and theexpires_atof the signer keys of both slots are RFC 3339 instants of the subset the SDKs already parse, compared in milliseconds, both limits included. The verification time is floored to the millisecond, also before the epoch. - What changes: Python read these times as
datetime, so it rejected year 0000 and any instant an offset moves out of years 1 to 9999, which TypeScript, Go and Rust accept. It also compared them with anowin microseconds. Go compared with anowin nanoseconds and TypeScript with an unfloored numericnow, so a key or window ending in the same millisecond as the verification time could be read as expired. Anowof another type made TypeScript throw and Python raise; both now returnverification time is not an instant. - Acceptance: it widens in two places, under the common contract and without a normative change. With the floor, a verification time inside the last millisecond of a window or of a key's validity, which a finer clock rejected, is accepted. Python now accepts year 0000 and offsets that cross year 1 or 9999, as the other SDKs do.
- Unchanged: the clock conventions, which differ from the validity helpers: Python reads a naive
datetimeas UTC, and Go reads the zero time as the current time. Also unchanged: the error messages, signatures, contexts, hybrid policy, trust, attenuation, revocation and the validity helpers. Rust already read times this way, and WASM exports no chain function. - Shared vectors:
tests/interop/v2/mandate_chain_time_vectors.json, 22 signed chains (9 to accept, 13 to reject), run by the four SDK suites. - Acceptance change: mandate validity helpers (source on
main, not in a published version).verifyMandateValidity(TypeScript),verify_mandate_validity(Python),VerifyMandateValidity(Go) andverify_mandate_validity(Rust) now apply one contract. The mandate is an object with a non-emptymandate_id, and revocation is an exact match.valid_fromandvalid_untilare RFC 3339 instants withZor±hh:mm, read by each SDK's existing strict parser, compared in milliseconds and never as text.valid_frommust not be aftervalid_until, and the verification time lies within the window, both limits included. The first failure is thereason, and a value of the wrong type gives a result, not an exception. - What changes: TypeScript and Go accepted a missing or malformed date. Python raised, and accepted a space
separator. Rust compared the dates as text, so offsets and fractions could be ordered wrongly in either
direction. The input contract changes too: Python requires the revoked IDs to be a
collections.abc.Set(aset, afrozensetor a dict keys view), so a list or a string, tolerated before, is refused (inon a string matches substrings). - Date format: the subset the SDKs already parse (
tests/interop/v2/interop_vectors.json#rfc3339), not all of RFC 3339: uppercaseTandZ, a real calendar date, no second 60, and fraction digits beyond milliseconds dropped. - Clock: the verification time can be injected: TypeScript
now(aDateor milliseconds), Pythonnow=(an awaredatetime; a naive one is refused), GoVerifyMandateValidityAt(nowtaken as given) and Rustverify_mandate_validity_at(milliseconds). The existing calls keep the current time. - Shared vectors:
tests/interop/v2/mandate_validity_vectors.json, 94 cases. TypeScript, Python and Rust run all of them; Go runs 90 and skips by name the 4 whose mandate is neither a JSON object nornull, which its typed map cannot take. - WASM: it exposes the
DelegationMandatetype only, no helper. - Scope: the helpers check no signature, schema, scope or chain and authorize nothing. The chain verifier, admission, signatures, profiles, dependencies and versions are unchanged.
- Acceptance change: V2-2 schema validation in every SDK verifier (source on
main, not in a published version). The V2 verifiers of the TypeScript, Python, Go, Rust and WASM SDKs (verifySignedBundleV2and its counterparts, the combined admission API, which verifies first, and the experimental candidate verifier; evaluating admission from an earlier result does not run it) validate the signed bundle envelope and the four payloads againstschemas/v2after the policy and clock checks and before checking the manifest, payload hashes, keys and signatures. Every SDK interprets the same representation generated from the schemas (tools/schema-ir/), which CI regenerates and compares. Schema-invalid bundles they accepted before now returnverified: false, among them unknown members in closed objects andnullsession times in the manifest. The errors are thenschema: <JSON pointer> <keyword>[ <member>]diagnostics (at most 16 and a truncation marker) without cryptographic details, so some rejections change wording: CI judges the evidence catalog's SDK runs on the verdict (--assessment verdict), and the committederror_containsstrings are unchanged.date-timekeeps the ajv-formats 3.0.1 boundary of the reference verifier, which is not strict RFC 3339. No public API, schema, profile, dependency or version changes. In the experimental phase 4 matrix the 50 results ofS09are now expected refusals. See SECURITY_COVERAGE.md. - TypeScript
VerificationCache(source onmain, not published;@dcp-ai/sdk2.2.1 keeps the previous behaviour). The cache key stays the bundle hash for existing calls, and an opt-incontextchosen by the caller separates results under tagged internal keys that a legacy hash cannot collide with.invalidateByAgent, which removed nothing, now removes the entries stored with that agent in the newagentIdsoption and every entry stored without one. Replacing an entry no longer leaves its old kids in the index (with the default bound the index grew without limit) or evicts another entry; the kids ofresult.details.signer_keysare indexed too;''as a hash no longer disablesmaxEntries. Acceptance changes: invalid options (NaN,Infinity, negative or non-number TTLs, TTLs aboveNumber.MAX_SAFE_INTEGER, amaxEntriesthat is not a safe integer from 1 toNumber.MAX_SAFE_INTEGER) throw instead of disabling expiry or the bound;setcopies the result andgetreturns a new copy on every hit; an entry expires at exactly its TTL, measured on the monotonic clock as well as the wall clock, so a clock step back no longer extends it; a TTL of 0 is never stored;setvalidates its arguments (a hole insignerKidsoragentIdscounts asundefined) and changes nothing when it throws. A hit still does not verify the evidence or trust again and is not admission, and nothing is invalidated automatically. See API_REFERENCE.md. - Acceptance change: Ed25519 signatures with S >= L are now rejected by the TypeScript SDK
(
Ed25519Provider.verify, hence every V2 and composite signature,@dcp-ai/expressand@dcp-ai/openclaw; andverifyObject, henceverifySignedBundleV1) and by the reference verifier (tools/crypto.js, hencelib/verify.jsV1,bin/dcp.js, conformance and the transparency-log gossip). RFC 8032 §5.1.7 requires 0 <= S < L, and TweetNaCl does not check it, so it accepted S + k·L, another encoding of a valid signature (§8.4, malleability). Python, Go and Rust already rejected them. TweetNaCl is still the verifier: the range is checked only on input it accepts, so its errors for wrong types, sizes and base64 are unchanged, as are the decoding of keys and R and the acceptance of weak keys. Signers produce S < L, so no newly generated signature changes verdict; a stored signature with S >= L that an earlier version accepted no longer verifies. The repository inventory found such signatures only in the intentional negative review corpus; it does not cover external stored evidence. Evidence verified before this change is not re-signed automatically: keep it as historical, with its hash and the version that verified it, or sign it again. - The same acceptance change in
dcp-ai verify(@dcp-ai/cli) and the playground, which called TweetNaCl directly.dcp-ai verifychecks S < L in its own V1 check ofsignature.sig_b64, and V2 bundles get it from@dcp-ai/sdk, whose verifier it now uses (see Changed). The historicalsignature.classical.valuepath had the check too when it was added; that path has since been removed and its format is rejected. The playground checks it inverifyDetached. The check is the SDK's, made only on input TweetNaCl accepts, so its errors are unchanged. - The four V2 verifiers split on non-canonical encodings of a valid signature: with junk after
the padding, extra padding, a newline inside, trailing bits set or no padding, TypeScript
accepted every variant, Python and Go some, Rust none. Every
sig_b64must now be canonical base64 (spec/VERIFICATION.mdV2-5), so a signature has one encoding and every SDK decodes the same bytes; 13 newmust_rejectvectors cover it. - V2 verifiers in all four SDKs accepted bundles that the signatures alone allowed but the
protocol does not. Reported by an external review and confirmed by our own; the reference
verifier
lib/verify.jsruns the fixed verifier through the workspace SDK until 2.2.0 is on npm (see Changed). - Any agent could claim any principal. The bundle signature verified with the passport keys
as well as the RPR
binding_keys, and the agent signs its own passport. Pairing a published, signed RPR with a new passport and keys gave a verified bundle without the principal's private key. The bundle signature now verifies only with the RPRbinding_keys(spec/VERIFICATION.mdV2-6), so the principal's signature over the manifest, which commits topassport_hash, is what binds the agent. - Kids were not checked against their keys. A key list could claim another party's kid for a key of its own. Every kid that verifies a signature must now be derived from its key, and the key must be canonical base64 so every SDK decodes the same bytes (V2-13).
- Signed but inconsistent bundles verified. A revoked or decommissioned passport, an expired
RPR, a policy decision for another intent, and audit entries for another intent, agent or
principal, with another
intent_hashor not recording the policy decision. The V1 checks are now V2-14: the passport isactiveordeclining, the RPR has not expired, and every artifact names the same principal, agent and intent. An escalated or blocked action is still valid evidence. - The session duration bound read an unsigned field.
max_session_duration_secondswas measured fromsignature.created_at, outside the signed manifest. The manifest has a newsession_issued_at, whichsignBundleV2andsign_bundle_v2write withsession_expires_at; the maximum is measured from it, and under a maximum a bundle without it is rejected (V2-4). It must not be more than 30 seconds ahead of the verifier's clock, and a maximum also rejects a bundle withoutsession_expires_at. Timestamps compare at millisecond precision in every SDK. Bundles signed before this change verify as before under a policy without a maximum session duration. - The unsigned signature block could contradict the signature.
signature.signer.idmust now be the RPR'shuman_id,signature.signer.kidsthe kids of the composite signature,manifest_hashthe hash of the manifest andhash_algone of the two the schema allows.signBundleV2andsign_bundle_v2refuse a signer id other than the RPR'shuman_id; Go'sSignBundleV2and Rust'ssign_bundle_v2already take it from the RPR. - PQ checkpoints were not verified. A checkpoint with a changed Merkle root or PQ signature, or a missing one the manifest lists, verified. The bundle must now carry exactly the listed checkpoints, each over its audit entries and signed by the agent with both algorithms (V2-12).
- The Go and Rust verifiers accepted an artifact or audit entry without
session_nonce; every one must now carry the manifest's, as in TypeScript and Python. - New vectors in
tests/interop/v2/bundle_vectors.json, each signed over one defect: 35must_reject_bundlescases, 4must_rejectmutations of the unsigned signature block and 8must_accept_bundlescontrols. A case may carryverifier_policy_overrides. lib/verify.jspins adcp verify-bundlekey among the RPRbinding_keys, and every entry under the signing kid must carry the pinned key, so the pin also holds with the published SDK 2.1.1, which looks the signer up in the passport first.@dcp-ai/express(dcpVerify),@dcp-ai/openclaw(dcp_verify_bundle) and the Python FastAPI middleware (dcp_ai.fastapi.DCPVerifyMiddleware) accepted V2 signed bundles without verifying any signature: they checked only the bundle's structure and session nonces (the FastAPI middleware also setcomposite_sig_validfrom thebindingfield), so an unsigned or forged bundle, or one whose intent was changed after signing, was reported as verified. All three now verify every V2 bundle with the SDK verifier (verifySignedBundleV2from@dcp-ai/sdk,verify_signed_bundle_v2fromdcp_ai.v2) and an Ed25519 / ML-DSA-65 / SLH-DSA-192f registry, under the configured policy overDEFAULT_VERIFIER_POLICY(verifierPolicy;verifier_policy, a newDCPVerifyMiddlewareargument; the OpenClaw operator policy below).requireHybridraises every risk tier belowhybrid_requiredto it (an operator'spq_onlystayspq_only), and adefault_modewithoutrisk_overridesapplies to every tier. The verifier's errors and warnings are returned, andcomposite_sig_validcomes from it. Until@dcp-ai/sdk2.2.0 is on npm, both TypeScript packages depend on the workspace SDK ("@dcp-ai/sdk": "file:../../sdks/typescript"), whose verifier has the fixes above; CI builds it before testing them, and all three pass every case oftests/interop/v2/bundle_vectors.jsonunder itsverifier_policy_overrides. Publishing them needs the dependency raised to^2.2.0first:publish-integrations.ymlnow refuses a package that depends on a local path.- The same three verifiers sent every bundle not detected as V2 to the V1 verifier, whatever the
verifier policy: a V1 bundle (one Ed25519 signature, no post-quantum one) got past
requireHybridor ahybrid_requiredpolicy, and a bundle of undetectable version was tried as V1. A V1 bundle is now accepted only where VERIFICATION.md V2-10 would accept a classical-only bundle:allow_v1_bundlesis true,requireHybridis off, and the stricter of the policy mode for the V1 passport'srisk_tierand the mode of the security tier of the V1 intent and policy decision (V1risk_score, 0 to 1, scaled by 1000; a missing one counts aselevated) isclassical_onlyorhybrid_preferred. UnderDEFAULT_VERIFIER_POLICYthe V1 conformance bundle (a standard-tiersend_email) passes and the same bundle with aninitiate_paymentintent is refused. A bundle of undetectable version is rejected. @dcp-ai/openclaw:dcp_verify_bundletook its verifier policy from theverifier_policytool parameter, which the calling model controls, and returned the unverified bundle'sagent_idandhuman_idwith a failed result. The policy is now the operator's (register(api, { verifierPolicy })or the newconfigureVerifier, overDEFAULT_VERIFIER_POLICY); the parameter can only make it stricter (a stricter mode per tier, withpq_onlyand any other mode givinghybrid_requiredso the model cannot drop the classical signature the operator requires,require_*only turned on,allow_v1_bundlesonly turned off, any other field an error). The result echoesverification_mode, and names the agent, principal and session only when the bundle verified.@dcp-ai/express: the verdict cache was keyed bysignature.composite_sigand shared by every middleware, so a different bundle carrying a cached signature, or a middleware with a stricter policy, reused a "verified" verdict; it also grew without bound and kept a verdict for the whole TTL after the session or a key had expired or the agent had been revoked. Each middleware now has its own cache keyed by the SHA-256 of the bundle's canonical form (canonicalizeV2; a bundle without one, such as one holding1e999, whichJSON.stringifyprinted likenull, is verified but not cached), holding at mostcacheMaxEntries(new, default 1000) bundles, least recently used evicted first; an entry expires at the earliest of the TTL, the manifest'ssession_expires_at, the RPR'sexpires_atand theexpires_atof every passport key and RPR binding key. A deeply nested bundle no longer gives a 500.@dcp-ai/express:checkRevocationasked only about the agent and its passport keys, and failed open silently. It now also asks about every RPR binding key and the bundle signature's classical and PQ kids, on every request including cache hits. A lookup that gives no answer (service unreachable, a non-2xx response, a body that is not JSON or has norevoked) still lets the request through by default; the newrevocationFailClosed: truerejects it.@dcp-ai/express: an unknown option orverifierPolicyfield (defaultMode) and a value of the wrong type (allow_v1_bundles: "false") were ignored, leaving the default in force. They now throw when the middleware is created.dcp_ai.fastapi: anX-DCP-Bundleheader holding an integer over Python's 4300-digit limit or nested too deeply, and a bundle too deep for the verifier, gave a 500; they now get a 403.pq_checkpointsthat is not a list is rejected (it was read as no checkpoints).verifier_policy={"risk_overrides": None}now keeps the default tiers instead of applyingdefault_modeto all of them, and an unknown risk tier raises.dcp_versionis documented as not enforced.- The three READMEs now say what "verified" means: the bundle is internally consistent and signed
by the keys it lists itself, which does not prove who the principal is unless the verifier pins
the principal's keys (none of the middlewares can yet), and
require_audience_bindingwith averifier_idlimits replay to the intended verifier. - Go and Rust verifiers accepted an audit entry (or PQ checkpoint) whose
session_noncediffered from the manifest's, although the manifest committed to it (spec/VERIFICATION.mdV2-4: every artifact and audit entry that carries a nonce shares the manifest's). Both now check the trail and the checkpoints as TypeScript and Python do, with the errorSession nonce mismatch in audit_entries[i]. Shared vectoraudit_entry_session_nonce_differs. - The Rust verifier did not check
manifest.canonicalization_profileat all, and the Go verifier ignored a value that was not a string, so a bundle could claim a profile no verifier implements (spec/CANONICALIZATION_PROFILE.md§ 4, V2-3). Both now reject any value other than"dcp-jcs-v1", as TypeScript and Python do. Shared vectorscanonicalization_profile_unknownandcanonicalization_profile_not_a_string.
Added¶
-
Enterprise integration rehearsal (new tools/enterprise-rehearsal/README.md; an optional kit outside the protocol, the SDKs and the adapters, with no SDK, adapter, profile, default or wire change,
dcp-jcs-v1unchanged, and nothing published). It builds the TypeScript SDK, the Express adapter and the Python SDK from one commit, installs them into a new consumer, and runs an Express and a FastAPI application on loopback against expectations written before the run, under a supervisor that owns every process. Keys, tokens and tenants are synthetic and marked TEST ONLY, and the run deletes the secrets. The guide also states what the rehearsal does not show: Go, Rust or WASM end to end, production TLS, PKI or OIDC, KMS or HSM, exactly-once execution, or anything about the published packages. -
Conformance through the public interfaces (local source; no new rule, schema or profile, and
dcp-jcs-v1is unchanged; the one new export is the WASM fix below).tools/conformance/wrappers/wasm-browser.mjsruns kit catalogs through the publicDcpWasmclass in Chrome:/1,/2, policy, structural, schema and/5, 590 cases. Separate direct sections add 103 cases: domain separation, security tier, hybrid sign-verify round trips and the P-ED-1 composite preimages. The runner has self-tests. sdks/rust/tests/wasm_ped1_vectors.rsruns the P-ED-1 corpus at the provider level on wasm32 in Node.- Each of the four SDKs adds tests of the configuration errors of admission from bytes and of delegation activation.
- CI runs all of these.
-
WASM still has no admission, mandate chain or activation API.
-
Delegated admission, admission from bytes and mandate acknowledgment (opt-in) (local source of the TypeScript, Python, Go and Rust SDKs, not in a published version; no version bump). No default calls them, and existing functions, results, codes and vectors do not change. WASM, the CLI, the playground, the reference server, the adapters and the kit do not expose them.
admitDelegatedBundleV2and its counterparts admit a leaf bundle with its DCP-09 mandate chain and one bundle per delegating agent, at onenow, under the operator's trust set, profile or policy and revocation snapshots of agents, keys and mandates (spec/profiles/admission§ 10,DELEGATION_*codes,tests/interop/v2/delegated_admission_vectors.json).admitSignedBundleV2FromBytesand its counterparts take the signed bundle as bytes, reject before any parser what the experimental scanner rejects and numbers that are not plain integers (NUMBER_NOT_PORTABLE), and require the SDK parser to reproduce the scanner's canonical bytes (INGRESS_PARSER_MISMATCH); § 11,tests/interop/v2/v1_ingress_vectors.json.dcp-jcs-v1does not change.-
A new record,
schemas/v2/mandate_acknowledgment.schema.json(dcp09-mandate-ack-v1), signed under the new contextDCP-AI.v2.MandateAcknowledgment, withcreateMandateAcknowledgmentandevaluateDelegationActivationV2and their counterparts (DCP-09 § 3.3.1, admission profile § 12, orderedACK_*codes,tests/interop/v2/mandate_acknowledgment_vectors.json). TheDCP_CONTEXTSmaps of TypeScript and Python keep their 19 entries; the context is a separate constant. Activation authorizes no action and does not show that the agent understood the mandate. -
Effective-policy catalog (contract
/5, opt-in) (kit and tests onmain, not a package release;tools/conformance/).external-effective-policy-catalog.jsonjudges 130 signed-bundle verifications on the verdict and on the verification mode and security tier that each implementation's verifier resolved in that same verification: 112 cases require both values; in 14 bundles that fail V2-2 and 4 earlier rejections they may be absent, and reported values must be known and consistent.tests/interop/v2/effective_policy_vectors.jsonadds 17 cases signed with the committed TEST-ONLY keys: elevated and maximum positives under each requested mode,risk_scoreboundaries,security_tierfloors and passport overrides that replace the default. The five wrappers answer/5from their SDK's own result fields;/1to/4, their catalogs, the SDKs and their APIs are unchanged. CI runs the self-tests and the catalog through the five wrappers. -
ML-DSA-65 verification vectors (
tests/nist-kat/ml-dsa-65/), developed on 05/11/2026; tests, CI and documentation only, with no SDK code, API, manifest or lock changes. The existing ML-DSA-65 property tests are unchanged. - 204 cases with an empty context go through the real provider of the TypeScript, Python, Go and Rust SDKs, and
through the Rust provider compiled to wasm32 (Node in CI, a browser locally): NIST ACVP sigVer tc39 (test group 3,
ML-DSA-65, external interface, pure mode, commit
a7f283cd) and 203 C2SP Wycheproof cases (commit20b25694, Apache-2.0, not a NIST source); 77 are valid and 127 invalid. Each file's SHA-256 is checked. - The providers verify with the empty FIPS 204 context, so the cases with a non-empty context (NIST tc31 to tc45 except tc39; Wycheproof tc3 to tc5 and tc153 to tc156) are excluded and checked by ID. An error counts as a rejection only for the key and signature length cases, with the provider's exact error.
- The WASM CI job runs the wasm32 target and fails unless its three tests pass. Passing these cases is not NIST validation of a library or module. NIST KAT.
- Bounded workload scaling (
tools/conformance-perf-scale.js, report contractdcp-ai-perf-scale/1), developed on 05/11/2026; no SDK, manifest or lock changes, anddcp-ai-perf/1with its children is unchanged. - It covers four dimensions with three sizes each (string bytes, nesting, audit entries, mandate chain length). Each
size has a re-signed valid control, a stale control and a schema control, with the category of each negative checked
in its first errors. Bundles run in five SDKs, mandate chains in four, through separate
dcp-ai-perf-mandate/1children. - The corpus is generated once with TEST-ONLY keys, measured from its exact bytes, and checked before any child with Ajv and the SDK-free integrity and signature checks.
- Memory is the kernel high-water RSS of a fresh child per observation (
tools/conformance/perf/rusage.py); the heap is not measured. - A supervisor stops the run and every process it started at the deadline, and reports an incomplete cleanup as such.
- CI runs its self-tests and a smallest-size smoke run; they check verdicts, counts, units and cleanup, and set no speed or memory threshold. Conformance kit.
- Differential testing (
tools/difffuzz), developed on 05/11/2026; no SDK, manifest or lock changes. run-mandates.mjssends the same re-signed DCP-09 mandate chain to the TypeScript, Python, Go and Rust chain verifiers through contract/4(formatdcp-mandatefuzz/1). Every run covers 73 variants in 11 families at every depth they apply to (167 cases), then bounded random mutants. It keeps positive and stale-signature controls in every batch, writes expectations only where DCP-09 §3.5.2, the canonicalization profile or RFC 8259 decides, and stops at the first finding for review. Its minimization re-signs each candidate, and its replay is typed and bound by hashes.--transport-proberecords, per wrapper, what contract/4does with aNaNtoken, a byte order mark and1e400: an observation. A wrapper's refusal is never counted as an SDK rejection, and the rejection the TypeScript and Python SDKs return for1e400credits no semantic rule.- The bundle fuzzer takes
--sdks fiveto add the Rust SDK compiled to WASM (Node-hosted) as a fifth verifier. Reports and new findings record their SDK set; older findings replay with exactly four. -
The differential CI job runs both harnesses.
-
External conformance kit: an opt-in delegation catalog,
--catalog delegation(contractdcp-ai-conformance-external/4,tools/conformance/external-delegation-catalog.json, 97 cases frommandate_chain_vectors.json,mandate_chain_time_vectors.jsonandmandate_domain_vectors.json, pinned by SHA-256). Its one operation,verify_mandate_chain, sends the chain and the keys as the exact bytes of the fixtures, with the revoked ids and the verification time. Only the verdict is judged:valid, and the effective tier of an accepted chain (DCP-09 §3.5.2 rule 6); error texts and depth are reported as diagnostics. 87 cases are normative. 10chain_timecases rest on conventions DCP-09 does not state (millisecond precision, signer key validity) and are informative: reported apart, never a pass or a fail, though a failed answer to one is an error. Each rejection names its rule and counts only if its control passed; the runner checks on the bytes how the two relate. The TypeScript, Python, Go and Rust wrappers answer through their SDK at the request's time; WASM exports no chain function and refuses/4. CI runs the judge's self-tests (17), the wrapper tests (19, the four wrappers required) and the 97 cases through each of the four wrappers./1,/2and/3are unchanged. - A performance baseline tool for the external conformance kit,
tools/conformance-perf.js(contractdcp-ai-perf/1): the normative V2 verifier of the five SDKs of this checkout on six committed workloads (a hybrid bundle, an invalid Ed25519 and an invalid ML-DSA-65 signature, and three secondary SHA3-256 cases), with the same input bytes, policy and verification instant. One in-process child per SDK through its public entry point, warmed and timed round-robin, every call checked; every sample is kept, with nearest-rank statistics. It builds what it measures and records sources, manifests and locks, artifact hashes and tool versions per SDK (--no-buildrefuses any difference or gap). No speed threshold, ranking or attributed percentage; results stay local. CI runs its self-tests and a five-SDK smoke run in the native job. RawAuditEventV2in the Rust (dcp_ai::v2::types) and Go (v2) SDKs: one received V2 audit entry kept as it was decoded, every member at every depth with its presence andnull-ness, unknown members included, so wherever the dcp-jcs-v1 canonicalizer accepts it its canonical bytes,prev_hashand Merkle leaf stay those of the input. Rust holds the decodedserde_json::Value(RawAuditEventErrorfor anything but an object); Go holds the literal bytes, copied in and out, and refuses anything but exactly one JSON object. Neither repairs nor re-signs: invalid evidence stays invalid, for the same reason. The existingAuditEventV2andAuditEvidenceV2are unchanged and now documented as lossy for forwarding (an explicitnull, an unknown member, an absentpq_checkpoint_refthat becomesnull). New shared vectors,tests/interop/v2/audit_entry_roundtrip_vectors.json, give 14 entries with their hand-written dcp-jcs-v1 text, which the TypeScript, Python, Go and Rust canonicalizers reproduce, and a signed bundle whose mutations the new type keeps rejected with the same errors. The Go bundle builder, the V1 types and the PythonAuditEventV2model are unchanged.--assessment verdictfor the evidence catalog (contract/2) of the external conformance kit: the 146 rejections that carry anerror_containsare judged onverified: falsealone, with every other rule, control and exit code unchanged, so an implementation with correct verdicts and messages of its own can show it. The wording is reported as a diagnostic:matched,differs,no_message, ornot_evaluatedwith its reason. Each of those cases names its step ofspec/VERIFICATION.md, and before the run the runner checks those anchors and that every control is a plain positive. The report declaresdcp-ai-conformance-assessment/1, apart from the/2wire contract, and keeps the catalog's prose incatalog_claimandcatalog_limits. Without the option, or with--assessment legacy, judgement and report are unchanged, and the/1and/3catalogs refuse it. The catalog, the fixtures and the wrappers are unchanged; CI runs the assessment through the five wrappers, all required.- External conformance kit: an opt-in admission catalog,
--catalog admission(contractdcp-ai-conformance-external/3,tools/conformance/external-admission-catalog.json, 140 cases: the 65 outcome vectors ofadmission_vectors.json, 71 configuration cases and 4 cases composed from the vectors' inputs). The TypeScript, Python, Go and Rust wrappers answer through their SDK's combined admission function and loaders, at the request'snow. The judge checks the field types, the rules of spec/profiles/admission (action_authorizedisfalse;admittedexactly when no code was added), the exact fields, the order of the raw codes by phase and their repetition; a negative counts only if its controls passed. The answer is a projection: the embedded verification is not judged. The WASM build has no admission API and is not emulated. CI runs the judge's self-tests (60), the wrapper tests (26, the four wrappers required) and the 140 cases through each of the four wrappers. The fake of the evidence self-tests now reaches the TypeScript wrapper through an asynchronous spawn, with a deadline and its exit checked: a synchronous spawn with the 9 MB request was observed waiting until the deadline./1and/2are unchanged. - Canonicalization candidate: gate 7 measured on the
dcp-jcs-v1side. The new public fixturetests/interop/candidate/gate7-v1-negatives.jsoninjects a defect of each of the 8 unit classes, at 18 byte positions, into two frozendcp-jcs-v1bases: 38 cases, the 2 bases included as controls. It records a reasoned expectation for 10 configurations of five implementations: the TypeScript, Python, Go and Rust SDKs, and the WASM package hosted in Node. New tests in each run the 380 comparisons: the experimental verifier must give the result of today's route of the same implementation on the same bytes, clock and policy, and both must match the expectation. Three envelope cases (30 checks) stay a separate boundary, with no equivalence claimed. A strict standard-library checker and its negative controls run in the conformance CI job. The ledger marks gate 7 met, on samples. Tests and documentation only: no SDK API, profile, default ordcp-jcs-v1change; the candidate stays experimental and opt-in, and nothing is published. See docs/proposals/canonicalization-candidate/README.md. - Evidence catalog for the external conformance runner: Merkle and signature verification.
--catalog evidenceruns an opt-in catalog (tools/conformance/external-evidence-catalog.json, contractdcp-ai-conformance-external/2, 241 cases) on committed public fixtures: exact dual Merkle roots for 1, 2, 3 and 5 leaves, composite and classical-only signature verification with the committed attacks and deterministic alterations, normative V2 signed-bundle verification ofbundle_vectors.jsonunderdefault_verifier_policywith each case's committed overrides, and the secondary SHA3-256 vectors, all at a pinnedverification_time. A negative case counts only if its control passed;verified: truewith errors,errorandunsupportedare never a pass. The five wrappers accept both contracts; the default catalog, contract/1and its 38 cases are unchanged. CI runs the 241 cases and the self-tests through all five wrappers. A pass is not admission, trust or authorization. Apart from thewasm_dual_merkle_rootfix under Fixed, no SDK changes; no package is published. See tools/conformance/README.md. - Go, Rust and WASM wrappers for the external conformance runner.
tools/conformance/wrappers/build.mjsbuilds wrappers for the Go SDK, the Rust SDK and the WASM build of the Rust SDK from this checkout (-mod=readonly,--locked; no SDK manifest or lockfile changes), and each runs the same 38 cases through the real API, with one call to the canonicalizer and only its documented rejection classified as such. The WASM wrapper runs the real.wasmhosted in Node, not in a browser, and treats the export's in-band{"error": …}collision conservatively. CI builds the three and runs them as required jobs. No package is published. See tools/conformance/README.md. - External conformance runner (
tools/conformance-external.js). It runs a separate, versioned catalog (tools/conformance/external-catalog.json, contractdcp-ai-conformance-external/1) against any implementation given as argv, through one JSON request on stdin and one JSON response on stdout: 30dcp-jcs-v1canonicalization cases (including the 19 committededge_cases, 3 of which must be rejected) and 8 SHA-256 / SHA3-256 digests, all with committed expectations. Wrappers for@dcp-ai/sdkand the Pythondcp_aiare included, and CI runs both. The existing conformance command, catalog and report are unchanged. A pass shows these 38 cases passed, nothing more. See tools/conformance/README.md. cancel_on_disconnectin FastAPI works in any order of middlewares. The ASGI response of a client that left now ends with an empty499("client closed request": not registered, not a DCP status), withContent-Length: 0andCache-Control: no-store, instead of no message at all.- These are two ASGI messages from DCP's response, sent only after
http.disconnectwas received, never for a client that stays. ABaseHTTPMiddlewareoutside DCP needshttp.response.start, and before this it failed with "No response returned."; it now completes normally and records a 499. - What reaches the network is up to the server: uvicorn (measured, 0.54 with h11) writes and logs
nothing for a connection that is gone; under ASGI another server may raise an
OSErrorsubclass. - The admission's
request.state.dcp_agentis now published only once the client is known to be still there, so a client that left leaves no admitted context. A value set by the operator's own code is left alone. - A client that leaves in the middle of its body gets the same ending (instead of nothing).
- Genuine errors of any layer still propagate. Nothing else changes: options, defaults, the body
budget, the supervisor and
DenyListWorkers. - Client disconnect, opt-in:
cancelOnDisconnectin@dcp-ai/express,cancel_on_disconnect=Trueindcp_ai.fastapi.DCPVerifyMiddleware. - What it does. When the client really leaves during admission, the supervised source
invocation of that request, and only that one, is cancelled. Its slot stays taken until the
source really settles. Nothing more is done or sent for the request: no handler,
next, cache write,onFailureor response. - Express. Leaving means the response closing before it was sent, or the request closing with
its body incomplete; a healthy request's own
closeis not it.- It needs every source function supervised, no
checkRevocation, and a supervisor that supports cancellation: the newrunWithSignalcapability of this version's supervisors (ESM and CJS, both packages). run, its four arguments andversion: 1are unchanged, and a supervisor from an older package keeps working without the option.- The decision of a supervised invocation is now taken once, before its signal is aborted: an
abort listener that re-enters
close()changes nothing. - New
cancelledresult, counter (stats().cancelled, optional in the type) and telemetry reason.
- It needs every source function supervised, no
- FastAPI. It needs
max_body_bytes, every source callable supervised and a synchronoustenantcallable.- The middleware watches one pending
receive()during admission and hands it over to the next reader: nothing is read twice or lost, and thehttp.disconnectsent after a complete response is not taken as the client leaving. - With the option, a client that leaves in the middle of its body gets nothing instead of the 400.
- In its first form the middleware had to wrap the application's other
BaseHTTPMiddlewarelayers; that requirement is gone (see the entry above). SourceSupervisorandDenyListWorkersare unchanged. OpenClaw has no such option.
- The middleware watches one pending
- Without the option nothing changes.
dcp_ai.v2.DenyListWorkersanddeny_list_revocation_source(..., workers=...), opt-in: a budget of real download threads for the Python deny-list helper (max_workersfrom 1 to 64,max_workers_per_tenantoptional, per object). Each download runs in a non-daemon thread of its own, started after its slot is reserved and freeing it only when the download has really ended, also after a timeout, a cancelled call, C3's supervisor deadline or a closed event loop; a call that finds no free slot getsREVOCATION_UNAVAILABLEat once, with no queue and no connection. The download, the body checks, the parsing and the errors are those without the option; a result that arrives or is used aftertimeoutisREVOCATION_UNAVAILABLE.close()refuses new downloads without waiting for or stopping running ones;stats()has no URL, tenant or header. In a forked process it refuses without touching the parent's lock. Process exit waits for a download still running, as it does withasyncio.to_thread; nothing stops DNS, TLS or urllib. Withoutworkersnothing changes. The documentation oftimeoutis corrected: it bounds each socket operation and the reading of the body, not DNS resolution or a status line that trickles in.- Revocation under the source supervisor (C3), opt-in, in
@dcp-ai/express,@dcp-ai/openclawanddcp_ai.fastapi.DCPVerifyMiddleware:supervise: { revocation: true }/supervise={"revocation": True}, refused until now, runs the strict-v1 revocation source under the supervisor withrevocationTimeoutMs/revocation_timeoutas its deadline, then limited to the timer range (an integer from 1 to 2147483647 ms; up to 2147483.647 s).supervisetakes the trust deadline,revocationor both, so revocation can be supervised with a static trust set; trust and revocation calls share the supervisor's capacity, and a slot is freed only when its source really settles. The responses do not change: a timeout, no free slot or a closed supervisor give theREVOCATION_UNAVAILABLEa failing source gives (403 in Express and FastAPI), and aREVOCATION_INVALIDerror counts only before the deadline. TypeScript sources get(subjects, { tenant, signal }),denyListRevocationSourcealso aborts its fetch on that signal, and telemetry reportssource: 'revocation'; OpenClaw'sconfigureVerifierrefuses to replace a supervisor with calls in flight whichever source it supervises. FastAPI needs an asyncrevocation_source. Behaviour change with the option only: a FastAPI source that suppresses its cancellation givesREVOCATION_UNAVAILABLEat the deadline instead of its late snapshot. Not bounded: work a source starts on its own, such asdeny_list_revocation_source'sasyncio.to_threadworker, whose I/O is not cancelled. Without the option nothing changes. dcp_ai.fastapi.DCPVerifyMiddleware: a deadline and a capacity for an asynctrusted_principals, opt-in.SourceSupervisor(max_in_flight=..., max_in_flight_per_tenant=None), exported fromdcp_ai.fastapionly, andsource_supervisorwithsupervise={"trust_timeout": seconds}(both together). The source runs as a Task the supervisor owns. A trust set that arrives after the deadline (loop.time(), checked on arrival and on use), a call with no free slot (no queue), a closed supervisor or one bound to another event loop gives the same 503 as a failing source. A slot is freed only when the Task really finishes.close(drain_timeout)andstats()are provided. A static or synchronoustrusted_principalswith these options raisesAdmissionConfigError; so did{"revocation": True}in this first step, now accepted (see the revocation entry above). One supervisor per event loop; it does not stop a source that ignores cancellation or isolate CPU. Without these options nothing changes.@dcp-ai/expressand@dcp-ai/openclaw: a deadline and a capacity for atrustedPrincipalsfunction, opt-in.createSourceSupervisor({ maxInFlight, maxInFlightPerTenant?, onSourceEvent? })makes a supervisor, andsourceSupervisorwithsupervise: { trustTimeoutMs }applies it (both together, withtrustedPrincipalsas a function). The function then gets{ tenant, signal }. A trust set that arrives after the deadline, measured on a monotonic clock when it arrives, is not used, and neither is a call that finds no free slot (no queue) or a closed supervisor: the result is the same as for a failing source (TRUST_SOURCE_FAILED, 503 in Express). A slot is freed only when the function really settles. The capacity is per supervisor object, shareable between both packages and their ESM and CJS builds.close({ drainMs })andstats()are provided; counters stop atNumber.MAX_SAFE_INTEGER. OpenClaw'sconfigureVerifierrefuses to replace or remove a supervisor while a call that started under it is in progress (also before it calls its source) or its source still runs, even afterclose(). It does not stop a source's work or isolate CPU. Without these options nothing changes. In this first stepsupervise.revocation: truewas refused and revocation keptrevocationTimeoutMs; it is now accepted (see the revocation entry above).dcp_ai.fastapi.DCPVerifyMiddleware(max_body_bytes=...): an opt-in budget, in bytes, on the request body (defaultNone, no budget, as before; a positive integer, elseAdmissionConfigErrorCONFIG_INVALID_VALUE). The middleware then reads the body first, also with the bundle in the header orrequire_bundle=False: aContent-Lengthover the budget, or received bytes over it (noContent-Length, chunked, or a wrong one), get413without receiving the rest, parsing, resolving trust, verifying or reaching the route; an invalid or repeatedContent-Lengthor a disconnect while sending gets400. A body within the budget is handled as before and still reaches the route. It bounds the bytes received, not decompression, JSON depth or time, and it is a deployment limit, not part of the protocol. The option needs Starlette 0.28.0 or later, which passes a body read in the middleware on to the route; with an older or unidentified Starlette it raisesAdmissionConfigErrorCONFIG_INVALID_VALUEwhen the middleware is built. The package's minimum versions are unchanged, and without the option nothing is checked.- Admission: trusted principals and the
strict-v1profile (spec/profiles/admission,docs/ADMISSION.md). A verified bundle proves integrity and signature by the keys it lists; an operator trust set (schemas/v2/principal_trust_set.schema.json) now lets a verifier admit a bundle only when the keys that verified its principal signatures (RPR and manifest, classical and PQ) are trusted, active keys of thathuman_id.strict-v1(schemas/v2/admission_profile.schema.json) adds operator-fixed requirements: V2 only, hybrid signatures for every tier, session binding, a signedintended_verifierequal to the operator'sverifier_id, a signed session of at most 300 s, and a revocation snapshot (schemas/v2/revocation_snapshot.schema.json) at most 300 s old covering the agent and every verified signer key. Its configuration can only tighten it; a weakening value is aCONFIG_PROFILE_CONTRADICTION. Results keepverifiedand addprincipal_trust,profile_compliance,admitted,recorded_decision,trust_set_version, codederrors, andaction_authorized, alwaysfalse: admission does not authorize an action. - SDKs: TypeScript (
admitSignedBundleV2,evaluateAdmissionV2,createPrincipalTrustStore,resolveStrictV1Profile,revocationSubjects,revocationSnapshotFromDenyList), with the same functions in Python, Go and Rust. The four verifiers take an optional clock and report the key each signature component was verified with (signer_keys). In Rust,V2VerificationResultkeeps its ten fields andverify_signed_bundle_v2/verify_signed_bundle_v2_with_policytheir types, so code built against them still compiles: the keys come from the newverify_signed_bundle_v2_at, which returnsV2VerificationDetails(#[non_exhaustive]; the result asresult, andsigner_keys, from the same verification). Admission takes and returns it; its JSON is the result's fields followed bysigner_keys. - Integrations:
@dcp-ai/expressdcpVerify,dcp_ai.fastapiDCPVerifyMiddlewareand@dcp-ai/openclawconfigureVerifiertaketrustedPrincipals,profile,revocationSourceand, in the middlewares, an operatortenantfunction. A request reaches the route only when admitted. A trust function that throws or returns no valid trust set (undefined,null, an invalid set) refuses the request:503in the middlewares,admitted: falsein OpenClaw. The Express cache keeps only the cryptographic verification, keyed by the bundle, the policy and the tenant; trust, the profile and revocation are evaluated on every request.denyListRevocationSourcereads a complete deny list; a source that fails or times out refuses the request. - Reference verifier, server and CLI:
admitSignedBundleinlib/verify.js,POST /v2/admitconfigured byDCP_TRUSTED_PRINCIPALS_FILE,DCP_ADMISSION_PROFILE_FILEandDCP_REVOCATION_LIST_URL, anddcp admit-bundle. tests/interop/v2/admission_vectors.json: 65 cases with hand-written expected outcomes and the rule each checks, and 49 configuration cases, run by the four SDKs and bynpm run conformance.- Rust SDK: native V2 bundle building and signing,
dcp_ai::v2::bundle.build_bundle_v2(&rpr, &passport, &intent, &policy, &audit_entries, session_nonce)checks eachpayload_hashand thesession_nonceof every artifact and audit entry, and writes the manifest with both audit Merkle roots (an empty trail gets roots of 64 zeros, as in Go);sign_bundle_v2(bundle, &classical, &pq)composite-signs the manifest, withsigner.idthe RPR'shuman_id, as the GoSignBundleV2does. Like Go's, the builder does not take PQ checkpoints yet. The interop matrix's Rust agent uses them instead of assembling the bundle by hand. tools/interop-matrix: live interoperability between the four SDKs. One agent process per SDK (TypeScript, Python, Go, Rust) creates its identity, declares intents, acts and signs its citizenship bundles (DCP-01 to DCP-03); every other agent, and the reference verifierlib/verify.js, verifies them under the default policy andhybrid_required, untouched and after seven tamperings in transit (changed intent, reordered or dropped audit entries, spliced session, swapped bundle signature or passport, stripped PQ signature). The agents share only the messages the orchestrator relays..github/workflows/interop-matrix.ymlruns it on every pull request and nightly.@dcp-ai/google-a2a2.1.0: the signed DCP Agent Passport travels inside the A2A Agent Card.passportToAgentCard(signedPassport, url)(orattachPassportToAgentCard(card, signedPassport)) puts theSignedPayload<AgentPassportV2>in the card'scapabilities.extensionsunderhttps://dcp-ai.org/a2a/extensions/passport/v2, A2A's own extension mechanism;verifyAgentCardPassport(card)verifies it offline (payload hash,pq_over_classicalcomposite signature under the AgentPassport context with the active, unexpired keys the passport lists,status: active, andmetadata.dcp_agent_idnaming the same agent), failing closed on anything malformed;agentCardToPassportreturns that passport instead of a skeleton when the card carries one. The package is now TypeScript (dist/build, vitest suite in CI).@dcp-ai/anthropic-mcp2.1.0: signed receipts for MCP tool calls.withDcpReceipt(name, handler, { identity })wraps any MCP tool handler: the call is classified into an Intent (DCP-02) and composite-signed before the tool runs, the policy decides (approve runs the tool; escalate and block return anisErrorresult without running it; a throwing handler yieldsoutcome: "failed"), and one audit entry (DCP-03) records the SHA-256 of the arguments and of the result. The receipt in_meta.dcp_receiptis an ordinary DCP v2.0 signed bundle, so any DCP verifier checks it offline;verifyDcpReceiptalso recomputes the evidence hash from the arguments and result the caller saw and checks the tool name.createReceiptIdentityissues a session identity (Ed25519 + ML-DSA-65 keys, signed RPR and passport) for tests and first runs. The package is now TypeScript (dist/build, vitest suite in CI) anddcp_verify_bundleruns the realverifySignedBundleV2instead of a structural check; its unusedsecurity_tierinput is gone.tests/interop/v2/bundle_vectors.json: 28 more rejection cases (5must_rejectmutations, 23must_reject_bundles), one per vulnerability of the v2.10 round, that all four SDK verifiers (Python included) run under every verification mode: artifacts without anycomposite_sig(V2-8);dcp_bundle_version1.0and3.0(V2-1); a key expired with a non-UTC offset, malformed keyexpires_atvalues (impossible date, date only, no offset) on the passport and an impossible date on the RPR (V2-13); classical-only bundles whose intent or policy decision implieselevatedwhile the intent declaressecurity_tier: "routine"orestimated_impact: "high"(V2-10); prototype keys (constructor,__proto__,toString,hasOwnProperty) in the passportrisk_tierandconstructorinaction_type,estimated_impactandsecurity_tier(V2-9); an unknown or non-stringcanonicalization_profile(V2-3); an intent, an audit entry or the manifest with anothersession_nonce(V2-4); a manifest hash, audit root or chain hash that does not match (V2-3).tools/difffuzz: a differential fuzzer that runs the same mutated V2 signed bundle through the TypeScript, Python, Go and Rust verifiers (one shim per SDK, built from this checkout) and fails on any disagreement, crash or timeout, saving each finding undertools/difffuzz/findings/. Seeds are the V2 signed bundles intests/interop/v2/; mutators cover deleted and null fields, type swaps, sibling swaps, base64 character flips, prototype keys, unknown keys, duplicated artifacts, number and RFC 3339 variants, oversized strings, deep nesting and key reordering (which must never change a verdict)..github/workflows/difffuzz.ymlruns the fuzzer on every pull request (300 iterations), onworkflow_dispatchand nightly (3000 iterations).- Verifiable delegation chains (DCP-09 §3.5) in all four SDKs.
schemas/v2/delegation_mandate.schema.jsongains the optional fieldsparent_mandate_id,delegation_depth,sub_delegation({ permitted, max_depth }),minimum_security_tieranddelegator_composite_sig; a root mandate keepshuman_composite_sigand is unchanged byte for byte, a sub-mandate carriesdelegator_composite_siginstead, signed by the delegating agent under the new domain-separation contextDCP-AI.v2.SubMandate(domain-separation.{ts,py,go,rs},interop_vectors.json).verifyMandateChain/verify_mandate_chain/VerifyMandateChainverifies an ordered chain offline back to the human: root signature against the human's keys, each hop's signature against the parent agent's keys, parent link and depth,sub_delegation.permittedand the smallestmax_depthof the ancestors, the samehuman_id, nested validity windows compared as RFC 3339 instants, attenuation (actions_permittedanddata_classessubsets,limitsnever looser, a child cannot adddata_classesits parent has none of, and numericlimitsonly as integers within ±(2^53 − 1): beyond them a verifier that reads JSON numbers as doubles and one that keeps every digit disagree on what a mandate says, so such a limit isnot comparableat any depth), aminimum_security_tierthat never drops below the parent's, and revocation, where a revoked ancestor invalidates every descendant. The result carries the chain's effective tier (the greatestminimum_security_tieralong it). Anything unknown, missing for the depth or not comparable fails closed. Signing keys must beactiveand unexpired at the verification time, and a mandate signature must bepq_over_classical. - Sub-mandate builders next to the root builder:
createSubMandate/create_sub_mandate/CreateSubMandate, andsub_delegation/minimum_security_tieron the root builder (create_delegation_mandate_with_optionsandMandateChainOptionsin Rust). computeSecurityTier/compute_security_tier/SecurityTierInput.MandateChainTier/compute_security_tier_with_floor, and the rootlib/security-tier.jscopy (computeSecurityTier(intent, policyDecision, mandateChainTier)), take the effective tier of a verified chain as an optional input that can only raise the tier (spec §14.2); in Go an emptyMandateChainTiermeans no chain, any other name that is not a tier counts aselevated. The bundle verifiers do not read it yet: a bundle has no field that carries its mandate chain.tests/interop/v2/mandate_chain_vectors.json(generated bysdks/typescript/src/__tests__/generate-mandate-chain-vectors.ts): chains of depth 1 and 2 every SDK must accept with their depth and effective tier, chains every SDK must reject (widened actions, added data classes, loosened limits, a limit of 2^53 or of 2^53 + 1 written as the wire integer a double cannot hold, validity outside the parent's, depth beyondmax_depth, missing or wrongparent_mandate_id, lowered tier, revoked root or intermediate, a sub-mandate signed by the wrong agent, both signatures present, wrongdelegation_depth, and more) and the tier-floor cases. All four SDK suites run it.- Experimental, opt-in APIs for the canonicalization candidate
dcp-jcs-v2(docs/proposals/canonicalization-candidate, §15–§18). The candidate is not normative and not active:dcp-jcs-v1stays the only normative and default canonicalization profile, and no default, builder, signer or verifier changes. Only callers who opt in reach these APIs, in TypeScript, Python, Go, Rust and WASM. - Ingress: checks the original bytes of one unit (UTF-8, BOM, duplicates, exact integers in ±(2^53−1), depth 64) and gives its canonical bytes or a class and offset.
- Router: extracts the unit from its transport, reads the profile and decides: reject,
delegate to
dcp-jcs-v1through a named historical decoder, or select the candidate. - Verifier: verifies from the original bytes with at most one verifier: today's for
dcp-jcs-v1, and today's accepting exactlydcp-jcs-v2for the candidate. A verified bundle grants no trust or admission. - Producers: sign artifacts (
experimentalCandidateSignPayloadand its equivalents) and bundles whose manifest namesdcp-jcs-v2(experimentalCandidateSignBundle), from original bytes or strictly checked host values. A rejection for the domain or form of the inputs is raised before any signer is called; a key, registry or provider error can still come after a signature was made. - Tests: a cross matrix of five producers × five consumers
(
tests/interop/candidate/phase4, 975 results with an all-or-nothing verdict) and its cost measurement, run by the newcanonicalization-candidate-matrixworkflow, which publishes nothing. Vectors are intests/interop/candidate/.
Changed¶
- Verifier policy, a dated clarification (05/11/2026, normative):
spec/VERIFICATION.mdStep V2-10 settles three questions the policy corpus left open. - Q1: under an effective
pq_onlymode, the classical algorithm lists and advisories do not apply to the classical component. - Q2: under
pq_only, a classical component that fails cryptographic verification does not reject a bundle whose PQ component verifies over those classical bytes. Canonical base64, contexts, kid lookup and key checks still apply. - Q3: PQ lists do not reject a bundle without a PQ component; the effective mode alone admits or rejects its binding.
spec/DCP-AI-v2.0.md § 7.3 replaces two items that read as if pq_only accepted a signature without
pq_over_classical, or a classical-only bundle with a warning. The five SDK verifiers already gave these verdicts,
so no SDK changes. tests/interop/v2/verifier_policy_clarification_vectors.json (34 cases, with its generator) is
new, and verifier_policy_vectors.json does not change. The opt-in policy catalog is now version
2026-11-05.verifier-policy-2.0, with a second suite, external.v2.verifier_policy_clarification (108 cases in
all). Version 1.1 (74 cases) is archived byte for byte as tools/conformance/external-policy-catalog-1.1.json and
still runs by its path.
- Delegation, a dated clarification (05/11/2026, normative): DCP-09 §3.5.2 compares the instants of rule 4 and the
signer keys' expires_at in milliseconds, and requires the key each signature slot names to be active and
unexpired at the verification time, as for bundles (V2-4, V2-13). Nothing is required of created_at. The four
chain verifiers already behaved so, so no SDK changes. The opt-in delegation catalog is now version 2.0.0 and judges
all 97 cases: the ten chain_time cases version 1.0.0 kept informative are normative (rule 1 or 2 for the signer
keys, every rule for the three accepted chains). Version 1.0.0 is archived byte for byte as
tools/conformance/external-delegation-catalog-1.0.0.json and still runs by its path, with 87 normative and 10
informative cases. The runner pins each version's classification, so neither is judged with the other's. An
implementation that differed on one of the ten passed 1.0.0 and fails 2.0.0.
- Translations now live under
i18n/<locale>/, at the same path as their English source, which does not move:<dir>/<name>.<locale>.mdis nowi18n/<locale>/<dir>/<name>.mdfores,pt-BR,jaandzh-CN(388 files). The move changed only link paths: those in the moved files, and the language links of ten English documents. The English README of each SDK links to its translations by absolute URL, so its language links do not depend on paths outside the package. The documentation site keeps its URLs, anchors and language selector in all five languages. The old paths no longer exist onmainand there are no redirect files; an earlier commit or tag still has each file at its old path. Future versions of the npm packages of the SDKs and integrations, packaged from this tree, no longer include the translated READMEs; packages already published are not modified, and their links to the old GitHub paths may become outdated. The documentation check (scripts/docs_i18n.py) also rejects files underi18n/that are not a listed translation, links intoi18n/that do not reach one, and translation notices that do not link to the exact English source, and it checks links to this repository'smainbranch like local links. dcp-ai verify(@dcp-ai/cli) verifies V2 bundles (bundle.dcp_bundle_version"2.0" or abundle.manifest) with the normative verifier: the envelope and the four signed payloads are validated againstschemas/v2, whose copies ship in the package, thenverifySignedBundleV2from@dcp-ai/sdkruns underDEFAULT_VERIFIER_POLICYwith the Ed25519, ML-DSA-65 and SLH-DSA-192f providers. The pre-normative envelope (top-leveldcp_version"2.0" orbundle_manifest, orsignature.binding: "composite") is rejected before anything else, also when those markers sit on a V1 or a V2 bundle, and itssignature.classical.valuecheck is gone, with no classical fallback. When.dcp/keys/ed25519_public.keyexists it pins the principal's signing key among the RPRbinding_keys(an empty file is rejected); without it the bundle's own keys are used, which checks the signatures, not trust or admission. V1 verification is unchanged.dcp-ai signis unchanged too: it appends tosignatures[], a formatverifydoes not read; V2 bundles are signed withsignBundleV2ordcp sign-bundle --composite. The package now requires@dcp-ai/sdk^2.2.0and declaresajvandajv-formats.dcp-ai verify(@dcp-ai/cli) exits 0 only when it prints VALID. On V1 files it now exits 1, where it exited 0, when the Ed25519 signature is INVALID, when there is no key or no usable signature (it prints "No public key available", also when the key is there and the signature is missing or empty), and when the file is not a signed bundle (an object without a truthybundle, an array or a primitive). Those messages are unchanged, and a missing argument or file, invalid JSON, base64 or a wrong size still exit 1 with their error. A script that took exit 0 to mean "ran" must now expect 1 in those cases. On a V1 file exit 0 means that the Ed25519 signature over the canonical bundle verified, nothing more; on a V2 bundle it means the schemas and the normative verifier passed (above). Neither is an admission decision.- Go and Rust V2 verifiers: a policy that narrows
accepted_classical_algsoraccepted_pq_algswas refused as a field they did not enforce, so every bundle failed under it; they now enforce both lists as TypeScript and Python do (the classical list except underpq_only), and a signature with an algorithm outside them fails as not accepted by the verifier policy. They also acceptallow_v1_bundles: false, which strict-v1 sets: they verify V2 bundles only, so the field changes nothing for them. Policies that verified before behave the same. - The reference verifier (
lib/verify.js,dcp verify-bundle, the verification server and its Docker image) depends on the workspace@dcp-ai/sdk(file:sdks/typescript, packed throughinstall-linksin.npmrc) until 2.2.0 is on npm, so it runs the verifier fixes of this release instead of the published 2.1.1, which accepted 39 of the 79must_reject_bundlesvectors. Buildsdks/typescriptbeforenpm installat the root; CI anddocker/Dockerfiledo. Publishing needs the dependency raised back to^2.2.0. lib/verify.js: schema validation builds one Ajv per schema root and compiles each schema once, on first use, instead of loading and compiling every schema on eachvalidateOnecall (five per V2 bundle). Validating a V2 bundle's schemas went from about 34 ms to 0.14 ms (median of 20 runs after the first, which still compiles).
Fixed¶
- WASM: error results told apart from values (local source, not in
@dcp-ai/wasm2.1.3). DcpWasm.canonicalizethrew for a value whose canonical form begins with anerrormember, such as{ error: 'x' }. It now readswasm_canonicalize_v2_result, a new raw export (additive) that returns a tagged result.- The
{"error": …}the wasm exports return is now escaped JSON. A message with a backslash or a control character, from a key name for instance, could be invalid JSON or decode to other text. Messages without such characters keep their bytes, double quotes still shown as apostrophes. hashObjectandderiveKidthrow anErrorinstead of returning that envelope as a hash or a key id.-
wasm_canonicalize_v2is unchanged: it still cannot tell its error from a value whose canonical form is that object. -
Go: SLH-DSA-192f keys are read without a panic (local source, not in a published version;
sdks/go/dcp/providers/slh_dsa_192f_provider.go). The provider read secret and public keys with no parameter set (a zeroID), and circl panicked on reading them; it now setsslhdsa.SHA2_192ffirst, so signing and verifying with SLH-DSA-192f keys work, and an input of the wrong size gives an error or a rejection, not a panic. No format or API changes. -
Transparency log: a peer added by
POST /gossip/peersto a log started withoutGOSSIP_PEERSwas never polled (source onmain, not in a published version). Polling started only at startup, and only with configured peers. The first accepted addition now starts it.startPollingkeeps one schedule, so repeated additions or replacements do not multiply polls, and a refused request (401 or 400) starts nothing. Each poll still reads 16 KB at most and times out after the interval or 10 s, whichever is shorter; cycles never overlap, and an answer whose alias changed meanwhile is still discarded. The exchange, pinned keys, alerts and the other endpoints are unchanged. - Acceptance change: Go
v2.VerifyPayloadHashnow checks the normativepayload_hash(source onmain, not published; Go v2.11.1 keeps the defect). It compared the bare hex SHA-256 withpayload_hash, so it returnedfalseforSignPayloadoutput and for every schema-valid value (sha256:<hex>), andtruefor the bare digest. It now returnstrueonly whenpayload_hashis exactly"sha256:"followed by the lowercase hex SHA-256 of thedcp-jcs-v1bytes, the value thatSignPayload,schemas/v2/signed_payload.schema.jsonand the TypeScript, Python and Rust helpers use. A bare digest, uppercase hex, surrounding spaces or any other form is nowfalse, nil. Migration: passpayload_hashas received; to build it fromPreparePayload, write"sha256:" + hash.PreparePayloadstill returns the bare digest (its comment now says so), theniland canonicalization errors are unchanged, and the bundle verifier, which never used this helper, does not change. The helper checks hash consistency only, not the signature, the keys, the policy or admission. Newsdks/go/dcp/v2/signed_payload_test.go. The Go README and theAuditEvidenceV2comment now also say that decoding a received bundle into the typed structs dropsnulland unknownevidencemembers, so the received bytes are the ones to keep and verify. - Acceptance change: WASM
wasm_dual_merkle_rootand@dcp-ai/wasmdualMerkleRootnow compute the V2-11 dual Merkle root (source onmain, not published;@dcp-ai/wasm2.1.1 and earlier keep the old helper). The helper hashed the hex text of two digests concatenated and promoted the odd node, so for 2 or more leaves its roots matched neitherspec/VERIFICATION.mdV2-11 (raw 32-byte digests concatenated, the last node duplicated on an odd layer) nor the TypeScript, Python, Go and Rust SDKs. It now delegates to the Rust corev2::dual_hash::dual_merkle_root. The change is not limited to multi-leaf roots: those change because they were incompatible with V2-11; a leaf that is not a 32-byte hex digest is now an error instead of being hashed as text; and the roots are always lowercase hex, so a single leaf given in uppercase now comes back in lowercase. A single valid lowercase leaf, the result format and the errors for an empty array and for input that is not JSON are unchanged. Bundle verification, the WASM verifier included, already used the correct reduction and does not change; earlier bundles and their evidence are not rewritten or re-signed, since the change is confined to this helper. New public vectorstests/conformance/v2/merkle_vectors.json(exact roots for 1, 2, 3 and 5 leaves and reordered leaves, generated offline with the Python standard library and anchored on the golden 2-leaf roots) are checked bysdks/wasm/test/dual-merkle-root.mjsandsdks/rust/tests/dual_merkle_vectors.rs. denyListRevocationSource(@dcp-ai/express,@dcp-ai/openclaw) left responses open. A non-2xx answer or a declaredContent-Lengthover 16 MiB was refused without reading or closing its body: the response stayed open after the call settled and its slot was freed, out of reach of the timeout and the caller's signal. And with Node 22.15 (undici 6.21.2), a garbage collection after the headers kept the fetch's signal from reaching a body being read: its own timeout and the supervisor's deadline no longer stopped a body still arriving, and the call never settled, holding its slot. It now aborts its fetch and cancels the body on every exit, stops its own read on its signal and releases the reader. Codes, defaults and capacities are unchanged; nothing is drained, and the remote close is not waited for.- Reference CLI (
bin/dcp.js): paths were confined to the working directory as written, not where they lead, and several reads and writes skipped the check. A symlink inside the working directory, or a path to another directory (keys rotate --key-dir,governance sign-advisory --key-dir,governance verify-advisory --sigs), let a command read files outside it, secret keys included; a symlink or hard link at an output path (keygen,keys rotate,recovery-setup, the governance commands, the V1sign-bundleoutput) made it write outside it; an existing secret key file kept a wider mode; anddcp validate,conformanceand V1sign-bundlerantools/*.jsfrom the working directory, so a planted script ran. Every path is now resolved with its symlinks (a dangling one is refused) and checked, every read and write of a user path goes through that check, files are written to a new file (O_EXCL, exact mode) renamed over the target, and the CLI runs its own tools. The CLI assumes nothing else changes the working directory while a command runs; on Windows, whereO_NOFOLLOWdoes not exist, the rename still replaces a link instead of writing through it. @dcp-ai/anthropic-mcpand@dcp-ai/google-a2a: the"types"export condition came first, so a CommonJS consumer got the ESM declarations (dist/index.d.ts) and TypeScript refusedrequire()undernode16with TS1471. The condition is removed, sorequiregetsdist/index.d.ctsandimportgetsdist/index.d.ts.@dcp-ai/sdk,@dcp-ai/expressand@dcp-ai/openclawdrop the same condition, which came after"import"and"require", never matched and made tsup warn on every build. In@dcp-ai/wasm, whose exports have no"require", it gave a CommonJS consumer types undernodenextfor arequire()that Node refuses withERR_PACKAGE_PATH_NOT_EXPORTED. TypeScript now reports TS2307 there as well.- Go SDK:
VerifySignedBundleV2WithPolicyfailed every bundle under a policy that setrequire_session_expiry,require_audience_bindingorverifier_id, fields thatverifier_policy.schema.jsonlists and the TypeScript and Python verifiers enforce.VerifierPolicynow has them (RequireSessionExpiry,RequireAudienceBinding,VerifierID, off when zero) and the verifier applies them as TypeScript does: a manifest withoutsession_expires_at, withoutintended_verifier, or intended for another verifier thanverifier_idis rejected. - Rust SDK and
@dcp-ai/wasm:VerifierPolicyrefusedrequire_session_expiry,require_audience_bindingandverifier_id, whichschemas/v2/verifier_policy.schema.jsonlists and the TypeScript and Python verifiers enforce, so a policy that set any of them failed every verification. They are now enforced as in TypeScript: underrequire_session_expirya manifest withoutsession_expires_atis rejected, and underrequire_audience_bindingone withoutintended_verifieror, withverifier_idset, with another one. Rust'sBundleManifestgainsintended_verifier. - Python SDK,
dcp_ai.langchain(DCPTool.run,DCPTool.arun) anddcp_ai.openai(dcp_verify_bundleinhandle_dcp_tool_call): both sent every bundle to the V1 verifier, so every V2 bundle was rejected and a V1 bundle was accepted under no verifier policy. They now verify V2 bundles withverify_signed_bundle_v2and an Ed25519 / ML-DSA-65 / SLH-DSA-192f registry built once, under a verifier policy the caller sets (DCPTool(verifier_policy=...),handle_dcp_tool_call(..., verifier_policy=...), overDEFAULT_VERIFIER_POLICY), and return the verifier's errors, at most 20 errors and 20 warnings of 500 characters each, with the control and format characters of bundle-supplied text (anagent_id, an invalid timestamp) escaped, so a bundle cannot put megabytes or new lines into the model's context. A V1 bundle is accepted only under the V2-10 rule of the FastAPI middleware, now shared by all three entry points (dcp_ai._verify_any) and extended to the whole policy: the policy allows V1 bundles, accepts ed25519 (inaccepted_classical_algs, not inadvisory_rejected_algs), requires no audience binding, session expiry or maximum session duration (a V1 bundle has nointended_verifierand no session window), and would accept a classical-only bundle for its risk tier and security tier; the FastAPI middleware applies the added conditions too. A V1 bundle whose artifacts are not objects, or whoseaudit_entriesis not a list of objects, is refused in all three (the FastAPI middleware answered 500 for a non-objectagent_passport, now 403), anddcp_ai.verify.verify_signed_bundlereturns an error instead of raising on a malformed signature block, key or audit trail. A V1 bundle whose passportagent_idor RPRhuman_idis not a string (or whose passportpublic_key,statusorcapabilitieshas the wrong type) is refused too, instead of reaching the FastAPI agent context. The FastAPI middleware's 403 responses and its context'swarningsare bounded and escaped like the bridges' output. AVerifierPolicyinstance is validated like a dict, so one built withmodel_constructor changed by assignment cannot carry an invalid value, and a risk tier itsrisk_overridesleaves out gets itsdefault_mode, as the V2 verifier and the TypeScript SDK'sresolveModeresolve it (a V1 bundle for that tier was refused with "is not low, medium or high"). The sync entry points work inside a running event loop (the verification runs on a worker thread) and run on a private loop, leaving the thread's current event loop as it was;DCPTool.arunawaits the verification, and the newaverify_bundle_tool_callis the async form of the OpenAI tool call. Both pass every case oftests/interop/v2/bundle_vectors.json. - Go SDK:
BundleManifesthad nointended_verifier, so a manifest decoded into it and encoded again lost the field and its signature no longer verified. The field is there now (IntendedVerifier, omitted when empty); a test round-trips a manifest with every schema field. - Python SDK,
dcp_ai.crewai: the agents of aDCPCrewnever shared the crew's session nonce.DCPCrewAgentalways generated its own, so the crew's check for an agent without one never applied, and each agent's audit entries carried a differentsession_nonce. A crew now gives its nonce to every agent that still has the nonce it generated for itself and has not logged yet; a nonce given to the agent, or set on it later, stays. - Rust
wasmfeature and@dcp-ai/wasm:wasm_sign_bundle(signBundle) wrotecreated_at ""and nosigner.id, so the reference verifier, the CLI and the server rejected every bundle it signed (created_at must match format "date-time",signer must have required property 'id').wasm_build_bundle(buildBundle) joined the SHA3-256 audit leaves with SHA-256, unlike every other SDK, and skipped an audit entry it could not canonicalize while still counting it. Both now callbuild_bundle_v2andsign_bundle_v2, sobuildBundlealso rejects apayload_hashthat does not match its payload and an artifact or entry from another session. WithoutsessionNonce,buildBundlegenerated a nonce no artifact carried, so evenverifyBundlerejected the bundle; it now takes the RPR'ssession_nonce. Theexample.htmldemo passed raw payloads towasm_build_bundle, which never accepted them; it now signs each artifact first. TheSignedBundleV2type declaressigner.id. - Rust SDK:
Ed25519Provider::signpanicked on a secret key shorter than 32 bytes (in wasm, a trap instead of a JSON error); it now returnsinvalid secret key length. - Mandate chains (all four SDKs): a sub-mandate entry without
data_classesunder a parent entry that has them passed attenuation, although nothing then bound it to the parent's data classes. It is now rejected (data_classes for domain "<d>" missing), as a missinglimitskey already was; DCP-09 §3.5.2 rule 5 says so. Shared vectordropped_data_classes. - Go SDK: an
A2ASessionused from several goroutines crashed the process (fatal error: concurrent map writesinDecryptMessage). The session now holds a mutex, soEncryptMessage,DecryptMessageandNeedsRekeyingmay run concurrently; sequences stay unique and each message is still accepted once.IntentV2gainsSecurityTierandPolicyDecisionV2gainsResolvedTier(both optional in the schema and omitted when empty), so a Go integrator using the typed payloads can declare them. tools/difffuzz:prototype_key_as_keyassigned__proto__as a plain property, which sets the prototype instead of adding a key, so those mutants never carried the key they claimed (and a cycle could crash the run); it now defines an own key. A shim that cannot start or that died between verdicts is reported as a crash instead of an unhandled error.spec/VERIFICATION.mdnow names Rust's bundle builder correctly (wasm_build_bundle,wasmfeature only).- Go SDK:
SignBundleV2leftsignature.signer.idempty, whichsigned_bundle.schema.jsonforbids, so the reference verifier, the CLI and the server rejected every bundle it signed while the four SDK verifiers accepted it (found bytools/interop-matrix). The signer is now the bundle's responsible principal (signer.id= the RPR'shuman_id), and a bundle whose RPR has nohuman_idis refused before signing. -
A2A sessions (TypeScript, Python, Go):
decryptMessage/decrypt_message/DecryptMessagekeep the sequences accepted within the receive window and accept each one once, out of order included; a sequence that is not a non-negative integer is rejected, and a sequence is only recorded after its authentication tag verifies. A sequence a full window or more ahead of the receive counter is rejected (DCP-04 §8.4). A session restored without the new field (TypeScriptundefined, Gonil, PythonNone, the constructor default) counts every sequence of the window below its receive counter as received.verifyResumeProof(TypeScript) compares in constant time, as Python and Go already did. The Go session is documented as not safe for concurrent use. -
Rust SDK:
serde_jsonnow hasfloat_roundtrip. Its fast float parser is off by one ULP on long decimal literals, so9007199254740991.0(whichdcp-jcs-v1reads as the integer 9007199254740991, as every other SDK does) became 9007199254740990 and2251799813685247.0a non-integer, and the Rust verifier rejected signatures and limits the TypeScript, Python and Go verifiers accept. Shared vectorlimits_written_in_decimal_form. @dcp-ai/wasm: the npm package shipspkg/(the.wasmbinary and its glue). wasm-pack writespkg/.gitignore=*, npm honours nested.gitignorefiles, and@dcp-ai/wasm@2.0.0was published withoutpkg/, so its runtime import of../pkg/dcp_ai.jsfailed. The build removes that file and the test suite checks the tarball.@dcp-ai/sdk: the bundled telemetry version is read frompackage.jsonat build time (tsup.config.ts) instead of a literal in the build script; 2.2.0 would have reported itself as 2.1.2.- Python
verify_signed_bundle_v2reported an artifact without acomposite_sigasMalformed signed bundle: KeyError('composite_sig'); the error now names the artifact (intent: Missing composite signature), as in the other SDKs. - TypeScript
verifySignedBundleV2returns a failed verdict (Malformed signed bundle: ...) for a structurally malformed signed bundle instead of throwing: a missing,nullor non-objectbundle,signature,manifestoraudit_entries, anullaudit entry, or a non-integer number (0.5,1e400) in a canonicalized object (spec/VERIFICATION.mdV2-3, V2-5). The Python, Go and Rust verifiers already rejected these; found bytools/difffuzz. - Go V1
VerifyObjectpanicked on a public key that did not decode to 32 bytes (ed25519.Verifypanics on any other length). It now checks the decoded length and returns an error instead.VerifySignedBundlekeeps its behaviour around it: an empty public key falls back to the one embedded in the signature, both missing givesmissing public key, and an error fromVerifyObjectis reported asSIGNATURE INVALID. - The TypeScript CBOR decoder (
sdks/typescript/src/wire/cbor.ts, optional) rejects duplicate map keys, invalid UTF-8, and integers and lengths outside the safe integer range. It keeps U+FEFF as text data, defines a__proto__key as an own property instead of invoking the setter, and the encoder rejects unpaired surrogates instead of replacing them. This hardens the decoder's input handling; it is not a complete canonical CBOR decoder and adds no canonicalization profile.
Documentation¶
-
Operational limits (new docs/OPERATIONAL_LIMITS.md; no code, default, profile or wire change). What bounds inputs, trust and revocation sources, concurrency, client disconnection and the Express verdict cache, which layer sets each limit, what the operator has to set, and a sample pilot configuration that is neither a default nor normative. Linked from
docs/ADMISSION.mdand the Express and FastAPI READMEs; the Express README now says thatreq.dcpAgent.mandateIdis the manifest's value, with no mandate chain verified. -
spec/profiles/admission/README.md(admission profile): editorial precisions, with no change to any vector, schema or SDK behaviour; the four SDKs already behave as they say. §5.1 step 2 verifies the bundle as a V2 signed bundle: a bundle that is not V2 is not verified, whateverallow_v1_bundlessays, and under the profile P1 stops before verification and reports the bundle as not verified. A profile requires a trust set: the combined function and step 3 on its own refuse a profile without one withCONFIG_PROFILE_CONTRADICTION, and the combined function refuses a profile together with a caller's verifier policy with the same code; whether that happens before or after verification is not fixed. In §4.1, a mode outside the enumeration of the four modes isCONFIG_INVALID_VALUE, like a value of the wrong type, while a mode of that enumeration other thanhybrid_required, or an algorithm outside the allowed subset, isCONFIG_PROFILE_CONTRADICTION. §2.2: when a trust set has several problems, the code of any one of them may be reported; it is rejected whichever is reported, with no precedence defined. §5.2:errorsfollow the order of the phases of §5.1, a check may add its code once per component or subject, and no order is fixed within a phase.tools/conformance/README.md: theerror_containsstrings that the evidence catalog judges (146 of its 241 cases) are the SDKs' diagnostic wording, not normative. docs/QUICKSTART_CREWAI.md: neither example ran. The V1 one importedbuild_bundle, which the Python SDK does not have, and wrote signatures whereintent_hashandprev_hashtake hashes; the V2 one used an API that was never released and said the Python SDK had no V2 bundle builder, signer or verifier, althoughdcp_ai.v2ships them (from 2.9.0). Both now use the SDK as it is: a policy gate decides before the tool runs, the search query stays out of the record, each tool call seals its own bundle, and verification pins the operator (V1: its public key; V2: its RPR hash and kids underhybrid_required, and that every kid is derived from its key, since the bundle's own keys only make it self-consistent). The bundles verify in Python and withdcp verify-bundle, the V2 ones also with the TypeScript, Go and Rust SDKs. The guide importstoolfromcrewai.tools, as CrewAI documents it, and describes thenpm create @dcp-ai/crewaiscaffold as the placeholder walkthrough it is.integrations/crewai/README.md: the examples used V1 payloads with values the schemas refuse (liability_mode: "full", capabilitywrite_file) and printed anaction_typefield audit entries do not have. They now run; the README says thatDCPCrewAgentandDCPCrewkeep unsigned records and do not run CrewAI, and lists thesession_nonceparameters.- Documentation in English and in the es, zh-CN, ja and pt-BR translations brought up to date: delegation chains in every SDK README, the A2A receive window, Testing & Assurance (interop matrix, differential fuzzer, shared vectors), corrected facts (Go 1.25, Rust's FIPS crates, a TypeScript DCP-09 example that used fields outside the schema), and the README rows the translations had missed.
docs/EU_AI_ACT_MAPPING.md: how DCP-AI artifacts support the logging, human oversight, record-keeping, explanation and incident-evidence obligations of Regulation (EU) 2024/1689 (EU AI Act) as amended by Regulation (EU) 2026/1744, with the application timeline, an implementer checklist and the gaps DCP does not cover. Articles verified on EUR-Lex on 22 October 2026.spec/DCP-09.md§3.5 now specifies sub-delegation on the real wire fields with normative chain verification rules; §2, §10.3 and §10.6 list theDCP-AI.v2.SubMandatecontext.spec/DCP-AI-v2.0.md§4.1 lists theDCP-AI.v2.DelegationandDCP-AI.v2.SubMandatecontext tags, and §14.2 states that a delegation chain's effective tier is an input that can only raise the tier.
[2.10.0] - 2026-10-22¶
Package versions¶
dcp-ai(PyPI) 2.8.2 → 2.9.0: pqcrypto 1.x, FIPS 205 SLH-DSA, ML-KEM-768, V2 bundles and a V2 verifier; breaking forblind_rpr(requirespii_salt) andpqcrypto<1.0installs.@dcp-ai/sdk(npm) 2.1.2 → 2.2.0: security fixes inverifySignedBundleV2, security tier enforcement, Blinded RPR exports; breaking forblindRpr(requirespii_salt).github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2v2.9.0 → v2.10.0: V2 verifier hardening and tier enforcement; breaking forBundleBuildInput(signed artifacts) andBlindRPR.dcp-ai(crates.io) 2.9.0 → 2.10.0: native V2 verifier, tier enforcement, RFC 3339 expiry parsing; breaking forblind_rprand thewasmbundle builder.@dcp-ai/wasm2.0.0 → 2.1.0:buildBundletakes signed artifacts;verifyBundletakes a full verifier policy.@dcp-ai/openclaw2.0.0 → 2.1.0: security tier floor andpii_salton signed RPRs.- Unchanged:
@dcp-ai/cli2.0.0 and the other integration packages.
Security¶
- Blinded RPR commitments are salted, in all four SDKs.
pii_hashwassha256(canonical({ contact, legal_name })), so anyone who guessed a principal's name and contact could confirm the guess against a published blinded RPR, and on an immutable log or anchor that stays true forever. The full RPR now carriespii_salt(32 bytes from a CSPRNG, lowercase hex), which is signed with it and never published, andpii_hashissha256(canonical({ contact, legal_name, pii_salt })). Deleting the full RPR, and with it the salt, leaves the published commitment unopenable (spec/DCP-AI-v2.0.md§12.1). A missingcontactorlegal_namehashes asnullin every SDK; TypeScript used to omit the key. The newblinded_rprcategory intests/interop/v2/interop_vectors.json(salted and legacypii_hash, expected blinded record) runs in all four SDK suites, andgenerate-interop-vectors.tsemits it. - Blinded RPR verification (
verifyBlindedRpr,verify_blinded_rpr,VerifyBlindedRPR) now also comparessession_nonceandbinding_keys, in all four SDKs. Before, a full RPR with different binding keys or a different session nonce still matched the blinded record. In Go, list fields decoded from JSON no longer fail to match the same list built as[]string. - Verification server:
POST /v2/passport/registerverifies the passport's composite self-signature and registers only its signer keys; re-registering anagent_idneeds a newer passport signed by all of its active keys. Anyone could re-register an agent with their own keys, plant their emergency token and emergency-revoke it.POST /v2/keys/rotateverifies the proof of possession and the old key's authorization (verifyKeyRotation), andPOST /v2/multi-party/authorizeverifies each party's signature against its registered keys, counting each party once. - Verification server:
/v2/policy/mode,/v2/governance/register,/v2/advisory/publishand/v2/advisory/auto-applyrequireAuthorization: Bearer $OPERATOR_TOKEN(locked when unset). Advisories are schema-validated and capped at 1000. Rate limits key on the socket address, not the client-suppliedX-Forwarded-For(TRUST_PROXY=truebehind a proxy), andVERIFIER_MODE/ALLOW_V1_BUNDLESare read from the environment;allow_v1_bundlesis now enforced. - Revocation service: agents register with their signed passport (
POST /v2/register), which records kid ownership, key expiry and the emergency token; re-registration needs a newer passport signed by the agent's active classical and PQ keys, and/revokeand/v2/revokemust be signed by its active keys.GET /v2/check/kid/:kidreports emergency revocations of registered kids without?agent_id=. - Anchor service: no more random
tx_hash(records carrytx_hash: null,onchain: false), bodies are capped at 1 MB,POST /flushneeds the operator token, and malformed input is a 400 instead of a crash. Transparency log: tree heads are Ed25519-signed instead of an HMAC nobody else could verify;OPERATOR_KEYmust be the 64-hex seed (the public compose placeholders were removed), the Merkle tree uses RFC 6962 leaf/node prefixes, andPOST /gossip/peers(an SSRF vector) needs the operator token. - Verification server and revocation service: expired keys can no longer re-register a passport,
authorize a key rotation or sign for a party or a revocation, and re-registering keeps the
registered key entries, so a new passport cannot extend a key's
expires_at. - Fly configs keep one verification and one revocation machine running: scaling to zero wiped their in-memory registries whenever the machine idled.
- TypeScript SDK:
verifySignedBundleV2implementsspec/VERIFICATION.mdSteps V2-7 and V2-8. A PQ signature that is present (pq_over_classical) must verify in every mode;hybrid_preferred(the default for medium risk) accepted an invalid one with a warning andclassical_onlyignored it. Only an absent one (classical_onlybinding) is tolerated, and not underhybrid_requiredorpq_only. Each artifact's owncomposite_sigis now verified under the same rules with its context tag: the RPR against itsbinding_keys, the passport, intent and policy decision against the passportkeys. Before, onlypayload_hashwas checked, so a forged agent signature on the intent verified even underhybrid_required. The reference verifier,dcp verify-bundleand the server get this with the next@dcp-ai/sdkrelease. - TypeScript SDK:
verifySignedBundleV2fails closed on a verification mode it does not recognise, and rejects a verifier policy whose mode is notclassical_only,hybrid_preferred,hybrid_requiredorpq_only. - Go (
v2.VerifySignedBundleV2) and Rust SDKs: each artifact'scomposite_sigis verified the same way (V2-8), and signer keys are looked up bykid(V2-6), the bundle signer in the passportkeysand the RPRbinding_keys. They used to take the passport's Ed25519 and ML-DSA-65 keys by algorithm, so they rejected bundles signed with the principal's keys and reported a bundle whose passport listed no Ed25519 key as verified, with a warning, without checking its signature. - TypeScript, Go and Rust SDKs: in every composite signature, in every verification mode, the
classical slot must hold Ed25519 and the PQ slot ML-DSA-65 or SLH-DSA-192f, from a key whose
algis the same. Go also rejects a bundle whose RPR, passport, intent or policy decision is null or not an object, where it skipped every check on that artifact, and TypeScript returnsverified: falsefor such a bundle instead of throwing. - Security tier (spec §14.2): the Go SDK and the
@dcp-ai/wasmbinding (wasm_compute_security_tier) matchedpayment/transfer,financial,health,legalandbiometric, which the intent schema does not allow, so schema-validinitiate_payment,financial_dataandhealth_dataintents wereroutinethere andelevatedin the TypeScript, Python and Rust SDKs. All four SDKs and the binding now compute the same tier, checked by the newsecurity_tiervectors intests/interop/v2/interop_vectors.json. - Verification server:
resolved_tierinPOST /verifyandPOST /v2/bundle/verify, and the tier check of/v2/bundle/verify, tookintent.security_tieras given, so an agent that declaredroutinelowered its own tier, and readrisk_scorefrom the intent, which the schema does not allow. They now use the §14.2 rule (lib/security-tier.js):risk_scorefrom the bundle's policy decision,estimated_impact, unknown values toelevated, and an explicit tier that can only raise the result./v2/intent/declarecomputesresolved_tierthe same way. - OpenClaw integration:
dcp_verify_bundleanddcp_log_actionusedintent.security_tieras given (security_tier ?? computeSecurityTier(intent)), so a declared tier could lower the computed one, and passed no policy decision. The explicit tier now only raises the tier. The policy decision is passed too, sorisk_scorecounts once@dcp-ai/sdkreads it (the release after 2.1.2). - Denial of service in the reference services, whose state lives in memory and does not survive a restart:
- Verification server: registrations are rate limited per client (
REGISTRATIONS_PER_HOUR, default 20 per hour), rotation keeps only an agent's 4 most recent retired keys, and onlycreated_atis stored from a registered passport. One agent rotating over and over, or a flood of registrations, filled the 10,000-key registry (507 until a restart), and every stored signed passport could take up to 1 MB. Dropped keys stay revoked as tombstones and can never be registered again, and an agent can register at mostMAX_KEYS_PER_AGENTkeys (64) over its lifetime. A rotation drops a retired key before the cap check, so it still fits in a full registry. The key-family check on re-registration no longer rescans the registry quadratically. - Transparency log:
POST /gossip/exchangeonly accepts a well-formed tree head signed by a pinned peer, in a body of at most 16 KB, and keeps the last 100 distinct alerts, so a replayed tree head cannot push older ones out. Polled tree heads must verify under the peer's pinned key, and a poll reads at most 16 KB, times out after one interval and never overlaps the previous one. Before, anyone could fill the alerts list with 1 MB strings, no peer tree head was ever verified, and a hanging peer stacked polls. - Revocation service:
POST /v2/registeris rate limited per client and capped atMAX_AGENTS(default 10,000), and stored fields are bounded, so a flood of registrations or oversized records can no longer run it out of memory and wipe every revocation. - Verification server and revocation service: registering a new
agent_idneeds the operator token. A passport is only self-signed, so anyone could register a victim'sagent_idfirst, with their own keys and token, then emergency-revoke it or lock the real agent out with 409. Every restart reopened everyagent_idto this.GET /v2/check/kid/:kidanswers 404 (registered: false) for a kid it never registered, instead of the verdict of whatever agent the caller named in?agent_id=. - Verification server and revocation service: public keys and signatures must be the canonical
base64 of exactly their algorithm's size, and the V2 schemas restrict
public_key_b64andsig_b64to the base64 alphabet and cap their length. Lenient decoders skipped whitespace, so an ML-DSA key padded with 900 KB of spaces registered and was served back, and padded signatures were stored and published by/v2/list. - TypeScript SDK
verifySignedBundleV2enforces the security tier (spec §14.3): it verifies against the stricter of the policy mode and the mode of the tier computed from the signed intent and policy decision, and reports it indetails.resolved_tier. Before, the mode came only from the agent's ownpassport.risk_tier, so underDEFAULT_VERIFIER_POLICYa passport declaringrisk_tier: "low"gotclassical_onlyeven for a payment intent. - TypeScript SDK:
AdaptiveRateLimiter.check,generateSessionExpiryandVerificationCache.setcount an unknown tier name, or a prototype key such asconstructor, aselevated. The rate limiter gave every made-up name its own bucket,generateSessionExpirythrew onconstructor, and the cache never expired such entries. - OpenClaw integration:
dcp_declare_intentcomputed the tier it signs into the intent and the policy decision before scoring the intent. It now passes the risk score, which counts once@dcp-ai/sdkreads it (the release after 2.1.2). - Go (
v2.VerifySignedBundleV2), Rust (v2::verify_bundle::verify_signed_bundle_v2and thewasm_verify_signed_bundle_v2binding behindverifyBundlein@dcp-ai/wasm) and Python (verify_signed_bundle_v2) verifiers enforce the security tier like the TypeScript one: the mode is the stricter of the policy mode and the tier's mode, applied to the bundle signature and to each artifact signature; an unknown policy mode fails closed; and the result reports the tier and the mode. The Go and Rust verifiers had no verifier policy and acceptedclassical_onlysignatures for any intent. - Python SDK
verify_signed_bundle_v2: in every composite signature, in every mode, the classical slot must hold Ed25519 and the PQ slot ML-DSA-65 or SLH-DSA-192f, from a key whosealgis the same, as in the TypeScript, Go and Rust SDKs. Underpq_onlyit accepted ML-DSA-65 in the classical slot. - Go and Rust SDKs: a composite signature must carry its
pqmember,nullforclassical_only(schemas/v2/composite_signature.schema.json). They decoded an absentpqlikenulland verified such aclassical_onlysignature, which the TypeScript and Python verifiers reject.bundle_vectors.jsonpins it withclassical_only_bundle_signature_without_pq. - Go and Rust V2 verifiers tie the audit trail to the signed manifest, as the TypeScript and Python
ones do: they recompute
audit_merkle_root, checkaudit_count, and require the chain to start atGENESIS. Before, anyone could replace the audit entries of a validly signed bundle with a fabricated, self-consistent chain, and both still reported it as verified. - Go and Rust V2 verifiers fail closed when a manifest check cannot run: a missing artifact hash
such as
rpr_hash, or a payload that is missing or cannot be canonicalized, is an error instead of a skipped check. - Go and Rust V2 verifiers check each artifact's
payload_hashagainst its payload, as the TypeScript and Python ones do; they ignored it. - Go and Rust V2 verifiers reject a bundle whose
manifest.session_expires_athas passed, so an expired session can no longer be replayed against them. The@dcp-ai/wasmverifier reads the clock from JavaScript, sinceSystemTime::now()panics onwasm32-unknown-unknown. - Rust SDK:
is_session_expiredcompared RFC 3339 strings with the UTC clock lexicographically, so a timestamp with an offset was misjudged by up to 14 hours: a session ending in an hour, written-05:00, counted as expired, and one that ended an hour ago, written+05:00, as still valid. It now parses the timestamp (parse_rfc3339_millis) and compares instants, for session and key expiry and in the advisory helpers. - Expiry timestamps in all four verifiers: a
session_expires_ator keyexpires_atthat is present but is not an RFC 3339 timestamp with aZor±hh:mmoffset is rejected; absent ornullstill means no expiry. TypeScript accepted anythingDatecould not parse ("n/a") or parsed leniently ("2099-12-31"), and skipped an emptysession_expires_at; Rust accepted"n/a"; Go skipped a non-string keyexpires_at; Python raised.isSessionExpiredandis_session_expiredcount a malformed value as expired. - Key validity covers every key that can sign. All four verifiers reject a revoked or expired key in
the principal's RPR
binding_keys, which sign the RPR and may sign the bundle; TypeScript and Python checked only the passport keys. The Go and Rust verifiers checked no key status at all and now reject revoked or expired passport keys too. - All four verifiers reject a key whose
statusisexpired, likerevoked. The key entry schema allowsexpired, but onlyrevokedwas checked, so a key marked expired with noexpires_atstill signed. - An empty audit trail must have the Merkle root
sha256:followed by 64 zeros, which the Go and Rust builders already wrote. All four verifiers skippedaudit_merkle_rootwhen a bundle had no audit entries, so it could hold any value.
Changed (breaking)¶
- Verification server (
POST /v2/passport/register) and revocation service (POST /v2/register): a newagent_idneedsAuthorization: Bearer $OPERATOR_TOKEN(401 otherwise); re-registering stays self-service. The revocation service'sGET /v2/check/kid/:kidignores?agent_id=and returns 404 for unregistered kids. - Verification server: an agent can register at most
MAX_KEYS_PER_AGENTkeys (default 64) over its lifetime; further rotations get 409, and the agent has to register a newagent_id. - Schemas:
key_entry.public_key_b64andcomposite_signaturesig_b64must match^[A-Za-z0-9+/]+={0,2}$and are capped at 3,456 and 66,476 characters. - Transparency log: each
GOSSIP_PEERSentry is<url>#<base64 Ed25519 public key>. Tree heads from peers without a pinned key are ignored, and/gossip/exchangerejects them (403); the Docker Compose logs only gossip once their keys are set (seedocker/README.md). - Go SDK:
BundleBuildInputtakes signed artifacts (SignedPayload, seev2.SignPayload) and[]AuditEventV2audit entries.BuildBundleV2wrapped unsigned payloads, which verifiers now reject (V2-8), and filled the bundle with empty audit entries. - WASM (
wasm_build_bundle, andbuildBundlein@dcp-ai/wasm): each artifact is aSignedPayloadfromwasm_sign_payload/signPayload, not a raw payload. Bundles built from raw payloads carried no artifact signatures and no longer verify. - Revocation service:
POST /v2/register-emergency-tokenandPOST /v2/register-key-expirywere removed in favour ofPOST /v2/register. V1/revokeneeds an agent registered there, which takes a hybrid (Ed25519 + ML-DSA-65 or SLH-DSA-192f) passport signature, so Ed25519-only (V1) agents can no longer publish revocations. A revoked agent can no longer be revoked again (409). - Verification server and revocation service: re-registering an
agent_id(for example to replace the emergency revocation token) needs a passport whosecreated_atis newer than the registered one, signed by all of the agent's active keys; a passport without a token keeps the registered one. An agent that has lost every active key of a family (classical or PQ) cannot re-register: recovery goes through the emergency revocation token or the operator. - Transparency log: leaf hashes, roots and inclusion proofs change with the RFC 6962 prefixes, and
STH
signature/public_keyare now base64 Ed25519./addonly acceptssha256:<64 hex>hashes, and the anchor's/anchor64 hex characters with an optionalsha256:prefix. - Verification server:
/v2/keys/rotaterequiresauthorization_sigandtimestamp(KeyRotationRecord).dcp rotate-keyanddcp keys certifystill send a simulated proof of possession, which the server now rejects. - Python SDK: SLH-DSA-192f now implements FIPS 205 (pure SLH-DSA, empty context), like the
TypeScript, Go and Rust SDKs. It was built on PQClean's pre-standard SPHINCS+, which signs the
bare message instead of
0x00 || 0x00 || message, so SLH-DSA signatures did not verify across SDKs in either direction. Signatures made bySlhDsa192fProviderindcp-ai2.8.2 and earlier no longer verify. - Python SDK: requires
pqcrypto>=1.0,<2, now used for ML-DSA-65, SLH-DSA-192f and ML-KEM-768, instead of the<1.0cap of 2.8.2 (pqcrypto1.0 changed its API and dropped SPHINCS+).dcp_ai.providersno longer swallows anImportError, and the PQ interop tests no longer skip when a provider is missing. - Python SDK:
VerifierPolicyrejects unknown fields instead of dropping them, and gains the fields it was missing (allow_classical_fallback_disable,warn_classical_only_deprecated,advisory_rejected_algs,require_session_expiry,max_session_duration_seconds,require_audience_binding,verifier_id). Before,VerifierPolicy(require_audience_binding=True, verifier_id=...)silently lost both fields. Inverify_signed_bundle_v2, a dict policy is validated as aVerifierPolicy(unknown keys raise), and fields it leaves out take the model's defaults (hybrid_requiredfor every risk tier), not the default policy'slow→classical_only. Onlyrisk_overrides=Nonemakesdefault_modeapply to every tier. - Blinding requires
pii_salt, in all four SDKs.blindRpr(TypeScript) andblind_rpr(Python,ValueError) throw, andBlindRPR(Go) andblind_rpr(Rust) return an error, when the full RPR has nopii_saltor it is not 64 lowercase hex characters. Set it with the new helper before signing the RPR: an RPR signed without one has to be re-signed before it can be blinded. - Blinded RPR verification rejects a full RPR whose
pii_saltis present but is not a string of 64 lowercase hex characters, in all four SDKs. Before, a record blinded elsewhere with an empty or short salt verified with no warning, while unsalted records were flagged. - Blinded RPR verification results:
verify_blinded_rpr(Python) returns a newwarningskey and Go'sBlindedRprCheckhas a newWarningsfield, so code that compares whole results or buildsBlindedRprCheckwith unkeyed literals must change. schemas/v2/responsible_principal_record.schema.jsonacceptspii_salt. The schema hasadditionalProperties: false, so verifiers that validate against the previous schema reject RPRs that carry a salt: update verifiers before producers start setting it.- Rust SDK:
ResponsiblePrincipalRecordV2has a new public field,pii_salt: Option<String>, so struct literals must set it. - Security tier computation (spec §14.2), all SDKs:
risk_scoreis read from the bundle's policy decision, never from the intent (the intent schema does not allow it, so the tier was neverstandardfor a valid intent).estimated_impactmediumraises the tier tostandardandhightoelevated; a missing, mistyped or unknownaction_type,estimated_impactordata_classesvalue gives at leastelevated; an explicitintent.security_tiercan only raise the result. - TypeScript:
computeSecurityTier(intent, policyDecision?). - Python:
compute_security_tier(intent, policy_decision=None);compute_security_tier({})is nowelevated. The LangChain and CrewAI integrations use it instead of their own copies. - Go:
SecurityTierInputgainsEstimatedImpactandSecurityTier, and its zero value (or nilDataClasses) is nowelevated.v2.SecurityTierInputFromPayloads(intent, policyDecision)builds the input from decoded payloads. - Rust:
compute_security_tier(intent, policy_decision: Option<&Value>).wasm_compute_security_tierandcomputeSecurityTierin@dcp-ai/wasmtake an optional policy decision. - Security tier computation, all SDKs and the reference server: a policy decision
risk_scorethat is missing or not an integer from 0 to 1000 counts as unknown, so the tier is at leastelevated, like an unknown intent value. A non-numeric value used to count as 0, and other numbers were compared as they were, so-1gaveroutineand1e30gavemaximum(nowelevated, with the samehybrid_requiredmode). With no policy decision the score is still 0. In Go,SecurityTierInputFromPayloadssetsRiskScoreto -1 for such a value, andComputeSecurityTiertreats anyRiskScoreoutside 0..1000 as unknown. - TypeScript SDK
verifySignedBundleV2: bundles that pass today with aclassical_onlysignature are rejected when their intent is sensitive:initiate_payment,pii,financial_data,health_data,credentialsorchildren_data,estimated_impact: "high", a policy decisionrisk_scoreof 500 or more, or a value outside the intent schema. Sign them with a hybrid (pq_over_classical) signature. The root verifier (lib/verify.js,dcp verify-bundle, the server) gets this with the next@dcp-ai/sdkrelease. - Go, Rust and Python verifiers reject the same
classical_onlybundles. GoVerifySignedBundleV2and Rustverify_signed_bundle_v2apply the TypeScriptDEFAULT_VERIFIER_POLICYinstead of no policy;wasm_verify_signed_bundle_v2takes an optional policy JSON (aVerifierPolicy) and, with one, rejects V1 bundles. Their policy may differ from the default only indefault_mode,risk_overridesand the Phase 3 fields (advisory_rejected_algs,warn_classical_only_deprecated,allow_classical_fallback_disable, enforced as in TypeScript): any other change, or an unknown field in the RustVerifierPolicyJSON, fails verification instead of being ignored. - Python SDK
verify_signed_bundle_v2: errors and warnings about the bundle signature start withbundle:, a null or non-object artifact is reported as<name>: not a SignedPayload object, and an unknown policy mode fails withUnknown verification mode in verifier policybefore any signature is checked, as in TypeScript.
Added¶
generatePiiSalt(TypeScript),generate_pii_salt(Python, Rust) andGeneratePIISalt(Go). The RPR types of all four SDKs,schemas/v2/responsible_principal_record.schema.jsonandapi/proto/dcp.protocarrypii_salt. Python'sResponsiblePrincipalRecordV2leaves an unsetpii_saltout when serialized, since the schema has no null for it.- Legacy unsalted blinded RPRs still verify, with a deprecation warning: the results of
verifyBlindedRpr(TypeScript),verify_blinded_rpr(Python) andVerifyBlindedRPR(Go) carry awarningslist. Rust'sBlindedRprCheckkeeps its two fields, because a new public field would break callers that build or destructure it; there, a missing or nullpii_saltin the full RPR marks a legacy record. @dcp-ai/openclaw:dcp_identity_setupsigns its RPR with apii_salt, so the record can be blinded later without re-signing.- Revocation service:
REVOCATION_STATE_PATHkeeps registrations and revocations in a file (default: memory only). Writes are appended as fsynced JSON lines, and the file is replayed and compacted through an atomic rename at start; the service refuses to start if the path is not writable or the file is over 512 MB. The Docker Compose stack sets it, on a volume. - Verification server:
MAX_REGISTERED_KEYSandMAX_KEYS_PER_AGENTsize the key registry. effectiveVerificationMode(TS),effective_verification_mode(Python, Rust) andEffectiveVerificationMode(Go): the stricter of a policy mode and a tier's mode, withpq_onlyunchanged (spec §14.3). Shared vectors insecurity_tier.effective_mode.- Go SDK:
v2.VerifySignedBundleV2WithPolicy(registry, json, policy)andv2.DefaultVerifierPolicy(). Rust SDK:verify_signed_bundle_v2_with_policyandimpl Default for VerifierPolicy(the TypeScript default policy). The Go and RustVerifierPolicygain the Phase 3 fields ofschemas/v2/verifier_policy.schema.json, so the TypeScriptDEFAULT_VERIFIER_POLICYdecodes into them as is.@dcp-ai/wasm:verifyBundle(signedBundle, policy?)and theVerifierPolicytype. The Python result'sdetailsgainsresolved_tier. tests/interop/v2/bundle_vectors.json:must_reject_bundles, classical-only bundles with sensitive intents from a low-risk passport that every verifier must reject in every mode, and a classical-only routine control with its expected verdict per mode. The TS, Go, Rust and Python tests run them, and Python now runs the whole file.default_verifier_policycarries the TypeScriptDEFAULT_VERIFIER_POLICY, which every SDK must accept as a policy.tests/interop/v2/bundle_vectors.json: rejection cases for a wrongpayload_hash, replaced or truncated audit entries, an audit chain that does not start atGENESIS, a manifest withoutrpr_hash, an expired session, and revoked or expired passport and binding keys.@dcp-ai/wasm:npm testrunsverifyBundlefrom the built package overtests/interop/v2/bundle_vectors.jsonin Node, and CI runs it. It used to only type-check, and the Rust wasm32 tests are only compiled, so the wasm verifier never ran in CI.tests/interop/v2/interop_vectors.jsonrfc3339: the instant of RFC 3339 timestamps with offsets and fractions, and the malformed values every verifier rejects.bundle_vectors.jsonadds malformed and offset expiries, and its signed bundle carries far-future expiries written with offsets. TypeScriptparseRfc3339, Pythonparse_rfc3339and Rustparse_rfc3339_millisare the parsers the verifiers use.- TypeScript SDK:
blindRpr,verifyBlindedRpr,isBlindedRprandcomputePiiHashare exported from the package entry point. They were implemented but unreachable from@dcp-ai/sdk2.1.2 and earlier; Python, Go and Rust already exported them. lib/verify.jsexportsverifyCompositeSig(a composite signature over a payload, with signer keys looked up by derived kid) and itsv2Registry;lib/http.jsholds the body parsing, client-IP and operator-token helpers shared byserver/andservices/. The anchor and transparency-log services havenode:testsuites, run in CI with the existing ones.- Rust SDK: the V2 bundle verifier moved out of the
wasmfeature intodcp_ai::v2::verify_bundle::verify_signed_bundle_v2, so native builds andcargo testrun it;wasm_verify_signed_bundle_v2delegates to it. tests/interop/v2/bundle_vectors.json: a signed bundle that every V2 verifier must accept, and mutations (forged bundle PQ signature, forged or misattributed artifact signatures, algorithms in the wrong slot, null or non-object artifacts) after which it must reject it with an error naming the bundle or artifact, checked by the TypeScript, Go and Rust suites. Regenerate withsdks/typescript/src/__tests__/generate-bundle-vectors.ts.- Go SDK:
v2.SignPayloadsigns an artifact into aSignedPayload: a composite signature, or a classical-only one when no PQ key is given. - Python SDK:
MlKem768Provider(FIPS 203), the ML-KEM-768 primitive the other SDKs already had. The Python DCP-04 handshake does not run the X25519 + ML-KEM-768 hybrid key exchange yet. - Python SDK: V2 bundles.
BundleBuilderV2,sign_bundle_v2,sign_bundle_v2_classical_only,verify_signed_bundle_v2,DEFAULT_VERIFIER_POLICYandPQ_ONLY_VERIFIER_POLICYport the TypeScriptbuilder-v2.ts,signer-v2.tsandverify-v2.ts. The verifier also checks each artifact's composite signature (VERIFICATION.md V2-8), rejects a present but invalid PQ signature in every mode (V2-7) and fails closed on an unknown verification mode. Unlike the TypeScript verifier, it binds the bundle's PQ checkpoints to the manifest'spq_checkpointsIDs, and in themax_session_duration_secondscheck it clamps the unsignedsignature.created_atto the verification time: a rewritten value can shorten that check down to the session's remaining lifetime, not to zero. Closing it needs a signed session start, which the manifest does not carry. Checkpoint contents (V2-12) andaudit_merkle_root_secondaryare still not verified. tests/nist-kat/ml-kem-768andtests/nist-kat/slh-dsa-192f: NIST ACVP vectors (ML-KEM-768 decapsulation and key checks, SLH-DSA-SHA2-192f signature verification).tests/interop/v2/cross_sdk_vectors.json: SLH-DSA-192f, ML-KEM-768 and signed V2 bundle vectors produced by the TypeScript and Python SDKs, each verified by the other.
Fixed¶
- The V2 conformance examples (
scripts/generate-v2-examples.js) use an opaquehuman_idinstead of one derived from the legal name, which §12.1 now forbids, and their RPR carries apii_salt. - Python SDK:
BundleManifest.pq_checkpointsholds checkpoint IDs, as inschemas/v2/bundle_manifest.schema.json, and the model gainssession_expires_atandintended_verifier.
Documentation¶
spec/DCP-AI-v2.0.md§12.1 specifies the salted commitment, the private salt disclosed only with the full RPR, erasure by deleting the full RPR, an opaquehuman_idthat MUST NOT contain or be derived from PII, and the deprecation of unsalted commitments.spec/CANONICALIZATION_PROFILE.md,spec/DCP-AI-v2.0.md, the WASM SDK README (all languages),docs/NIST_CONFORMITY.mdand the TypeScript, Python and Rust canonicalizer comments describedcp-jcs-v1as based on RFC 8785, not a strict subset of it or plain RFC 8785: Rule 1 orders keys by code point, RFC 8785 by UTF-16 code units. The profile is unchanged.- The TypeScript SDK README (all languages) and
docs/API_REFERENCE.mdlist the real signatures of the composite-signature, bundle, presentation, PQ checkpoint, A2A, error, emergency revocation, key recovery, hashing and CBOR functions. docs/API_REFERENCE.md: the V2 verification pipeline lists the per-artifact signature checks, and the known-limitations note on V2-7 and V2-8 now applies only to@dcp-ai/sdk2.1.2 and earlier.- The Go and WASM SDK READMEs (all languages) and
docs/MIGRATION_V1_V2.mdsign each artifact before building the bundle, and list the signing keys in the passport and the RPR. spec/DCP-AI-v2.0.md§14.2 describes the tier computation above, §14.3 and §14.4 describeresolved_tieras informational (§14.2 does not read it), andspec/VERIFICATION.mdcomputes the tier as in §14.2. The SDK READMEs (all languages),docs/API_REFERENCE.md, the quickstart, migration and agent-creation guides andsdks/wasm/example.htmlpass the policy decision to the tier function and use the intent schema's values.spec/VERIFICATION.mdV2-7 andspec/DCP-AI-v2.0.md§7.1 state that a PQ signature that is present must verify in every mode, as the verifiers already do.docs/API_REFERENCE.mddocumentsdetails.resolved_tierand the tier-aware mode ofverifySignedBundleV2, and the TypeScript SDK README (all languages) describes it.docs/MIGRATION_V1_V2.md§6.3 no longer shows adcp migrate-checkcommand, which the CLI does not have, or arisk_scoreon the intent; it validates migrated bundles withdcp verify-bundle --policy hybrid_required.spec/DCP-AI-v2.0.md§14.3 makes tier enforcement a MUST for V2 verifiers (§20.1): the mode is the stricter of the policy mode and the tier's mode.spec/VERIFICATION.mdcomputes the tier and the effective mode in Step V2-9 and enforces the policy with it in Step V2-10, where the tier check used to be optional.docs/OPERATOR_GUIDE.mdanddocs/SECURITY_MODEL.mddescribe the tier-aware mode, and the Go, Python, Rust and WASM SDK READMEs (all languages) describe the verifier policy.- Spec §18 (verification pipeline),
docs/OPERATOR_GUIDE.mdand the migration guide's step 8 describe tier enforcement as the verifiers implement it. The operator guide no longer says that the maximum tier verifies PQ checkpoints, which no verifier does yet (V2-12), and says what the reference server enforces until its@dcp-ai/sdkdependency is bumped. spec/VERIFICATION.mdV2-3, V2-4 and V2-13 spell out these checks:payload_hash, fail-closed manifest hashes, the audit trail tied to the manifest, session expiry, and the validity of the RPRbinding_keys.docs/API_REFERENCE.mdlists the binding keys in the key validity step.
[2.9.0] - 2026-10-21¶
Package versions¶
dcp-ai(PyPI) 2.8.1 → 2.8.2: fixes only.@dcp-ai/sdk(npm) 2.1.1 → 2.1.2: fixes only.github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2v2.8.1 → v2.9.0: addsv2.MarshalCanonical.dcp-ai(crates.io) 2.8.1 → 2.9.0: OpenTelemetry 0.32, breaking for the optionalotlpfeature (see "Changed (breaking)").- Unchanged:
@dcp-ai/wasm2.0.0,@dcp-ai/cli2.0.0 and the integration packages.
Security¶
- The reference verifier (
lib/verify.js),dcp verify-bundleand the verification server now verify normative V2 bundles (schemas/v2/signed_bundle.schema.json) by delegating toverifySignedBundleV2from@dcp-ai/sdk, after JSON Schema validation. Before, bundles produced by any SDK were rejected as "Invalid signed bundle format",dcp verify-bundleprintedV2 BUNDLE STRUCTURE VALID(exit 0) for forged bundles, and the server only checked structure. The pre-normative envelope (top-leveldcp_version/bundle_manifest) is rejected. The server verifies against its advertisedVERIFIER_POLICY(mode, accepted and advisory-rejected algorithms), and a public-key pin is refused inpq_onlymode, which skips the classical signer. dcp sign-bundle --compositeemits a normativeSignedBundleV2; the ML-DSA-65 kid used to be derived from a hash of the secret key.tools/bundle_sign.js --v2was removed.- Revocation service: one emergency revocation no longer reports every kid of every agent as
revoked (
GET /v2/check/kid/:kidnow takes?agent_id=), and malformed percent-encoding returns 400 instead of crashing the process.
Changed (breaking)¶
lib/verify.js(the root packagemain):verifySignedBundleis nowasyncand takes a fourthoptionsargument ({ policy, mode }); results always carry anerrorsarray. Theverify-bundlecomposite GitHub Action,bin/dcp.js,server/andtools/bundle_verify.jswere updated.tools/bundle_verify.jsno longer takes a PQ public key argument.- Rust SDK, optional
otlpfeature: OpenTelemetry 0.24 → 0.32. OtlpHandles.tracer_provideris nowSdkTracerProviderandOtlpHandles.tracerisSdkTracer, following the upstream renames.- The feature no longer pulls in
tokio: the bridge exports from the SDK's background threads over the blockingreqwestclient. - Errors prefixed
tracer install:now readtrace exporter:.
Added¶
- Go SDK:
v2.MarshalCanonical(obj), thedcp-jcs-v1encoder without the integer-only rule. The V1Canonicalizeand the proof-of-possession encoder now use it.
Fixed¶
- Go SDK: canonicalization no longer escapes
<,>,&or U+2028/U+2029 and emits-0as0(dcp-jcs-v1Rules 3 and 4) in the V2, proof-of-possession and V1 paths. - TypeScript SDK: object keys are sorted by Unicode code point (Rule 1), not UTF-16 code units.
- Python SDK:
pqcryptois capped below 1.0. The 1.0 release removed thepqcrypto.sign.ml_dsa_*modules, so fresh installs ofdcp-ai2.8.1 fail to import the ML-DSA-65 provider. tests/interop/v2/interop_vectors.json: edge-case vectors for negative zero, HTML characters, U+2028/U+2029, control-character escapes and astral-plane key ordering.tools/conformance.js: theedge_casesentry no longer passes vacuously, the never-executed domain-separation block was removed, and V2 signed bundles (valid, tampered, forged, PQ-stripped, key-pinned, legacy) are checked against the reference verifier.
Dependencies¶
- All 95 open Dependabot alerts are closed (11 critical, 54 high). #22 superseded the 15 Dependabot PRs (#5–#21); #24 bumped grpc.
- TypeScript SDK,
@dcp-ai/express,@dcp-ai/openclaw(development): - vitest and
@vitest/coverage-v84.1.12, bumped together. - vite 7.3.7, pinned through
overrides. - postcss 8.5.31, nanoid 3.3.20, source-map-js 1.2.2, esbuild 0.28.2, fast-uri 3.1.9.
- Express runtime lockfile: proxy-addr 2.0.8, qs 6.16.1, body-parser 2.3.0.
- Root harness: fast-uri 3.1.9, and the new
@dcp-ai/sdkdependency. - Go SDK:
golang.org/x/cryptov0.55.0,golang.org/x/netv0.58.0,google.golang.org/grpcv1.83.2,go.opentelemetry.io/otel/*v1.46.0. - Rust SDK: opentelemetry, opentelemetry_sdk and opentelemetry-otlp 0.32.
opentelemetry_sdk0.32.1 fixes unbounded memory allocation in W3C Baggage propagation. - CI:
- New integration test job for express and openclaw.
- The Rust job builds and tests
--features otlp. - The Go job reads its version from
go.mod. - The generated V2 bundle is verified with the CLI.
- The server and revocation tests run.
Documentation¶
spec/VERIFICATION.md:- The V2 checklist now points at
schemas/v2/signed_bundle.schema.json(the cited*_v2schemas never existed). - The non-existent
independentbinding was removed. - The reference implementation is named.
spec/CANONICALIZATION_PROFILE.md§3 gained rows for string escaping and code-point key order. This clarifies Rules 1 and 3; the profile is unchanged.- The server, operator, migration, API reference, CI and bundle docs now describe the real V2 verification, the actual CLI syntax and the SDK APIs that exist:
- The migration guide's API tables named several functions that do not exist.
- The server README showed the pre-normative envelope.
- The Python SDK README (all languages) no longer claims ML-KEM-768 or V2 bundle support, and the CrewAI quickstart flags its V2 snippet as planned API.
- Translations and project docs:
- 11 integrations including Agno; canonicalization profile and Agno quickstart links.
- Rust install snippets for this release and Go
@latestpins. - Per-package supported versions in
SECURITY.md, and the v2.8.x and v2.9.0 releases inROADMAP.md. - The Zenodo concept DOI in
CITATION.cff.
[2.8.1] - 2026-04-26¶
Wired the canonicalization_profile field across the four SDKs¶
v2.8.0 declared the canonicalization_profile field in
schemas/v2/bundle_manifest.schema.json and described it in
spec/CANONICALIZATION_PROFILE.md § 4, but no SDK actually wrote or
read the field — the typed BundleManifest structures shipped without
it, and verifiers ignored it. v2.8.1 closes that gap end-to-end.
- TypeScript (
@dcp-ai/sdk2.1.0 → 2.1.1) —BundleManifestinterface insdks/typescript/src/types/v2.tsgainscanonicalization_profile?: 'dcp-jcs-v1'.BundleBuilderV2.build()insdks/typescript/src/bundle/builder-v2.tsnow emitscanonicalization_profile: "dcp-jcs-v1"on every produced manifest.verifyV2Bundleinsdks/typescript/src/core/verify-v2.tsaccepts an absent field (assumesdcp-jcs-v1), accepts"dcp-jcs-v1"explicitly, and rejects any other value with"Unknown canonicalization_profile: <value>". - Python (
dcp-ai2.8.0 → 2.8.1) —BundleManifestinsdks/python/dcp_ai/v2/models.pygainscanonicalization_profile: Literal["dcp-jcs-v1"] | None = "dcp-jcs-v1". Pydantic enforces unknown-value rejection at parse time; absence triggers the default value. No separate verifier change is needed because validation lives in the model. - Go (
sdks/go/v2.8.0→v2.8.1) —BundleManifeststruct insdks/go/dcp/v2/types.gogainsCanonicalizationProfile stringwithjson:"canonicalization_profile,omitempty".BuildBundleV2insdks/go/dcp/v2/bundle_builder.gosets it to"dcp-jcs-v1"on every produced manifest.VerifySignedBundleV2insdks/go/dcp/v2/verify_bundle.gonow rejects bundles whose manifest carries acanonicalization_profilevalue other than"dcp-jcs-v1". - Rust (
dcp-aicrates.io 2.8.0 → 2.8.1) —BundleManifestinsdks/rust/src/v2/types.rsgainspub canonicalization_profile: Option<String>with#[serde(skip_serializing_if = "Option::is_none")].wasm_build_bundleinsdks/rust/src/lib.rssets"canonicalization_profile": "dcp-jcs-v1"on the manifest it emits.
Spec & profile document¶
spec/DCP-AI-v2.0.md§ 9 (Bundle Manifest) now listscanonicalization_profilealongside the other manifest fields, with an inline note that an absent field MUST be assumed equivalent to"dcp-jcs-v1".spec/CANONICALIZATION_PROFILE.md§ 2 — Rule 6 (undefined / null handling) is rewritten as a cross-language table that explicitly lists how TypeScript, Python, Go, and Rust host values map onto the JSON wire format for both object slots and array slots.spec/CANONICALIZATION_PROFILE.md§ 2 — new Rule 9 makes Unicode normalization (NFC, NFD, NFKC, NFKD) explicitly out of scope: the canonicalizer emits strings byte-for-byte as the host parser delivers them, and a future profile (dcp-jcs-v2or later) MAY pin a normalization form. RFC 8259 § 8.1 is referenced as the conventional choice (NFC) for application-layer normalisation.spec/CANONICALIZATION_PROFILE.mdRule 9 also pins the rationale for not shippingNaN/±Infinityinterop fixtures: those values are not part of RFC 8259 wire format, so most parsers reject them before the canonicalizer is reached. Rejection is verified by per-SDK unit tests rather than shared fixtures.
Behavioural compatibility¶
The new field is additive and optional in serialization:
- A
v2.8.1-produced bundle carriescanonicalization_profile: "dcp-jcs-v1"on its manifest. Older verifiers that don't know about the field will treat it as an unknown property — which is fine because the JSON Schema declares the field asadditionalProperties- compatible (it lives amongpropertiesand isoptional). - A bundle produced by
v2.8.0or earlier has nocanonicalization_profilefield.v2.8.1verifiers accept it: perspec/CANONICALIZATION_PROFILE.md§ 4, an absent field MUST be assumed equivalent to"dcp-jcs-v1". - A bundle that carries
canonicalization_profilewith an unknown value (e.g."dcp-jcs-v2") is rejected by everyv2.8.1SDK. This is the forward-compatibility hook for future profiles: a verifier that hasn't been updated to a new profile MUST refuse it rather than silently accept a manifest it cannot validate.
The bundle-level signature covers canonical(manifest), so adding the
field changes the manifest hash of any newly-produced bundle relative
to one that would have been produced under v2.8.0. Bundles produced
under v2.8.0 and earlier, and their stored signatures, remain valid
under v2.8.1 verifiers.
Versions bumped¶
dcp-ai(PyPI) 2.8.0 → 2.8.1dcp-ai(crates.io) 2.8.0 → 2.8.1github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2v2.8.0 → v2.8.1@dcp-ai/sdk(npm) 2.1.0 → 2.1.1 — first TypeScript code change since 2.1.0 (the 2.8.0 release was a docstring-only edit)
[2.8.0] - 2026-04-26¶
Added — Canonicalization profile dcp-jcs-v1¶
A public crosswalk against another protocol surfaced three real
asymmetries in how the four SDKs canonicalised JSON: 1.0/1.00
were rejected by Python and Rust but accepted by TypeScript and Go;
1e2 was rejected by Rust alone; and undefined had no formal
treatment in the spec. The "RFC 8785 + integer-only" summary in
spec/DCP-AI-v2.0.md § 15 was correct as far as it went, but did not
pin the cases JCS leaves implementation-defined. This release closes
all of them.
- New normative document:
spec/CANONICALIZATION_PROFILE.mdformalises profiledcp-jcs-v1with eight numbered rules, an edge-case acceptance table covering 22 inputs, an undefined-handling contract that documents the TypeScript-only asymmetry (omit in objects, serialize asnullin arrays), and a versioning policy for future profiles. - Bundle manifest field:
canonicalization_profile: "dcp-jcs-v1"added toschemas/v2/bundle_manifest.schema.jsonas an optional field. Verifiers that encounter a bundle without it MUST assumedcp-jcs-v1. Future profiles will make the field required. - Python (
dcp-ai2.7.1 → 2.8.0):assert_no_floatsnow accepts floats whose value is a finite integer (1.0,1e2,1.00); they are normalised to integer form beforejson.dumps. Non-integer floats (0.1),NaN, and infinities remain rejected. - Rust (
dcp-ai2.7.0 → 2.8.0):assert_no_floatsnow acceptsNumber(f64)whosefract()is zero. Newformat_numberhelper incanonical_recurseemits integer-valued floats without decimal point or exponent, matching the other SDKs byte-for-byte. - Go (
sdks/go/v2.7.0→v2.8.0): behaviour already aligned (v != math.Trunc(v)check). Version-bump only. - TypeScript (
@dcp-ai/sdkstays 2.1.0): behaviour already aligned. Docstring insdks/typescript/src/core/canonicalize.tsformalises the undefined-vs-null asymmetry.
Added — interop edge-case fixtures¶
tests/interop/v2/interop_vectors.json now ships
canonicalization.edge_cases with 14 vectors covering null
preservation (with key, inside arrays), integer-valued floats,
trailing zeros, scientific notation, fractional rejection, non-finite
rejection, code-point key sorting (including é), nested nulls,
empty containers, deeply nested objects, and arrays with null. Each
SDK's interop test loads the same block and asserts byte-identical
output (or matching error) for every vector. CI runs all four in
parallel on every PR; divergence is a release blocker.
Versions bumped¶
dcp-ai(PyPI) 2.7.1 → 2.8.0dcp-ai(crates.io) 2.7.0 → 2.8.0github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2→ v2.8.0@dcp-ai/sdk(npm) stays 2.1.0 — TypeScript already implemented the profile; this release adds the docstring formalisation only.
Behavioural compatibility¶
Bundles produced by previous releases remain valid. The change is
additive for SDKs that previously rejected 1.0/1.00/1e2:
inputs that used to throw TypeError/Err now succeed and produce
the same canonical output other SDKs were already producing. No
input that previously canonicalised successfully canonicalises
differently in 2.8.0.
[2.7.1] - 2026-04-23 (Python only)¶
Fixed¶
dcp-ai(PyPI) 2.7.0 was missingcryptographyas a hard dependency. The package is required by the DCP-04 A2A session layer introduced in 2.5.0 for AES-256-GCM encryption. Freshpip install dcp-ai==2.7.0installs did not pullcryptography, so three A2A session tests failed under a clean environment (test_encrypt_decrypt_round_trip,test_tamper_ciphertext_rejected,test_session_id_mismatch). 2.7.1 addscryptography>=41.0to the wheel's required dependencies.
Rust (dcp-ai crates.io) and Go (sdks/go/v2) are unaffected and remain at
2.7.0 — the cryptography Python package only matters for the Python wheel.
[2.7.0] - 2026-04-23¶
Added — Canonical error codes + wire-format detection in PY / GO / RS¶
Ports the DcpErrorCode enum and accompanying DcpError / DcpProtocolError
machinery from TS to Python, Rust, and Go, plus the lightweight
detect_wire_format helper that routes an incoming byte stream to
"json" or "cbor" based on the first byte.
Every SDK now agrees on the same 38 canonical error codes covering:
schema errors (E001-E099), signature errors (E100-E199), hash/chain errors
(E200-E299), identity errors (E300-E399), policy errors (E400-E499),
session errors (E500-E599), A2A errors (E600-E699), rate limiting
(E700-E799), and internal errors (E900-E999). Each code carries a canonical
message + a retryable flag so clients can implement policy-matching
retry logic consistently across SDKs.
Python (dcp-ai 2.6.0 -> 2.7.0) — new dcp_ai.v2.error_codes module
with DcpErrorCode (str-enum), DcpError dataclass,
DcpProtocolError exception, create_dcp_error, is_dcp_error,
detect_wire_format, and ERROR_DESCRIPTIONS mapping.
Rust (dcp-ai 2.6.0 -> 2.7.0) — dcp_ai::v2::error_codes with the
DcpErrorCode enum (Display → the stable DCP-E### identifier),
DcpError struct, create_dcp_error, and detect_wire_format.
Go (sdks/go/v2.6.0 -> v2.7.0) — v2.DcpErrorCode typed-string with
37 Err* constants, v2.DcpError struct implementing the error
interface, CreateDcpError, IsDcpError, DetectWireFormat.
CBOR encoder/decoder scope decision¶
The full CBOR encoder/decoder remains TypeScript-only in 2.x. Porting a
deterministic RFC 8949 implementation to three more SDKs is a substantial
project that deserves its own milestone; detect_wire_format is the
single helper every SDK needs today to route CBOR payloads to an
existing ecosystem library (e.g. cbor2 in Python,
github.com/fxamacker/cbor/v2 in Go, ciborium in Rust). A dedicated
v3.x release will add a canonical port.
Test counts¶
- Python: 231/231 pass (unchanged; import surface verified)
- Rust: 152/152 pass (+5 new)
- Go: 115/115 pass (+8 new)
Versions bumped¶
dcp-ai(PyPI) 2.6.0 -> 2.7.0dcp-ai(crates.io) 2.6.0 -> 2.7.0github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2-> v2.7.0
[2.6.0] - 2026-04-23¶
Added — blinded RPR, multi-party auth, advisory helpers in Rust + Go¶
Ports three Python-exclusive features to Rust and Go, closing the gap noted in the crosswalk where Python was ahead of the other non-TS SDKs.
- Blinded RPR (Gap #2):
compute_pii_hash,blind_rpr,verify_blinded_rpr,is_blinded_rpr. - Multi-Party Authorization (Gap #5):
verify_multi_party_authorization MultiPartyPolicywith the four default operation policies (revoke_agent,rotate_org_key,change_jurisdiction,modify_recovery_config).- Algorithm advisory helpers (Gap #4):
check_advisory,evaluate_advisories,apply_advisories_to_policy,build_algorithm_advisory.
All three modules match the Python semantics: SHA-256 PII commitment,
M-of-N structural validation (crypto verification stays at the gateway),
and routing by action (revoke / deprecate / warn) with grace-period
handling.
Python (dcp-ai 2.5.0 -> 2.6.0): no code change, version bump only to
keep the SDK suite synchronised.
Test counts¶
- Rust: 147/147 (+14 new)
- Go: 106/106 (+12 new)
- Python: 231/231 (unchanged)
[2.5.0] - 2026-04-23¶
Added — DCP-04 Agent-to-Agent protocol parity¶
Ports DCP-04 (Agent-to-Agent) scaffolding to the remaining SDKs.
Python (dcp-ai 2.4.0 -> 2.5.0) — full parity with TS including the
symmetric session layer (AES-256-GCM via the cryptography package).
- Discovery:
create_agent_directory,find_agent_by_capability,find_agent_by_id,validate_directory_entry. - Handshake:
generate_nonce,create_hello,create_welcome,derive_session_id,create_close_message. - Session:
A2ASession,create_session,encrypt_message,decrypt_message,needs_rekeying,generate_resume_proof,verify_resume_proof,derive_rekeyed_session_key.
Go (sdks/go/v2.4.0 -> v2.5.0) — full parity with TS, using Go stdlib
crypto/aes + crypto/cipher for AES-256-GCM (no new dependencies).
Rust (dcp-ai 2.4.0 -> 2.5.0) — discovery + handshake scaffolding.
The AES-256-GCM session layer is scheduled for a follow-up release; Rust
users can still build directories, HELLO/WELCOME messages, and derive
session IDs.
Test counts: Python 231/231, Rust 133/133, Go 94/94.
[2.4.0] - 2026-04-23¶
Added — Production hardening primitives in Python, Rust, Go¶
Ports the four protocol-critical hardening primitives from the TS core:
- Session nonce helpers:
generate_session_nonce,is_valid_session_nonce,verify_session_binding,generate_session_expiry,is_session_expired. - Adaptive security tier:
compute_security_tier,max_tier,tier_to_verification_mode,tier_to_checkpoint_interval(Go already hadComputeSecurityTier; v2.4 adds the three helper functions). - Emergency revocation (Gap #13):
generate_emergency_revocation_token,verify_emergency_revocation_secret,build_emergency_revocation. - Lazy PQ checkpoints:
audit_events_merkle_root,create_pq_checkpoint, andPQCheckpointManagerclass with tier-driven interval selection and automatic flush behaviour.
All four modules match the TS semantics exactly: constant-time secret
verification for emergency revocation, tier-driven checkpoint intervals,
session-nonce validator regex, and signed checkpoint payloads under the
DCP-AI.v2.AuditEvent domain-separation context.
What's not in v2.4¶
Deliberately scoped out (will ship as follow-up releases if demand appears):
- Shamir secret sharing (Gap #1) — requires careful crypto review; TS-only.
- Verification cache — app-level concern, not protocol.
- Rate limiting / circuit breaker / retry — app-level patterns best delegated to existing ecosystem libraries.
Test counts¶
- Python: 214/214 pass (+24 new)
- Rust: 126/126 pass (+10 new)
- Go: 81/81 pass (+9 new)
Versions bumped¶
dcp-ai(PyPI) 2.3.0 -> 2.4.0dcp-ai(crates.io) 2.3.0 -> 2.4.0github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2-> v2.4.0
[2.3.0] - 2026-04-23¶
Added — Dispute + Rights + Delegation parity across SDKs¶
Python, Rust, and Go now implement DCP-07 (Dispute Resolution + Arbitration), DCP-08 (Rights & Obligations), and DCP-09 (Delegation, Awareness-threshold, Principal Mirror) behavior — matching the TypeScript feature set.
New function groups (identical semantics across PY/RS/GO, matching TS):
DCP-07 Conflict Resolution:
create_dispute·escalate_dispute·resolve_dispute·create_objection- Arbitration:
create_arbitration_panel·submit_resolution·build_jurisprudence_bundle·lookup_precedent
DCP-08 Rights & Obligations:
declare_rights·record_obligation·report_violation·check_rights_compliance
DCP-09 Delegation & Representation:
- Delegation:
create_delegation_mandate·verify_mandate_validity·revoke_delegation·generate_interaction_record - Awareness threshold:
evaluate_significance·should_notify_human·create_awareness_threshold·create_advisory_declaration - Principal mirror:
generate_mirror
All new factories use the existing composite-sign path under the
DCP-AI.v2.Dispute, DCP-AI.v2.Rights, DCP-AI.v2.Delegation, and
DCP-AI.v2.Awareness domain-separation contexts. The delegation mandate
artifact carries its human-principal signature in the human_composite_sig
field, matching the TS wire format.
Test counts¶
- Python: 190/190 pass (+21 new)
- Rust: 116/116 pass (+12 new)
- Go: 72/72 pass (+12 new across outer
dcp_testpackage) - TypeScript: 460/460 pass (no changes)
Versions bumped¶
dcp-ai(PyPI) 2.2.0 -> 2.3.0dcp-ai(crates.io) 2.2.0 -> 2.3.0github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2-> v2.3.0@dcp-ai/sdk(npm) stays at 2.1.0
[2.2.0] - 2026-04-23¶
Added — Lifecycle + Succession parity across SDKs¶
Python, Rust, and Go SDKs now implement DCP-05 (Agent Lifecycle) and DCP-06 (Digital Succession) behavior. TypeScript already shipped these in 2.0.x; the 2.2.0 release closes the first behavioral-parity gap identified in the cross-SDK crosswalk.
New functions (identical semantics across Python/Rust/Go, matching TS):
DCP-05 Agent Lifecycle:
validate_state_transition/ValidateStateTransitioncompute_vitality_score/ComputeVitalityScorecreate_commissioning_certificate/CreateCommissioningCertificatecreate_vitality_report/CreateVitalityReporthash_vitality_report/HashVitalityReportcreate_decommissioning_record/CreateDecommissioningRecord
DCP-06 Digital Succession:
create_digital_testament/CreateDigitalTestamentupdate_digital_testament/UpdateDigitalTestamentclassify_memory/ClassifyMemorycreate_memory_transfer_manifest/CreateMemoryTransferManifestexecute_succession/ExecuteSuccession
All new factories use the existing composite-sign path under the
DCP-AI.v2.Lifecycle and DCP-AI.v2.Succession domain-separation contexts,
so artifacts produced by any SDK are wire-compatible and cross-verifiable.
Test counts after this release¶
- Python: 169/169 pass (+24 new)
- Rust: 104/104 pass (+10 new)
- Go: 60/60 pass (+13 new across outer
dcppackage; v2 package unchanged) - TypeScript: 460/460 pass (no changes)
Known protocol-level note¶
DCP v2.0 canonicalisation forbids floats for cross-SDK determinism, but
the vitality-report schema expresses per-metric values as number
(0.0..1.0). Factories in all four SDKs now accept metrics at the API
boundary but signers require integer representations on the wire. This
is documented in each SDK's tests. A future release may tighten the
schema to integers (or basis-points) to eliminate the gap entirely.
Versions bumped¶
dcp-ai(PyPI) 2.1.0 -> 2.2.0dcp-ai(crates.io) 2.1.0 -> 2.2.0github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2-> v2.2.0@dcp-ai/sdk(npm) stays at 2.1.0 (TS already had these functions)
[2.1.0] - 2026-04-22¶
Added¶
OpenTelemetry (OTLP) exporter for all four core SDKs — TypeScript, Python, Rust, and Go. Adopters can now pipe DCP observability into any OTel-compatible backend (Grafana, Jaeger, Honeycomb, Datadog, New Relic, self-hosted Collectors) without hand-rolled bridges. Every SDK that performs DCP cryptography now reports the same metric names, counters, and span shapes.
-
Rust (
dcp-ai2.0.0 → 2.1.0)- New
dcp_ai::observabilitymodule with thread-safeDcpTelemetrysingleton, recorder methods, span lifecycle,on_eventlistener bus, andMetricsSummarywith p50/p95/p99. - Optional
otlpCargo feature (cargo add dcp-ai --features otlp) pullsopentelemetry,opentelemetry_sdk,opentelemetry-otlp, andtokio. Without the feature, requestingExporterType::Otlpsurfaces a clear error event instead of panicking. - Hot paths instrumented:
crypto::sign_objectandcrypto::verify_objectemit latency + error telemetry when enabled. Zero overhead when disabled (default). - 10 unit tests covering disabled-by-default behavior, percentiles, span lifecycle, tier distribution, cache hit rate, and listener panic isolation.
- New
-
Go (
sdks/go/v2.0.0 → v2.1.0)- New
github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2/dcp/observabilitypackage with mutex-protectedTelemetrytype,Default()singleton, recorder methods, subscribers with panic recovery, percentile math matching the other SDKs. - OTLP bridge gated by the
otlpbuild tag (go build -tags otlp). Without the tag, requestingExporterOTLPemits a clearotlp_initerror event via the listener bus. Thego.opentelemetry.io/oteldependencies are listed ingo.modbut are not required at runtime by default builds. - Hot paths instrumented:
dcp.SignObjectanddcp.VerifyObjectemit latency + error telemetry when enabled. Zero overhead when disabled (default). - 10 unit tests for the observability package + 2 integration tests against
dcp.SignObjectcovering the enabled/disabled paths.
- New
-
TypeScript (
@dcp-ai/sdk2.0.0 → 2.1.0)dcpTelemetry.init({ exporterType: 'otlp' })now actually works (the config field was declared in 2.0.x but not wired).- OTel SDK is imported lazily — default installs are unchanged. The
@opentelemetry/*packages are declared as optional peer dependencies; when omitted,init()emits a clear error event via the listener bus and the app keeps running. - New
dcpTelemetry.shutdown()to flush pending exports. - Endpoint resolution:
otlpEndpointconfig →OTEL_EXPORTER_OTLP_ENDPOINTenv var →http://localhost:4318. service.name+service.versionresource attributes auto-populated.- Metric mapping: DCP recorders map to OTel histograms (
dcp.sign.latency_ms,dcp.verify.latency_ms,dcp.kem.latency_ms,dcp.checkpoint.latency_ms,dcp.bundle_verify.latency_ms) and counters (dcp.signatures.created,dcp.signatures.verified,dcp.bundles.verified,dcp.a2a.sessions,dcp.errors). - 2 new vitest cases; 460/460 tests pass.
-
Python (
dcp-ai2.0.2 → 2.1.0)- New
dcp_ai.observabilitymodule providingdcp_telemetrysingleton with the same API surface as the TypeScript version: spans, metric recorders (record_sign_latency,record_verify_latency,record_kem_latency,record_checkpoint_latency,record_bundle_verify,record_cache_hit/miss,record_a2a_*,record_error),on_eventlistener bus,get_metrics_summarywith p50/p95/p99. - Exporter modes:
none(default) /console/otlp. - New install extra:
pip install 'dcp-ai[otlp]'pullsopentelemetry-sdk+opentelemetry-exporter-otlp-proto-http. Without the extra,init(exporter_type="otlp")surfaces a clear install hint without crashing. - Hot paths instrumented:
sign_objectandverify_objectnow emit latency + error telemetry when enabled. No overhead when disabled (default). - Public export:
from dcp_ai import dcp_telemetryworks. - 15 new pytest cases; 145/145 tests pass.
- New
Documentation¶
- New
docs/OBSERVABILITY.mdwith three copy-paste backend recipes (local Jaeger via Docker, Grafana Cloud OTLP endpoint, Honeycomb).
Backward compatibility¶
- Fully backward compatible across all four SDKs. Defaults unchanged — telemetry ships disabled, recorders are no-ops. No code changes needed for adopters who don't want observability.
- Synchronized semver:
@dcp-ai/sdk(npm),dcp-ai(PyPI),dcp-ai(crates.io), andgithub.com/dcp-ai-protocol/dcp-ai/sdks/go/v2all move to 2.1.0. Other publishable packages (CLI, WASM, integrations, create-* scaffolders) remain at their current versions. - WASM SDK observability is deferred to a future minor; browser telemetry has different transport requirements.
Template packages - 2026-04-21¶
First publication of four npm create @dcp-ai/* scaffolders, all at v2.0.0:
@dcp-ai/create-langchain@dcp-ai/create-crewai@dcp-ai/create-openai@dcp-ai/create-express
Usage: npm create @dcp-ai/langchain my-app (or any of the four). Each scaffolder ships a self-contained starter template — same content as the existing templates/<name>/ in the repo — plus a tiny 35-line bin.js with zero runtime dependencies.
Infrastructure¶
- JSON-LD context now served at
https://dcp-ai.org/credentials/v2(Content-Typeapplication/ld+json, CORS*, 24 h cache). Required by@dcp-ai/w3c-didso every emitted Verifiable Credential/Presentation is dereferenceable by strict JSON-LD processors (Veramo, SpruceID, Trinsic). - Codecov coverage reports aggregated across the four SDKs (TypeScript, Python, Go, Rust) with per-SDK flags. Badge live in the README.
- Publish workflows hardened: every job (npm / PyPI / crates.io) now pre-checks the registry and exits green with a skip notice if the manifest version is already live. No more red jobs on routine releases.
Go SDK v2.0.0 - 2026-04-19¶
First installable release of the Go SDK via go get. The previous module path (github.com/dcp-ai/dcp-ai-go) pointed at a repository that did not exist, so external consumers could not install it. The module now lives as a sub-directory module inside this monorepo:
- New module path:
github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2(/v2suffix required by Go for major versions ≥ 2) - Tag format:
sdks/go/v2.0.0(sub-directory-prefixed, as required by the Go module proxy for multi-module repos) - Install:
go get github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2@v2.0.0
Code is unchanged — only the module path and internal imports were rewritten. All tests still pass.
[2.0.2] - 2026-04-18¶
Python SDK¶
- Fixed:
dcp_ai.__version__now reflects the installed version dynamically viaimportlib.metadatainstead of a hardcoded string. In 2.0.1 the attribute still reported"1.0.0"even though the wheel was2.0.1.
CI / publish workflows¶
- Every publish job (
publish.yml,publish-pypi.yml,publish-integrations.yml) now pre-checks whether the manifest version is already on the registry and, if so, exits green with a skip notice. Previously, any release-triggered run re-attempted to publish already-published versions and left four red jobs in the Actions tab. No more cosmetic red ghosts on routine releases. - Fixed two outdated package names in the README integrations table:
@dcp-ai/a2a→@dcp-ai/google-a2a,@dcp-ai/mcp→@dcp-ai/anthropic-mcp. - Live npm / PyPI version badges added per row in the integrations table.
[2.0.1] - 2026-04-18¶
Python SDK¶
Framework bridges are now shipped inside the package. Prior to 2.0.1
they lived under integrations/<name>/ in the repo and were only
loadable through a pytest hack; anyone who ran pip install dcp-ai[fastapi]
got the upstream fastapi library but not the dcp_ai.fastapi
middleware. 2.0.1 fixes that: the four modules now live under
dcp_ai/ and are included in the published wheel.
- Added:
dcp_ai.fastapi(middleware, dependency injection, V2 structure checks) - Added:
dcp_ai.langchain(agent wrapper, callback, risk scoring) - Added:
dcp_ai.openai(client, tool schema, audit trail builder) - Added:
dcp_ai.crewai(crew agent, combined audit trail, session consistency checks) - Removed:
tests/conftest.pydynamic-module loader (no longer needed)
Installation¶
pip install dcp-ai[fastapi] # brings fastapi + uvicorn
pip install dcp-ai[langchain] # brings langchain + langchain-core
pip install dcp-ai[openai] # brings openai
pip install dcp-ai[crewai] # brings crewai
Backward compatibility¶
Fully compatible with 2.0.0 imports. The new modules only add API
surface; existing from dcp_ai import ... calls are unaffected.
[npm integrations] - 2026-04-18¶
First publication of the six framework bridges as standalone npm
packages, all at v2.0.0:
@dcp-ai/anthropic-mcp— Anthropic MCP bridge@dcp-ai/autogen— Microsoft AutoGen bridge@dcp-ai/google-a2a— Google Agent-to-Agent bridge@dcp-ai/w3c-did— W3C DID/VC bridge@dcp-ai/express— Express middleware@dcp-ai/openclaw— OpenClaw plugin
@dcp-ai/express and @dcp-ai/openclaw depend on @dcp-ai/sdk ^2.0.0.
2.0.0 - 2026-04-18¶
First public package release. Published simultaneously to npm (@dcp-ai/sdk, @dcp-ai/cli, @dcp-ai/wasm), PyPI (dcp-ai), and crates.io (dcp-ai).
Added¶
Protocol stack (DCP-01 through DCP-09)
- DCP-01 Identity & Human Binding — agent identity, operator attestation, key binding
- DCP-02 Intent Declaration & Policy Gating — declared intents, tier enforcement, policy evaluation
- DCP-03 Audit Chain & Transparency — hash-chained audit entries, Merkle proofs, transparency logs
- DCP-04 Agent-to-Agent Communication — discovery, handshake, encrypted sessions
- DCP-05 Agent Lifecycle — commissioning, vitality reports, decommissioning records, state machine with four termination modes
- DCP-06 Digital Succession & Inheritance — digital testaments, succession ceremonies (planned/forced/emergency), selective memory transfer manifests
- DCP-07 Conflict Resolution & Arbitration — typed dispute records, three-level escalation, M-of-N arbitration panels, jurisprudence bundles, objection records
- DCP-08 Rights & Obligations Framework — four agent rights (memory integrity, dignified transition, identity consistency, immutable record), obligation tracking, violation reports linked to DCP-07
- DCP-09 Personal Representation & Delegation — delegation mandates, awareness thresholds, advisory declarations, principal mirrors, dual-layer interaction records; extends DCP-04 handshake with mandate verification
Cryptography (4-tier: Routine / Standard / Elevated / Maximum)
- Post-quantum composite signatures — Ed25519 + ML-DSA-65 with
pq_over_classicalbinding - SLH-DSA-192f (FIPS 205) as hash-based backup signature
- Dual-hash audit chains — SHA-256 + SHA3-256 for long-term integrity
- Session binding —
session_nonceon every artifact to prevent splicing - Bundle manifest — per-artifact hashes, secondary Merkle root, PQ checkpoint tracking
- Algorithm governance — signed advisories with auto-response policies
- Key recovery — M-of-N social recovery via Shamir Secret Sharing
- Emergency revocation — pre-registered panic-button revocation tokens
- Blinded RPR mode — privacy-preserving responsible principal records
- Domain separation — context tags on all signatures
- Deterministic kid derivation —
hex(SHA-256(alg || 0x00 || pk))[0:32] - Wire format options — JSON (default) + CBOR (optional, 30–40% smaller)
SDKs, integrations, and tooling
- 5 SDKs — TypeScript, Python, Go, Rust, WASM
- 10 integrations — Express, FastAPI, LangChain, OpenAI, CrewAI, OpenClaw, W3C DID, Google A2A, Anthropic MCP, AutoGen (DCP-01..09 coverage)
- V2 JSON Schemas — full schema set under
schemas/v2/ - NIST KAT test vectors — Known Answer Tests for FIPS 203/204/205 conformance
- Interoperability tests — cross-SDK bundle verification
- Docker Compose stack — verification, anchor, transparency-log, revocation services
- CI/CD pipelines — conformance tests, multi-SDK builds, publish workflows
- Playground — browser-based bundle creation and verification with modular DCP-01..09 tabs, mobile-responsive
- Project templates — Express, LangChain, OpenAI, CrewAI starter projects
- CLI — interactive
@dcp-ai/cliwithdcp-initanddcp-aicommands - Server hardening — production-grade auth and rate-limiting for DCP-05..09 endpoints
Documentation & citation
- Companion paper on Zenodo: doi.org/10.5281/zenodo.19040913
CITATION.cff— machine-readable citation metadata
Changed¶
human_binding_recordrenamed toresponsible_principal_record(RPR)- Signature format changed from
{ alg, public_key_b64, sig_b64 }to{ composite_sig: { classical, pq, binding } } - Bundle envelope changed from flat
bundle_hashto structuredmanifest - Numeric fields are now integer-only (floats prohibited in signed payloads)
- Verification is now verifier-authoritative (policy-driven)
Fixed¶
sdk/pyPQ providers: ML-DSA-65 module import andpqc_verifysignature (pk,sig,msgseparated)sdk/pyPQ providers: SLH-DSA-192f module import andpqc_verifysignaturesdk/pyPQ providers: capture return value ofpqc_verify(was always returning True becausepqcrypto.verifyreturns bool instead of raising)sdk/pycrypto: base64-encode Ed25519 secret key ingenerate_keypair- CI: conformance, crypto, and multi-SDK test failures
Backward Compatibility¶
- V2 verifiers accept V1 bundles when
allow_v1_bundles: true(default) - V1 verifiers reject V2 bundles with a clear error message
Pre-release milestones¶
The following dates mark the evolution of the specification prior to the first package release. No packages were published at these points.
- 2026-01-15 — Genesis spec frozen (DCP-01, DCP-02, DCP-03)
- 2026-02-28 — V2.0 normative specification finalized (post-quantum hybrid crypto, 4-tier security model, DCP-04 A2A)
- 2026-03-16 — Paper preprint on Zenodo
- 2026-04-15 — DCP-05..09 constitutional framework merged to
main - 2026-04-18 — v2.0.0 released to npm, PyPI, and crates.io