Skip to content

Commit 2542cc7

Browse files
committed
feat(v2.8.1): wire canonicalization_profile across the 4 SDKs + close spec gaps
v2.8.0 declared the `canonicalization_profile` field in the bundle manifest schema and described it normatively, but no SDK actually wrote or read the field — the typed `BundleManifest` structures shipped without it, and verifiers ignored it. v2.8.1 closes the gap end to end. SDK changes (TS / Python / Go / Rust) ------------------------------------- - TypeScript (`@dcp-ai/sdk` 2.1.0 → 2.1.1): `BundleManifest` interface in `types/v2.ts` gains `canonicalization_profile?: 'dcp-jcs-v1'`. `BundleBuilderV2.build()` in `bundle/builder-v2.ts` emits the field on every produced manifest. `verifyV2Bundle` in `core/verify-v2.ts` accepts an absent field (assumes `dcp-jcs-v1`), accepts `"dcp-jcs-v1"` explicitly, and rejects any other value. - Python (`dcp-ai` 2.8.0 → 2.8.1): `BundleManifest` in `dcp_ai/v2/models.py` gains `canonicalization_profile: Literal["dcp-jcs-v1"] | None = "dcp-jcs-v1"`. Pydantic enforces unknown-value rejection at parse time; absence triggers the default value. - Go (`sdks/go/v2.8.0` → `v2.8.1`): `BundleManifest` struct in `dcp/v2/types.go` gains `CanonicalizationProfile string` with `json:"canonicalization_profile,omitempty"`. `BuildBundleV2` in `dcp/v2/bundle_builder.go` sets `"dcp-jcs-v1"` on every produced manifest. `VerifySignedBundleV2` in `dcp/v2/verify_bundle.go` rejects manifests carrying any other value. - Rust (`dcp-ai` crates.io 2.8.0 → 2.8.1): `BundleManifest` in `src/v2/types.rs` gains `pub canonicalization_profile: Option<String>` with `skip_serializing_if = "Option::is_none"`. `wasm_build_bundle` in `src/lib.rs` emits the field on its manifest. Spec & profile document ----------------------- - `spec/DCP-AI-v2.0.md` § 9 (Bundle Manifest) now lists `canonicalization_profile` alongside 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 is rewritten as a cross-language table that explicitly maps TypeScript, Python, Go, and Rust host values onto the JSON wire format for both object slots and array slots. The previous prose-only rule left Python `None`, Go `nil`, and Rust `Option::None` underspecified for verifiers cross-checking foreign producers. - `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-v2` or later) MAY pin a normalization form. RFC 8259 § 8.1 is referenced as the conventional choice (NFC) for application-layer normalisation. - Rule 9 also pins the rationale for not shipping `NaN` / `±Infinity` interop 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 carries `canonicalization_profile: "dcp-jcs-v1"` on its manifest. Bundles produced by `v2.8.0` or earlier have no such field, and `v2.8.1` verifiers accept them per `spec/CANONICALIZATION_PROFILE.md` § 4 (absent ≡ `dcp-jcs-v1`). A bundle that carries an unknown value (e.g. `"dcp-jcs-v2"`) is rejected by every `v2.8.1` SDK — the forward-compatibility hook for future profiles. The bundle-level signature covers `canonical(manifest)`, so newly- produced bundles have a different manifest hash than they would under `v2.8.0`. Existing signed bundles and their stored signatures remain valid under `v2.8.1` verifiers. Tests ----- - Conformance (root): DCP-AI CONFORMANCE PASS (V1 + V2) - TypeScript: 461/461 - Python: 236/236 - Go: 4 packages OK - Rust: 181 tests across 10 binaries
1 parent 486db50 commit 2542cc7

17 files changed

Lines changed: 206 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,100 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [2.8.1] - 2026-04-26
8+
9+
### Wired the `canonicalization_profile` field across the four SDKs
10+
11+
`v2.8.0` declared the `canonicalization_profile` field in
12+
`schemas/v2/bundle_manifest.schema.json` and described it in
13+
`spec/CANONICALIZATION_PROFILE.md` § 4, but no SDK actually wrote or
14+
read the field — the typed `BundleManifest` structures shipped without
15+
it, and verifiers ignored it. `v2.8.1` closes that gap end-to-end.
16+
17+
- **TypeScript (`@dcp-ai/sdk` 2.1.0 → 2.1.1)**`BundleManifest`
18+
interface in `sdks/typescript/src/types/v2.ts` gains
19+
`canonicalization_profile?: 'dcp-jcs-v1'`. `BundleBuilderV2.build()`
20+
in `sdks/typescript/src/bundle/builder-v2.ts` now emits
21+
`canonicalization_profile: "dcp-jcs-v1"` on every produced manifest.
22+
`verifyV2Bundle` in `sdks/typescript/src/core/verify-v2.ts` accepts
23+
an absent field (assumes `dcp-jcs-v1`), accepts `"dcp-jcs-v1"`
24+
explicitly, and rejects any other value with
25+
`"Unknown canonicalization_profile: <value>"`.
26+
- **Python (`dcp-ai` 2.8.0 → 2.8.1)**`BundleManifest` in
27+
`sdks/python/dcp_ai/v2/models.py` gains
28+
`canonicalization_profile: Literal["dcp-jcs-v1"] | None = "dcp-jcs-v1"`.
29+
Pydantic enforces unknown-value rejection at parse time; absence
30+
triggers the default value. No separate verifier change is needed
31+
because validation lives in the model.
32+
- **Go (`sdks/go/v2.8.0``v2.8.1`)**`BundleManifest` struct in
33+
`sdks/go/dcp/v2/types.go` gains `CanonicalizationProfile string` with
34+
`json:"canonicalization_profile,omitempty"`. `BuildBundleV2` in
35+
`sdks/go/dcp/v2/bundle_builder.go` sets it to `"dcp-jcs-v1"` on every
36+
produced manifest. `VerifySignedBundleV2` in
37+
`sdks/go/dcp/v2/verify_bundle.go` now rejects bundles whose manifest
38+
carries a `canonicalization_profile` value other than `"dcp-jcs-v1"`.
39+
- **Rust (`dcp-ai` crates.io 2.8.0 → 2.8.1)**
40+
`BundleManifest` in `sdks/rust/src/v2/types.rs` gains
41+
`pub canonicalization_profile: Option<String>` with
42+
`#[serde(skip_serializing_if = "Option::is_none")]`. `wasm_build_bundle`
43+
in `sdks/rust/src/lib.rs` sets `"canonicalization_profile":
44+
"dcp-jcs-v1"` on the manifest it emits.
45+
46+
### Spec & profile document
47+
48+
- `spec/DCP-AI-v2.0.md` § 9 (Bundle Manifest) now lists
49+
`canonicalization_profile` alongside the other manifest fields, with
50+
an inline note that an absent field MUST be assumed equivalent to
51+
`"dcp-jcs-v1"`.
52+
- `spec/CANONICALIZATION_PROFILE.md` § 2 — Rule 6 (undefined / null
53+
handling) is rewritten as a cross-language table that explicitly
54+
lists how TypeScript, Python, Go, and Rust host values map onto the
55+
JSON wire format for both object slots and array slots.
56+
- `spec/CANONICALIZATION_PROFILE.md` § 2 — new Rule 9 makes Unicode
57+
normalization (NFC, NFD, NFKC, NFKD) explicitly out of scope: the
58+
canonicalizer emits strings byte-for-byte as the host parser
59+
delivers them, and a future profile (`dcp-jcs-v2` or later) MAY pin
60+
a normalization form. RFC 8259 § 8.1 is referenced as the
61+
conventional choice (NFC) for application-layer normalisation.
62+
- `spec/CANONICALIZATION_PROFILE.md` Rule 9 also pins the rationale
63+
for not shipping `NaN` / `±Infinity` interop fixtures: those values
64+
are not part of RFC 8259 wire format, so most parsers reject them
65+
before the canonicalizer is reached. Rejection is verified by
66+
per-SDK unit tests rather than shared fixtures.
67+
68+
### Behavioural compatibility
69+
70+
The new field is **additive and optional in serialization**:
71+
72+
- A `v2.8.1`-produced bundle carries `canonicalization_profile:
73+
"dcp-jcs-v1"` on its manifest. Older verifiers that don't know about
74+
the field will treat it as an unknown property — which is fine
75+
because the JSON Schema declares the field as `additionalProperties`-
76+
compatible (it lives among `properties` and is `optional`).
77+
- A bundle produced by `v2.8.0` or earlier has no
78+
`canonicalization_profile` field. `v2.8.1` verifiers accept it: per
79+
`spec/CANONICALIZATION_PROFILE.md` § 4, an absent field MUST be
80+
assumed equivalent to `"dcp-jcs-v1"`.
81+
- A bundle that carries `canonicalization_profile` with an unknown
82+
value (e.g. `"dcp-jcs-v2"`) is rejected by every `v2.8.1` SDK. This
83+
is the forward-compatibility hook for future profiles: a verifier
84+
that hasn't been updated to a new profile MUST refuse it rather than
85+
silently accept a manifest it cannot validate.
86+
87+
The bundle-level signature covers `canonical(manifest)`, so adding the
88+
field changes the manifest hash of any newly-produced bundle relative
89+
to one that would have been produced under `v2.8.0`. Bundles produced
90+
under `v2.8.0` and earlier, and their stored signatures, remain valid
91+
under `v2.8.1` verifiers.
92+
93+
### Versions bumped
94+
95+
- `dcp-ai` (PyPI) 2.8.0 → 2.8.1
96+
- `dcp-ai` (crates.io) 2.8.0 → 2.8.1
97+
- `github.com/dcp-ai-protocol/dcp-ai/sdks/go/v2` v2.8.0 → v2.8.1
98+
- `@dcp-ai/sdk` (npm) 2.1.0 → 2.1.1 — first TypeScript code change
99+
since 2.1.0 (the 2.8.0 release was a docstring-only edit)
100+
7101
## [2.8.0] - 2026-04-26
8102

9103
### Added — Canonicalization profile `dcp-jcs-v1`

sdks/go/dcp/observability/telemetry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424

2525
// SDKVersion is reported to OpenTelemetry as `service.version`. Bumped in
2626
// lockstep with the Go module tag under sdks/go/v*.
27-
const SDKVersion = "2.8.0"
27+
const SDKVersion = "2.8.1"
2828

2929
// ExporterType selects how events are forwarded outside the in-memory bus.
3030
type ExporterType string

sdks/go/dcp/v2/bundle_builder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func BuildBundleV2(input BundleBuildInput) (*CitizenshipBundleV2, error) {
8282
AuditMerkleRoot: "sha256:" + auditMerkleSHA256,
8383
AuditMerkleRootSecondary: "sha3-256:" + auditMerkleSHA3,
8484
AuditCount: len(input.AuditEntries),
85+
CanonicalizationProfile: "dcp-jcs-v1",
8586
}
8687

8788
bundle := &CitizenshipBundleV2{

sdks/go/dcp/v2/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ type BundleManifest struct {
143143
AuditMerkleRootSecondary string `json:"audit_merkle_root_secondary,omitempty"`
144144
AuditCount int `json:"audit_count"`
145145
PQCheckpoints []string `json:"pq_checkpoints,omitempty"`
146+
// CanonicalizationProfile identifies the JSON canonicalization profile
147+
// under which this bundle was produced. Optional today (only "dcp-jcs-v1"
148+
// is defined; absence is assumed equivalent to "dcp-jcs-v1" — see
149+
// spec/CANONICALIZATION_PROFILE.md § 4). Future profiles will make the
150+
// field required.
151+
CanonicalizationProfile string `json:"canonicalization_profile,omitempty"`
146152
}
147153

148154
// BundleSignerV2 describes who signed a V2 bundle.

sdks/go/dcp/v2/verify_bundle.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ func VerifySignedBundleV2(registry *AlgorithmRegistry, signedBundleJSON []byte)
6767
}
6868
result.SessionBindingValid = manifestNonce != ""
6969

70+
// Per spec/CANONICALIZATION_PROFILE.md § 4: a missing canonicalization_profile
71+
// MUST be assumed equal to the only profile defined today, "dcp-jcs-v1".
72+
// An unknown value is rejected; future profiles will register their own
73+
// canonicalizer here.
74+
if profile, ok := manifest["canonicalization_profile"].(string); ok && profile != "dcp-jcs-v1" {
75+
result.Errors = append(result.Errors, fmt.Sprintf("Unknown canonicalization_profile: %s", profile))
76+
}
77+
7078
verifyManifestHashes(bundle, manifest, result)
7179
verifySessionNonceConsistency(bundle, manifestNonce, result)
7280
verifyBundleSignature(registry, bundle, signature, manifest, result)

sdks/python/dcp_ai/v2/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ class BundleManifest(BaseModel):
134134
audit_merkle_root_secondary: str | None = None
135135
audit_count: int
136136
pq_checkpoints: list[PQCheckpoint] | None = None
137+
# Per spec/CANONICALIZATION_PROFILE.md § 4: optional today, only
138+
# "dcp-jcs-v1" is defined. Pydantic Literal enforces unknown-value
139+
# rejection at parse time. A bundle without the field is accepted
140+
# and the default below makes any new manifest emit it.
141+
canonicalization_profile: Literal["dcp-jcs-v1"] | None = "dcp-jcs-v1"
137142

138143

139144
# ── Capabilities & Verifier Policy ──

sdks/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "dcp-ai"
7-
version = "2.8.0"
7+
version = "2.8.1"
88
description = "Python SDK for the Digital Citizenship Protocol for AI Agents"
99
readme = "README.md"
1010
license = "Apache-2.0"

sdks/rust/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdks/rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dcp-ai"
3-
version = "2.8.0"
3+
version = "2.8.1"
44
edition = "2021"
55
description = "Rust SDK for the Digital Citizenship Protocol for AI Agents"
66
license = "Apache-2.0"

sdks/rust/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,8 @@ pub mod wasm {
830830
"policy_hash": policy_hash,
831831
"audit_merkle_root": audit_merkle_sha256,
832832
"audit_merkle_root_secondary": audit_merkle_sha3,
833-
"audit_count": audit_entries.len()
833+
"audit_count": audit_entries.len(),
834+
"canonicalization_profile": "dcp-jcs-v1"
834835
});
835836

836837
let bundle = json!({

0 commit comments

Comments
 (0)