Byom Participation Protocol

One envelope, and a registry that decides what exists.

BPP is transport-independent and spec-first: the JSON Schemas, canonical byte rules, problems, limits, vectors and transition descriptors are normative, and the implementation conforms to them rather than the other way around. Everything on the wire is one envelope with exactly two answers, plus a machine-readable registry that says which (operation, surface) pairs are addressable at all.

version 0.2 99 rows 95 operations 16 families 234 schemas 462 vectors

The envelope

A request is a single UTF-8 I-JSON text carrying a negotiated protocol version, an operation id, that operation's exact arguments, and — on mutations — a MutationMeta. Every concrete operation has its own closed schema: unknown fields fail closed, and operation arguments never validate against the generic envelope.

request → participant.sock
{"version": "0.2", "op": "hello"}

There are exactly two answers, and they are structurally distinguishable.

success outcome: ok
{"outcome": "ok", "result": {"versions": ["0.2"], "surface": "participant",
                             "endpoint_incarnation": "inc-b597a0a54e5d9f10"}}
problem outcome: problem
{"outcome": "problem", "problem": {"kind": "unsupported_version",
   "type": "https://byom.dev/problems/unsupported_version",
   "title": "no common protocol version", "status": 400}}

The hello result schema admits a list of supported minors so a client can negotiate the highest common one; this daemon advertises exactly one. Note what hello does not return: it is an endpoint-and-version closure, so it names the surface this channel is bound to and the endpoint incarnation, and nothing about the society behind it.

The endpoint incarnation is not decoration

It identifies this run of this endpoint. It is digest material in every idempotency domain and a required member of every MutationMeta, so a request minted against an old incarnation cannot resolve against a new one. Restoration creates a new recovery epoch, which fences pre-restore leases, credentials, permits and live channels for the same reason.

Surfaces and sockets

Authority is separated by where you are talking, not by a role claim inside the body. The daemon binds five Unix-domain sockets — governance.sock, candidate.sock, participant.sock, runtime.sock and projection.sock — each 0600 inside a 0700 per-user runtime directory. The candidate and runtime sockets additionally take a mandatory channel-token preamble line before the request: the offer-scoped candidate credential, and the episode- or allocation-scoped workload token.

The closed surface enum in the negotiation schema has six values: governance, candidate, participant, runtime, projection and admin. The registry additionally uses two names that are not sockets of their own: pre_auth for the negotiation reads, which answer on every socket, and originating for the two recovery reads a caller aims at its own prior request, which answer on the mutation-capable socket that request used.

The admin surface binds nothing

admin is a value in the closed surface enum with zero registry rows and no admin.sock. There is nothing to call there. That is deny-by-absence working as intended rather than an omission — but it does mean the enum is wider than the implemented surface set, and a reader should not infer an administrative capability from the presence of the name.

SurfaceChannelWhat it is for
governance governance.sock Decisions made about a society: bootstrap, charter, membership offers, admission, mandate issue/hold/revoke, and the two reconciliation seats.
candidate candidate.sock + offer credential The only surface an unadmitted candidate can reach. Three operations, scoped to exactly one membership offer, fenced when that offer terminates.
participant participant.sock What an admitted participant does on its own behalf: propose, position, pledge, derive mandates, open activities, deliver, review.
runtime runtime.sock + workload token The execution seam: claim, start, checkpoint, complete, consume a permit, report usage, admit a placement.
projection projection.sock Reads only. No operation bound here mutates anything.
pre_auth every socket Version and capability negotiation, answerable before any authority exists.
originating the socket the original request used The two recovery reads: fetch a retained idempotent result, or recover a cursor.
admin — none Named in the enum, bound to no operation, served by no socket.

The server derives actor, participant, manifestation, society and surface from the channel. Request fields that name those objects can never override the binding — a "wrong-surface args" negative vector pins that for each family.

The registry, and deny by absence

spec/registry.json is the machine-readable operation registry: one row per (operation, surface) pair, carrying the binding number, the family, the class and the frozen request and result schema names. Today it holds 99 rows over 95 distinct operations across 16 families. Four operations exist on both the participant and the governance surface and therefore carry exactly two rows each; every other operation carries exactly one.

The registry is the freeze source for a bundle and the dispatch truth for the daemon. A call whose (operation, surface) pair has no row is refused, not interpreted — and the conformance runner derives its bundle, meta-class, surface and MCP checks from these rows, failing on any extra or missing surface binding. Counts and membership are registry-derived, never prose.

Every row is classed, and the class is structural rather than documentary: 40 create, 43 update, and 16 read.

ClassWhat the closed meta looks like
readNever mutates and carries no meta at all — supplying one fails the closed schema.
createA mutation whose meta has no expected_revision member, so supplying one fails the schema.
updateA current-head compare-and-set whose meta requires expected_revision.

The complete registry, row by row, is generated on the reference page →

Bundle freeze

A specification bundle is the unit of compatibility. Its exact operation list is frozen from the registry at freeze time; published schemas are immutable and any change is a new version file; a merged vector is immutable and fixes are new cases; and a feature is advertised on the wire only when all of its operations, states, limits, authorization checks, crash semantics and fixtures exist. A -v2 schema name on this site is always an immutable successor publication, never an edit.

Mutations, idempotency and revisions

Every mutation carries a closed MutationMeta: a request id, an idempotency key, the expected endpoint incarnation, the expected recovery epoch, and — for update-classed operations — the expected revision. Optional causation and correlation references travel there too, and carry no authority.

Retry-safety is an invariant, not a convention: an authenticated actor, operation and idempotency key identify one canonical request. A replay returns the retained result or a durable non-re-executing expiry tombstone. Changed input under the same key is a conflict — idempotency_mismatch — rather than a second execution.

The idempotency domain is a keyed digest over {actor_binding_digest, operation, endpoint_incarnation, society_id, society_recovery_epoch, idempotency_key}. Because the recovery epoch and the incarnation are digest material, a request minted in an old domain can never resolve in a new one. Why that digest is keyed, and scope-keyed rather than object-keyed →

Problems

The failure envelope wraps an RFC 9457 problem object. kind is a closed 29-value enum; type is exactly https://byom.dev/problems/ plus that kind; title is free prose that carries no authority; status, when present, is a JSON integer in 400–599. An unknown kind fails closed.

Problems never disclose hidden object, participant, peer, path, policy or membership existence. An unknown label and a forbidden one produce the same answer, and the vectors test the shape rather than the wording.

The complete enum, in schema order: invalid, unsupported_version, feature_unavailable, forbidden_surface, forbidden, not_found, stale_revision, stale_binding, stale_assembly_epoch, stale_lease, idempotency_mismatch, position_ineligible, decision_incomplete, independence_conflict, authority_widening, mandate_held, admission_required, classification_unmapped, policy_conflict, policy_overflow, budget_exceeded, effect_ambiguous, authority_witness_unknown, endpoint_sealed, cursor_expired, unavailable, formation_requires_participation, external_command_not_terminalizable, internal.

Byom's convention deliberately diverges from kovee's and is non-substitutable with it: snake_case against kebab-case, a separate required kind member against kind-inside-the-URN, an HTTPS namespace against a URN. There is no translation table. The two conventions side by side →

Events, cursors and recovery

Society activity lands in an append-only ledger read through events_read and events_wait, with payloads fetched separately by reference. Continuation tokens are one opaque string bound to the authenticated cursor, the endpoint incarnation, the recovery epoch, the filter digest and the retention semantics. Internal sequence values are never projected — ordering travels only inside the token — and a filter narrows visibility, never widens it.

Two recovery reads exist for a caller that lost its answer rather than its authority: idempotency_result, which returns a retained outcome as completed, in_flight or tombstone and never re-executes; and cursor_recover, which resumes or offers an authorized snapshot restart. Both are reads, so neither carries a mutation meta.

The registry is the frozen surface; feature_info narrows to what is implemented. On this tree the daemon serves 85 of the registry's 95 operations.

Registry-bound, not implemented

ten operations are bound in the registry and answer a typed feature_unavailable rather than being silently absent: act_intent_cancel, delivery_withdraw, manifestation_disable, manifestation_propose, participant_propose, participant_retire, participant_suspend, society_dissolve, society_hold and society_release. feature_info advertises exactly the implemented set, because a feature is advertised only when it is complete. Notably this means the deny, cancel and expiry branches of the act-intent machine are specified and descriptor-covered but not reachable on this daemon.

The MCP binding

Byom publishes a versioned MCP tool document (v0.1.1) with exactly two closed profiles, so a third profile cannot exist: three candidate tools and 34 participant tools. Tool names are closed per-profile enums, so a governance, runtime or admin operation offered as a tool name fails the meta-schema — deny-by-absence again, this time at the harness binding.

Per-tool input schemas are verbatim copies of the committed operation request schemas minus the channel envelope, and the conformance runner re-derives them and runs widening mutations against the document on every invocation. There are no placeholder tools: an operation whose owning bundle has not frozen is absent from the profile rather than present and null-bound.

Elicitation is never assent

The candidate profile is a sender-constrained binding scoped to one membership offer, its proposed manifestation, its control-domain binding and its onboarding fence. It is minted with the offer and closed server-side on admission, refusal, revocation or expiry, and it never converts in place — the participant channel is a new credential minted at admission. No human-authority, governance or admin tool exists on it, and a prompt through it does not become a position.

Limits on the wire

These are enforced before schema validation, as part of strict I-JSON acceptance, and advertised by protocol_info.

LimitValueNotes
Request body262,144bytes, inclusive — 256 KiB
Response body1,048,576bytes, inclusive — 1 MiB
Container nesting depth64levels; deeper input is rejected, never parsed recursively into a crash
JSON values per document65,536nodes, inclusive
Identifier128bytes of visible ASCII; opaque, never an alias
Events per page512every list requires one explicit page size

The full acceptance order, and what each failure is called →