The C1 family encoding profile
The bytes all three projects must agree on.
Akson, kovee and byom are separate systems with separate stores and separate operators, and they still have to accept, canonicalize and digest the same bytes identically. The profile is the one place that is decided. Byom hosts it; the other two bind to this text through a lock manifest rather than to a copy, so there is exactly one source and no second copy to drift.
One profile, three projects
The profile pins six families of behaviour, one directory of vectors each. A vector is a JSON file naming its input and its expected output — canonical bytes, a digest, an error class — and it is frozen once merged.
| Family | Cases | What it pins |
|---|---|---|
digest-class/ | 58 | The six classes, their wire shape, and the complete ordered substitution matrix. |
idempotency/ | 9 | The idempotency-domain derivations, byom's and kovee's side by side, pinned as distinct. |
ijson/ | 20 | Strict I-JSON acceptance: the ordered check list, its error classes, and the caps. |
jcs/ | 7 | RFC 8785 canonical bytes, including UTF-16 key sorting and ECMAScript number form. |
privacy/ | 4 | The PrivacyAccessRecord preimage and the chain link, including genesis absence. |
problem/ | 13 | The RFC 9457 problem shape, its closed kind enum, and the exact type prefix. |
A complete vector — jcs/basic-ordering:
{
"name": "jcs/basic-ordering",
"description": "object keys serialize in sorted order regardless of input order",
"input": { "value": { "b": 2, "a": 1, "c": [3, "x"] } },
"expected": {
"canonical": "{\"a\":1,\"b\":2,\"c\":[3,\"x\"]}",
"sha256_hex": "7f26f35fd8404944dd9a2441528c810b6c55fb2ae7b569b0438d309d81806acf"
}
}
Strict I-JSON acceptance
A request body is a single UTF-8 I-JSON text within hard caps, checked in this order. The first failure names the error class, and the order-3 classes surface in token order during one parse — an early duplicate key beats a later unsafe number.
| Order | Check | Error class |
|---|---|---|
| 1 | at most 262,144 bytes (the 256 KiB request cap) | oversize |
| 2 | valid UTF-8 | invalid-utf8 |
| 3 | parses as exactly one JSON text | syntax, trailing-data |
| 3 | no duplicate member names, at any depth (RFC 7493) | duplicate |
| 3 | no $domain member name, at any depth | reserved-domain-collision |
| 3 | integers within ±(253 − 1) | unsafe-integer |
| 3 | no NaN / Infinity literals | non-finite |
| 3 | floats finite; integer-valued floats within the safe range | unsafe-number |
| 4 | no unpaired surrogates after escape decoding | unpaired-surrogate |
| 5 | container nesting depth at most 64 | over-depth |
| 6 | at most 65,536 JSON values per document | over-nodes |
Within a single member-name token, the reserved-name check precedes the duplicate check. Implementations must process tokens iteratively: input nested thousands of levels deep — bounded only by the size cap — may never crash a rederiver, and the depth cap is enforced after the scan rather than by running out of stack. A vector pins exactly that pathological case. Responses follow the same rules under the 1,048,576-byte cap, and a vector opts into that context explicitly.
Canonical bytes, and the $domain tag
Canonical bytes are RFC 8785 JCS over exactly the value space the acceptance rules admit:
object keys sorted by UTF-16 code units, numbers in ECMAScript minimal form, short escapes
plus \u00xx for the remaining C0 controls, everything else literal UTF-8.
Key sorting by UTF-16 code units is not the same as UTF-8 byte order — an astral key
sorts as its surrogate pair — and a vector pins that difference rather than leaving it to
be discovered.
Type-tagged bytes inject a reserved top-level member and then canonicalize:
tagged(tag, obj) = JCS(obj ∪ {"$domain": tag}) # obj must not carry $domain
$domain (0x24) sorts before every letter, so the tag always leads the
canonical bytes. It is reserved at every depth of wire bodies and fails closed on
collision, which is why it can never be smuggled in as data. This mirrors kovee's own
$domain envelope member so the tag namespace is shared while the byte layouts
stay distinct.
RFC 9457 problems, and deliberate non-substitutability
Byom's problem object requires type, title and
kind; type is exactly
https://byom.dev/problems/ + kind. Extension members are allowed
only under reverse-domain names, and they carry no authority.
Byom and kovee both emit RFC 9457 problems and their conventions diverge on purpose. A validator that accepts the other project's spelling or namespace fails these vectors' intent.
| byom (BPP) | kovee (KCP) | |
|---|---|---|
| kind casing | snake_case — stale_revision | kebab-case — stale-revision |
| where the kind lives | a separate required kind member and the type suffix | only inside the type URN |
type | https://byom.dev/problems/<kind> | urn:kovee:error:<kind> |
status | optional; a JSON integer in 400–599 when present | a per-kind table value |
There is no translation table, and no value from one convention ever satisfies a field of the other, even for semantically matching errors.
The six digest classes
Every digest field on the wire is a typed DigestRef with a closed member set,
never an unlabelled hash:
{ "class": "local_erasure_safe", "algorithm": "hmac-sha-256",
"key_ref": "society-key:soc-garden/object:engram-42", "value_hex": "…" }
Exactly class, algorithm, value_hex, plus
key_ref — and nothing else. Class and algorithm pairing is closed;
key_ref is required exactly for the keyed classes and forbidden for the
public ones; value_hex is exactly 64 lowercase hex characters. Raw keys,
per-object secrets and salts are never part of a ref, and a member whose name looks like
key material fails closed.
| Class | Algorithm | key_ref | What it is for |
|---|---|---|---|
structural_public | sha-256 | forbidden | Knowingly non-sensitive, non-erasable protocol or schema bytes only. |
portable_public | sha-256 | forbidden | Content whose owner explicitly accepted a durable, publicly dictionary-testable identifier — and the class the A8 rule demands of any digest one protocol asks the other for. |
disclosed_party | sha-256 | forbidden | Bytes already disclosed to named recipients, always carrying the external-copy obligation. |
ciphertext_public | sha-256 | forbidden | Sealed blob bytes only — never a commitment to low-entropy plaintext. |
local_erasure_safe | hmac-sha-256 | required | Ordinary erasable local content and every authority subject, under a random per-object secret. |
scope_erasure_safe | hmac-sha-256 | required | Shared-key index and chain constructions, under a per-scope key. |
The classes are never interchangeable. A well-constructed digest of the wrong class is
digest_class_mismatch even when the 32-byte value spaces coincide, and the
vectors pin the complete ordered substitution matrix with values that are
arithmetically correct under the class they are offered as — so every rejection
is proven to be a typing decision rather than a wrong-bytes accident.
The two keyed classes are mutually non-substitutable for a concrete reason. Erasing one object must destroy exactly that object's verifiability, so an authority subject takes a per-object secret. Verifying an index or a chain must survive the erasure of one member, so the index takes a per-scope key. Swapping them silently changes what "delete this" means.
A digest offered where a schema requires class R is checked in ten steps: typed
at all, carries no key material, no unknown member, known class, class/algorithm pairing,
key_ref presence, 64-hex value, per-class construction rule, class equals
R, and finally value re-derivation where the verifier holds the content and key.
Wire-shape failures are reported before construction failures, and construction failures
before the generic mismatch — so an error message names the actual defect. A verifier
without the key accepts a keyed ref as well-typed but unverified; that
is the honest limit of keyed digests, and it is written into the rule rather than
glossed.
The A8 cross-boundary rule
This is the profile's newest rule and the only one found by running code rather than by review. Wiring kovee's episode pipeline to a real byomd found that byom was demanding four keyed digests from kovee — values kovee could only echo as opaque blobs, one of which was obtainable in practice only by reading byom's database — and that a fifth digest byom required at placement was published nowhere at all.
A digest one protocol demands from the other MUST be
portable_public, taken over a frozen cross-boundary fragment whose
members both sides hold. Crossing the boundary is the durable-identifier
disclosure that class requires, and because the fragment is never the owner's whole
erasable record, the ban on public hashes over erasable content is untouched.
The converse is equally normative: a digest the owner recomputes from its own
state keeps local_erasure_safe and is therefore
never a request member at all. Asking a counterparty to echo a value
you compute yourself proves only that your own value equals itself, while costing the
counterparty per-object erasure storage for nothing.
A portable_public field does not mean "trust what the peer sent". The
receiving side rebuilds the frozen fragment from its own committed state and refuses a
value that does not re-derive. A digest that is merely stored because a caller
sent it is asserted, not verified — even when its class is right.
Byom's applied instances each name a $domain tag and an exact, closed member
list; widening or narrowing that list changes the digest and is a wire change on both
sides.
| Frozen fragment | Who derives it, and why it has to be portable |
|---|---|
bpp-resource-allocation-binding-v0 |
The allocation's cross-boundary identity. Kovee asserts which allocation revision it placed, so it must stay a request member — and byom now publishes a second, fragment-scoped digest on the episode_request result so the value can actually be derived. Mutable members and byom-minted internal ids are deliberately out: the pin names an identity that does not change under it. |
bpp-mandate-use-binding-v0 |
On the consumption receipt. The consumer never supplied a mandate use and holds no per-object secret, so byom's keyed record commitment could only ever be echoed. That commitment is unchanged, stays keyed, and is now demanded from nobody. |
bpp-execution-consumption-receipt-binding-v0 |
The receipt's own binding digest — the one value a broker relies on, derived by the consumer from the receipt it just received. Byom keeps no keyed twin, because nothing inside byom compares this value; it exists to cross the boundary. |
bpp-parent-budget-fragment-v0 |
The parent budget worst case a subordinate reservation must stay under, pinned by a cross-repo vector so the check goes red on whichever side moves. |
bpp-provider-context-source-v0 |
The provider-context source fields byom composes for kovee's context manifest. |
kovee-host-effect-binding-v1 |
Kovee's tag, not byom's: a nine-member host effect fragment byom rebuilds from its own committed act and refuses if it does not re-derive. Byom's expectation is pinned against kovee's own recorded vector rather than against byom's code, so the cross-repo pin goes red on whichever side moves its domain tag. |
Two further optional pairs on the claim path — kovee's context-assembly digest and its provider-context-manifest digest — name kovee objects byom cannot recompute, so the rule would move them too. They fell outside the ratified decision and are recorded for the family-contract owner rather than re-classed unilaterally. This is what the rule looks like when it is being followed rather than announced.
Idempotency domains
Byom's idempotency domain is
{actor_binding_digest, operation, endpoint_incarnation, society_id,
society_recovery_epoch, idempotency_key}, digested as
HMAC-SHA-256(per-society index key, tagged("bpp-idempotency-domain-v1", domain))
and emitted as a scope_erasure_safe ref.
It is keyed because the domain embeds an actor binding and a caller-chosen key — erasable, low-entropy material that must never carry a public hash. It is scope-keyed rather than object-keyed because the index key is one key across every entry: destroying it erases offline verifiability of the whole index, never a single entry. Saying so is the whole point of having six classes instead of one.
Kovee's command idempotency digest, by contrast, is a public structural SHA-256 over non-sensitive command material, and akson separates domains by DSSE payload type in the pre-authentication encoding rather than in JSON at all. All three are correct for their store, and a vector pins that no value from one ever satisfies a field of another — even byte-equal.
The privacy access chain
Allowed and denied sensitive reads both append to a hash-chained access record. The preimage members are exact and all required — society, access sequence, event id, endpoint incarnation, recovery epoch, actor binding, operation, purpose, query or scope digest, result object count, result bytes, outcome, dependency digest, occurred-at — plus the chain link, which at genesis is wholly absent: no member, never a null-valued pseudo-ref.
The record's own self-referential digest is excluded from its preimage, and a record that
offers it inside the preimage fails closed. Both the record digest and the chain link are
scope_erasure_safe under the chain key.
The release rule has two failure arms and neither of them serves bytes: an outcome other than allowed releases nothing and still chains a record; a failed journal commit releases nothing at all. Records carry cardinality and byte counts, never result plaintext.
Two independent rederivers
Every expected value in the corpus is re-derived from scratch by two programs that share no code and no dependencies — one Python stdlib only, one Node builtins only. Both go nonzero on any mismatch and on an empty vector tree, so a deleted corpus is a failure rather than a pass.
python3 family-vectors/xcheck.py # exit 0 = every `expected` re-derived
node family-vectors/tscheck/check.mjs # the same, in a second language
Kovee and akson consume this directory through the family lock: their CI checks out the
byom commit the manifest pins, asserts that the checked-out commit is that
commit, and runs xcheck.py from that tree. A local copy alone is never
provenance.