Schema versioning
schema_version field on receipts and certificates, legacy rules for missing field, forward compatibility guarantees.
Treeship's wire format evolves. v0.9.0 introduced a schema_version field on Session Receipts and Agent Certificates so verifiers can keep doing the right thing across schema generations.
The rules are intentionally simple:
- New documents (v0.9.0+) emit
schema_version: "1". - Documents with no
schema_versionfield are treated as"0"and verified under legacy rules (the pre-v0.9.0 shape). - The field is optional in the struct definition (
Option<String>) and skipped from output whenNone, so legacy documents round-trip byte-identical and any signature over those bytes is preserved.
Where it lives
{
"type": "treeship/session-receipt/v1",
"schema_version": "1",
"session": { ... },
...
}{
"type": "treeship/agent-certificate/v1",
"schema_version": "1",
"identity": { ... },
"capabilities": { ... },
"declaration": { ... },
"signature": { ... }
}Both fields are at the top level of the document, immediately after type.
Effective version
The library helper treeship_core::agent::effective_schema_version resolves the Option<String> to its effective string, defaulting to "0" for missing values:
use treeship_core::agent::effective_schema_version;
let v = effective_schema_version(receipt.schema_version.as_deref());
// "1" for v0.9.0+ receipts, "0" for legacy receipts.Verifiers branch on v, not on receipt.schema_version.is_some(), so the legacy default flows from one place.
What v0.9.0 changes vs v0.8.0
The v0.9.0 wire format is a strict superset of v0.8.0. Two new optional fields on the receipt:
| Field | Type | Purpose |
|---|---|---|
schema_version | Option<String> (top level) | Forward compatibility marker |
session.ship_id | Option<String> | Cross-verification target — must equal certificate's identity.ship_id |
And one new optional field on the certificate:
| Field | Type | Purpose |
|---|---|---|
schema_version | Option<String> (top level) | Forward compatibility marker |
That's it. Every other v0.8.0 field carries forward unchanged. Pre-v0.9.0 receipts continue to verify cleanly under v0.9.0 code via the regression suite at packages/core/tests/legacy_receipt_fixtures.rs. Pre-v0.9.0 certificates continue to verify under v0.9.0 code; the cross-verification path simply reports Unknown for ship-ID status when the receipt has no ship_id.
Cryptographic binding
In v0.9.0, schema_version is informational on the envelope, not bound by a signature.
- For receipts, integrity is via the Merkle tree over individual artifact envelopes; the receipt itself is a composed view, not a signed document.
schema_versionsits on that view. - For certificates, the embedded Ed25519 signature covers
{identity, capabilities, declaration}(canonical JSON, declaration-order).schema_versionis on the certificate wrapper, outside the signed payload, for backward compatibility with pre-v0.9.0 certificates whose signatures were computed without it.
In v0.9.0 this distinction is harmless because the rules for "0" and "1" are identical — "1" is just "the field happens to be present". Future versions that need schema_version to be cryptographically bound (because v1 and v2 rules diverge) will move it inside the signed payload and bump to "2". The migration path:
- New documents emit
"2"with the field inside the signed payload. - Verifiers see
"2"and apply the v2 ruleset. - Verifiers see
"1"(or missing) and apply the v1 ruleset (current behavior). - After enough adoption, drop
"0"/"1"support in a major release.
Forward compatibility guarantee
What this commits to: any field added in a future minor release will be added with #[serde(default, skip_serializing_if = "...")] so old documents continue to round-trip byte-identical and the determinism check inside verify_package keeps passing. This is enforced by the regression suite — old fixtures must verify in every release.
What this does not commit to: existing fields cannot change semantics within the same schema_version. If field X means one thing in v0.9.0 and a different thing in v0.10.0, the schema_version bumps and the verifier branches.
If you're parsing receipts in your own code, check effective_schema_version rather than the raw Option. New schema versions land first in the library helper; depending on it shields your code from the field being absent on legacy documents.
Parity between CLI and WASM
From v0.9.1, the @treeship/core-wasm bundle applies identical schema-version rules to the CLI. Both paths use the same effective_schema_version helper and the same legacy defaults. treeship verify <url> on the CLI and verifyReceipt(url) from @treeship/verify (or the WASM-backed @treeship/sdk / @treeship/a2a / @treeship/mcp surfaces) produce the same check list on the same receipt. A receipt that verifies on one path verifies on all of them; a receipt that fails fails on all of them with the same reason.
This is enforced by the core library: all verifiers route through treeship_core::verify::verify_receipt_json_checks. The WASM export is a thin wrapper. Future schema-version migrations land in the core function; every WASM consumer picks them up the next time @treeship/core-wasm is installed at the exact pinned version.