Treeship
Reference

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_version field 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 when None, 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:

FieldTypePurpose
schema_versionOption<String> (top level)Forward compatibility marker
session.ship_idOption<String>Cross-verification target — must equal certificate's identity.ship_id

And one new optional field on the certificate:

FieldTypePurpose
schema_versionOption<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_version sits on that view.
  • For certificates, the embedded Ed25519 signature covers {identity, capabilities, declaration} (canonical JSON, declaration-order). schema_version is 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:

  1. New documents emit "2" with the field inside the signed payload.
  2. Verifiers see "2" and apply the v2 ruleset.
  3. Verifiers see "1" (or missing) and apply the v1 ruleset (current behavior).
  4. 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.