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.
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.
{"version": "0.2", "op": "hello"}
There are exactly two answers, and they are structurally distinguishable.
{"outcome": "ok", "result": {"versions": ["0.2"], "surface": "participant",
"endpoint_incarnation": "inc-b597a0a54e5d9f10"}}
{"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.
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.
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.
| Surface | Channel | What 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.
| Class | What the closed meta looks like |
|---|---|
read | Never mutates and carries no meta at all — supplying one fails the closed schema. |
create | A mutation whose meta has no expected_revision member, so supplying one fails the schema. |
update | A current-head compare-and-set whose meta requires expected_revision. |
The complete registry, row by row, is generated on the reference page →
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.
What this daemon actually serves
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.
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.
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.
| Limit | Value | Notes |
|---|---|---|
| Request body | 262,144 | bytes, inclusive — 256 KiB |
| Response body | 1,048,576 | bytes, inclusive — 1 MiB |
| Container nesting depth | 64 | levels; deeper input is rejected, never parsed recursively into a crash |
| JSON values per document | 65,536 | nodes, inclusive |
| Identifier | 128 | bytes of visible ASCII; opaque, never an alias |
| Events per page | 512 | every list requires one explicit page size |
The full acceptance order, and what each failure is called →