Skip to main content
Specification

Bundle wire format
v1.

The on-disk shape every Attestix portability bundle speaks. Tamper-evident, deterministic, JCS-canonical, and identical byte-for-byte across the OSS Python exporter and the cloud TypeScript worker. Published 2026-05-28. v1 is frozen.

01 / Identifier

The canonical URI for this version of the spec is:

https://attestix.io/spec/bundle/v1

Every bundle produced by Attestix — Python core ≥ v0.4.0 via the attestix.portability.bundle_writer module, the TypeScript cloud worker, and both the attestix export and attestix import CLIs — emits this exact string as the attestix_bundle_format field in manifest.json. The URI is dereferenceable to this page so a verifier holding only the bundle on disk can find the spec.

02 / Wire format at a glance

PropertyValue
ContainerUSTAR + gzip (.tar.gz). zstd planned for v2.
Per-table fileOne JSONL per table; one row per line; trailing newline; empty tables write zero bytes.
CanonicalizationJCS-style: json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False) followed by Unicode NFC. NOT strict RFC 8785 — documented in the Python attestix.auth.crypto.canonicalize_json and the equivalent canonicalizeJson in the npm attestix package (today @vibetensor/attestix until the unscoped publish completes).
Member orderAlphabetical across every member of the tarball. Required for byte-stable re-exports.
Tar timestampsinfo.mtime = 0; info.uid = info.gid = 0; info.uname = info.gname = "". Deterministic across runs.
Gzip headermtime = 0. Deterministic across runs.
EncodingUTF-8 throughout. No BOM. Manifest carries no whitespace between tokens.
Size capsBundle: 256 MiB. Single member: 128 MiB. Verifiers MUST refuse anything larger.

03 / Manifest schema

Every bundle carries one manifest.json whose body is the JCS-canonical serialisation of the following object. Field order is irrelevant on the wire (the canonicaliser sorts keys); the table below lists fields in semantic order.

{
  "manifest_version": "1.0",
  "attestix_bundle_format": "https://attestix.io/spec/bundle/v1",
  "workspace": {
    "id": "<UUID or slug>",
    "slug": "<tenant slug>",
    "region": "<aws-style region or 'local'>",
    "data_residency": "<eu|uk|us|in|ap|self-host>"
  },
  "exported_at": "<RFC-3339 UTC, ms precision>",
  "exported_by": {
    "user_id": "<UUID|'local'|null>",
    "email": "<email|'self-host@local'|null>"
  },
  "tables": [
    { "name": "<table>", "format": "jsonl",
      "row_count": <int>, "bytes": <int>, "sha256": "<hex>" }
  ],
  "core_version": "attestix==<semver>",
  "schemas": { "db_migration_max": "<zero-padded migration number>" },
  "notes": [ "<free-form producer note>" ]
}
FieldTypeReqMeaningExample
manifest_versionstringyesThe version of this spec the manifest conforms to. v1 manifests carry the literal string "1.0". Verifiers MUST refuse any other value."1.0"
attestix_bundle_formatstring (URI)yesThe dereferenceable identifier for this wire format. v1 carries the literal value https://attestix.io/spec/bundle/v1. Verifiers MUST refuse any other value."https://attestix.io/spec/bundle/v1"
workspace.idstring (UUID for cloud, slug for OSS)yesThe producer's internal workspace identifier. Cloud producers emit a Postgres UUID; the OSS exporter emits the tenant slug (the same value as workspace.slug)."11111111-1111-4111-8111-111111111111"
workspace.slugstringyesHuman-readable workspace identifier. MUST match the tenant_id every audit_events row was chained against — verifiers re-derive the chain_hash using this value and will fail the bundle if it disagrees."fixture-tenant"
workspace.regionstringyesCloud region the bundle was produced in (e.g. eu-west-1, us-east-1). OSS producers MUST emit the literal string "local"."eu-west-1"
workspace.data_residencystringyesResidency commitment for the producer (eu, uk, us, in, ap, self-host). Informational; not verified."eu"
exported_atstring (ISO-8601 UTC)yesWall-clock timestamp the bundle was assembled. RFC-3339 with millisecond precision and the trailing Z."2026-05-28T12:00:00.000Z"
exported_by.user_idstring | nullyesIdentifier of the user who triggered the export. OSS producers emit the literal string "local"; cloud producers emit the requester's user UUID or null for automated jobs."22222222-2222-4222-8222-222222222222"
exported_by.emailstring | nullyesEmail of the requester. OSS producers emit "self-host@local"; cloud producers emit the requester's email or null."operator@fixture.attestix.io"
tables[]array of objectsyesOne entry per table in the bundle. Each entry is {name, format, row_count, bytes, sha256}. Order MUST match the on-disk member order so a reader can stream-verify the tarball without seeking.see Section 5
core_versionstringyesProducer build pin in the form attestix==<semver>. The OSS exporter stamps its installed package version; the cloud worker stamps the CORE_VERSION_PIN constant."attestix==0.4.0rc2"
schemas.db_migration_maxstring (zero-padded migration number)yesHighest cloud database migration the producer knows about. Consumers compare against their own SUPPORTED_DB_MIGRATION_MAX and refuse strictly newer bundles with BundleSchemaTooNewError."0010"
notes[]array of stringsnoFree-form producer notes. v1 producers append "format=jsonl" and may append "exporter=oss" (OSS) or a Parquet-best-effort note (cloud).["format=jsonl", "exporter=oss"]

04 / Side-car: manifest.sha256

A plain-text file inside the tarball alongside manifest.json. It contains the lowercase hex SHA-256 of the JCS-canonical manifest body followed by a single trailing newline (65 bytes total). Critical for verifier round-trips: the consumer can recompute the hash and compare without first parsing the manifest, and producers can transport the digest separately without round-tripping the full body.

The sha is computed over the manifest as it was written — the manifest's own sha256 appears only inside per-table entries; there is no self-reference at the manifest root, so no field needs to be stripped before re-canonicalising.

# Producer side
canonical = canonicalize_json(manifest)            # JCS bytes
sha = sha256(canonical).hexdigest()                # 64 hex chars
write_member("manifest.json", canonical)
write_member("manifest.sha256", sha + "\n")

# Verifier side
canonical = canonicalize_json(json.loads(manifest_bytes))
assert sha256(canonical).hexdigest() == sidecar.strip()

05 / Per-table tables

Thirteen tables. Order in the manifest tables[] array and on-disk MUST match the order below — cloud and OSS producers agree on this byte-for-byte. Cloud-only tables are emitted as empty JSONL members by OSS producers so the bundle's member set stays symmetric across producers.

#TablePurposeNotes
01identitiesAgent identity rows — DID, did_document, signing key reference (public material only), status, revocation metadata.Maps to the OSS identities collection. Each row carries the agent's full DID document for offline re-resolution.
02key_referencescloud-onlyCloud-side KMS key handles. Not a v1 OSS concept — emitted empty by OSS producers.Empty JSONL on OSS. Cloud bundles may carry KMS ARNs / Vault key references — never the private key bytes.
03credentialsW3C Verifiable Credential envelopes with proof intact. Each row is {id, workspace_id, credential} where credential is the full VC body.Round-trippable: a verifier can re-validate the Ed25519Signature2020 proof against the issuer DID in the same bundle.
04credential_schemascloud-onlyCustom credential schema registry (cloud-only).Empty JSONL on OSS.
05membershipscloud-onlyCloud workspace membership rows.Empty JSONL on OSS (single-tenant).
06team_invitescloud-onlyCloud team-invite rows.Empty JSONL on OSS.
07subscriptionscloud-onlyCloud billing-subscription rows.Empty JSONL on OSS.
08compliance_profilesEU AI Act compliance profiles — Article 43 risk classification, provider metadata, transparency obligations, required-obligation list.Maps to the OSS compliance.profiles list. One profile per agent_id.
09conformity_assessmentsAnnex IV / Annex V conformity assessment records — assessor, result, findings, CE marking eligibility.Maps to the OSS compliance.assessments list. References its compliance_profile by agent_id.
10agent_dependenciescloud-onlyCloud agent dependency graph.Empty JSONL on OSS.
11audit_eventsHash-chained audit event log (the Article 12 evidence body). Each row carries event_id, actor, action, target_id, target_collection, occurred_at, change_digest, prev_hash, and chain_hash.Order MUST be (created_at asc, id asc). Verifiers MUST re-run verify_chain end-to-end and abort on any break — see the verifier algorithm in Section 6.
12anchorsBase L2 Sepolia anchor records — tx_hash, attestation_uid (EAS), schema_uid, attester address, block_number, gas_used, anchored_at, issuer_did, and the artifact_hash (Merkle root).Anchored on Base Sepolia testnet (chain_id 84532). Mainnet schema not yet registered.
13webhook_endpointscloud-onlyCloud webhook subscription rows.Empty JSONL on OSS. Webhook deliveries are intentionally out of scope (dispatcher state, not customer data).

Per-row schema for each table mirrors the Postgres column names used by the cloud database — snake_cased, with Date values rendered as ISO-8601 UTC strings, bigint values rendered as strings (JCS rejects numeric overflow), Buffer and Uint8Array rendered as lowercase hex, and null preserved verbatim. See the row projectors in attestix.portability.bundle_writer for the authoritative shape.

06 / Verifier algorithm (reference)

Two reference implementations: Python in attestix.portability.bundle_reader (PyPI attestix) and TypeScript in attestix-js (today @vibetensor/attestix@0.2.0; unscoped attestix publish in flight). Both follow the same algorithm:

1. Open bundle.tar.gz; extract every member to memory (bundle cap: 256 MiB; per-member cap: 128 MiB).
2. Read manifest.json; parse as JSON object.
3. JCS-canonicalise the parsed manifest dict (sort_keys + tightest separators + ensure_ascii=False).
4. Compute SHA-256 of the canonical bytes; read manifest.sha256 side-car; refuse on mismatch.
5. For each entry in manifest.tables[]:
     read <entry.name>.jsonl from the bundle
     compute SHA-256 over the raw bytes (no normalisation — the writer
       already wrote JCS-canonical lines, so re-canonicalising would drift)
     compare to entry.sha256; refuse on mismatch
     count newline-terminated lines; compare to entry.row_count; refuse on mismatch
6. For audit_events specifically: walk the rows in stored order, verify
     row[i].prev_hash == row[i-1].chain_hash and recompute row[i].chain_hash
     from the JCS-canonical body; refuse on any chain break.
7. Compare manifest.schemas.db_migration_max to the verifier's
     SUPPORTED_DB_MIGRATION_MAX; raise BundleSchemaTooNewError if strictly newer.
8. If every check passes: bundle integrity verified.

07 / Compatibility and versioning

  • v1 is frozen. No breaking changes will land within v1. A future v2 will be published at a new URI (e.g. /spec/bundle/v2) and producers will stamp the new URI in attestix_bundle_format.
  • Forward compatibility. Producers MAY add fields to the manifest or new tables to tables[]; verifiers MUST ignore unknown manifest fields and MUST ignore unknown tables that are not referenced by a verification rule.
  • Schema gating. The schemas.db_migration_max field carries the producer's database migration version. Consumers refuse bundles whose db_migration_max is strictly newer than the consumer's supported max — see BundleSchemaTooNewError in bundle_reader.py.
  • v2 plans (non-binding). zstd compression in place of gzip; optional Parquet representation for audit_events for large tenants; manifest signed via the producer's DID using Ed25519Signature2020. In v1 the manifest is unsigned; integrity is by SHA-256 only.

08 / Security model

Guarantees
  • Byte-level tamper evidence. Any modification to a table body, the manifest, or a sha256 breaks verification.
  • Audit chain integrity. audit_events chain is re-verified end-to-end at import; any break aborts.
  • Row-count consistency. Per-table row counts in the manifest must match the JSONL line count.
  • Schema gating. Bundles from a strictly newer producer are refused rather than silently losing rows or columns.
Does NOT guarantee
  • Confidentiality. Bundle contents are plaintext JCS. Encrypt at rest separately.
  • Producer authenticity. The manifest is UNSIGNED in v1 — anyone can produce a structurally valid bundle. v2 will add a DID-signed manifest.
  • Freshness. The manifest carries no nonce or anti-replay marker beyond exported_at. Consumers that need freshness MUST pair the bundle with a short-lived attestation.
  • Anchor freshness. An anchor row records that a hash was once posted to Base Sepolia; re-verifying the anchor against the chain is the consumer's responsibility.

Recommended deployment. Pair every bundle export with a signed BundleExportedCredential (W3C VC) issued at export time that attests to the bundle's SHA-256. The credential provides producer authenticity that the v1 wire format does not, without bloating the manifest with signature material.

09 / Example

The bytes below are extracted verbatim from the deterministic test fixture at tests/fixtures/bundles/sample-v1.tar.gz in the attestix repo. The bundle is 2,768 bytes on disk and contains 15 tar members (13 table JSONLs + manifest + sha side-car).

manifest.json (verbatim, JCS-canonical, 2,367 bytes)

{"attestix_bundle_format":"https://attestix.io/spec/bundle/v1","core_version":"attestix==0.4.0rc2","exported_at":"2026-05-28T12:00:00.000Z","exported_by":{"email":"operator@fixture.attestix.io","user_id":"22222222-2222-4222-8222-222222222222"},"manifest_version":"1.0","notes":["format=jsonl","fixture=oss-roundtrip"],"schemas":{"db_migration_max":"0010"},"tables":[{"bytes":2004,"format":"jsonl","name":"identities","row_count":2,"sha256":"8e0fba149ead951d8412a8634cb7d29fa7f9c2749929d7dee0d83e561e7d992f"},{"bytes":0,"format":"jsonl","name":"key_references","row_count":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":586,"format":"jsonl","name":"credentials","row_count":1,"sha256":"450e860134d4a00fdcbc66d76469fcbbea82181ae6f7740db042fb7d863d8575"},{"bytes":0,"format":"jsonl","name":"credential_schemas","row_count":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":0,"format":"jsonl","name":"memberships","row_count":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":0,"format":"jsonl","name":"team_invites","row_count":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":0,"format":"jsonl","name":"subscriptions","row_count":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":651,"format":"jsonl","name":"compliance_profiles","row_count":1,"sha256":"b58f382b75d0804e695fbe7e4708cf09326ca1f73498ca6504ec53fe7a8ae0fd"},{"bytes":383,"format":"jsonl","name":"conformity_assessments","row_count":1,"sha256":"e98f58b40249c985f05b8fb2203c8129b03184201b1059e3766b0d8d6bc9455e"},{"bytes":0,"format":"jsonl","name":"agent_dependencies","row_count":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":1999,"format":"jsonl","name":"audit_events","row_count":3,"sha256":"e3a185627ed4cd67781cd64011f3ca20d6d942751a1f0665c37d1823b498b316"},{"bytes":798,"format":"jsonl","name":"anchors","row_count":1,"sha256":"58d9dc30f6151c89a2cb8f0ef85b16cb8e4d6521faa33d884447a522f7adc034"},{"bytes":0,"format":"jsonl","name":"webhook_endpoints","row_count":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}],"workspace":{"data_residency":"eu","id":"11111111-1111-4111-8111-111111111111","region":"eu-west-1","slug":"fixture-tenant"}}

manifest.sha256 (verbatim, 65 bytes including trailing newline)

b52c2baa896c82991f0c86afb6b771f4ab192d25dc153b463eb125183cf737c9

credentials.jsonl (one row; full W3C VC envelope; 586 bytes)

{"credential":{"credentialStatus":{"id":"urn:uuid:cred-fixture-0001#status","revocation_reason":null,"revoked":false,"revoked_at":null,"type":"RevocationList2021Status"},"credentialSubject":{"id":"attestix:fixture0000000001","role":"Fixture Subject"},"expirationDate":"2027-05-01T10:00:00Z","id":"urn:uuid:cred-fixture-0001","issuanceDate":"2026-05-01T10:00:00Z","issuer":{"id":"did:key:fixture-issuer","name":"Fixture Issuer"},"type":["VerifiableCredential","AgentIdentityCredential"]},"id":"cccccccc-cccc-4ccc-8ccc-cccccccccc01","workspace_id":"11111111-1111-4111-8111-111111111111"}

audit_events.jsonl (first row of a 3-row hash chain)

{"action":"identity.create","actor":"user:operator@fixture.attestix.io","anchor_id":null,"chain_hash":"5d1034b220c98c5a839df512fef2dff46e34e86df7dfb2d288a96528c0dfac88","change_digest":"d36b020fbbdb0baacea68cd168bcad261b3d283b8eb69554f3d31eb193947fd2","created_at":"2026-05-01T10:00:01Z","event_id":"evt:fixture000000","id":"99999999-9999-4999-8999-999999999900","occurred_at":"2026-05-01T10:00:00+00:00","occurred_at_month":"2026-05-01","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","retention_days":7,"target_collection":"identities","target_id":"attestix:fixture0000000001","workspace_id":"11111111-1111-4111-8111-111111111111"}

The first row's prev_hash is the genesis sentinel (64 zero bytes). Each subsequent row's prev_hash equals the previous row's chain_hash; each row's own chain_hash is computed over the JCS-canonical row body using the canonicaliser documented above.

10 / Test vectors

The deterministic fixture above is regenerated by tests/fixtures/bundles/generate_sample_bundle.py. Anyone building a third-party verifier can run the generator, byte-compare against the committed fixture, then assert their verifier accepts the clean bundle and rejects each of the three tamper variants the generator can produce (manifest body mutation, table body mutation, and schema-too-new bump).

The round-trip suite at tests/portability/ exercises every code path on this page against the fixture and is part of the 481-test CI matrix.

11 / Versions index

VersionStatusPublishedIdentifier
v1current2026-05-28https://attestix.io/spec/bundle/v1
v2planned/spec/bundle/v2 (not yet allocated)