Skip to content

PETRA 1.0 Zenoh and DDIL data plane

This page is the normative PETRA 1.0 contract for operating the tactical data plane through multi-day disconnection. It applies the Zenoh abstractions to PETRA: live delivery is publish/subscribe, and delayed delivery is a Storage (subscriber plus queryable) behind PETRA's validation boundary.

Coordinated release boundary

This is the only PETRA 1.0 data-plane contract. The component hard-cut changes must land at one Common revision and deploy together before traffic resumes. Earlier key shapes, auth queryables, unsigned publication paths, retained readers, and protocol fallbacks are invalid; they are not an interoperability profile.

Static roles and authority

Trust and data-plane roles are separate. A privileged Core process can still act as a Zenoh endpoint; a capture-assumed Node can be explicitly provisioned to co-host storage.

Role Members May do Must not do
Core authority Directory Issue identity, policy, publish/query capabilities, revocations, and snapshot authority Delegate minting authority to Relay + Storage
Core application Web Publish, subscribe, query, retain authoritative history/audit, and create snapshots under Directory authority Re-sign an endpoint event or present a snapshot as endpoint-authored traffic
Endpoint Android, ordinary Node, Gateway, and Web's data-plane clients Publish, subscribe, and query within Directory-issued capability Mint authority or assume a storage role dynamically
Relay + Storage Server and specifically provisioned Nodes Route traffic; validate, retain, and return opaque values Decode application content or infer/enforce membership, ownership, ORBAT, drawing, or plan semantics

Every fully capable tactical partition has at least one statically provisioned Relay + Storage role. Additional copies and endpoint locators are configured explicitly. PETRA 1.0 has no leader election, consensus, automatic role promotion, or dynamic discovery of storage authority. Automatic reconnect across that static locator list is permitted.

Android router locator contract

First-party Android clients receive one ordered, device-scoped MDM locator list for the runout or deployment. The list is the complete transport seed set; it is not an authority grant and it never bypasses ServerToken verification or the per-session TLS trust bundle. For the Infrastructure #125 two-relay fixture the order is fixed as:

  1. primary quic/host:port;
  2. primary tls/host:port;
  3. alternate quic/host:port;
  4. alternate tls/host:port.

The client preserves this order on cold start and reconnect. It must not require a user to edit application settings when the primary stops, and it must retain enough device-scoped locator configuration to try the alternate after a cold restart while Directory is unreachable. There is no singular legacy bootstrap fallback in the PETRA 1.0 runout contract.

A verified, unexpired Directory ServerToken may validate or annotate a configured locator, including its hostname, port, and coverage cells. A token received at runtime does not add a new route, reorder the configured list, or promote an unconfigured router. An unknown, expired, revoked, or otherwise unverified router remains unusable. This keeps transport discovery separate from Directory authority and makes primary-loss behavior deterministic and operator-understandable.

The runout fixture is deliberately non-HA: co-located Relay + Storage processes have independent identities and state, but there is no election, state replication, promotion, or physical fault isolation. If both configured paths are unavailable, the client reports degraded connectivity and uses only existing cached authority and bounded durable outbox/replay behavior. It does not claim lossless failover or extend authority beyond the signed offline horizon. See the Infrastructure #125 runout contract and docs #125 for the operator procedure and evidence matrix.

Directory remains the only authority even while it is unreachable. Cached, unexpired Directory-signed material lets a partition continue within its configured horizon; an isolated Relay + Storage verifies that authority but never extends or replaces it.

Publish, query, and channel-membership capabilities are exact Directory-issued grants, not endpoint assertions. An endpoint may cache them for no longer than the configured offline horizon. While Directory is unreachable, an endpoint may continue normal shared operation only for channels whose current capabilities were already cached and remain valid. Expiry fails closed.

Creating or activating a shared channel; inviting or joining; kicking, removing, or authoritatively leaving; transferring ownership; retiring or deleting; and every other membership or ownership change require Directory connectivity. An edge endpoint may create an offline local draft, but that draft is not a channel authority fact: it is visibly marked unactivated, remains local to that endpoint, cannot publish or query shared traffic, and cannot appear successfully activated. The API returns the stable code DIRECTORY_CONNECTIVITY_REQUIRED; it neither queues the authority change nor claims completion. The user may retry after reconnecting. A user may locally mute a channel, stop using it, or discard cached capability, but that local action does not claim a shared membership or lifecycle change.

On Directory/Core reconnection, an endpoint refreshes trusted Directory keys and the current revocation state, fetches the committed authority ledger and exact capabilities, validates them, atomically replaces its capability cache (removing every absent grant), re-gates pending content, and only then resumes shared operation or drains authorized content outboxes. A removed member, revoked subject, expired capability, or changed ownership therefore fails closed even if older application state remains cached. A Relay-only reconnect while Core remains unreachable uses cached unexpired authority; it does not perform this control-plane refresh and cannot mutate authority. PETRA does not support endpoint-issued or owner-delegated sub-capabilities. Any future exception requires a new explicit product and threat-model decision; it cannot arise as an implementation extension.

During Core disconnection, cached authority remains usable until its signed effective deadline unless the partition already knows that the subject or signing key is revoked. A membership or capability removal that occurred beyond the partition cannot take effect there before refreshed authority arrives. This bounded malicious-holder window is an explicit residual risk, not an assertion that disconnected revocation is instantaneous. On Core reconnect, endpoints refresh revocation and atomically replace the complete capability set before shared operation or outbox drainage; omission from that signed set removes the cached grant even when its earlier expiry has not arrived.

Delivery and validation

Live samples use Zenoh put and pub/sub. A reconnecting endpoint issues get against queryables. A Relay + Storage subscribes to authorized durable families, validates each sample, retains the opaque bytes, and answers authorized queries. Non-retained live samples pass through the native Relay without becoming Storage values.

The validation boundary fails closed in this order:

  1. Parse the request without interpreting application content. Reject an oversized value, malformed or non-canonical key expression, unknown or duplicate selector parameter, and any selector that widens the authorized expression.
  2. Verify the Directory signature and validity of the publish or query capability. Bind it to the subject principal and device signing key, action, canonical opaque expression, allowed envelope purpose, classification/audience ceiling, and validity interval.
  3. Verify a fresh StorageUseProofV1 by that subject over the capability digest, exact canonical publication key or query selector, value digest for a publication, nonce, and timestamp. A reusable capability is not a bearer token; stolen capability bytes alone are inert.
  4. Verify the outer envelope signature and purpose, freshness policy, key/envelope binding, classification, audience, and current revocation state before accepting a publication or returning a stored value.
  5. Persist or return the original opaque bytes. Never decode membership, ownership, ORBAT, drawings, plans, chat, or other sealed application semantics.

For a non-retained live sample, the publisher performs the same capability, proof, classification, and key-binding checks as a preflight, but that preflight is not the authorization boundary. Every receiving subscriber authoritatively verifies the envelope identity/signature, capability and typed containment, proof freshness/replay, exact key digest, purpose/classification, known revocation, and applicable channel or command policy before decoding the payload.

The native Zenoh Relay remains default-deny at its peer, action, and key-expression ACL. It cannot inspect an AuthEnvelope, capability, or use proof and therefore makes no claim to capability or payload verification for non-retained live traffic. PETRA 1.0 does not add a Relay plugin, payload interception, or intercept-and-republish path. A Relay ACL is defence in depth; it is not a substitute for authoritative subscriber verification. No Server-rewrapped voice path or application-plane ACL exception exists in the hard-cut runtime. Field clients use the one registered wildcard transport subject because stock Zenoh cannot dynamically update and revoke per-device ACL subjects. That coarse subject is limited to the exact closed action/key registry; the ACL remains deny by default for every unregistered action or key expression.

Production Server data-plane listeners accept only tls/ and quic/ locators. Browser transport reaches the remote-api bridge through HTTPS/WSS. The sole plaintext listener is the bounded health endpoint, which carries no application value, token, capability, credential, mutation, or administrative action.

Web uses one browser-facing remote-api bridge for live traffic and one Docker-network-only remote-api query bridge per configured Server. Each private bridge is pinned to one router, so the Web backend can send an authenticated retained query to every router-local store; federation does not pretend to replicate durable history. The query bridge supplies reachability only. Directory capability, fresh use proof, reply validation, and the Server-signed terminal receipt remain authoritative for every accepted page.

Stock-Zenoh live-subscription boundary

PETRA 1.0 uses stock Zenoh and adds no capability-aware subscriber-declaration hook, Relay plugin, subscription-lease wire, or Zenoh fork. Durable get remains different from live subscription: Relay + Storage verifies the Directory-signed QUERY capability, fresh exact-selector StorageUseProofV1, current revocation, and reply containment before serving retained bytes. A live subscriber declaration carries no equivalent PETRA proof that the native Relay can authoritatively evaluate.

First-party endpoints derive only registered subscriber selectors from their current verified complete capability set and Directory-owned relationships. Field endpoints declare separate exact opaque channel, cell, target, or command scopes; the Web machine may declare only its registered all-cell position selector. Every receiver authoritatively verifies every received LIVE envelope, capability, proof, typed containment, subject/key binding, classification, known revocation, and relationship before decode or use. Voice subscriptions are bounded to one exact opaque channel at a time; cell-scoped position and heartbeat subscriptions are bounded to assigned exact cells; command subscriptions are bounded to the endpoint's exact opaque target; and an acknowledgement reader uses one exact authorized opaque device source with only the command-identifier slot variable, then verifies the signed target and correlates each command identifier before accepting the acknowledgement. The separate durable plan-ack reader uses only the exact retained record/plan-ack/<opaque-plan-version>/* selector issued for one authorized plan version; it is not part of the live command-ack family. There is no waypoint/**, unregistered global position/voice/command/ack selector, waypoint/global/ack/**, or standalone sensor selector in first-party PETRA 1.0 code.

Those requirements constrain honest PETRA clients and prevent forged or mis-scoped LIVE values from changing application state. They are not a confidentiality boundary against a modified authenticated client. Field endpoints share one coarse wildcard transport subject, limited to the exact closed action/key registry. It controls reachability and topic families and is distinct from the Directory token that establishes PETRA application identity. A transport-reachable endpoint that holds the current deployment group-key epoch can widen its own stock-Zenoh live subscription and decrypt traffic it can observe, including after app-layer revocation but before key rotation. Receiver verification does not stop a key holder from removing its local checks; LIVE publication authentication prevents forgery, not observation.

Consequently, capability omission, membership removal, or app-layer revocation does not claim immediate remote teardown of an already declared live subscription. Future LIVE confidentiality against a captured or modified key holder is restored only when the revocation is known and the deployment group key is rotated so the holder cannot obtain the new epoch. Previously observed ciphertext and epochs already held remain exposed. This bounded audience-confidentiality limitation is an accepted PETRA 1.0 residual and is not extended to retained reads: widened or replayed durable queries still fail at Relay + Storage.

Capabilities contain authority; a selector never creates it. Wildcards are allowed only when the signed capability contains the requested expression. Reply keys must be within both the request selector and capability. Non-canonical aliases, _anyke, path-like traversal, wildcard injection, and selector parameters that alter routing scope are rejected. The shared positive and negative cases are in capability-containment-v1.json.

For channel-scoped families, a capability is bound to the exact opaque channel expression. A deployment-wide family wildcard is not a substitute: the deployment group key is shared, so over-broad ciphertext access would also expose content to every holder of that key. Directory issues or refreshes the exact channel grant only after evaluating current membership and ownership. Cached grants never outlive the shorter of their own expiry and the configured offline horizon.

Directory capability contract

Directory keeps the authoritative channel-ownership and membership ledger needed to mint exact grants. Web's Comms Matrix and connected edge activation workflows submit requested changes to Directory; they do not become effective merely because Web SQL or an endpoint projection changed. Directory authenticates the requester, applies current role, ownership, membership, revocation, and classification policy, commits the authority change, and only then issues capabilities. Web and endpoints project the committed result. The actor-signed wire, idempotent receipt, narrow Web core-application relationship, and non-impersonation rules are defined in Connected authority mutations and Web application authority. Directory derives and seals the committed result; Web may forward an actor request but cannot sign it, supply authority bytes, or publish a SQL-first approximation.

An authenticated capability refresh returns the current exact grants for that token's principal and principal signing key. It never accepts caller-declared membership, cells, or another requested scope as proof of entitlement. The refresh response supersedes the local capability set atomically: grants not present are removed before shared operation resumes. A capability's lifetime is at most the configured offline horizon.

Authenticated full-set refresh

The client first obtains Directory's existing 32-byte, single-use challenge. Challenges expire after 120 seconds and are shared infrastructure, not bearer authority. The capability-refresh proof has its own domain so a signature accepted by login, enrollment, session extension, or another challenge consumer cannot be replayed here. Common owns these additive messages in server/directory.proto:

message CapabilityRefreshRequestV1 {
  uint32 version = 1;             // exactly 1
  bytes identity_token = 2;       // exact canonical IdentityToken bytes
  bytes server_nonce = 3;         // exactly 32 bytes
  uint64 issued_at_ms = 4;
  bytes subject_signature = 5;    // Ed25519, exactly 64 bytes
}

message CapabilitySetEntryV1 {
  bytes directory_signed_capability = 1; // canonical StorageCapabilityV1
  bytes committed_canonical_value = 2;   // committed mode only; exact stored bytes
  waypoint.ConfidentialityLabel committed_classification = 3;
  string committed_owner_principal_id = 4;
  bytes snapshot_authority_grant = 5;    // purpose 3 publication only; canonical SnapshotAuthorityGrantV1
}

// `server/directory.proto` imports the closed enum from `server/authority.proto`.
// The two values below are the only non-UNSPECIFIED version-1 values.
message OperationContextV1 {
  string operation_id = 1; // current Directory-canonical operation identifier
  repeated AuthorityChannelKindV1 creatable_channel_kinds = 2;
  // strictly ascending numeric enum order, unique: CHAT (1), then VOICE (2)
}

message CapabilityRefreshResponseV1 {
  uint32 version = 1;                  // exactly 1
  string subject_principal_id = 2;
  bytes subject_signing_key = 3;       // exactly 32 bytes
  bytes identity_token_sha256 = 4;     // exact request token bytes
  uint64 generation = 5;               // positive and strictly monotonic per principal
  uint64 issued_at_ms = 6;
  uint64 expires_at_ms = 7;
  bytes request_nonce = 8;              // exact request challenge
  repeated CapabilitySetEntryV1 entries = 9;
  bytes entries_sha256 = 10;            // exactly 32 bytes
  bytes directory_key_id = 11;          // SHA-256(Directory public key), 32 bytes
  bytes directory_signature = 12;       // Ed25519, exactly 64 bytes
  repeated OperationContextV1 operation_contexts = 13;
  bytes operation_contexts_sha256 = 14; // exactly 32 bytes
}

OperationContextV1 is the complete Directory-authorized create-context projection, not a capability, relationship, or caller assertion. It tells the bound subject only which current operation contexts permit a connected chat and/or voice creation request. Its operation_id is the exact immutable value that the subject must place in ChannelCreateMutationV1.operation_id; it does not authorize a mutation by itself.

An operation identifier is non-empty, at most 256 UTF-8 bytes, has no Unicode control code point or leading/trailing Unicode whitespace, and is compared byte-for-byte with no case folding, Unicode normalization, trimming, alias, or display-name substitution. Directory emits only its current canonical operation identifier. Each context has one or more creatable_channel_kinds; values must be the closed, non-UNSPECIFIED AuthorityChannelKindV1 values CHAT (1) and VOICE (2), in strictly ascending numeric order with no duplicate. Contexts are in strictly ascending bytewise UTF-8 operation_id order and are unique. Empty/noncanonical/duplicate operation ids, unknown/unspecified/duplicate/unsorted kinds, an empty kind list, unknown protobuf fields, duplicate scalar fields, or non-deterministic encoding invalidate the whole response. There are at most 4,096 contexts; the maximum response size below includes both the capability entries and this projection. Directory must not truncate contexts or kinds.

For a valid context, canonical_context is the deterministic protobuf encoding of OperationContextV1. Its set digest is:

SHA-256(
  ascii("petra-capability-operation-contexts-v1\0")
  || u32be(context_count)
  || concat(bytes32(canonical_context))
)

The digest of the empty ordered context set is required when no create context is currently authorized. It is not an omitted optional field.

The request signing input is:

ascii("petra-capability-refresh-request-v1\0")
|| u32be(version)
|| bytes32(identity_token)
|| server_nonce[32]
|| u64be(issued_at_ms)

Directory requires a canonical, Directory-signed, unexpired IdentityToken; the request signature must verify under that token's exact signing key, and the token must still match the current Directory principal/device credential, role, clearance, and principal/signing-key revocation state. issued_at_ms is accepted only within the shared 60-second clock-skew window. Directory validates the canonical token and proof before it atomically consumes the matching unexpired challenge; concurrent reuse has exactly one winner. A missing, expired, already-consumed, cross-protocol, or non-32-byte challenge fails closed.

For a valid entry, canonical_entry is the deterministic protobuf encoding of CapabilitySetEntryV1. Entries are sorted by ascending bytewise directory_signed_capability. Two entries with the same (action, grant_mode, canonical_expression, canonical purposes, authority_ledger_version, committed_mutation_sha256) are a semantic duplicate and invalidate the complete response even if capability ids or signatures differ. The semantic fields and exact canonical key are read from the verified capability. The entry-set digest is:

SHA-256(
  ascii("petra-capability-set-members-v1\0")
  || u32be(entry_count)
  || concat(bytes32(canonical_entry))
)

snapshot_authority_grant is non-empty exactly when the entry's verified publication capability permits ENVELOPE_PURPOSE_AUTHORITATIVE_STATE_SNAPSHOT. It contains the canonical deterministic bytes of the matching SnapshotAuthorityGrantV1. Its Web principal/signing key, family/scope, audience segment/digest, classification ceiling, validity, and Directory key must match the capability and refresh subject. A purpose-3 capability without the grant, a grant on another purpose/action, an orphan grant, or a capability/grant mismatch invalidates the complete response. Each signed family/scope/audience grant has exactly two matching entries: the bounded sequence selector .../<audience>/*/manifest and chunk selector .../<audience>/*/chunk/*. Common permits those wildcards only in the sequence/index slots and contains only canonical concrete snapshot keys with the same exact family/scope/audience. The grant is repeated byte-identically in both matching entries; normal semantic-duplicate rejection still applies to the capability entries.

There is no separate snapshot-authority refresh or cache. The verified complete-set replacement installs each snapshot grant and its matching publication capability as one entry and removes both by omission. Snapshot work, shared traffic, and outbox drainage remain blocked until that atomic replacement succeeds. The 4,096-entry and 16 MiB limits include field 5.

The Directory response signing input is:

ascii("petra-capability-set-response-v1\0")
|| u32be(version)
|| str32(subject_principal_id) || subject_signing_key[32]
|| identity_token_sha256[32]
|| u64be(generation) || u64be(issued_at_ms) || u64be(expires_at_ms)
|| request_nonce[32] || entries_sha256[32] || operation_contexts_sha256[32]
|| directory_key_id[32]

The response and every contained capability use the same trusted Directory signing key. The response subject/key and token digest must equal the verified request, and every capability must bind that subject/key and lie within the response validity interval. The response requires issued_at_ms < expires_at_ms; its expiry is no later than the verified identity expiry, the configured offline horizon from response issuance, and any earlier Directory policy deadline. A consumer rejects at now_ms >= expires_at_ms. Directory allocates generation under a per-principal row lock and never reuses a value; overflow fails closed. Clients persist and compare generations in the namespace (subject_principal_id, subject_signing_key), reject a response whose generation is not strictly greater than the last accepted value in that namespace, and never use arrival time to order sets.

COMMITTED_AUTHORITY_MUTATION entries require every committed field. The capability expression must be one exact concrete canonical key, and the exact persisted once-sealed value, classification, owner, purpose, key digest, and ledger version must reproduce the signed commitment. Directory reads these bytes and metadata from the current authority head and never decrypts, re-seals, reconstructs, or re-encodes them during issuance. A reusable or live capability requires an empty value, absent/default classification, and empty owner. A malformed mode/metadata combination invalidates the whole response.

A response contains at most 4,096 entries, its deterministic encoded form is at most 16,777,216 bytes, and one committed canonical value is at most 1,048,576 bytes. Common checks encoded length before decoding and checks count, per-entry length, cumulative length, and integer arithmetic before allocation or hashing. Exactly-at-limit values are valid; overflow or one byte/entry over fails closed. Directory rejects, in the authority mutation transaction, any change that would make any affected subject's complete set unissuable. It never truncates a set.

Only a fully decoded, canonical, bounded, correctly signed, fresh, strictly newer response causes one atomic cache replacement of both capability entries and operation contexts. A verified signed empty set is a successful replacement and removes all cached authority and all create eligibility. Transport failure, timeout, oversize, decode/canonicalization failure, invalid signature or binding, stale generation, or one invalid entry/context is not an empty set: the prior cache stays byte-for-byte unchanged, the endpoint remains visibly degraded, and shared traffic and outbox drain remain blocked. Omission from a successful complete set removes the grant or operation context before either operation can resume; a client also clears any selected context that is absent or no longer permits the selected channel kind.

Operation-context selection and connected activation

The complete context projection has the same subject/key binding, echoed nonce, generation, absolute expiry, offline-horizon bound, Directory signature, revocation handling, rollback floor, persistence/sealing, session wipe, and reconnect barrier as the capability entries. It has no separate endpoint, signature, cache, refresh lifetime, or fallback reader.

  • Zero contexts means the subject currently has no chat or voice creation eligibility. The UI may retain or edit a local draft, but must show it as unactivated and cannot submit, publish shared state, alter membership, or enqueue authority work.
  • One context may be visibly preselected as a convenience. The selected operation_id remains a non-authorizing local preference, and the UI must show the selected operation and supported channel kinds.
  • Multiple contexts require an explicit operator selection from the verified ordered list before create is enabled. The client never guesses a default from the first list item, a nearby ORBAT unit, a prior selection, or a display label.

The client may use only the selected verified operation_id and a kind present in that context to construct the actor-signed ChannelCreateMutationV1. Directory revalidates the operation's existence, the actor's current relationship, effective capability, clearance, selected kind, and all ordinary create rules in the serializable commit transaction; a context can therefore become invalid between display and submit. A valid receipt is evidence of a commit, not permission to optimistically activate. After receipt the client obtains and verifies a new complete refresh, atomically installs its higher generation, and only then displays the channel as activated or begins its shared traffic.

On Directory loss, an expired/offline-horizon context, a failed or stale refresh, a revocation, or context omission/removal, a channel draft is visibly inert. It creates no authority outbox, Zenoh authority sample, channel definition, membership, or publication; there is no retry queue that later commits an unreviewed create. On reconnect the client refreshes revocation and the full set before enabling a draft submission, shared operation, or outbox drain. A removed context invalidates its saved selection rather than silently retaining, remapping, or substituting it.

No alternate operation-authority source

IdentityToken, ProfileBundle, ORBAT/content projections, local settings, user input, display labels, and a prior receipt are not operation-context authority. A client must not infer, mint, normalize, alias, persist as a fallback, or manually type an operation id. There is no separate eligible-operations endpoint, second cache, token claim, or compatibility reader. Only the verified current complete refresh supplies the selectable IDs; Directory remains the final authority at commit.

Finite Directory-owned entitlements

Directory derives the complete set only from current ledgers, roles, channel memberships and ownership, operation assignments, clearance, connected cell provisioning, and principal/signing-key revocation state. Its per-subject entitlement index is a transactional materialized index over those facts, is rebuildable, and is never an authority source.

Channel membership has one complete current semantic record and one retained, once-sealed membership projection at the exact channel-membership key. Common owns the projection shape:

message ChannelMembershipMemberV1 {
  string principal_id = 1;
  bool is_admin = 2;
}

message ChannelMembershipSetV1 {
  uint32 version = 1; // schema version, exactly 1
  string channel_id = 2;
  repeated ChannelMembershipMemberV1 members = 3; // principal UTF-8 order, unique
}

The ownership projection remains separate. The current owner receives owner authority from that projection; is_admin is not an owner flag. Authority ordering exists only in the enclosing StorageCapabilityV1.authority_ledger_version; the membership payload has no second semantic revision. Every membership change publishes the complete sorted set at the next channel aggregate ledger version and atomically updates the current semantic state and every affected subject-index row. Removal is a higher-version full projection plus index removal in the same transaction; a delta, caller-maintained index, or lower/equal projection cannot grant or preserve access.

Connected provisioning may assign one subject at most 256 exact canonical geohash-5 cells. ServerToken routing coverage is a distinct deployment configuration and does not confer endpoint drawing or target authority. Cell provisioning is a connected, transactional Directory-internal operation in 1.0; there is no temporary caller-declared HTTP provisioning surface.

The entitlement matrix always composes action authority with exact scope. When token roles supply the action, Directory resolves them only through Common's effectiveCapabilities; it never checks raw role-name strings. Current owner and command appointments are separate Directory-owned action authorities where the matrix names them, not token roles. A current Directory membership, active channel ledger, ORBAT visibility, cell, invite, appointment, or target assignment supplies the exact relationship scope. Both the named action authority and relationship must pass. Unknown roles and missing relationships grant nothing. Every emitted capability remains bound to the exact subject and machine signing key, bounded by current clearance and the offline horizon, and disappears by omission from the next verified complete set when either gate stops passing.

Channel matrix
Current Directory relationship Required action authority Exact grant
channel member view_operation query that channel's retained chat and current definition, membership, ownership-transfer, and lifecycle projections
channel member send_chat publish retained chat for that channel
channel member transmit_voice publish the closed non-retained LIVE voice expression for that channel and the subject's fixed opaque source
exact pending invitee view_operation query only the exact invite and the minimum channel definition needed to display it
current owner current owner authority query that channel's control projections required for connected management; no content grant follows without membership
current active channel ledger manage_channels query that channel's control projections required for connected management; no content grant follows without membership
current active channel ledger manage_members plus (view_operation or manage_channels) deployment-wide retained-chat and control-projection query, or connected management override; content publication still requires membership and its content action

Invite scope never includes chat, voice, membership, another invite, or another channel. Channel creation, membership change, transfer, and retirement remain connected Directory mutations. A reusable capability never authorizes any of them; their current retained heads are exact COMMITTED_AUTHORITY_MUTATION entries.

Cell and COP matrix
Current Directory relationship Required action authority Exact grant
assigned exact geohash-5 cell view_operation query draw/**, target/**, and their tombstones in that cell
assigned exact geohash-5 cell draw_annotations or manage_annotations publish drawings and drawing tombstones in that cell
assigned exact geohash-5 cell manage_targets publish targets and target tombstones in that cell

nominate_target is a Web targeting-board action and grants no COP publication. The subscriber still enforces drawing ownership after capability verification: draw_annotations may mutate only the verified sender's drawings, while manage_annotations is the cross-owner override. There is no retained cross-cell or moving-cell wildcard. Common's closed retained-family registry includes exact-cell target and target-tombstone forms alongside drawing forms; no standalone sensor family is added.

ORBAT and record matrix

Directory constructs Common's visible_unit_ids closure from current complete ORBAT records and the subject's assigned units: each assignment contributes itself, its direct parent, and every descendant. It does not interpret membership as deployment-wide record access. The deployment administrator keeps the existing oversight scope across active units.

Current Directory relationship Required action authority Exact grant
unit in visible_unit_ids view_operation query the exact per-unit ORBAT, plan, report, and report-requirement families
current command authority over the exact scope current command appointment authority publish plans and report requirements for that exact unit scope
unit in visible_unit_ids inside an accessible operation publish_plan publish plans and report requirements for that exact unit scope
current appointment on the reporting unit itself current command appointment authority publish reports for that exact unit, including the documented provisional-team lead case
subject entitled to one exact published plan version exact plan-version receipt entitlement publish one PlanAcknowledgementV1 under the subject-bound exact key
authorized commander/author reader for one exact plan version exact plan-version acknowledgement readership query only that version's typed record/plan-ack/<opaque-plan-version>/* prefix
current active unit manage_members plus view_operation deployment-wide oversight query for that unit's ORBAT, plan, report, and report-requirement families; publication still requires its ordinary action authority and scope

An appointment is the explicit Directory-owned action authority for these commander publications; it does not make an ordinary observer/member write-capable outside that appointment. author_plan permits drafting only and never wire publication. Membership or publish_plan alone never permits report publication. Appointment removal removes the report grant by full-set omission. ORBAT changes remain exact #145 committed mutations; there is no reusable ORBAT publication authority.

Command and self-publication matrix

A subject with command_device_camera or command_device_movement receives an exact non-retained LIVE command target only when the active device principal is inside the subject's current ORBAT visibility/operation scope. A device absent from ORBAT requires a connected, explicit Directory-owned command-target assignment. No command capability is derived from the global active-device list. The transport capability may carry either command action, but the receiving Node must still authorize the decoded subtype: camera-only authority never permits movement.

An active device principal receives only its self-bound live command-acknowledgement source. Plan-ack authority is separately derived for one exact published version under the durable plan-ack contract. Every active, non-revoked, provisioned tactical endpoint may receive self-bound position and heartbeat LIVE grants. A browser/human bearer-only session receives none. A current FIDO-bound browser endpoint is a separate short-lived issuance class: it may receive an exact self-bound heartbeat, but receives no position grant, seven-day offline horizon, Web-core grant, committed publication grant, or snapshot authority. Its direct human chat, voice, drawing, command, and query authority remains subject/key-bound and requires the ordinary action-plus-relationship gates. The complete contract is FIDO-bound browser endpoint capabilities. Gateway retains one authenticated opaque position source; external track identity stays inside the verified payload. Cached grants remain usable during Directory disconnection only until their signed expiry; reconnect performs complete-set reconciliation and omission before shared operation resumes.

This contract is tracked by docs #140, Common #198, and Directory #147, which must land before Directory #144.

Common owns this deterministic protobuf:

enum StorageActionV1 {
  STORAGE_ACTION_V1_UNSPECIFIED = 0;
  STORAGE_ACTION_V1_PUBLISH = 1;
  STORAGE_ACTION_V1_QUERY = 2;
}

enum StorageGrantModeV1 {
  STORAGE_GRANT_MODE_V1_UNSPECIFIED = 0;
  STORAGE_GRANT_MODE_V1_REUSABLE_CONTENT = 1;
  STORAGE_GRANT_MODE_V1_COMMITTED_AUTHORITY_MUTATION = 2;
  STORAGE_GRANT_MODE_V1_LIVE_PUBLICATION = 3;
  STORAGE_GRANT_MODE_V1_REGISTERED_CONTENT = 4;
}

message StorageCapabilityV1 {
  uint32 version = 1;                    // exactly 1
  StorageActionV1 action = 2;
  string subject_principal_id = 3;
  bytes subject_signing_key = 4;         // Ed25519, exactly 32 bytes
  string canonical_expression = 5;       // exact opaque key or contained wildcard scope
  repeated EnvelopePurpose purposes = 6; // ascending unique, no unspecified value
  Clearance classification_ceiling = 7;
  uint64 issued_at_ms = 8;
  uint64 expires_at_ms = 9;
  bytes capability_id = 10;              // exactly 16 random bytes
  bytes directory_key_id = 11;           // SHA-256(Directory public key), 32 bytes
  bytes directory_signature = 12;        // Ed25519, exactly 64 bytes
  StorageGrantModeV1 grant_mode = 13;
  uint64 authority_ledger_version = 14;  // committed mutation only; otherwise zero
  bytes committed_mutation_sha256 = 15;  // committed mutation only, exactly 32 bytes
  bytes registered_content_sha256 = 16;  // registered content only, exactly 32 bytes
}

One capability authorizes one action and one canonical expression. Channel capabilities contain the exact opaque channel segment; a trailing /** may contain that channel's registered subkeys but never a sibling channel. purposes is a set, encoded in ascending numeric order. The exact expression is also the opaque audience boundary in version 1; there is no separate caller-controlled audience claim.

str32(s) = u32be(len(utf8(s))) || utf8(s) and bytes32(b) = u32be(len(b)) || b.

canonical_clearance(c) is defined here so the foundational storage capability does not depend on a later recovery implementation:

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

Categories are sorted by (category_type, UTF-8 tag) with no duplicate type/tag; values are unique and sorted by UTF-8 bytes. Missing/malformed clearance or a non-canonical set fails closed. Directory signs these bytes:

ascii("petra-storage-capability-v1\0")
|| u32be(version) || u8(action)
|| str32(subject_principal_id) || subject_signing_key[32]
|| str32(canonical_expression)
|| u32be(purpose_count) || concat(u32be(purpose))
|| bytes32(canonical_clearance(classification_ceiling))
|| u64be(issued_at_ms) || u64be(expires_at_ms)
|| capability_id[16] || directory_key_id[32]
|| u32be(grant_mode) || u64be(authority_ledger_version)
|| u8(len(committed_mutation_sha256)) || committed_mutation_sha256
|| if grant_mode == REGISTERED_CONTENT:
     u8(len(registered_content_sha256)) || registered_content_sha256

The final conditional append is the version-1 extension point for REGISTERED_CONTENT. Capabilities in the three earlier modes retain their exact historical signing bytes. Every earlier mode requires registered_content_sha256 to be empty; REGISTERED_CONTENT requires committed_mutation_sha256 to be empty. The two fields and their domains are never interchangeable.

directory_signed_capability is the deterministic protobuf encoding of the message. Common's encoder emits ascending fields, one encoding of every scalar, one packed purposes field, minimal varints, and no unknown fields. A verifier decodes, validates, deterministically re-encodes, and requires byte-for-byte equality with the received bytes; this rejects reordered fields, duplicate scalars, alternate default encodings, unknown fields, and non-canonical nested clearance bytes before hashing the capability.

All verifier modes require version/action validity, canonical ordering and expression grammar, non-empty subject id, exact key and signature lengths, lifetime no longer than the horizon, a trusted Directory key whose SHA-256 equals directory_key_id, a valid Directory signature, current subject/signing-key revocation state, and exact equality with the proof or envelope signer's verified principal and device key.

  • Active use (new publish/query attachment) requires issued_at_ms <= use_proof.issued_at_ms < expires_at_ms and a fresh proof timestamp at current time.
  • Retained authority (restart/serve/consumer verification) requires issued_at_ms <= envelope.issued_at_ms < expires_at_ms; it does not compare capability expiry with the later catch-up time, but current revocation remains mandatory.

Publication requires Common::decide(envelope.classification, capability.classification_ceiling) == Permit; missing or malformed classification or ceiling, a policy mismatch, or any other CMBAC denial fails closed. Unknown enum values are rejected.

There are exactly four grant modes:

  • REUSABLE_CONTENT authorizes a read-only QUERY over any registered retained family, or PUBLISH for ordinary non-authority content, within the exact expression, purpose set, classification ceiling, and validity interval. It requires authority_ledger_version == 0 and an empty commitment. For PUBLISH, it is never valid for channel definition, membership/invite/join/leave/kick, ownership transfer, retire/delete, ORBAT appointment/membership, or another authority/lifecycle key. A query grant never authorizes a mutation.
  • COMMITTED_AUTHORITY_MUTATION is publish-only and represents one mutation that Directory has already validated and committed to its authority ledger while connected. It requires a positive ledger version, exactly one allowed authority purpose, and a 32-byte commitment. Re-signing or replaying it can only reproduce that already committed mutation; it cannot create, expand, remove, or transfer different authority offline.
  • REGISTERED_CONTENT is publish-only and represents one exact immutable content value that Directory authorized through a connected, dual-signed registration. It permits exactly the registered plan purpose DURABLE_GRANT, requires zero authority-ledger version and a separate 32-byte registered_content_sha256, and is retained and replayable within the offline horizon. It is exact-key only and valid solely for the closed registered-content family, initially Web-authored plan versions. It never authorizes an authority mutation, LIVE traffic, QUERY, or arbitrary reusable content. Read authority for the same exact key remains a separate REUSABLE_CONTENT capability.
  • LIVE_PUBLICATION is publish-only, permits exactly the single LIVE purpose, and is valid only for a family in the closed non-retained registry below. It requires authority_ledger_version == 0 and an empty commitment. It never authorizes QUERY, Relay + Storage persistence, mesh catch-up, restart loading, historical mesh serve, transport-outbox replay, or another envelope purpose. Its normative cryptographic deadline is min(capability.expires_at_ms, identity.expires_at_ms, checked_add(capability.issued_at_ms, offline_horizon_ms)); overflow fails closed. A deployment may impose an earlier locally configured policy deadline, but that local limit cannot extend the signed grant and is not part of the canonical wire calculation or cross-language vectors. Directory folds any earlier signed policy deadline into capability.expires_at_ms. A fresh envelope and proof are required for every new publication.

The exact committed-authority commitment is:

SHA-256(
  ascii("petra-authority-mutation-v1\0")
  || storage_key_digest[32]
  || SHA-256(payload)[32]
  || bytes32(canonical_label(classification))
  || str32(owner_principal_id)
  || u32be(purpose)
  || u64be(authority_ledger_version)
)

canonical_label(l) is 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), with categories and values sorted/unique by the same rules as clearance. Directory returns this grant only after committing the exact semantic authority request. Relay + Storage and consumers recompute the commitment from the signed envelope metadata and payload bytes. A mode/key-family mismatch, zero ledger version, or commitment mismatch fails closed. Directory is the only component that advances the ledger version.

The registered-content commitment is deliberately different from the final-envelope digest:

SHA-256(
  ascii("petra-registered-content-v1\0")
  || str32(exact_canonical_publication_key)
  || bytes32(exact_sealed_payload_bytes)
  || bytes32(canonical_label(classification))
  || str32(owner_principal_id)
  || u32be(purpose)
  || storage_key_digest[32]
)

For the registered plan family, owner_principal_id is required empty and purpose is exactly DURABLE_GRANT. The payload input is the exact sealed payload bytes, not a plaintext PlanRecord and not a completed AuthEnvelope. Directory's registration binds this digest to the semantic plan tuple, exact classification, recipients, and readers before the capability exists. Relay + Storage can therefore recompute it without decrypting, while the capability and final envelope do not form a commitment cycle.

An authority-ledger version is allocated within exactly one concealed Directory aggregate. A channel aggregate covers definition, membership, invites, ownership, and lifecycle/tombstone mutations for the same semantic channel. Its logical projection streams are the channel definition, membership set, each invitee's invite, ownership, and lifecycle/tombstone. Directory assigns a distinct next aggregate version to every committed channel mutation, including multiple records produced by one user action.

ORBAT is deliberately different because Common defines one full OrbatRecord and one retained key per unit. A Directory-issued unit_id is canonical and deployment-global: it is never reused by another operation, and Directory state binds it to exactly one operation. The one key waypoint/global/record/orbat/<opaque-record-orbat-unit> carries the atomic full unit record: definition/name, parent, membership, commander appointment, origin/provisional state, classification, ownership, and lifecycle. These subfields are not independent wire projection streams. Every accepted change republishes the complete record at seq + 1; StorageCapabilityV1.authority_ledger_version equals that exact OrbatRecord.seq. Retirement republishes the complete record with retired=true at a higher sequence. A multi-unit Directory transaction commits all affected full records atomically, with each unit advancing its own sequence exactly once.

The acceptance high-water mark is per channel logical projection stream and per atomic ORBAT unit record. Within the same stream or unit, a lower version is rollback; equal version plus the same commitment is an idempotent duplicate; equal version plus a different commitment is Directory equivocation and fails closed. A higher channel version observed in another channel stream does not reject a delayed non-superseded record. Cross-stream semantic precedence still uses the channel aggregate version: a valid channel tombstone suppresses earlier definition, membership, invite, and ownership records, while an unrelated later membership version does not suppress an earlier invite. For ORBAT, there is no cross-subfield merge: the higher complete unit record replaces the lower one, so membership cannot erase definition or appointment and appointment cannot erase definition or membership. Relay + Storage can compare repeated values on one exact key but does not decode or correlate the cross-key channel aggregate or semantic ORBAT operation.

A compromised disposable Storage may still withhold a newer value from a cold consumer, which remains an availability/rollback attack handled by replicas, persisted endpoint state, Directory refresh, and snapshot recovery rather than by trusting storage arrival order.

Closed grant-mode registry

The generic retained store accepts only these PETRA families in version 1. Every <opaque-...> component is exactly 32 lowercase hexadecimal characters derived under the named registered domain; <cell> retains the canonical five-character geohash grammar. Snapshot <family>, <snapshot-seq>, and <index> use the exact grammar in #126.

Exact canonical opaque publication family Envelope purpose PUBLISH grant mode QUERY grant mode
waypoint/global/chat/<opaque-chat-channel>/<opaque-chat-message> LIVE REUSABLE_CONTENT REUSABLE_CONTENT
waypoint/global/channel/<opaque-channel-id> DURABLE_GRANT COMMITTED_AUTHORITY_MUTATION REUSABLE_CONTENT
waypoint/global/channel/del/<opaque-channel-id> DURABLE_GRANT COMMITTED_AUTHORITY_MUTATION REUSABLE_CONTENT
waypoint/global/channel/members/<opaque-channel-id> DURABLE_GRANT COMMITTED_AUTHORITY_MUTATION REUSABLE_CONTENT
waypoint/global/channel/transfer/<opaque-channel-id> DURABLE_GRANT COMMITTED_AUTHORITY_MUTATION REUSABLE_CONTENT
waypoint/global/invite/<opaque-invite-invitee>/<opaque-invite-channel> DURABLE_GRANT COMMITTED_AUTHORITY_MUTATION REUSABLE_CONTENT
waypoint/<cell>/draw/<opaque-drawing-id> DURABLE_CONTENT REUSABLE_CONTENT REUSABLE_CONTENT
waypoint/<cell>/draw/del/<opaque-drawing-id> DURABLE_CONTENT REUSABLE_CONTENT REUSABLE_CONTENT
waypoint/<cell>/target/<opaque-target-id> DURABLE_CONTENT REUSABLE_CONTENT REUSABLE_CONTENT
waypoint/<cell>/target/del/<opaque-target-id> DURABLE_CONTENT REUSABLE_CONTENT REUSABLE_CONTENT
waypoint/global/record/plan/<opaque-record-plan-unit>/<opaque-record-plan-id> DURABLE_GRANT exact REGISTERED_CONTENT for a registered Web-authored version; existing non-Web paths remain REUSABLE_CONTENT exact REUSABLE_CONTENT
waypoint/global/record/plan-ack/<opaque-plan-ack-version>/<opaque-plan-acknowledger> DURABLE_CONTENT exact REUSABLE_CONTENT typed exact-version-prefix REUSABLE_CONTENT
waypoint/global/record/report/<opaque-record-report-unit>/<opaque-record-report-id> DURABLE_CONTENT REUSABLE_CONTENT REUSABLE_CONTENT
waypoint/global/record/requirement/<opaque-record-requirement-unit>/<opaque-record-requirement-id> DURABLE_GRANT REUSABLE_CONTENT REUSABLE_CONTENT
waypoint/global/record/orbat/<opaque-record-orbat-unit> DURABLE_GRANT COMMITTED_AUTHORITY_MUTATION REUSABLE_CONTENT
waypoint/global/snapshot/<family>/<opaque-snapshot-scope>/<opaque-snapshot-audience>/<snapshot-seq>/manifest AUTHORITATIVE_STATE_SNAPSHOT REUSABLE_CONTENT plus the #126 field-9 grant REUSABLE_CONTENT
waypoint/global/snapshot/<family>/<opaque-snapshot-scope>/<opaque-snapshot-audience>/<snapshot-seq>/chunk/<index> AUTHORITATIVE_STATE_SNAPSHOT REUSABLE_CONTENT plus the #126 field-9 grant REUSABLE_CONTENT

Snapshot PUBLISH and QUERY capabilities use the two bounded selector forms defined by

126 rather than preallocating a concrete sequence. Action remains exact and only the

PUBLISH entries carry the field-5 snapshot grant.

While at least one operation is current, the Directory-managed automatic Web relationship may also carry these closed all-cell ordinary selectors:

Automatic Web selector Actions Purpose
waypoint/*/draw/** PUBLISH, QUERY DURABLE_CONTENT
waypoint/*/target/** PUBLISH only DURABLE_CONTENT
waypoint/*/pos/** QUERY only LIVE

In each form, * contains exactly one canonical five-character geohash cell and ** contains only the registered descendants of that family. Drawing authority includes its typed add and tombstone keys; target authority includes its typed value and tombstone keys. Target query is deliberately absent. Position authority is read-only and does not retain position samples. None of these selectors contains another key family, a global subtree, an extra action, or another purpose. Manual relationships and field endpoints remain cell-exact, and Directory removes all three through complete-set replacement after the final current operation retires.

The Directory-managed automatic Web relationship has a separate closed aggregate retained-query profile. While at least one operation is current, Directory may issue Web QUERY + REUSABLE_CONTENT for exactly these selectors and purposes:

Automatic Web selector Purpose
waypoint/global/channel/** DURABLE_GRANT
waypoint/global/record/plan/** DURABLE_GRANT
waypoint/global/record/report/** DURABLE_CONTENT
waypoint/global/record/requirement/** DURABLE_GRANT
waypoint/global/record/orbat/** DURABLE_GRANT

For these five query capabilities only, Common classifies the audience as the reserved single sentinel ['*']: Relay + Storage scans every concrete retained key in that one family, globally sorts and deduplicates the merged results, then applies the ordinary signed pagination and reply bounds. The sentinel never matches chat, invites, plan acknowledgements, a generic record/** tree, or another family.

Automatic Web may also receive PUBLISH + REUSABLE_CONTENT for the sole global-record aggregate selector waypoint/global/record/requirement/** with DURABLE_GRANT, because Web is the privileged HQ author of operation-wide report tasking. The other four global aggregates remain query-only; they cannot publish channel or ORBAT mutations, plans, or reports. Manual relationships, field endpoints, and browser principals remain exact. Directory removes the aggregate profile through complete-set replacement after the final current operation retires.

This is Web's privileged HQ publication role. A current Web service identity can publish drawings and targets in any canonical cell without an administrator pre-enumerating map cells, and it can read the closed all-cell position and aggregate retained families above. The privilege is still a finite Directory-issued family/action contract: it does not grant waypoint/**, arbitrary record publication, endpoint impersonation, group-key issuance, or connected authority mutation. Browser sessions never inherit it.

Plan acknowledgements use an exact concrete PUBLISH expression and only waypoint/global/record/plan-ack/<exact-opaque-plan-version>/* for QUERY/live read. The single * is the typed 32-lowercase-hex plan-acknowledger slot. A wildcard plan version, **, and operation- or deployment-wide containment are not registered. See the plan-acknowledgement contract.

A family, purpose, action, or mode combination absent from this table is rejected by the generic retained store. The non-retained families registered below are not thereby made durable. Router token/liveliness and revocation control retain only their closed, non-storage behavior.

Closed non-retained LIVE registry

LIVE_PUBLICATION accepts exactly these signed capability expressions. In this table, * is a PETRA typed placeholder for one canonical segment in the declared position; it is not evaluated through generic Zenoh key-expression containment.

Directory-signed capability expression Concrete publication requirement
waypoint/*/pos/<exact-source> * is one canonical five-character geohash; source is unchanged
waypoint/*/heartbeat/<exact-source> * is one canonical five-character geohash; source is unchanged
waypoint/global/voice/<exact-channel>/<exact-source>/* * is one canonical opaque session; channel and source are unchanged
waypoint/global/cmd/<exact-target>/* * is one canonical opaque command id; target is unchanged
waypoint/global/ack/<exact-source>/* * is one canonical opaque command id; source is unchanged

<exact-source>, <exact-channel>, and <exact-target> are exact 32-character lowercase-hex opaque segments from their registered domains. The source in position, heartbeat, voice, and acknowledgement authority is bound to the capability's verified subject and signing key by Directory. Command target authority is exact; there is no cmd/*/* grant.

The typed parser rejects **, $*, a wildcard in any other slot, aliases, missing or extra segments, malformed cells or opaque segments, generic prefix containment, and any attempt to cross families. A concrete publication key contains no wildcard. Its AuthEnvelope.storage_key_digest binds that exact key, and the fresh StorageUseProofV1 binds the capability digest, exact concrete key, digest of the complete encoded envelope bytes, nonce, and timestamp. Capability bytes in the proof attachment are byte-identical to signed envelope field 11.

Closed non-retained live-read QUERY registry

QUERY is PETRA's read action. In addition to bounded retained get, an exact Directory-issued QUERY capability may be used by first-party code to derive an exact live subscriber selector. Version 1 registers only these non-retained live-read forms:

Directory-signed QUERY expression Exact audience boundary
waypoint/<exact-cell>/pos/** One canonical geohash-5 cell; only position-source children in that cell
automatic Web waypoint/*/pos/** Every canonical geohash-5 cell; only position-source children
waypoint/global/voice/<exact-channel>/** One exact opaque voice channel; only source/session children of that channel
waypoint/global/ack/<exact-source>/* One exact opaque device source; only its command-identifier slot varies

Each uses REUSABLE_CONTENT, exactly the LIVE purpose, zero authority-ledger version, and no mutation commitment. These entries are read authority only: they never authorize publication, turn position/voice/acknowledgement into retained content, admit a Storage query, or create replay/restart/outbox behavior. Retained chat remains in the closed retained registry and uses the exact per-channel waypoint/global/chat/<opaque-chat-channel>/** selector for both catch-up and live ingestion.

Common's closed classifier accepts an already verified capability and returns its typed family and exact channel, cell, or device audience with no generic string fallback. The all-cell position audience is reserved to Directory's automatic Web relationship; manual input cannot request it. For the non-retained live-read registry it rejects waypoint/**, any other wildcard or noncanonical cell, waypoint/global/voice/**, waypoint/global/ack/**, heartbeat, sensor, command, snapshot, membership, and every unregistered family. Exact retained invite QUERY classification remains registered for its ordinary invitee path; it is not a Web recorder grant. The complete-set consumer layer admits classifier output only after Directory signature, subject/session signing-key binding, generation, validity, classification ceiling, and known revocation pass. Consumer setup therefore admits only exact audiences from the current complete set, so substituting a sibling channel or cell that has no entry fails exact matching rather than being inferred.

Because stock Zenoh carries no capability attachment on subscriber declarations, this classification constrains first-party subscription setup but is not a Relay security boundary. A durable query still requires a fresh StorageUseProofV1; a live subscriber instead verifies every delivered publisher envelope/capability/proof and the read capability's current classification/audience constraints before opening or using the sample. Full-set replacement closes omitted first-party selectors atomically, subject to the accepted stock-Zenoh residual above.

Retained chat remains deliberately distinct. A chat message has envelope purpose LIVE but uses the retained REUSABLE_CONTENT row above so Storage may preserve and serve its bounded history. It never uses LIVE_PUBLICATION. No other non-retained LIVE family may enter Relay + Storage persistence, restart, serve, mesh replay/query, or transport-outbox paths. A verified trusted application subscriber may archive a live value under its own application retention/audit policy; that archive is not a Zenoh Storage value and cannot be republished as original endpoint traffic.

Gateway bridge identity

Directory grants each authenticated Gateway principal one opaque position source. A Gateway publishes every external track under that source while the geohash cell alone may vary. The external origin_uid is carried only inside the signed and sealed TacNetMessage payload during source migration; the direct-payload contract assigns it to the future canonical position field. It never appears in a key expression or operational log. A consumer identifies a bridged track by (verified Gateway principal, origin_uid), not by an endpoint identity inferred from the publication key. Two external tracks therefore share the Gateway source segment but remain distinct signed payload origins.

Only a Directory-authorized Gateway identity may emit a non-empty origin_uid; native position publishers leave it empty. It is valid only when its UTF-8 encoding is at most 256 bytes, contains no Unicode control character and none of /, *, ?, #, or $, and otherwise preserves exact code points and case. Android, Web, and every other position consumer must key, deduplicate, expire, and render Gateway tracks by that tuple before Gateway switches to its one-source key. A sender-key-only projection would collapse multiple external tracks and is not release-compatible. This dependency is tracked by Android #280 and Web #617.

The Gateway may continue publishing newly observed external tracks while disconnected inside its cached grant horizon. It receives no source wildcard and does not mint a grant for an external origin.

Retired standalone sensor plane

PETRA 1.0 has no waypoint/<cell>/sensor/<sensor-id> publication family and no SensorInfo or SensorRemoved data plane. A sensor represented on the tactical map is a normal drawing/annotation; removal is its signed drawing tombstone. Web and Android derive sensor-specific views from that one retained model instead of maintaining a second outbox, subscriber, key expression, or deletion stream.

Consumer parity lands before deleting the duplicate producer/outbox/subscriber/key paths, and the Common message types are removed only after every production reference has migrated. Android #281 and Web #618 own consumer parity and duplicate-path removal; Common #196 then removes the unused messages and key helpers. Node's existing live camera-advertisement metadata is a different transient surface and remains unchanged. LIVE_PUBLICATION registers no sensor family, alias, or compatibility exception.

Capability-use proof

Publications and queries carry this deterministic-protobuf attachment as the Zenoh publication/query attachment. It is transport authorization, not part of the stored value:

message StorageAuthorizationV1 {
  bytes directory_signed_capability = 1;
  StorageUseProofV1 use_proof = 2;
}

message StorageUseProofV1 {
  uint32 version = 1;                 // exactly 1
  uint32 action = 2;                  // 1 = publish, 2 = query
  bytes capability_sha256 = 3;        // SHA-256 of field 1, exactly 32 bytes
  string canonical_key_or_selector = 4;
  bytes value_sha256 = 5;             // publish: SHA-256(value); query: empty
  bytes nonce = 6;                    // exactly 12 random bytes
  uint64 issued_at_ms = 7;
  bytes subject_signature = 8;        // Ed25519, exactly 64 bytes
}

subject_signature uses the device signing key bound into the capability. It signs these bytes, independent of protobuf field order:

ascii("petra-storage-use-v1")
|| u8(action)
|| capability_sha256[32]
|| u32be(len(utf8(canonical_key_or_selector)))
|| utf8(canonical_key_or_selector)
|| u8(len(value_sha256)) || value_sha256
|| nonce[12]
|| u64be(issued_at_ms)

For a publication, value remains the signed AuthEnvelope; the value digest binds the authorization to those exact bytes and the canonical key binds it to the transport address. For a query, the selector string is the exact canonical routed key expression; allowed selector parameters are normalized and included only when the capability contract explicitly names them. Relay + Storage verifies retained-publication and query attachments; a receiving subscriber verifies a non-retained live attachment. Neither boundary persists or replays the request use proof. For a publication, attachment field 1 must be byte-identical to the canonical publication capability retained inside the envelope. For a query, the request is not an AuthEnvelope: it is a QUERY StorageCapabilityV1 plus fresh StorageUseProofV1 bound to the exact canonical selector, and it remains ephemeral. A reply is eligible only when its concrete key is contained by both the request selector and query capability; its original stored envelope must then pass its own publication authority, signature, purpose, classification, revocation, and exact key-digest verification. A query capability or proof never substitutes for the original publisher's authority. Before each reply, Relay + Storage also requires Common::decide(stored_envelope.classification, query_capability.classification_ceiling) == Permit. A missing/malformed stored label or query ceiling, policy mismatch, or any CMBAC denial suppresses that reply and records a fail-closed diagnostic without exposing payload content.

Authenticated retained-page completion

Relay + Storage returns one canonical RetainedReplayReceiptV2 exactly once as the final successful reply on the exact requested selector, then closes the stream. This applies to every page, including an empty one. The receipt commits to the exact selector, parameters, request authorization bytes, ordered reply sequence, fresh issue time, and selected Directory-signed ServerToken. The token carries replay_signing_key; the Server signs with its matching private key from the active atomic credential generation.

A requester keeps the page provisional until Common verifies the receipt against the exact selected ServerToken and the replies it observed in order. Any error reply, missing, early, duplicate, malformed, stale, or wrongly signed terminal, reply after the terminal, selector escape, or different selected ServerToken fails the page. The receipt embeds no service IdentityToken; a replay result never discloses a reusable Directory API bearer.

The executable positive cross-language bytes for reusable-content and committed-mutation capability encoding, Directory signatures, envelope key binding, subject proof, and Zenoh attachment are in storage-authorization-v1.json. That fixture also pins purpose-3 field ordering and enumerates the required hostile mutations; Common's executable corpus materializes each checklist item into changed bytes. Common #195 extends the shared Rust/TypeScript corpus with canonical bytes for all five LIVE_PUBLICATION expressions and every typed-grammar/proof failure listed below before any endpoint adopts the new Common revision.

Envelope-to-key binding

Every AuthEnvelope is valid for exactly one canonical opaque publication key and retains the Directory authority under which it was signed. PETRA adds these fields; field 9 remains the snapshot authority grant defined by #126:

bytes storage_key_digest = 10;
bytes publication_capability = 11; // canonical encoded StorageCapabilityV1, action=PUBLISH

storage_key_digest is exactly:

SHA-256(
  ascii("petra-storage-key-v1\0")
  || u32be(len(utf8(canonical_publication_key)))
  || utf8(canonical_publication_key)
)

The digest is exactly 32 bytes. The canonical envelope signing input appends, after the framed field-9 snapshot grant, storage_key_digest[32] || u32be(len(publication_capability)) || publication_capability. It binds the signed value to the exact transport address and Directory publication authority without exposing the semantic identifier or reverse-addressing salt.

The publication capability grants its bound subject signing key permission to sign values under its exact expression, allowed purposes, and classification ceiling during [issued_at_ms, expires_at_ms). It does not authorize another subject and does not make HLC or storage arrival order authoritative. Verification requires capability.issued_at_ms <= envelope.issued_at_ms < capability.expires_at_ms, exact subject/signing-key equality with the envelope's verified IdentityToken, PUBLISH action, purpose inclusion, key containment, classification permit, Directory signature, and current revocation state. A retained capability may have expired by a later catch-up; its historical publication remains verifiable because it was valid at the signed envelope time. LIVE_PUBLICATION has no historical mesh authorization mode and is rejected after its active-use deadline. A trusted application archive may retain the already verified value under separate application policy, but it cannot use the expired live grant to republish or reconstruct mesh traffic. New publication and query use require a currently valid capability and fresh proof.

Publishers compute the digest only after constructing and validating the final canonical key. The publication digest input is always a concrete key and can never contain a wildcard; a query proof may bind an authorized canonical selector. Every returned concrete key must satisfy both request and capability containment, and its envelope digest must independently match that concrete key. A retry may reuse an envelope only on that same key. Semantic fan-out to another key requires a freshly signed envelope and a fresh storage-use proof. Aliasing one signed envelope under multiple keys is invalid.

Relay + Storage recomputes and compares the digest before persistence, when reopening and loading every retained entry after restart, and immediately before returning a value. A mismatch, missing/wrong-length digest, cleartext legacy key, or non-canonical key fails closed and the value is never returned. At those same boundaries it re-verifies the retained publication capability, envelope signature, subject/key binding, purpose, classification, key containment, and current revocation. Consumers perform the same retained publication-authority and reply-key checks. The incoming publication key must match the envelope digest and be contained by the retained Directory capability and fresh use proof; query authorization is checked separately. Tombstones and snapshots follow the same rule and remain bound to their own exact canonical keys and authority.

For non-retained live traffic, no restart or serve boundary exists. The receiving subscriber recomputes the digest against the concrete delivered key and completes every live authorization check before payload decode or application dispatch.

The 1.0 flag-day reset has no verifier for envelopes lacking fields 10–11 and no fallback to semantic payload inspection. Existing production flows do not intentionally reuse one signed envelope under multiple key expressions; introducing such fan-out would require a new explicit wire-contract decision.

When more than one storage can answer, the requester queries every applicable provisioned copy and disables HLC-based Latest consolidation. It verifies all replies, removes byte-identical duplicates, then merges by opaque key and signed semantic version, snapshot cut, and tombstone precedence. Zenoh HLC timestamps are useful transport ordering metadata; they never confer application authority. HLC consolidation is allowed only for a family whose contract explicitly makes every candidate byte-identical.

Opaque addressing

Operational key expressions must not contain stable cleartext principal, channel, unit, or operation identifiers. Geohash cells, fixed protocol literals, classification labels, timing, sizes, and traffic volume remain observable; this is metadata minimisation, not traffic-flow confidentiality.

PETRA 1.0 uses one framed, domain-separated derivation for every semantic key slot:

opaque_segment = lower_hex(
  first_16_bytes(HMAC-SHA256(
    opaque_keyspace_salt,
    ascii("petra-opaque-v1\0")
    || u16be(len(utf8(domain))) || utf8(domain)
    || u32be(len(utf8(identifier))) || utf8(identifier)
  ))
)
  • opaque_keyspace_salt is a fresh 32-byte deployment-static PETRA 1.0 secret. Directory mints it during the 1.0 reset and distributes it only to eligible non-storage endpoints. Relay + Storage never receives it. Salt possession is not per-expression authority; Directory-issued capabilities provide that boundary. The dependent Common/Directory change replaces GroupKeyBundle.invite_keyspace_salt at field 12 with bytes opaque_keyspace_salt; the old name and semantics are not retained.
  • domain is an exact ASCII string from the registry below. Length framing makes every (domain, identifier) pair unambiguous; len is a byte length, not a character count.
  • Output is exactly 32 lowercase hexadecimal characters. Empty identifiers and salts not exactly 32 bytes fail closed.
  • The salt is independent of content-encryption keys and remains unchanged across ordinary group-key rotation. Later salt rotation is a flag-day re-addressing operation, not a dual-read migration.
  • A uniformly random identifier may remain clear only when it carries no principal, channel, unit, operation, or human-assigned semantics. Otherwise each key slot uses its registered domain before it enters the expression.

The context registry deliberately separates linkability between planes. Definition and tombstone forms of the same object reuse a context so a client can merge them; unrelated planes use different contexts even when their clear identifier happens to be equal.

Family or slot Registered domain
deployment audience audience-deployment
auth subject auth-principal
operation operation-id
position / heartbeat subject position-principal / heartbeat-principal
drawing / target object drawing-id / target-id
chat channel / message chat-channel / chat-message
channel definition, membership, transfer, tombstone channel-id
channel-member liveliness subject channel-member-principal
invitee / invited channel invite-invitee / invite-channel
voice channel / subject / session voice-channel / voice-principal / voice-session
command target / command / live command-acknowledgement source command-target / command-id / ack-source
plan unit / plan record-plan-unit / record-plan-id
plan-ack version / plan-scoped acknowledger plan-ack-version / plan-acknowledger
SitRep unit / report record-report-unit / record-report-id
requirement unit / requirement record-requirement-unit / record-requirement-id
ORBAT unit record-orbat-unit
router data-plane subject router-principal
snapshot scope / audience snapshot-scope / snapshot-audience

Registered domains are unique and exhaustive; length framing means they need not be prefix-free. New families register contexts here and in the shared corpus before implementation. The generic state/<class>/<key> namespace is not an escape hatch: it cannot carry durable PETRA 1.0 data until its class, opaque contexts, envelope purpose, and recovery behavior are registered.

The derivation and rejection corpus is opaque-addressing-v1.json. Common owns the executable cross-language copy when the generalized helper lands; Rust, TypeScript, Kotlin/JNI, and all consumers must run the same corpus rather than reproduce the algorithm independently.

1.0 addressing reset. This is an intentional flag-day wire and storage cutover. Before runout, operators erase retained development/test keyspaces and endpoint caches, deploy the new contract across every component, and re-enrol devices to receive the new salt and capabilities. Implementations must not read, write, probe, translate, or retain either cleartext legacy addresses or addresses produced by the old unframed invite derivation. There is no dual-read window, migration sweep, or retained-address compatibility promise.

Transport peers still know their own and directly connected peer certificate identities. Opaque router-principal data-plane segments prevent that transport identity from being published as a globally searchable operational key; they do not pretend the mTLS peer relationship itself is anonymous.

Retention and recovery

One deployment policy sets the offline horizon across Relay + Storage retention, endpoint outboxes, and group-key backfill. The PETRA 1.0 default is seven days. For the demo rig, the release lock renders the same value into every component and records the effective value in runout evidence; startup and CI must reject incompatible values rather than silently shortening recovery.

The lossless claim is bounded. It applies only while all of these are true:

  • traffic remains inside the declared maximum retained bytes and values, maximum value size, sustained/peak ingress, reconnect concurrency, and resource envelope;
  • the endpoint's local durable outbox survives until its existing Sent transition;
  • at least one provisioned storage copy that accepted the traffic survives; and
  • the endpoint returns before expiry with valid cached identity, keys, revocation state, and capabilities.

No universal hardware capacity is claimed. Each release/runout records the horizon, limits above, replica count, storage placement, disk reserve, and measured CPU, memory, battery, storage, and recovery time. Outside that envelope, PETRA reports degradation rather than claiming lossless replay. Sent means the upstream Zenoh put returned success; it is not a storage or end-to-end acknowledgement. Endpoints cannot infer retained availability from it, and PETRA 1.0 adds no receipt/HWM protocol. The lossless claim is therefore conditional on verified persistence by a surviving Storage copy, as stated above.

Sealed endpoint-cache rollback boundary

An endpoint seals cached identity, Directory trust, revocation, capability, and group-key state at rest. The seal provides confidentiality and integrity under the endpoint's wrapping key: altered bytes, a changed authentication tag, or ciphertext copied from a different wrapping-key context fail closed. It does not provide freshness or rollback resistance. An older byte-identical cache image produced under the same wrapping key still has a valid seal.

PETRA 1.0 accepts that older-valid image only as a bounded same-session endpoint-capture residual. An honest endpoint may load it only before the original signed identity, capability, configured offline-horizon, and any applicable signed absolute key deadline; while the Directory key remains trusted, the subject signing key remains bound and unrevoked, and each cached group-key epoch remains inside the unchanged Directory-served epoch/backfill range. No independent trust-key, subject-key, or backfill-entry TTL is invented. Here, same-session means the same authenticated subject and wrapping-key lineage; logout, authentication expiry or rejection, reassignment, re-enrolment, or wrapping-key destruction ends it. Loading or copying the image never rebases its issued_at_ms, expiry, group-key range, or horizon deadline; the earliest applicable original absolute deadline continues to win. A known subject/signing-key revocation still fails immediately. If the endpoint can detect that wall time is earlier than an authenticated issue time or than an in-memory same-process time floor, it treats that as clock rollback or an issue-time violation and blocks cached authority until a connected refresh succeeds.

Rollback of one endpoint never rolls back a peer. Every receiver applies its own current revocation snapshot and rejects a sender that it already knows is revoked, even when the sender restored an older locally valid cache. Once Directory is reachable, the endpoint refreshes trusted keys and revocation, verifies a complete signed capability response, and atomically replaces the old set before shared traffic, snapshot publication, or content outbox drainage. Omission supersedes an older grant before that grant's original expiry.

Android binds its sealed operational and authority cache to the current user session. Every session-wipe path destroys the session wrapping key and clears its in-memory copy before deleting the ciphertext. Restoring prior-session cache bytes after logout, expiry, rejection, fresh login, or reassignment therefore produces undecryptable bytes, not a prior operator's valid cache. Device/bootstrap configuration remains outside that session key.

Restoring an old group-key bundle does not provide a new epoch. Honest consumers enforce the original epoch/backfill and horizon bounds; an old key can open only ciphertext that was already sealed under an epoch it contains. Rotation is not retroactive: an attacker who extracted an old group key can continue to decrypt captured ciphertext from that old epoch, but cannot decrypt content sealed under a later epoch or fetch that epoch once revoked.

PETRA 1.0 makes no rollback-resistance claim against an attacker who controls both endpoint storage and the wall clock. Such an attacker can restore a mutually consistent old cache and report a time before its unchanged absolute deadlines. A local hash chain, duplicated cache, software high-water/monotonic file, or sequence sidecar can be restored with the cache and must not be added as a purported security boundary. Stronger protection requires a trusted hardware monotonic counter or an online freshness authority and is outside the 1.0 contract.

Domain deletion is a signed retained tombstone written with put. Zenoh delete is not used for application deletion. Authoritative snapshots are also signed retained put values. Signed semantic sequence/version, snapshot cut, and tombstone rules determine precedence; transport arrival time and HLC do not resurrect older state.

The authoritative snapshot contract owns the complete, exactly-one-class family inventory plus deterministic cut, rollback, and anti-resurrection rules. Web can retain class-3 history for a tactical class-1 or class-2 family, but it never republishes that history as endpoint-authored traffic.

Supported and degraded topologies

flowchart LR
    D["Directory<br/>authority"]
    W["Web<br/>authority + audit"]
    S1["Relay + Storage A"]
    S2["Relay + Storage B"]
    E["Tactical endpoints"]
    D -. "signed authority" .-> W
    D -. "signed authority" .-> E
    E <--> S1
    E <--> S2
    S1 <--> S2
    W <--> S1
State Required behavior
Connected Directory and Web are reachable. Live pub/sub and query catch-up operate; authority and snapshots refresh normally.
Core disconnected A tactical partition continues existing authorized channels within the horizon using valid cached authority, including non-retained live publication under the closed registry. It cannot mint identity, capabilities, channel-membership changes, revocations, or snapshots. An unknown remote removal remains effective only after refresh or signed expiry; a known subject/key revocation fails immediately. A newly created offline channel remains a visibly unactivated local draft and carries no shared traffic.
Alternate relay/storage Endpoints use a statically configured surviving path. There is no election or promotion. Query duplicates are verified and merged by semantic rules.
Storage unavailable Permitted non-retained live peer pub/sub may continue, but catch-up and delayed delivery are unavailable. Eligible retained content remains in its durable outbox; non-retained LIVE_PUBLICATION is never queued or replayed. A successful Sent transition is not proof of storage. The operator sees the degraded state.
Relay path restored within horizon Reconnect is automatic and uses cached unexpired authority while Core remains unreachable. Authorized catch-up resumes without a control-plane round trip; no authority changes are possible.
Core restored within horizon Endpoints refresh Directory trust/revocation/ledger/capability state and atomically replace cached grants before shared operation resumes. Only then do authorized outboxes drain idempotently; query-all catch-up merges verified values and tombstones without resurrection.
Restored after horizon Transient history is visibly incomplete. Declared durable models recover through a verified Web-signed snapshot under #126; Web never impersonates an endpoint.

Threat model

Threat Required containment
Stolen capability Capability bytes lack the subject's signing key; fresh proof is required, validity is bounded, and revocation is rechecked on publish and query.
Offline authority change Directory is the only issuer. New channels, invitations, ownership transfers, and other grants fail explicitly while Directory is unreachable; local drafts cannot publish, query, or masquerade as activated channels.
Stale cached membership Reconnect refreshes exact capabilities and reconciles Directory membership before shared operation resumes. Expired, removed, or revoked authority fails closed.
Older valid sealed endpoint cache The seal detects tamper but not restoration of authentic older bytes. Original signed token/capability/configured-horizon and any applicable signed absolute key deadline, current trust/binding/revocation, the unchanged cached group-key range, session-key destruction, independently newer peer revocation, and refresh-before-traffic bound the residual; PETRA 1.0 does not claim resistance when the attacker controls both storage and wall time.
Capability replay or selector widening Nonce/timestamp replay gate plus exact canonical-expression containment; unknown/duplicate parameters and replies outside selector are rejected.
Revoked or captured endpoint Known subject/signing-key revocation fails closed. A disconnected partition cannot know a remote removal until refresh, so a malicious holder may exercise an otherwise valid cached grant until its signed effective deadline. Atomic full-set replacement removes omitted grants before shared operation/outbox drain on Core reconnect. Previously cached ciphertext and old key epochs remain bounded capture residuals.
Modified live subscriber with a deployment group key First-party code declares only relationship-derived exact opaque scopes and verifies every LIVE value before decode, but stock Zenoh has no PETRA capability-aware subscription-admission boundary. A transport-reachable current-epoch key holder can widen its local live subscription and decrypt observed ciphertext, including after app-layer revocation but before rotation. It cannot forge an accepted publication; durable queries remain Relay-enforced. Revocation plus group-key rotation cuts it off from newly sealed epochs, without erasing traffic or epochs already observed.
Traffic analysis Opaque per-domain segments remove stable semantic identifiers, but timing, cell, size, repetition, and domain literals remain observable and must be treated as residual metadata.
Compromised disposable storage It can copy, drop, delay, duplicate, or roll back opaque bytes, but cannot decrypt, forge signatures, mint authority, reverse addresses, or make HLC authoritative. Consumers detect invalid and semantically stale replies. Availability attacks remain possible.
Salt compromise The attacker can dictionary-map address segments. Content remains encrypted and signatures remain unforgeable, but the deployment requires an explicit re-addressing migration; ordinary key rotation is insufficient.
Malformed key or selector Canonical parser, fixed domain registry, size bound, selector containment, and reply-key checks reject injection and wildcard expansion before storage access.

Required contract and runout tests

Common supplies executable Rust/TypeScript positive and hostile vectors; every producer and consumer adds integration coverage for its registered families. The release-candidate runout records these exact outcomes:

  • each of the five LIVE_PUBLICATION expressions accepts a canonical concrete key and rejects **, $*, a wildcard in the wrong slot, missing/extra segments, aliases, malformed cell/opaque segments, generic containment, and family crossing;
  • a position and heartbeat source moves across cells during Core disconnection while its exact opaque source remains fixed; substituting another source fails before decode;
  • voice cannot change channel/source, command cannot change target, and acknowledgement cannot change source; fresh session/command ids work only in their one declared slot;
  • a moved envelope, changed complete-envelope digest, capability/envelope attachment mismatch, stolen capability, proof signed by another key, stale proof, and replayed nonce all fail closed;
  • capability refresh vectors execute canonical signed empty and mixed sets, exact token bytes, full 32-byte Directory key id, echoed nonce, subject/key binding, and exact once-sealed committed ciphertext; token/timestamp/key/signature tamper, cross-protocol challenge use, replayed or wrongly echoed challenge, forged omission, malformed/noncanonical protobuf including unknown or duplicate scalar fields, reordered or semantically duplicate entries, stale/equal generation, and generation overflow all fail closed;
  • capability refresh accepts exactly 4,096 entries, a 16,777,216-byte encoded response, and a 1,048,576-byte committed value, while rejecting each limit plus one and every checked-arithmetic overflow before allocation; an entitlement mutation that would make the full set exceed a bound rolls back rather than truncating the later response;
  • operation-context vectors accept canonical signed zero, one, and multiple-context sets; prove bytewise UTF-8 context ordering, numeric CHAT/VOICE kind ordering, exact kind selection, and a required empty-set digest; reject empty/duplicate/noncanonical context ids, empty/duplicate/unsorted/unknown kinds, reordered contexts, context-digest/signature tamper, stale/equal generation, token/ProfileBundle/ORBAT/user-authored substitutions, and a caller-selected kind absent from its context. Higher-generation replacement removes an omitted context and clears its selection atomically with capability omission; offline, expiry, revocation, failed refresh, or removal leaves only an inert draft, and a Directory receipt refreshes before activation while commit revalidation defeats a relationship/capability change after UI selection;
  • a verified signed empty capability set atomically clears the cache; transport, decode, signature, canonicalization, bound, or entry-validation failure preserves the prior cache unchanged, retains degraded status, and blocks shared traffic and outbox drain;
  • a complete channel membership join/kick/role change publishes one higher-version once-sealed full set and updates the subject entitlement index in the same transaction; concurrent mutation, retry, and rollback expose neither a lost member nor a torn set, and removal is absent from the next verified refresh;
  • an endpoint assigned 256 canonical geohash-5 cells receives only the corresponding exact drawing/target grants allowed by its effective actions; 257, a noncanonical or unassigned cell, and any retained moving-cell wildcard fail closed;
  • a connected authority mutation binds the exact actor token/key, challenge, time, request id, kind, aggregate scope, and typed semantic body; actor substitution, conflicting retry, stale current role/appointment, machine-only mutation, caller-supplied opaque/committed authority, and partial ORBAT batch commit fail closed;
  • a Web core-machine relationship produces only its exact operation/cell/family/audience grants; wrong platform/key/scope/cell, relationship removal, committed-value resealing, snapshot-audience confusion, browser delegation, and machine impersonation fail closed;
  • a FIDO-bound browser set is tied to the current credential, human principal, token and session signing key, expires within one hour, grants heartbeat but not position, and contains no Web-core, committed-publication, plan/requirement-publication, or snapshot authority; bearer-only sessions, wrong-key/replayed proofs, removed credentials, and unsigned member liveliness fail closed;
  • every purpose-3 CapabilitySetEntryV1 carries its matching verified snapshot grant in field 5; missing/orphan/mismatched grants fail the whole refresh and full-set omission removes the grant and publication capability atomically;
  • entitlement-matrix tests prove both gates independently: role without relationship and relationship without the required action grant nothing; an observer/member is read-only, a non-member cannot publish chat or voice, an invite cannot escape its exact invite and minimum definition, and admin override never turns a reusable grant into an authority mutation;
  • COP tests preserve sender-owned drawing mutation versus the manage_annotations cross-owner override, separate manage_targets from nominate_target, and reject target publication outside an assigned exact cell;
  • ORBAT tests use Common's visible_unit_ids closure, reject a command target outside the subject's current operation scope, distinguish camera from movement at the decoded Node command gate, remove report publication when the exact unit appointment is removed, and preserve a cached valid DDIL grant only until its signed expiry;
  • capability purpose/action/mode, classification, identity/signing-key binding, known revocation, and every effective expiry boundary fail closed at the earliest applicable deadline; vectors pin capability expiry first, identity expiry first, horizon first, exact-boundary rejection, and checked-add overflow;
  • non-retained live samples are absent after Relay + Storage query/restart and cannot enter a transport outbox; retained LIVE chat still persists and catches up only under REUSABLE_CONTENT, while durable plan acknowledgements use DURABLE_CONTENT, retry the original outbox envelope, remain retained only within the horizon, and enter Web's verified application archive;
  • the native Relay denies disallowed peers/actions/keys without claiming to inspect live capability bytes, while a receiving subscriber rejects an invalid capability/proof before any payload decoder or application handler runs; Android voice receives and verifies the original subject envelope/capability/proof, with no Server re-wrap or device-signature bypass;
  • the automatic Web recorder receives only the registered all-cell position and closed aggregate retained-query selectors; manual Web relationships receive chat and voice selectors only for exact channels, position only for exact cells, and plan acknowledgements only for an exact related plan version. Unregistered families, caller-declared AOIs, cross-operation scope, heartbeat, sensor, command, live command acknowledgement, invite, and membership recorder grants are absent;
  • Web atomically rebuilds exact chat/voice/position/plan-ack subscriptions when a verified complete generation adds or omits an entry, stops all affected ingestion at the earliest absolute deadline, and preserves a previous generation after refresh failure only while its original deadlines remain valid;
  • exact retained chat, exact-version durable plan acknowledgements, and live voice/position samples continue to populate verified Web replay rows, while the deleted invite/membership recorder has no startup declaration, compatibility fallback, or broad selector; connected mutation receipts and Directory audit events still populate the membership activity/AAR timeline;
  • first-party live subscribers declare only registered relationship-derived opaque scopes: one exact channel or field-cell scope, Web's closed all-cell position scope, one exact command target, and one exact acknowledgement device source with only its command-identifier slot variable. Source changes, sibling channel/cell or target scopes, waypoint/**, global family selectors, waypoint/global/ack/**, and every standalone sensor selector are absent or rejected by the first-party adapters;
  • a runout-only modified authenticated endpoint widens a stock-Zenoh live subscription while it still holds the deployment group key and demonstrates the accepted residual: it can observe and decrypt matching LIVE ciphertext but cannot forge a value accepted by an honest receiver, use the live path to enter Relay + Storage, or turn the widened selector into a retained QUERY without a valid capability and fresh exact proof;
  • after that endpoint's revocation is known but before the automatically triggered, coalesced group-key rotation completes, prove its existing transport path is not claimed to close: the modified client can still decrypt an observed current-epoch LIVE sample, while honest receivers reject its new publications;
  • after the rotation completes, prove the revoked endpoint cannot obtain or decrypt a newly sealed LIVE sample under the new epoch. The evidence does not claim that app-layer revocation, capability omission, membership removal, or native Relay subscription teardown alone withdraws already observed ciphertext or an old epoch;
  • a query request is a QUERY capability plus exact-selector proof, never an AuthEnvelope; an out-of-selector/out-of-capability reply or an invalid original stored envelope is rejected;
  • two external tracks use the Gateway's one opaque source, remain distinct by the signed and sealed (verified Gateway principal, origin_uid) identity, and expose no origin_uid in keys or logs; Android and Web independently prove tuple-based key/dedup/expiry/render behavior, and a non-Gateway publisher with non-empty origin_uid fails closed; empty, 256-byte boundary, over-limit, Unicode-control, forbidden-character, case-preservation, and code-point-preservation cases execute;
  • standalone sensor publications/deletions produce no accepted sample or duplicate outbox/notification; sensor map objects and removal use drawing values/tombstones, while Node live camera metadata remains interoperable;
  • ORBAT membership and appointment mutations each republish a complete unit record and preserve every unchanged definition, parent, membership, commander, origin/provisional, classification, ownership, and lifecycle field; retirement is a complete retired=true record at the next sequence, and a delayed pre-retirement record cannot recreate the unit;
  • lower ORBAT unit sequences, equal sequence with different bytes, and a capability whose authority_ledger_version differs from OrbatRecord.seq fail closed; equal sequence with identical bytes is idempotent, and concurrent Directory mutations cannot lose an accepted membership or appointment update;
  • live/retained catch-up and authoritative snapshot reconciliation apply the same atomic per-unit key/sequence rule; a multi-unit Directory transaction is all-or-nothing while each affected unit independently advances exactly once; and
  • while Core is absent, an otherwise valid cached grant whose remote removal is unknown continues only until signed expiry; known subject/key revocation fails immediately. On reconnect before expiry, revocation refresh plus atomic full-set replacement removes an omitted grant before shared operation or outbox drain. The evidence records the bounded malicious-holder window as residual risk rather than claiming instant disconnected removal;
  • tampering with any sealed endpoint-cache ciphertext, authentication tag, or wrapping-key context fails closed without changing the last accepted cache or releasing shared traffic/outbox work;
  • restoring an authentic older cache image under the same session wrapping key before its original signed token, capability, configured horizon, and any applicable signed absolute key deadline is accepted as the documented bounded endpoint-capture residual only while its Directory key remains trusted, its subject key remains bound and unrevoked, and its cached group-key epochs remain inside the unchanged served range;
  • that same older image fails closed at and after the earliest original absolute deadline; restart or restore does not extend any deadline;
  • a detectable wall-clock rollback or authenticated issue-time violation blocks cached authority until connected refresh;
  • a peer holding a newer revocation snapshot rejects the restored endpoint independently, even while the endpoint's older local cache remains inside its otherwise valid window;
  • on Directory reconnect, refreshed revocation and the verified complete capability set supersede the restored cache before shared traffic, snapshot publication, or content outbox drainage;
  • after every Android session-wipe trigger, destruction of the prior session wrapping key makes restored prior-session cache bytes undecryptable while device/bootstrap settings remain available; and
  • restoring an old group-key bundle creates no later epoch and never expands or rebases its cached range or overall horizon. Ciphertext from an already held old epoch still opens, ciphertext from a later missing epoch does not, and a revoked endpoint cannot fetch that later epoch.

Non-goals for 1.0

  • Dynamic relay election or storage promotion.
  • Endpoint-issued or owner-delegated publish, query, or channel-membership authority; changing this permanent trust invariant requires an explicit product and threat-model decision.
  • Activating or silently queueing a shared channel-authority change while Directory is unreachable.
  • Consensus or a global total order.
  • Unlimited offline retention or a lossless claim outside the measured envelope.
  • Server-side application membership, ownership, ORBAT, or drawing interpretation.
  • Complete transient history after the configured horizon.
  • A standalone or compatibility sensor publication plane separate from drawings.
  • Rollback resistance against an attacker controlling both endpoint storage and wall time, or a local hash-chain, duplicate-file, or software-monotonic substitute for trusted time.
  • Hiding traffic timing, volume, geohash cells, or directly connected transport-peer identity.
  • Audience-confidential live subscription admission against a modified authenticated deployment-group-key holder, or immediate remote withdrawal of an already declared stock-Zenoh subscription.
  • A PETRA subscription-lease protocol, capability-aware Relay plugin, payload interception, or Zenoh fork. Audience-specific encryption and any upstream admission hook are post-1.0 security work, not hidden release dependencies.