Skip to content

Authoritative state snapshots

This page is the normative PETRA 1.0 contract for recovering declared durable read models after the configured offline horizon. It extends the Zenoh and DDIL data-plane contract; it does not turn Web history into endpoint-authored traffic.

Contract target, not current implementation

The shared Common schema, verifier, and vectors are the first dependent slice, but purpose 3 remains unusable end to end until the remaining components adopt it. Directory issues no snapshot authority, Web has no cross-family deterministic cut, and consumers have no snapshot applier. The protocol below is the interoperability gate for the dependent Common, Directory, Web, Android, Node, Gateway, and Server work. Existing realtime “snapshot” APIs and replay exports are not this protocol.

Authority and purpose separation

Common appends one purpose without renumbering the existing values:

enum EnvelopePurpose {
  ENVELOPE_PURPOSE_LIVE = 0;
  ENVELOPE_PURPOSE_DURABLE_GRANT = 1;
  ENVELOPE_PURPOSE_DURABLE_CONTENT = 2;
  ENVELOPE_PURPOSE_AUTHORITATIVE_STATE_SNAPSHOT = 3;
}

Only an authenticated Web machine identity with a matching Directory-signed snapshot_authority capability may sign purpose 3. Identity and snapshot authority are separate proofs:

  • machine identity is a Directory-signed IdentityToken plus a verified signature under that token's principal_sign_key; and
  • snapshot authority is the separate Directory-signed grant below whose principal and signing key exactly equal the authenticated token.

The grant alone is not identity proof and never substitutes for the token or machine signature. Directory issues it only while provisioning an active platform_type=web machine principal, only for that machine's current token principal and signing key, and only for the approved scopes, audience, and classification ceiling. platform_type remains a Directory-side issuance constraint rather than a new IdentityToken claim: Common verifies the token and exact grant/token binding, while Directory is responsible for never issuing this grant to a human, browser session, ordinary endpoint, Server, or non-Web machine. It is not a human role, cannot be delegated or re-issued by Web, and is never minted by Relay + Storage.

Issuance requires the current Web core-application relationship. Directory carries the canonical grant in field 5 of every matching purpose-3 CapabilitySetEntryV1, alongside the exact snapshot publication capability. There is no standalone snapshot-authority refresh. Complete-set verification installs the pair atomically; relationship, key, family, scope, or audience removal omits the entry and therefore removes both before snapshot publication, shared operation, or outbox drainage resumes. A missing grant, orphan grant, or capability/grant mismatch invalidates the whole refresh and preserves the prior cache only in degraded mode until its original absolute deadlines.

The outer AuthEnvelope is signed by Web's device key and uses purpose 3. Verification relaxes the short-lived identity-token expiry in the same way as other durable purposes, but still requires:

  • a valid Directory signature on the embedded machine IdentityToken, a valid machine signature, and a matching grant valid at the outer AuthEnvelope.issued_at_ms;
  • the current revocation gate for the Web principal and signing key;
  • exact snapshot key, family, scope, audience, classification, and capability binding;
  • a valid Web device signature over the PETRA 1.0 AuthEnvelope signing input; and
  • the fresh capability-use proof for publication or query.

PETRA 1.0 adds bytes snapshot_authority_grant = 9 to AuthEnvelope. It is empty for purposes 0–2 and contains the deterministic protobuf bytes below for purpose 3. The canonical signing input for every 1.0 envelope appends u32be(len(snapshot_authority_grant)) || snapshot_authority_grant after the existing purpose field, followed by the fixed 32-byte storage_key_digest field 10 defined by #124, then u32be(len(publication_capability)) || publication_capability for the canonical Directory-signed field 11. This is an intentional flag-day wire break: pre-1.0 envelopes and clients do not interoperate, and implementations carry no legacy signing-input verifier. Relay + Storage persists the complete envelope, so the grant, durable key binding, and publication authority remain available when a snapshot is queried later; the ephemeral storage-use proof is not a substitute for them.

message SnapshotAuthorityScopeV1 {
  SnapshotFamily family = 1;
  string scope_segment = 2;                 // 32 lowercase hex; closed aggregate exception below
}

message SnapshotAuthorityGrantV1 {
  uint32 version = 1;
  string web_principal_id = 2;
  bytes web_signing_key = 3;                // Ed25519, exactly 32 bytes
  repeated SnapshotAuthorityScopeV1 scopes = 4;
  bytes audience_digest = 5;                // exactly 32 bytes
  Clearance classification_ceiling = 6;
  uint64 issued_at_ms = 7;
  uint64 expires_at_ms = 8;
  bytes grant_id = 9;                       // exactly 16 bytes
  bytes directory_key_id = 10;              // SHA-256(Directory public key), 32 bytes
  bytes directory_signature = 11;           // Ed25519, exactly 64 bytes
  string audience_segment = 12;             // signed opaque key segment, 32 lowercase hex
}

Directory signs the exact snapshot_authority_grant_signing_input defined below. Verification requires the embedded IdentityToken principal and signing key to equal fields 2–3, the requested family/scope pair to be contained by field 4, the snapshot audience digest to equal field 5, and the parsed key's audience segment to equal signed field 12. Common's normative CMBAC decide(snapshot.classification, classification_ceiling) must return Permit. That decision validates the policy and classification rank, requires every restrictive label value, requires at least one held value for every permissive tag group, and ignores informative categories. Missing/malformed clearance, cross-policy denial, or any other CMBAC denial fails closed. It also requires grant.issued_at_ms <= AuthEnvelope.issued_at_ms < grant.expires_at_ms, a trusted Directory key whose SHA-256(public_key) equals field 10, a valid signature, and current Web revocation state.

Directory computes field 12 as opaque("snapshot-audience", lowercase_hex(audience_digest)) using the deployment opaque salt and rejects a supplied value that does not match. A salt-holding Web publisher or endpoint repeats that derivation when building or applying a snapshot. Relay + Storage never receives the salt and therefore performs only the checks available at its trust boundary: field 12 is canonical 32-character lowercase hex, its bytes exactly equal the audience segment in the publication key, the family/scope pair is present in the signed grant, and both the grant and exact-key StorageCapabilityV1 verify for the same authenticated principal and signing key. Relay cannot prove the opaque preimage, audience membership, or domain derivation and must not claim that it has done so.

All strings below are UTF-8. str32(s) = u32be(len(utf8(s))) || utf8(s) and bytes32(b) = u32be(len(b)) || b. A list is u32be(count) followed by its elements. Authority scopes are ascending unique (u32 family, UTF-8 scope_segment) pairs. canonical_clearance(c) is:

str32(policy_id) || str32(max_classification)
|| u32be(category_count)
|| concat(
     u32be(category_type) || str32(tag)
     || u32be(value_count) || concat(str32(value))
   )

Clearance categories are sorted by (category_type, UTF-8 tag) with no duplicate type/tag; their values are unique and sorted by UTF-8 bytes. The Directory rejects non-canonical inputs rather than silently normalizing them. The exact grant input is:

ascii("petra-snapshot-authority-v1\0")
|| u32be(version)
|| str32(web_principal_id)
|| web_signing_key[32]
|| u32be(scope_count) || concat(u32be(family) || str32(scope_segment))
|| audience_digest[32]
|| str32(audience_segment)
|| bytes32(canonical_clearance(classification_ceiling))
|| u64be(issued_at_ms) || u64be(expires_at_ms)
|| grant_id[16]
|| directory_key_id[32]

An endpoint or ordinary Web browser identity signing purpose 3 fails closed. A Web signature under LIVE, DURABLE_GRANT, or DURABLE_CONTENT is not a snapshot. A purpose-3 value is never a command, event, receipt, acknowledgement, proof that an endpoint was online, or replacement for the original endpoint signature.

Audience and encryption

Each snapshot is homogeneous for one recovery family, opaque scope, audience, classification label, and current encryption-key epoch. For PETRA 1.0, snapshot content uses the current deployment group-key epoch distributed by Directory. Relay + Storage is denied that key and retains only opaque sealed bytes.

The audience is enforced by the Directory-issued query capability, CMBAC label, current ORBAT/channel policy at the endpoint, and the signed audience_digest. The deployment group key remains a deployment-wide confidentiality boundary; it is not per-channel or per-unit encryption. This preserves the existing product model rather than introducing a new key hierarchy. A member that obtains snapshot ciphertext outside its authorized query path retains the same broad group-key residual documented in the security model.

Web and Directory use this closed audience schema:

enum SnapshotAudienceMemberKindV1 {
  SNAPSHOT_AUDIENCE_MEMBER_KIND_UNSPECIFIED = 0;
  SNAPSHOT_AUDIENCE_MEMBER_KIND_PRINCIPAL = 1;
  SNAPSHOT_AUDIENCE_MEMBER_KIND_UNIT = 2;
  SNAPSHOT_AUDIENCE_MEMBER_KIND_CHANNEL = 3;
}

message SnapshotAudienceMemberV1 {
  SnapshotAudienceMemberKindV1 kind = 1;
  string opaque_segment = 2;             // registered #124 domain, 32 lowercase hex
}

message SnapshotAudienceDescriptorV1 {
  uint32 version = 1;                    // exactly 1
  string deployment_segment = 2;         // audience-deployment, 32 lowercase hex
  repeated SnapshotAudienceMemberV1 members = 3;
}

Directory adds one deployment-static deployment_id, minted as a canonical lowercase RFC 4122 UUID during the 1.0 reset. The deployment segment is opaque("audience-deployment", deployment_id). The member segment domain and canonical identifier are fixed by kind: auth-principal over the exact Directory-issued IdentityToken.principal_id for PRINCIPAL, record-orbat-unit over the exact signed OrbatRecord.unit_id for UNIT, and channel-id over the exact signed channel id for CHANNEL. Identifiers are case-sensitive UTF-8 and are never trimmed or normalized by a consumer; their owning producer must emit the canonical form. members is a set sorted by (u32 kind, UTF-8 opaque_segment); duplicates, UNSPECIFIED, empty deployment, and non-canonical segments fail closed. An empty set means the whole named deployment subject to CMBAC and current ORBAT/channel policy. Canonical audience bytes are independent of protobuf encoding:

u32be(version) || str32(deployment_segment) || u32be(member_count)
|| concat(u32be(kind) || str32(opaque_segment))

audience_digest = SHA-256(canonical_audience_bytes). The snapshot key's audience segment uses the #124 snapshot-audience domain over the lowercase hexadecimal audience digest, not over cleartext identities. Web seals the snapshot under the current epoch; a snapshot sealed only under an old, unavailable epoch cannot complete post-horizon recovery.

The descriptor's domain-to-kind rule is enforced where the deployment salt and canonical source identifiers exist: Directory when issuing the matching grant, and a salt-holding endpoint when matching its current audience. Relay + Storage can validate only the enum, ordering, uniqueness, and opaque-segment syntax. Outputs from different opaque domains are intentionally indistinguishable without the salt and preimage, so Relay must not infer or claim that a PRINCIPAL, UNIT, or CHANNEL segment used the right derivation domain.

Key expression

Snapshots use a separate opaque namespace:

waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/<snapshot_seq>/manifest
waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/<snapshot_seq>/chunk/<index>
  • <family> is the exact literal mapped from SnapshotFamily: drawing, target, channel, invite, plan, report-requirement, sitrep-current, or orbat.
  • <scope_segment> is normally the #124 opaque derivation using domain snapshot-scope over the canonical family scope. The literal * is reserved for a Directory-issued automatic Web aggregate in the DRAWING, CHANNEL, or INVITE family. It contains only concrete 32-lowercase-hex scopes in that same family.
  • <audience_segment> uses domain snapshot-audience over the lowercase hexadecimal audience_digest and exactly equals signed SnapshotAuthorityGrantV1.audience_segment.
  • <snapshot_seq> is canonical positive uint64 decimal with no leading zero; it is monotonic per (family, scope_segment, audience_digest). Classification is deliberately absent from the key, so every classification partition sharing that opaque namespace draws from this one sequence stream and can never publish the same key/sequence.
  • <index> is canonical uint32 decimal starting at zero, with no leading zero.

Directory pre-authorizes future Web-allocated sequences with exactly two bounded PUBLISH selectors per signed family/scope/audience grant:

waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/*/manifest
waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/*/chunk/*

Common recognizes * in the sequence and chunk-index selector positions. It also recognizes the closed aggregate scope above only for DRAWING, CHANNEL, and INVITE; the requested concrete key must retain the same family and audience and supply a valid opaque scope. Every other scope remains byte-identical. It rejects **, wildcard family or audience, aggregate scope in any other family, aggregate-to-aggregate use where a concrete key is required, zero sequence, overflow, leading zero, manifest/chunk crossing, and every missing or extra segment. Action remains exact: a QUERY selector never publishes, and a PUBLISH selector never queries. Only PUBLISH entries carry field-5 snapshot authority. This does not authorize another family, scope, audience, action, or purpose.

Manifest and chunk keys are cross-bound to the decoded sealed payload. A value copied to another family, scope, audience, sequence, or chunk index fails before application. Zenoh delete is never used to retract a snapshot; supersession is semantic and retained values expire under the deployment horizon.

The canonical family scopes and atomic replacement boundaries are closed in version 1:

Family Canonical family scope Snapshot replacement boundary
DRAWING cell:<geohash5> Every current drawing and drawing tombstone in that exact cell
TARGET operation:<operation_segment> The current target board and target tombstones for that operation
CHANNEL channel:<channel_segment> One channel definition, membership projection, ownership state, and tombstone
INVITE principal:<principal_segment> Every effective invite/grant and revocation for that principal
PLAN operation:<operation_segment> Every latest plan and cancellation in that operation
REPORT_REQUIREMENT operation:<operation_segment> Every latest report requirement and cancellation in that operation
SITREP_CURRENT operation:<operation_segment> The whole current SitRep/compliance projection for that operation
ORBAT operation:<operation_segment> The whole effective ORBAT and retirement set for that operation

Each table boundary is further partitioned by the exact signed audience_digest and canonical classification label. Applying a snapshot replaces absence only inside that (family, scope, audience, classification) projection; it never deletes a differently classified or differently addressed projection. The shared sequence allocator above is wider than the replacement partition solely to keep classification-free storage keys collision-free. A gap in one classification's observed sequence is therefore normal and does not imply missing state for that partition.

geohash5 is exactly the lowercase five-character alphabet defined by the wire protocol. operation_segment, channel_segment, and principal_segment are #124 derivations over the exact signed Common/Web operation id, channel id, and Directory principal id under operation-id, channel-id, and auth-principal respectively. No whitespace, alternate prefix, percent encoding, cleartext identifier, or nested scope is canonical. A grant contains exact (family, scope_segment) pairs, so authority for an ORBAT scope and a drawing scope cannot be recombined into a Cartesian-product target scope.

Logical schema

The logical snapshot is deterministic protobuf. Common owns the actual .proto and generated cross-language types; field numbers below are reserved by this contract.

enum SnapshotFamily {
  SNAPSHOT_FAMILY_UNSPECIFIED = 0;
  SNAPSHOT_FAMILY_DRAWING = 1;
  SNAPSHOT_FAMILY_TARGET = 2;
  SNAPSHOT_FAMILY_CHANNEL = 3;
  SNAPSHOT_FAMILY_INVITE = 4;
  SNAPSHOT_FAMILY_PLAN = 5;
  SNAPSHOT_FAMILY_REPORT_REQUIREMENT = 6;
  SNAPSHOT_FAMILY_SITREP_CURRENT = 7;
  SNAPSHOT_FAMILY_ORBAT = 8;
}

message AuthoritativeStateSnapshotV1 {
  uint32 version = 1;                    // exactly 1
  SnapshotFamily family = 2;
  string scope_segment = 3;              // 32 lowercase hex
  uint64 snapshot_seq = 4;
  uint64 source_cut = 5;                 // Web durable-ingest high-water
  uint64 snapshot_created_at_ms = 6;
  ConfidentialityLabel classification = 7;
  bytes audience_digest = 8;             // exactly 32 bytes
  uint32 key_epoch = 9;
  bool complete = 10;                    // must be true for replacement
  repeated SnapshotEntryV1 entries = 11; // canonical key order
  SnapshotProvenanceV1 provenance = 12;
  bytes state_digest = 13;               // exactly 32 bytes
}

message SnapshotEntryV1 {
  string canonical_opaque_key = 1;
  SnapshotEntryKind kind = 2;             // VALUE = 1, TOMBSTONE = 2
  uint64 source_version = 3;              // family semantic seq/revision/timestamp
  bytes canonical_value = 4;              // current decoded application value
  bytes source_envelope_sha256 = 5;       // original envelope, when one exists
  bytes source_author_digest = 6;         // attribution, not snapshot signer
  uint64 source_issued_at_ms = 7;
  uint64 source_observed_at_ms = 8;       // Web durable-ingest time
  bytes source_audit_sha256 = 9;          // Web audit row when no envelope exists
}

message SnapshotProvenanceV1 {
  uint64 source_cut = 1;
  uint64 first_observed_at_ms = 2;
  uint64 last_observed_at_ms = 3;
  uint64 value_count = 4;
  uint64 tombstone_count = 5;
  bytes ordered_source_digest = 6;
}

canonical_value is the exact Common-defined deterministic protobuf encoding of the family value. The encoder orders map entries by UTF-8 key bytes and repeated set-like fields by their family rule. Consumers hash the received bytes before decoding and never derive the digest by re-encoding a platform object. Unknown fields are preserved only when the Common family contract says so; otherwise the snapshot builder rejects them rather than producing platform-dependent bytes.

The family-to-value mapping is closed for version 1:

Family VALUE encoding TOMBSTONE encoding and version
DRAWING DrawingShape; source_version = revision >= 1 DrawingDelete with the same shape id and a strictly higher revision
TARGET TargetCop; source_version = timestamp_ms > 0 TargetDelete; delete timestamp advances the same target version
CHANNEL SnapshotChannelStateV1; source_version = revision >= 1 Same aggregate with tombstoned=true; revision strictly advances
INVITE SnapshotInviteGrantV1; source_version = revision >= 1 Same aggregate with revoked=true; revision strictly advances
PLAN PlanRecord; source_version = seq >= 1 PlanRecord.cancelled=true at a higher seq
REPORT_REQUIREMENT ReportRequirement; source_version = seq >= 1 cancelled=true at a higher seq
SITREP_CURRENT SnapshotSitRepCurrentV1; source_version = revision >= 1 A period removed from the current projection uses status=RETIRED at a higher revision; no ReportRecord is synthesized
ORBAT OrbatRecord; source_version = seq >= 1 OrbatRecord.retired=true at a higher seq

The three snapshot-only aggregates are:

message SnapshotChannelMemberV1 {
  string member_segment = 1;
  bool joined = 2;
  uint64 revision = 3;
}

message SnapshotChannelStateV1 {
  uint32 channel_kind = 1;            // 1 = chat, 2 = voice
  bytes canonical_definition = 2;     // deterministic Chat or Voice
  repeated SnapshotChannelMemberV1 members = 3; // member_segment order
  string owner_segment = 4;
  uint64 revision = 5;
  bool tombstoned = 6;
}

message SnapshotInviteGrantV1 {
  string invitee_segment = 1;
  string channel_segment = 2;
  bytes canonical_grant = 3;
  uint64 revision = 4;
  bool revoked = 5;
}

enum SnapshotSitRepStatusV1 {
  SNAPSHOT_SITREP_STATUS_UNSPECIFIED = 0;
  SNAPSHOT_SITREP_STATUS_REPORTED = 1;
  SNAPSHOT_SITREP_STATUS_LATE = 2;
  SNAPSHOT_SITREP_STATUS_SILENT = 3;
  SNAPSHOT_SITREP_STATUS_RETIRED = 4;
}

message SnapshotSitRepCurrentV1 {
  string requirement_segment = 1;
  string unit_segment = 2;
  uint64 period_start_ms = 3;
  SnapshotSitRepStatusV1 status = 4;
  bytes latest_report_digest = 5;
  uint64 revision = 6;
  string operation_id = 7;
}

Drawing values and tombstones share one signed semantic revision stream per shape_id. Every accepted create, edit, or delete carries revision >= 1; a new mutation must strictly advance the prior value/tombstone revision. The direct canonical payload timestamp_ms remains signed event and expiry metadata, but neither timestamp nor receipt order can order the drawing projection or become its snapshot source_version. During the unpublished Android transition, its existing wrapped live and replay handlers continue requiring wrapper/body timestamp equality; removing that check ships only with a behavior-equivalent direct decoder. A producer allocates the revision atomically with its durable mutation/outbox write. Equal revision plus the same canonical value is an idempotent duplicate; equal revision plus different canonical bytes is a fork and quarantines the affected snapshot partition until a strictly higher authoritative mutation resolves it.

Channel and invite aggregates use the already signed StorageCapabilityV1.authority_ledger_version; Common does not add a second revision field to their control payloads. Directory maintains one strictly increasing ledger stream per channel channel_segment and one per invite principal_segment, matching the two snapshot replacement boundaries above. Every committed definition, membership, ownership-transfer, grant, revocation, or tombstone mutation in that aggregate advances the same stream and carries that value in its COMMITTED_AUTHORITY_MUTATION publish capability. Splitting mutation kinds into independent version streams is invalid because it would make their effective aggregate unordered.

Web verifies the committed capability before ingest, persists its authority_ledger_version with the mutation, and uses that exact value as the affected snapshot member's revision. SnapshotChannelStateV1.revision and the enclosing SnapshotEntryV1.source_version are the maximum effective ledger version included in the channel aggregate; each SnapshotChannelMemberV1.revision is the ledger version of that member's latest effective membership mutation. SnapshotInviteGrantV1.revision is the grant/revocation's latest effective ledger version, and its enclosing invite entry's source_version is that exact same value. Web observation order, row ids, local counters, and HLC cannot create, replace, or break a tie in this authority version stream.

ORBAT has one atomic projection per deployment-global unit_id, not independent definition, membership, appointment, or lifecycle wire projections. Every change emits the complete OrbatRecord on its one canonical opaque unit key with seq + 1, and its verified COMMITTED_AUTHORITY_MUTATION capability carries authority_ledger_version == OrbatRecord.seq. Snapshot ORBAT entries use that same full record and exact sequence as source_version; live ingest, retained catch-up, and snapshot reconciliation therefore share one merge rule. Lower sequence is rollback, equal sequence plus identical bytes is idempotent, equal sequence plus different bytes is equivocation, and a higher complete record replaces the lower one. A retired=true record remains the full unit tombstone and cannot be undone by a delayed earlier record. One Directory transaction may update several units atomically, but every affected unit advances its own sequence independently and exactly once.

SITREP_CURRENT is one durable derived cell per (operation_id, requirement_id, expected_unit_id, current_period_start_ms). A current one-shot or cadence requirement contributes cells only for live units in its ORBAT subtree that have a current commander, or a provisional owner while forming. The first policy-equivalent report in the period determines REPORTED (within the fixed 15-minute grace) versus LATE; absence is SILENT, while latest_report_digest identifies the latest equivalent report without embedding or re-authoring that event. Policy equivalence merges repeated same-tag categories and ignores only label creation metadata; the emitted partition label itself is canonical and exact. Cadence rollover, cancellation, command loss, audience removal and relabelling advance the durable cell revision and tombstone every prior exact partition before replacement. Web persists the previous/current audience identities and canonical audit evidence in the same transaction as its class-2 ledger mutation. The snapshot carries that audit digest, an empty original envelope digest, and operation_id so Common and every endpoint can bind the decoded cell back to the signed operation scope. It is evidence of the Web read model, never proof that an endpoint authored a report or was online at snapshot time.

source_author_digest, source_envelope_sha256, and source_audit_sha256 preserve attribution/provenance where Web retained it. They do not make the entry endpoint-signed and must never be exposed as proof that the endpoint authored the snapshot or was online at snapshot time. For a Web-derived read model with no original envelope, the source-envelope digest is empty and the required audit-row digest is carried in field 9 and contributes to ordered_source_digest.

Attachments such as KML remain content-addressed references. Snapshot entries carry the existing SHA-256 manifest reference, never blob bytes; clients fetch authorized blobs over the artifact path after state recovery.

Canonical ordering and digest

Entries are sorted by the UTF-8 bytes of canonical_opaque_key. Duplicate keys, non-canonical keys, unknown family/kind values, zero source versions for a versioned family, and equal key/version entries with different value digests are rejected.

source_version is family-defined and always greater than zero:

  • plan, report-requirement, and ORBAT entries use their signed semantic seq;
  • drawing entries use the signed DrawingShape.revision or DrawingDelete.revision;
  • target entries use their signed payload timestamp;
  • effective channel and invite projections use the highest verified StorageCapabilityV1.authority_ledger_version from their single aggregate stream; and
  • the Web-derived current SitRep/compliance projection uses its durable materialization revision, not a submitted report's event time.

Tombstones use the same version domain and must advance it. Arrival time, Web observation order, database row id, and Zenoh HLC never break a tie. Equal version plus identical digest is a duplicate; equal version plus different bytes quarantines that family/scope and prevents a new complete head assertion until the application authority resolves it with a strictly higher semantic version. Web must not pick a winner by receipt order.

The cross-language digest deliberately does not depend on protobuf field order. For each entry, encode:

u32be(key_len) || key_utf8
|| u8(kind)
|| u64be(source_version)
|| u32be(value_len) || canonical_value
|| source_envelope_sha256[32-or-empty-as-32-zero-bytes]
|| source_audit_sha256[32-or-empty-as-32-zero-bytes]
|| source_author_digest[32-or-empty-as-32-zero-bytes]
|| u64be(source_issued_at_ms)
|| u64be(source_observed_at_ms)

state_digest = SHA-256(concat(u32be(entry_len) || entry_bytes)) in sorted order. ordered_source_digest uses the same framing over each entry's source-envelope or audit digest. The shared digest, ordering, tombstone, rollback, and confusion cases live in authoritative-snapshot-v1.json.

Executable cross-language corpus

The docs JSON is the normative digest/precedence seed. The dependent Common change must publish one executable common/testdata/authoritative_snapshot_vectors.json consumed by Rust, TypeScript, and Kotlin/JNI. With fixed test keys, nonce, current group-key epoch, classification, audience, and entries, it includes exact hex for:

  • the authority-grant signing input including its signed audience segment, Directory signature, and encoded grant;
  • logical snapshot protobuf, per-entry/state/whole-snapshot digests;
  • manifest and every chunk protobuf, key expression, and chunk commitment;
  • sealed manifest/chunk payloads, purpose-3 AuthEnvelope signing inputs/signatures, and complete encoded envelopes;
  • nonce-bound head assertion signing input including the exact machine identity token, Web signature, and encoded assertion; and
  • exact shared-verifier bytes for the authority, audience, classification, chunk, and digest mutations materialized by Common, plus a structured manifest of the remaining rollback, fork, epoch, tombstone, and component-integration cases listed below.

Negative rows are structured hostile_cases with an exact target, mutation operation, value where applicable, and stable expected error. Rust and TypeScript execute the exact materialized shared-verifier bases and gate-specific cases across every shared category; the complete row list is also the required manifest for the dependent consumer suites. This avoids pretending Common can execute UI, database, outbox, or transport behavior it does not own. Idempotent behaviors such as an identical duplicate chunk live under positive_cases; they are never mislabeled as rejections. Directory/audience construction rows carry their test-only clear source identifiers and domains so salt-holding implementations can reproduce the expected opaque segments, while Relay drivers consume only the opaque signed outputs.

The protocol remains “target” in the status roadmap until all three language drivers run that byte corpus. A prose example or independently regenerated platform fixture is not a substitute.

Bounded manifest and chunks

The logical message may exceed one retained-value limit. PETRA 1.0 fixes these hard cross-language bounds; a deployment may configure lower limits but never higher ones:

Bound PETRA 1.0 maximum
Deterministic logical snapshot bytes 16 MiB (16,777,216 bytes)
Chunk data 49,152 bytes
Chunks per snapshot 342
Entries per logical snapshot 65,536
One SnapshotEntryV1.canonical_value 1 MiB (1,048,576 bytes)

Web deterministically encodes the logical snapshot, rejects it if any entry or the whole message exceeds those bounds, splits the bytes into 49,152-byte chunks (the final chunk may be shorter), and publishes the following inner transport value:

message SnapshotTransportValueV1 {
  oneof value {
    SnapshotManifestV1 manifest = 1;
    SnapshotChunkV1 chunk = 2;
  }
}

message SnapshotManifestV1 {
  uint32 version = 1;
  SnapshotFamily family = 2;
  string scope_segment = 3;
  uint64 snapshot_seq = 4;
  ConfidentialityLabel classification = 5;
  bytes audience_digest = 6;
  uint32 key_epoch = 7;
  uint64 logical_length = 8;
  uint32 chunk_size = 9;
  repeated bytes chunk_sha256 = 10; // index order, each exactly 32 bytes
  bytes logical_sha256 = 11;        // exact logical protobuf bytes
  bytes state_digest = 12;
  uint64 snapshot_created_at_ms = 13;
  uint64 source_cut = 14;
}

message SnapshotChunkV1 {
  uint32 version = 1;
  SnapshotFamily family = 2;
  string scope_segment = 3;
  uint64 snapshot_seq = 4;
  bytes logical_sha256 = 5;
  bytes state_digest = 6;
  uint32 index = 7;
  bytes data = 8;
}

It emits:

  • a signed/sealed manifest containing the header, logical byte length, chunk count, ordered chunk SHA-256 values, and state_digest; and
  • signed/sealed chunks containing snapshot sequence, logical_sha256, state_digest, zero-based index, and exact byte slice.

logical_sha256 covers the exact encoded AuthoritativeStateSnapshotV1, including family, scope, source cut, creation time, classification, audience, key epoch, provenance, entries, and state_digest. It is the canonical whole-snapshot identity used by manifests, chunks, head assertions, retries, fork detection, and persisted high-water.

Before allocating an assembly buffer, a consumer requires a fresh verified head and a manifest that exactly matches that head. It then validates 1 <= logical_length <= 16,777,216, chunk_size == 49,152, and chunk_sha256.len == ceil(logical_length / 49,152) <= 342; every listed digest is exactly 32 bytes. It may then preallocate exactly logical_length, never a size derived from a chunk index, encoded protobuf length, compressed length, or unverified manifest. Every non-final chunk is exactly 49,152 bytes and the final chunk is exactly the remaining length. The sum of accepted unique chunk bytes must equal logical_length without integer overflow. After reassembly, deterministic decoding rejects more than 65,536 entries or any canonical_value longer than 1,048,576 bytes even though the outer 16 MiB bound also holds.

The snapshot schema is uncompressed in version 1: manifest lengths and hashes describe the post-decryption logical protobuf bytes. A transport stack that performs compression outside this schema must enforce the same 16 MiB limit while streaming decompression and abort as soon as output would exceed it. It must not preallocate from an unauthenticated compressed or advertised length, and decompression never relaxes the per-entry or entry count limits.

Every manifest and chunk uses purpose 3, the same classification/audience/key epoch, and its exact cross-bound key. Every outer AuthEnvelope.issued_at_ms equals the manifest's snapshot_created_at_ms; after opening and reassembly the consumer also requires the logical snapshot timestamp to match. This lets opaque Relay + Storage enforce grant validity using signed-cleartext time without opening content. A consumer applies nothing until it has one valid manifest and all chunks. Missing, conflicting, oversized, or out-of-range chunks fail the assembly. It verifies each chunk digest, the reassembled byte length, deterministic decode, header equality, entry ordering, and whole-state digest before opening a database transaction.

An assembly exists only under one still-fresh verified head identity (family, scope, audience, classification, snapshot_seq, logical_sha256, source_cut). There is at most one in-flight assembly per (family, scope, audience, classification). An identical head and manifest reuse it; a verified newer head cancels and zeroes the older partial assembly before starting its replacement; an older or conflicting equal head never creates parallel state. Byte-identical duplicate chunks from multiple storages are idempotent and consume neither additional byte budget nor a new slot. Different bytes or commitments for an already accepted index fail and discard the whole assembly. Unrequested chunks and chunks received before the matching verified head and manifest are dropped without allocation.

The assembly expires no later than its head assertion. Receiving duplicate or later chunks does not extend that deadline. If the head, machine identity or revocation state, embedded grant, publication capability, or current encryption epoch ceases to be valid before atomic commit, the client discards the partial state, obtains a new head, and starts again. Implementations may impose a shorter local timeout or a lower global memory/concurrency budget; exhaustion fails closed and never evicts a verified complete projection in favour of partial data.

Deterministic Web cut

Web adds one transactional, gap-free durable-ingest counter covering every class-2 source row and tombstone. Each class-2 ingest transaction locks that counter row, increments it, stamps the mutation and audit row with the new value, and commits them together; rollback also rolls back the counter. A database sequence that can leave allocation gaps is not sufficient. To create a snapshot Web:

  1. starts one repeatable-read database transaction, locks the shared class-2 ingest counter (blocking new class-2 commits), then locks the snapshot counter for (family, scope, audience_digest);
  2. increments snapshot_seq and captures the committed ingest counter as source_cut;
  3. reads the complete latest projection and retained tombstones whose ingest sequence is at or below that cut;
  4. sorts, encodes, digests, seals with the current group-key epoch, and persists the exact manifest/chunk bytes plus digest before commit; and
  5. publishes those persisted bytes. A retry republishes byte-identical values for the same sequence; it never rebuilds the cut from newer rows.

The shared counter lock remains held through the snapshot transaction commit. Therefore every committed class-2 mutation at or below source_cut is visible and included, while every later mutation commits strictly above it; concurrent allocation cannot create a hole inside a snapshot marked complete.

This is a complete replacement snapshot, not a delta. Absence removes a local value only inside the declared family, scope, audience, and classification partition. Tombstones remain explicit so a later stale value cannot recreate a deleted object. Web history/audit tables are not replaced or projected back into the mesh.

Web now has the transactional gap-free class-2 ingest high-water and retains original author/event time separately from Web observation time. Each family remains ineligible for authoritative publication until all of that family's producers and recorder paths write this ledger atomically; drawing, target, and registered plan mutations currently do so, while the remaining class-2 families stay explicit runout work.

Web creates a new cut after a coalesced class-2 change, immediately after group-key epoch rotation, and on a refresh interval no longer than half the configured offline horizon. Even unchanged state is republished at a new sequence/current epoch so at least one head and complete manifest/chunk set remains inside horizon-based Storage retention. If Web is unavailable for a full horizon, recovery remains blocked until Web creates and asserts a new current cut; caches do not extend authority by refreshing old bytes themselves.

Partition discovery

A cold or wiped client cannot construct an exact head request from a snapshot query selector alone. The selector carries (family, scope_segment, audience_segment), while the replacement partition also requires the signed audience_digest and one exact canonical classification label. The audience segment is deliberately non-invertible without the deployment salt, and a clearance is a ceiling rather than an inventory of labels that actually exist. Storage presence is neither authority nor evidence of a complete set.

After installing a fresh complete Directory capability set, the client asks Common to enumerate its verified purpose-3 manifest QUERY selectors. For each selector it obtains one complete partition catalog directly from Web before asking for any exact heads. Directory remains the sole issuer of query authority; Web reports only which current class-2 partitions exist within that authority.

message SnapshotPartitionCatalogRequestV1 {
  uint32 version = 1;                    // exactly 1
  SnapshotFamily family = 2;
  string scope_segment = 3;              // 32 lowercase hex
  string audience_segment = 4;           // 32 lowercase hex
  bytes request_nonce = 5;               // fresh, exactly 12 bytes
  bytes query_authorization = 6;         // canonical StorageAuthorizationV1
}

enum SnapshotPartitionAvailabilityV1 {
  SNAPSHOT_PARTITION_AVAILABILITY_UNSPECIFIED = 0;
  SNAPSHOT_PARTITION_AVAILABILITY_READY = 1;
  SNAPSHOT_PARTITION_AVAILABILITY_PUBLICATION_PENDING = 2;
  SNAPSHOT_PARTITION_AVAILABILITY_KEY_EPOCH_PENDING = 3;
  SNAPSHOT_PARTITION_AVAILABILITY_HORIZON_EXPIRED = 4;
}

message SnapshotRecoveryPartitionV1 {
  ConfidentialityLabel classification = 1;
  SnapshotPartitionAvailabilityV1 availability = 2;
}

message SnapshotPartitionCatalogAssertionV1 {
  uint32 version = 1;
  SnapshotFamily family = 2;
  string scope_segment = 3;
  string audience_segment = 4;
  bytes audience_digest = 5;             // exactly 32 bytes
  repeated SnapshotRecoveryPartitionV1 partitions = 6;
  uint64 catalog_generation = 7;
  uint64 catalog_source_cut = 8;
  uint64 issued_at_ms = 9;
  uint64 expires_at_ms = 10;
  bytes request_nonce = 11;               // exact caller nonce, 12 bytes
  bytes query_capability_sha256 = 12;     // exact Directory capability, 32 bytes
  bytes snapshot_authority_grant = 13;
  bytes web_identity_token = 14;
  bytes web_signature = 15;
}

The client sends SnapshotPartitionCatalogRequestV1 to POST /api/snapshots/catalog with the same HTTPS, bearer-token, protobuf, no-compression, 64 KiB request, and Cache-Control: no-store rules as the head endpoint. The query authorization contains a current Directory-issued QUERY, REUSABLE_CONTENT, purpose-3 manifest capability. Its proof nonce exactly equals request_nonce, has an empty value digest, and names this exact selector:

waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/*/manifest

Web verifies the bearer and proof exactly as for a head request. It additionally requires the explicit family, scope, and audience segment to equal the parsed selector. It resolves that tuple through exactly one current Directory-signed Web SnapshotAuthorityGrantV1; ambiguity or absence fails the whole request. This obtains the Directory-approved audience_digest without exposing an audience descriptor or treating Web as an authority issuer.

In one repeatable-read transaction, Web reads the complete union of published partition rows and dirty/unpublished partition rows for the exact family, scope, and audience. It returns only classifications for which Common CMBAC permits both the bearer clearance and the query-capability ceiling. A current exact partition is never omitted merely because its newest cut is not yet usable: Web marks it PUBLICATION_PENDING, KEY_EPOCH_PENDING, or HORIZON_EXPIRED. Missing current revocation data, publisher identity/grant state, a complete inventory read, or a canonical classification fails the whole request rather than producing an empty or partial catalog.

catalog_source_cut is the class-2 durable-ingest counter observed by that transaction. catalog_generation is a transactional, monotonically increasing Web counter that advances whenever partition membership, dirty/readiness state, fully published head, current key epoch, or publisher-grant mapping changes. The signed partition list remains the source of truth; generation is a race detector, not authority and not a replacement for an exact head.

partitions is sorted by the unsigned bytes of canonical_label(classification) and contains no duplicate label. It is bounded to 4,096 entries and the complete canonical response is bounded to 4 MiB. PETRA 1.0 has no catalog pagination: either Web returns the complete set or recovery is visibly blocked. A successful signed zero-entry catalog means that no authorized partition exists at that catalog cut. It does not delete a warm client's existing projection; session replacement or wipe handles removed authority.

The assertion is deterministic protobuf. It has the same Web identity/grant binding, revocation checks, issue interval, and maximum five-minute lifetime as a head assertion. query_capability_sha256 is SHA-256 of the exact Directory-signed capability embedded in the request authorization. The client verifies that digest, exact request coordinates and nonce, sorted unique bounded partitions, every classification through Common, and the following Web signature:

ascii("petra-snapshot-partition-catalog-v1\0")
|| u32be(version) || u32be(family)
|| str32(scope_segment) || str32(audience_segment)
|| audience_digest[32]
|| u32be(partition_count)
|| concat(bytes32(canonical_label(classification)) || u32be(availability))
|| u64be(catalog_generation) || u64be(catalog_source_cut)
|| u64be(issued_at_ms) || u64be(expires_at_ms)
|| request_nonce[12] || query_capability_sha256[32]
|| bytes32(snapshot_authority_grant) || bytes32(web_identity_token)

For each READY partition the client uses a new nonce and fresh proof for the unchanged exact head flow below. A non-ready partition leaves recovery visibly blocked. A head lookup that races the catalog causes rediscovery rather than fallback to an older head. After applying the batch, the client obtains the catalogs again and declares recovery complete only when every authorized manifest selector still exists, every catalog has the same generation and exact partition list as its first pass, and every listed partition is ready and applied or idempotent. Capability-set generation, expiry, revocation, Web grant, or key-epoch change restarts the run. Missing a matching purpose-3 chunk-query selector also blocks recovery; manifest authority is never widened into chunk authority.

Catalog assertions cannot enter an application projection, authorize a head or Storage read, substitute for an AuthEnvelope, or clear incomplete transient-history status. Exact classification and audience metadata is active-session data and is erased on Android session wipe. Relay + Storage never sees the catalog and remains classification-blind.

Head assertion and rollback

A client persists the highest accepted (snapshot_seq, logical_sha256, source_cut) per family/scope/audience/classification replacement partition. A lower sequence is rollback. Sequence gaps caused by snapshots for another classification are valid. Any incoming source_cut below the persisted cut is stale-cut rollback even when snapshot_seq is higher. An equal cut is valid only for an unchanged-state refresh: state_digest, ordered_source_digest, entry count, value count, and tombstone count must equal the persisted snapshot. Any difference at an equal cut is Web equivocation because every class-2 state mutation must advance the gap-free counter. An equal sequence with the same whole-snapshot digest is idempotent; an equal sequence with a different digest is Web/storage equivocation and fails closed. A cold client has no persisted cut, so its fresh Web head supplies the required sequence and cut; the snapshot must match both exactly.

A cold or wiped client has no trustworthy local high-water, so a compromised cache could withhold the newest valid snapshot. For every READY catalog entry it must obtain a fresh, purpose-separated SnapshotHeadAssertionV1 directly from Web's authenticated HTTPS endpoint, using a fresh 12-byte request nonce and a fresh proof for its current Directory-issued snapshot QUERY capability. The assertion contains the exact family/scope/audience/classification, head sequence, whole-snapshot digest, source cut, issued time, and short expiry. A Zenoh storage/query path is not a head-assertion source in 1.0. Until HTTPS Web is available, the client remains visibly recovery-blocked and does not treat an arbitrary cached snapshot as current.

The request and assertion are deterministic protobuf returned outside retained Storage:

message SnapshotHeadRequestV1 {
  uint32 version = 1;                    // exactly 1
  SnapshotFamily family = 2;
  string scope_segment = 3;              // 32 lowercase hex
  bytes audience_digest = 4;             // exactly 32 bytes
  ConfidentialityLabel classification = 5;
  bytes request_nonce = 6;               // fresh, exactly 12 bytes
  bytes query_authorization = 7;         // canonical StorageAuthorizationV1
}

message SnapshotHeadAssertionV1 {
  uint32 version = 1;
  SnapshotFamily family = 2;
  string scope_segment = 3;
  bytes audience_digest = 4;
  ConfidentialityLabel classification = 5;
  uint64 snapshot_seq = 6;
  bytes logical_sha256 = 7;
  uint64 source_cut = 8;
  uint64 issued_at_ms = 9;
  uint64 expires_at_ms = 10;
  bytes request_nonce = 11;              // exact caller nonce, 12 bytes
  bytes snapshot_authority_grant = 12;
  bytes web_signature = 13;
  bytes web_identity_token = 14;          // exact Directory-signed machine IdentityToken
}

The client sends the request with POST /api/snapshots/head over HTTPS, an Authorization: Bearer <base64 canonical IdentityToken bytes> header, and Content-Type: application/x-protobuf. Compression is not accepted. The request is at most 64 KiB, and success is application/x-protobuf with Cache-Control: no-store. Malformed or non-canonical requests fail before any partition lookup.

query_authorization contains the caller's current Directory-signed QUERY, REUSABLE_CONTENT, purpose-3 capability and a fresh StorageUseProofV1. The proof's nonce exactly equals request_nonce, its value digest is empty, and its selector is the exact manifest capability expression:

waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/*/manifest

Web verifies the bearer token's Directory signature, canonical bytes, exactly one recognized kind:operator or kind:device role, current expiry, principal revocation, and signing-key revocation. Identity kind classifies the principal; it does not grant snapshot authority. Web then verifies the fresh query proof through Common, requires the capability principal and signing key to equal the bearer, requires purpose 3 and the exact family/scope selector, derives the audience segment from the requested digest under its current signed snapshot grant, and requires CMBAC Permit against both the bearer clearance and capability ceiling. A capability without its possession proof is not a bearer credential. The head request proof is consumed only by Web and is never reused for Storage; every later manifest or chunk query carries another fresh proof. This makes Directory remain the sole issuer of opaque-audience query authority without making Web a second authority source.

Web selects only the latest fully published exact replacement partition. It returns no older cut while a newer cut is incomplete, no old-key cut after rotation, and no cut outside the configured retention horizon. Missing current revocation state, current Web identity/grant, current key epoch, or signing authority fails recovery closed.

web_identity_token is the exact deterministic protobuf bytes for the Web machine's Directory-signed IdentityToken. web_signature uses the token's principal_sign_key; the embedded grant must name that same principal and key. The grant does not replace the identity token on this non-envelope path. A ConfidentialityLabel is canonicalized as:

str32(policy_id) || str32(classification)
|| u32be(category_count)
|| concat(
     u32be(category_type) || str32(tag)
     || u32be(value_count) || concat(str32(value))
   )
|| u64be(creation_dtg_ms) || str32(originator_id)

Categories and values use the same canonical sorting, uniqueness, and rejection rules as the grant clearance. The exact head input is:

ascii("petra-snapshot-head-v1\0")
|| u32be(version) || u32be(family) || str32(scope_segment)
|| audience_digest[32] || bytes32(canonical_label(classification))
|| u64be(snapshot_seq) || logical_sha256[32] || u64be(source_cut)
|| u64be(issued_at_ms) || u64be(expires_at_ms)
|| request_nonce[12] || bytes32(snapshot_authority_grant)
|| bytes32(web_identity_token)

The client verifies exact request-nonce equality; the identity token's canonical bytes, Directory signature, kind:device machine kind, and current expiry; the Web signature; exact token/grant principal and signing-key equality; current principal and signing-key revocation; grant.issued_at_ms <= head.issued_at_ms < grant.expires_at_ms; exact requested family/scope/audience/classification; issued_at_ms <= now < expires_at_ms; and expires_at_ms - issued_at_ms <= 300_000 (five minutes). This object is snapshot-head authority only; it cannot enter an application projection or substitute for an AuthEnvelope.

head.issued_at_ms is the time Web answers this request, not the retained snapshot's creation time. head.expires_at_ms is no later than five minutes after that time and no later than the current Web identity or embedded grant expiry. The manifest retains its original snapshot_created_at_ms; those timestamps need not be equal. The head's exact snapshot_seq, logical_sha256, source_cut, family, scope, audience, and classification bind it to the retained manifest, while the manifest and logical snapshot continue to bind their own creation timestamp. Requiring head issue time to equal snapshot creation would make every retained cut unrecoverable five minutes after publication and is therefore invalid.

An existing client may reject values below its persisted high-water while disconnected, but it cannot prove that a cache is not withholding a newer head. It reports the age of its last Web head assertion. Withholding remains an availability/staleness attack, not a way to roll state backward or forge a snapshot.

Apply and precedence

For each family/scope/audience/classification replacement partition, recovery is one atomic state transition:

  1. Mark transient history incomplete when the offline horizon or available key backfill is exceeded. This status is not cleared by a snapshot.
  2. Obtain and stabilize the complete Web partition catalogs, then obtain a fresh matching Web head assertion for every ready entry and query all applicable storages for that exact sequence's manifest and chunks, with HLC consolidation disabled.
  3. Verify purpose, Web identity/grant, revocation, capability/key binding, classification, audience, current encryption epoch, chunks, canonical state, digest, sequence, source cut, and head assertion.
  4. Treat ordinary values returned by that recovery query as historical candidates only. Run their normal verification/merge for diagnostics, but do not apply them after the complete snapshot.
  5. In one database transaction, replace only the declared durable projection, install its values and tombstones, persist the snapshot high-water and per-entry source-version floors, then commit.
  6. Only newly received live samples that pass FreshOnly, current token/revocation, authority, classification, key binding, and the family's normal semantic merge may advance the projection. Stored/AllowStale replay from the completed recovery cannot.

Zenoh HLC and arrival order never confer authority. A lower/equal family source version is stale; equal version with different content is a conflict and fails closed. A tombstone floor outranks an older value. Purpose 3 itself does not grant authority to modify another family or scope.

Consumer behavior

Consumer Required behavior
Android Atomically replace only declared operational projections; retain tombstones and high-water; never emit notifications, commands, acknowledgements, outbox items, or endpoint events merely because a snapshot was applied. Surface recovery progress, head age, and incomplete transient history.
Web Build/persist deterministic cuts, self-verify before publish, and keep snapshots separate from endpoint ingest and audit history. Never record them as proof of endpoint activity.
Node It owns no class-2 application projection in 1.0. Recognize purpose 3, verify or explicitly no-op at the dispatch boundary, and never execute it as a command.
Gateway Apply no class-2 snapshot in 1.0. Recognize purpose 3 and never translate/export it as partner traffic or endpoint-authored provenance.
Relay + Storage Verify the outer snapshot/storage contract and retain/return opaque bytes. Never open chunks, build application projections, mint authority, select winners, or rewrite provenance.

Authoritative recovery classes

Every current family has exactly one tactical recovery class. Web can retain class-3 history for a class-1/2 family without republishing that history.

Class 1 means there is no post-horizon tactical reconstruction. It does not itself grant mesh retention: a closed retained-family row may provide bounded replay (for example, chat), while a LIVE_PUBLICATION family resumes live and reports the gap. A verified Web recipient may keep separate class-3 application history without turning that archive into Zenoh Storage or endpoint-authored replay.

Family / read model Class PETRA 1.0 recovery behavior
router liveliness/token and Directory revocation/group-key refresh 1 — transient/control refresh Refresh from Directory; cached signed state remains bounded by its own validity
position, heartbeat, presence, channel-member liveliness 1 — live only No mesh retention/query/replay; resume live and report the gap. Verified Web position history is class 3
chat messages 1 — bounded transient replay Bounded mesh replay under retained REUSABLE_CONTENT; full Web chat history is class 3
voice frames 1 — live only No mesh retention or tactical reconstruction; verified Web audio archive is class 3
Node live camera-advertisement metadata 1 — live only Resume live; PETRA 1.0 has no standalone sensor namespace, and sensor map objects use the drawing class-2 model
device commands and live CommandAck responses 1 — live only Never retain, reconstruct, or re-execute expired commands
PlanAcknowledgementV1 records 1 — bounded transient replay Relay + Storage retains the original durable envelope only within the offline horizon; verified Web exact-version history is class 3 and is not synthesized by a snapshot
append-only submitted SitRep record/report events 1 — bounded transient replay Do not republish historical reports as endpoint events; Web audit is class 3
unregistered generic state classes 1 — unregistered transient No retention or latest-state promise until a distinct registered model exists
drawings/annotations and tombstones 2 — durable latest state Snapshot current shapes and delete floors by cell/scope
COP targets and tombstones 2 — durable latest state Snapshot current target projection and delete floors
channel definitions, membership, transfer, and tombstones 2 — durable latest state Snapshot one effective channel projection with its authority/tombstone floors
effective invite/grant state 2 — durable latest state Snapshot current grants only; activity history remains Web-only
plans and KML/content-addressed attachment references 2 — durable latest state Snapshot latest seq including cancelled plans; resolve authorized blobs separately
SitRep/report requirements 2 — durable latest state Snapshot latest seq including cancelled requirements
derived current SitRep/compliance projection 2 — durable latest state Snapshot current status as a distinct Web-derived model, never as reconstructed ReportRecord authorship
ORBAT 2 — durable latest state Snapshot latest unit seq, memberships, appointments, and retired tombstones
Web replay/audit, full lifecycle histories, provenance, and blob contents 3 — Web-only audit/history Retain in Core; never reconstruct as endpoint-authored Zenoh traffic

Required hostile tests

Common and every consumer run the same vectors and add integration coverage for:

  • forged Web identity/grant, a grant presented without identity proof, token/grant principal or signing-key mismatch, revoked Web, endpoint-signature confusion, wrong purpose, family, scope, key, audience, classification, or encryption epoch;
  • malformed or unequal signed audience segments at Relay, and wrong opaque-domain derivation at Directory and salt-holding endpoints without pretending Relay can perform that derivation check;
  • stale/lower snapshot, equal-sequence different-digest fork, stale head assertion, and a cold client offered a storage-only snapshot;
  • missing, stale, unsigned, non-canonical, truncated, oversized, differently nonce-bound, or wrong-capability partition catalogs; duplicate/unsorted classifications; a selector omitted from the complete capability set; unmatched manifest/chunk selector pairs; catalog/head and first-pass/second-pass generation races; any non-ready partition being silently omitted or treated as complete; and any zero-entry error path represented as a successful empty catalog;
  • non-canonical ordering, duplicate/conflicting entries, bad state/source digest, every exact and one-over assembly bound, preallocation/decompression overflow, missing/conflicting/out-of-range chunks, idempotent byte-identical duplicate chunks, superseding heads, assembly expiry, concurrency-budget exhaustion, and partial assembly;
  • split or reused channel/invite authority ledger versions; drawing values/tombstones that omit, reuse, or disagree on their signed revision; and any attempt to derive those families' snapshot source_version from observation order, HLC, event time, or an unsigned field;
  • ORBAT membership updates that lose definition or appointment fields, appointment updates that lose definition or membership fields, retirement followed by a delayed stale full record, lower or equal-conflicting unit sequences, capability/record sequence mismatch, concurrent mutation lost update, partial multi-unit commit, and any difference between live, retained, and snapshot per-unit reconciliation;
  • tombstone-before-value, value-before-tombstone, snapshot followed by older retained values, newer live value after snapshot, and duplicate replies from multiple storages;
  • application into the wrong projection and any snapshot-triggered command, ack, notification, outbox, interop export, or audit impersonation; and
  • recovery when old group-key epochs are unavailable, followed by a visible incomplete transient-history state.

Non-goals

  • Reconstructing complete chat, voice, presence, Node camera-advertisement, command, live command-acknowledgement, plan-ack mesh history, or submitted-SitRep event history beyond the horizon. Web may retain separately verified exact-version plan acknowledgements as class-3 audit/replay history.
  • Letting Relay + Storage or an endpoint mint snapshot authority.
  • Re-signing historical endpoint envelopes or presenting Web attribution as endpoint authorship.
  • Per-channel/per-unit encryption keys in PETRA 1.0.
  • Making HLC, cache arrival order, or snapshot presence evidence that an endpoint was online.