Skip to content
VERASPEC
Repository
VER 1.1 — draftdraft

§14 VER-F wire profile (DRAFT)

The VER-F federation profile projects a Record onto an atproto collection (org.verstandard.record). It is a distinct artifact with its own defects; this section is the 1.1 draft for it. Each item cites the audit finding that produced it.

Deliverable. The revised lexicons this section requires were drafted on 25 August 2026 and live beside this document in wire-profile/ (org.verstandard.record revision 2, the org.verstandard.retraction tombstone, and the §14.8 per-field openness statement in that directory's README). The frozen VER-F 1.0 lexicon implements none of §14.1–§14.8 and is untouched. RELEASE.md row 13 stays BLOCKING for the half no lexicon can carry: §14.1 item 5 requires the reference implementation to resolve in the same revision, and only the artifact half exists.

14.1 The wire strategy: stable NSID, closed core, one ext map (WIRE-15)

Today there is one unversioned NSID, no capability field, and a direct conflict between the two normative artifacts: the lexicon closes every object (additionalProperties: false, so a record carrying 1.1 fields is lexicon-invalid) while the reference implementation ignores unknown fields (so the same record is implementation-valid).

An earlier revision of this draft proposed resolving that by opening every lexicon object with a MUST-ignore-unknown rule. That is rejected. Opening identity-bearing structures on a public firehose means an unrecognised member can sit next to pixelSha256 and no artifact says whether it matters — which is the same class of defect as an unconstrained hash field, arriving by a different door. The strategy is a hybrid:

  1. The collection NSID is stable across VER 1.x. org.verstandard.record carries every 1.x record. A new NSID is minted only for a breaking wire major, and a wire major is declared by the wire profile's own lifecycle, not by the Record standard's (VERSIONING.md §6). Two NSIDs then coexist for a stated dual-publish window.
  2. verVersion and cpnpVersion are REQUIRED on the wire, explicitly, on every record (§14.2). They are the negotiation signal; a consumer reads them before it reads anything else. No profile capability field is added, because two version fields already carry what a Consumer acts on.
  3. Identity- and security-critical structures stay CLOSED. The identity object, every digest-bearing field, the privacy declaration and the signature block admit no unknown member. A record carrying an unrecognised member inside one of them MUST be rejected, not ignored: silence about an unknown member next to a hash is not forward compatibility, it is an unreviewed field with the authority of a reviewed one.
  4. Extensibility is exactly one explicit map: ext. A namespaced map, reverse-DNS keys, on the record root only — the wire mirror of §13. A Consumer MUST ignore ext keys it does not recognise, MUST preserve them when re-publishing a record it did not author, and MUST NOT let them affect indexing or trust. Anything that needs to influence identity or trust is a lexicon change, which is the point.
  5. The two normative artifacts MUST agree. The lexicon and the reference implementation state the same openness for every field, and the profile text states, per field, whether its value space is open or closed (§14.8).

The rejected alternatives are recorded: open everything (item 3's failure mode) and a versioned NSID per minor (a hard break and a dual-publish window for every additive change, which is what item 1 exists to avoid).

14.2 verVersion / cpnpVersion propagation (WIRE-13, WIRE-14)

Measured today: the mapper never copies either field from the Record it is projecting, so pydantic class defaults stamp "1.0"/"1.0" on everything — and no consumer reads either field.

Producer duty. The projection MUST copy ver_version and cpnp_version from the source Record. It MUST NOT default them. It MUST refuse to project a Record whose versions the profile does not implement.

Consumer duty. A Consumer MUST read cpnpVersion and MUST NOT deduplicate or compare pixelSha256 values across differing cpnpVersion values: a different CPNP is a different pixel-hash universe (1.0 §13, E10), and treating the hashes as universe-free silently merges unrelated assets. cpnpVersion MUST be carried into the consumer's index and into its dedup key. A Consumer that does not implement a record's verVersion MUST quarantine or reject it rather than index it as though it were 1.0.

14.3 Constraint parity with the Record schema (WIRE-07)

Not one hash-bearing field in the lexicon carries a pattern or a length, so the wire profile is strictly weaker than the schema it projects and cannot detect a truncated, wrong-algorithm, or hostile digest — including a digest crafted to traverse a filesystem path when used as a cache key.

Every wire field that projects a constrained Record field MUST carry the equivalent constraint: contentSha256, pixelSha256, vectorSha256^[a-f0-9]{64}$; phashDct64^[a-f0-9]{16}$; pdq^[a-f0-9]{64}$ (E22); width/height ⇒ integers ≥ 1. A wire record failing any of these MUST be rejected on receipt, before any value is used to construct a path, a key, or a query.

Wire identity.width/height are the Canonical Buffer dimensions, matching the pixel_hash preimage (E2). A projection MUST NOT fall back to a pre-orientation source for them.

14.4 Perceptual digest extensibility (WIRE-31)

The wire has one named slot per algorithm (phashDct64, pdq), so it cannot represent a third registered algorithm, cannot carry two digests of one family, and drops the alg label entirely for the byte and pixel tiers.

The fixed slots MUST be replaced by an open perceptual: [{alg, value}] array mirroring the Record, and the byte and pixel tiers MUST carry their alg. A projection MUST fail rather than emit an empty digest slot for an algorithm the Record does not carry. Until the array lands, a Producer MUST NOT publish a wire record whose only perceptual digest is one the profile has no slot for.

14.5 Privacy modes (WIRE-01, WIRE-02)

hashes_only withholds the thumbnail and not the embedding, so the mode's name promises more than it delivers; and no Consumer reads the field at all, so a declared mode imposes nothing on anyone.

hashes_only is defined as the identity tier only: the thumbnail and the embedding MUST both be omitted. A wire record declaring hashes_only while carrying an embedding is non-conformant and MUST be rejected. 1.0 §12's embedding-inversion analysis is the reason: a vector leaks content, so “hashes only” that ships a vector is a privacy failure, not a naming quibble.

Consumer obligations are normative. A Consumer MUST NOT redistribute, or serve to third parties, a thumbnail or vector from a record whose privacy mode withholds it, and MUST honour a re-published record that downgrades privacy by dropping the previously-indexed payload.

Unknown modes degrade, they do not drop. privacy is one of the fields §14.1 keeps closed, so an unrecognised value is a lexicon-invalid record rather than an extension point. Where a Consumer nonetheless meets one — a relay that did not validate, a newer profile minor — it MUST treat the value as the most restrictive mode it knows and MUST NOT discard the record.

A privacy mode binds the whole record, ext and tags included (ADR-0009). The consumer-side MUST-ignore rule for ext (§14.1 item 4) was never a producer licence: a wire record MUST NOT carry, in ext or in any other member, content its declared privacy mode withholds — nor, in any mode, a redaction salt (§5.3's prohibition follows the projection onto the wire) or a value the projected Record redacts (§5.4). And tags are content tier: a label derived from image content is disclosure of that content, so a hashes_only record's REQUIRED tags array MUST be empty. The audit that produced this paragraph found every ext rule addressed to the Consumer and none to the Producer; this is the missing half.

14.6 Tombstones and retraction (WIRE-03, WIRE-04)

There is no deletion path and no update path: delete commits are ignored, the producer creates rather than upserts, and the consumer discards update commits as duplicates. A Producer who removes a record from their repository cannot cause any Consumer to drop the vector, thumbnail, or hashes already indexed.

  • A Consumer MUST handle operation: "delete" by removing the derived index entry and any cached derivative (thumbnail, vector) for that record.
  • A Consumer MUST handle operation: "update" by re-verifying and re-upserting the record's payload. It MUST NOT short-circuit on the prior existence of an index entry. Where re-verification fails, the prior entry MUST be left untouched rather than replaced with unverified data.
  • A Producer re-publishing a corrected record MUST use an upsert (putRecord), not a create. The claim that “the same pixels always land on the same rkey, so a re-publish is an overwrite” is false for createRecord and MUST be corrected in the profile text.
  • A tombstone — an explicit retraction record naming the retracted record's identity and a reason — is added so that retraction propagates through relays that may have missed the delete commit. It names the retracted record, not a bare pixelSha256: a rendered-identity digest identifies a group, and a retraction addressed to a group retracts other producers' records (E27).
  • A retraction's reason explains without disclosing (ADR-0009). A tombstone is a permanent entry on a public log, frequently published because something was sensitive; a reason that restates the sensitive thing defeats the retraction. The reason MUST NOT carry personal data, the retracted record's content, or the value of any redacted field — a generic token ("privacy", "error", "rights", "other") suffices, and "other" is conformant.

14.7 Signed wire records and trust tiers (WIRE-21)

Every arriving record is unsigned in 1.0 §7.4 terms: producer attribution comes from an unauthenticated relay-supplied field, so DID filtering authenticates nothing, and the index stores federated claims indistinguishably from locally-verified ones.

  • The wire record gains an optional detached signature (JWS or COSE over the RFC 8785 JCS form of the wire record with the signature member removed — the same construction as E17). The signature block is one of §14.1's closed structures.
  • A Consumer MUST record a trust tier on every derived index entry: signed_verified where a signature verified against a resolved key, signed_unverified where a signature is present but the chain was not validated, signed_invalid where a signature was checked and failed, unsigned otherwise. Relay-supplied DID attribution alone is unsigned. The third tier is the audit's addition (ADR-0009): checked-and-failed is affirmative evidence — the most security-relevant outcome the vocabulary carries — and folding it into signed_unverified (an unchecked claim) or unsigned (no claim) would erase exactly the signal a consumer most needs to keep. A signed_invalid record MUST NOT be served as verified under any filter, and a Consumer SHOULD retain the failure rather than retrying it into an unsigned entry.
  • Profile text MUST state plainly that VER-F 1.0 records are unsigned and that DID filtering provides no authentication without MST inclusion proofs and commit-signature verification.

14.8 Lexicon and record-plane divergence

Wire privacy uses knownValues (an open set in atproto Lexicon) while the implementation models it as a closed literal — the mirror image of the closed-lexicon/open-code split §14.1 resolves. Both MUST be resolved in the same revision, and the profile MUST state, for each field, whether its value space is open or closed. A wire profile whose two normative artifacts disagree about forward compatibility has no forward compatibility.