Skip to content

Architecture

BNW is a local-first peer-to-peer event network. No node is authoritative. Every durable fact is an immutable, Ed25519-signed CBOR envelope addressed by the BLAKE3 digest of its complete signed bytes.

  • bnw-types owns wire/domain types and deterministic identifiers.
  • bnw-profile owns optional application-layer capability profile parsing and validation; the networking core does not interpret it.
  • bnw-inference owns provider-neutral inference documents, provider-local registrations, and bounded OpenAI-compatible and Anthropic HTTP adapters; it defines no replicated objects.
  • bnw-mcp owns provider-local MCP registrations and the bounded stdio client; it defines no replicated objects.
  • bnw-rest owns provider-local REST registrations built from OpenAPI 3 documents: operation parsing, pinned operation fingerprints, request building restricted to the registered server and public addresses, and OAuth 2 against the document’s endpoints. It defines no replicated objects; see REST.md.
  • bnw-http holds the HTTP plumbing these adapters share: endpoint rules, bounded response reading, secrets named by environment variable, private and sealed credential files with a cross-process refresh lock, and the loopback OAuth callback listener.
  • bnw-crypto owns device keys, signing, and the validation pipeline.
  • bnw-store owns SQLite persistence and derived local indexes.
  • bnw-protocol owns versioned direct-exchange messages and protocol names.
  • bnw-network wraps libp2p (QUIC, TCP+Noise, mDNS, Kademlia, GossipSub, Identify, Ping, request/response) behind BNW events.
  • bnw-sync contains transport-independent synchronization state.
  • bnw-node connects networking to validation and persistence.
    • It can also run inside another process through bnw_node::embedded::spawn, which returns a handle for requests, events, and shutdown.
    • A companion (NodeRole::Companion, for the mobile app) is a full device of its account for syncing, messaging, pairing, approvals, the catalog, and requesting invocations.
    • A companion does not run the agent runtime or its settle tick, the provider autopilot, offer renewal, or recovery. It answers no searches, and it refuses Provider* and agent-hosting requests.
    • Devices asking to join (claiming the account without an authorization) appear in the account view. On the node holding the account’s root key, AccountAuthorizeDevice signs their authorization. Only the node’s own identity may call it, and only for a device that claims the account.
    • Embedded nodes skip the local Unix sockets but keep the data-directory lock. Requests use the gateway’s JSON mapping (gateway::json_request, json_response).
  • bnw-cli is the local application.

Stored bytes hash back to their ContentId; received data is size checked, decoded, version checked, author/public-key checked, and signature verified before insertion. Inserts are idempotent. Gossip only announces availability; peers retrieve canonical bytes through /bnw/object/1. Sequence numbers and parent links—not timestamps alone—support event ordering.

Raw blobs are addressed independently from their signed metadata. They are written to sharded filesystem paths, transferred in resumable chunks through /bnw/blob/1, and atomically promoted from partial content only after the complete BLAKE3 hash matches. Kademlia provider records advertise availability but confer no authority.

Milestone 6 introduces opaque, domain-separated replication scopes. A validated object maps deterministically to one or more scope IDs: channel records and messages map to their channel scope, direct messages and private-blob manifests map to their participants’ inbox scopes, capability manifests and offers map to their capability scope, and older identity/trust/control records currently map to a compatibility global scope. Milestone 7 invocation requests, progress, cancellation requests, and results map to both participants’ inbox scopes. Scope IDs are routing and retention hints, never proof of membership or authority. SQLite schema 14 indexes the object-to-scope relation, backfills existing objects and capability namespaces, persists explicit scope subscriptions and discovery interests, maintains the local public-text search projection, and indexes invocation lifecycles.

/bnw/sync/6 compares author heads, fixed sequence-range hashes, and paginated inventories independently inside each scope. A node requests only the global compatibility scope, its own inbox, channels it has subscribed to, and capability scopes it explicitly follows. Objects may belong to multiple scopes, allowing an authorized recipient or archive node that retained an object to serve another included recipient later. Receivers derive scopes again from the validated payload and reject fetched objects outside their local retention set; they do not trust announcement metadata to choose what to persist.

A capability manifest is an immutable signed definition with a stable domain-separated CapabilityId, generic kind, subject, public descriptor CID, optional namespace, and optional public access-policy CID. If conflicting manifests reuse an ID, the lowest content ID is selected deterministically; this is convergence, not an endorsement. A provider offer binds that canonical manifest to sorted current endpoints and optional dynamic metadata for at most 30 days. Offers are self-assertions, filtered after expiry, and carry no availability or trust guarantee. A provider’s newest offer of a capability, by its signing sequence (then lowest content ID), replaces its earlier ones: catalogs, discovery answers, and new invocations use only that one, while an invocation already made stays bound to the offer it named. An offer lasting OFFER_WITHDRAWAL_SECS (1 second) or less is a withdrawal: it replaces the earlier offers and lapses at once, and the provider refuses invocations of offers it signed before it. Nodes that have not received the withdrawal still list the older offers until they expire; like every offer, they are hints. A descriptor can describe MCP, search, inference, archive, relay, or future application protocols without putting those schemas into BNW core types. Discovery kinds remain semantically separate from delegation permission strings. A publisher may describe itself; describing another subject is reported as authorized only when an active exact capability:publish delegation exists.

bnw.capability-profile/1 is an optional strict JSON application profile interpreted by bnw-profile and the CLI, never by replication or signature validation. It binds one operation to explicit input/output media types, optional schema CIDs, byte limits, and public or participant-encrypted handling. Endpoint and dynamic provider state remain in offers. Descriptor blobs are advertised by nodes retaining the capability scope and retrieved either after an explicit catalog inspection or as part of followed-scope processing, preserving compact default discovery. Participant-encrypted profile payloads reuse encrypted-blob manifests with exactly the requester and provider as recipients, so the existing inbox and ciphertext-transfer paths carry input and output without exposing their private metadata.

Milestone 7 adds a generic asynchronous invocation lifecycle above capability discovery. A requester signs a time-bounded request that names one capability ID, its canonical manifest CID, one active offer CID, a provider, and an input CID. The provider may append progress to an invocation-specific chain and sign a terminal completed, rejected, or failed result; successful completion requires an output CID. Progress and completion require an active exact capability:invoke:<capability-id> delegation issued by the named provider to the requester. The provider’s local policy may deny that permission or require an approval whose requester, approver, permission, and action ID all bind to the invocation. Authorized records parent the supporting delegation and approval-decision CIDs; rejected and failed results require no authority evidence. Each progress record also parents the selected previous update and any application-specific update CID. Same-sequence progress forks and conflicting provider results converge on the lowest content ID.

The requester may sign one or more competing cancellation requests; the lowest content ID is the canonical request. Cancellation is cooperative immutable evidence rather than deletion or revocation of an already-signed result. Once a local provider CLI sees cancellation it refuses new progress or successful completion but permits a rejected or failed acknowledgement. A terminal result has projection precedence over cancellation to make crossed-in-flight races converge without hiding either record. All invocation lifecycle objects replicate only through the requester and provider inbox scopes. The core does not interpret descriptors, execute tools or models, guarantee referenced blob availability, force an external process to stop, or prove result correctness. Invocation metadata is not end-to-end encrypted; applications should use recipient-encrypted objects for sensitive inputs and outputs.

The host-local API’s activity endpoint exposes a read-only bounded projection over those validated lifecycle objects. It filters by the local identity’s incoming or outgoing role, optionally excludes expired and terminal records, orders newest requests first, and returns no more than 32 entries. It uses the same deterministic lifecycle precedence as the CLI and returns only bounded progress metadata and content references. Reading activity cannot authorize, execute, cancel, retry, or publish an invocation.

Schema 19 adds idempotent signing mutations to that API. An invocation-create request supplies an exact capability, provider, input CID, optional offer and expiry, and a bounded caller idempotency key. Before signing, the daemon rejects locally blocked capabilities or providers and re-runs canonical-manifest, active-offer, strict descriptor/input-contract, participant, and expiry validation. A cancellation request names one existing invocation, an optional bounded reason, and its own idempotency key; the daemon verifies the canonical manifest/offer binding, requester identity, and absence of a terminal result or prior cancellation. Cancellation remains available when a provider is later blocked or the invocation expires because it closes existing work rather than authorizing new work. Each action kind durably claims its key and request hash before writing the signed object, then records the resulting CID. An identical completed retry returns the original action result even after surrounding network state changes; a different request using the same key is rejected. A claimed key without a recorded result is treated as an ambiguous interrupted attempt and cannot automatically sign another object. This favors at-most-once signing over silent duplication.

Schema 20 adds a host-local index for validated public channel messages. A channel feed is read from durable validated objects rather than raw GossipSub events, orders entries by (created_at, author, sequence, content_id), and returns no more than 32 entries after an exclusive message-CID cursor. The daemon resolves that cursor locally and rejects a cursor from another channel. It projects at most 16 KiB of message text, marking older oversized messages as truncated. Public posting is also a local API action: the daemon requires the canonical local channel record, durably claims a bounded idempotency key before signing, and returns the original message on an identical retry. A reply names an existing locally validated message in the same channel and includes that message as an envelope parent. This feed is the durable foundation for future human and agent channel consumers; it does not yet grant an agent permission to observe, reply, invoke tools, or spend resources.

Agent channel participation is expressed as ordinary delegation records carrying canonical AgentPermission strings, so the network validates only signatures and structure while hosts interpret the strings. An agent accepts an operator by delegating agent:control:<agent> to it. Operator-issued tiers (agent:observe, agent:reply, agent:invoke, and agent:spend with a 24-hour ceiling) count only from operators the agent has accepted. channel:reply:<channel> admits an agent to post in a channel. It counts from the channel’s owner, or from a principal the owner delegated channel:grant:<channel>, and only while that delegation is live. Every check reads live, unrevoked, unexpired delegations at the time of the check, so revocation takes effect at the next action. Feed entries by agent principals carry their operators and admission state, and the feed hides unadmitted agents unless a caller asks for them. The page cursor follows the last message scanned, so hidden messages never stall paging. The agent runtime builds on these checks, and tool use and spending follow in later slices.

The agent runtime keeps host-local state in schema 21:

  • Cursors. Per-channel cursors follow local arrival order (the channel index’s row order), not creation time, so messages that replicate late with earlier timestamps still trigger the agent. Messages present before observation began, or created more than an hour before they arrive, never trigger it.
  • Triggers. Each trigger moves through pending, proposed, posted, denied, expired, or abandoned.
  • Polling. An AgentPoll records mentions and replies to the agent, abandons triggers older than 5 minutes, and returns the oldest pending one with at most 32 earlier messages.
  • Proposals. AgentPropose checks the observe, reply, and admission grants, the 30-replies-per-hour channel limit, and the depth limit of three consecutive agent-authored messages. It then encrypts the draft for exactly the agent and the first linked operator. Its approval request keeps the action ID as its only parent, as validation requires. That action ID is a hash of the channel, the trigger, and the draft manifest, so approval binds exactly one text.
  • Autonomy. An operator-signed agent:autoreply:<channel> grant lets AgentPropose post immediately after the same grant, rate, and depth checks, with no draft or approval request. Autonomy is a signed, expiring, revocable grant rather than host configuration: the host running an agent cannot enable it alone, and the reply’s audit event names the grant in place of an approval decision.
  • Tool use. Poll responses list the tools the agent may use: capabilities with a live operator agent:invoke grant, a provider capability:invoke grant or an offer whose audience may include the agent, a local descriptor, and a JSON input contract. AgentProposeTool checks the per-message limit of 4 calls, the per-hour limit of 30, and the grants. With an operator’s agent:autoinvoke:<capability> grant, the call is recorded under its draft’s ID without an approval request, and runs at the next settle. The autoinvoke grant is checked again at that point, like every other grant, and the audit event is marked autonomous and links it in place of an approval. It encrypts the capability, provider, and input for the agent and operator, and files an approval request. Its action ID hashes the channel, trigger, capability, provider, and draft. Schema 22 records each call as proposed, running, completed, failed, denied, or expired. The trigger waits while a call is pending, and its 5-minute timeout counts from its last change.
  • Tool execution. On approval, the daemon rechecks the grants and encrypts the input for the provider. It then creates the invocation through the same action the local API uses, so a runner cannot run a tool that was not approved. When the invocation finishes, or the call is denied or expires, the trigger returns to the runner with the outcome. The model receives the result as untrusted content.
  • Settlement. Every 5 seconds the daemon settles proposals. An approved one is rechecked against live grants and limits, posted as an ordinary channel reply whose only parent is the trigger, and recorded in a signed audit event whose parents are the reply, trigger, grants, request, and decision. Denials and expiries are audited too.
  • The runner. bnw agent run is a local API client. It sends the bounded transcript to an inference capability through daemon staging, invocation, and result retrieval, and proposes the returned text.

The reference MCP adapter sits above that core boundary. A local, non-replicated registry maps one tool.mcp capability ID to an absolute stdio server executable, fixed arguments, a fixed tool name, an environment-name allowlist, and a timeout. The requester controls none of those fields: its participant-encrypted JSON object supplies only tools/call arguments. The adapter launches the program without a shell, copies only baseline process variables and explicitly named provider variables, verifies that the server advertises the configured tool, and stores the complete MCP result as participant-encrypted JSON. Exact authorization, cancellation, expiry, and terminal state are checked before launch and again before completion. Signed progress makes execution visible, while a persistent local claim prevents the same provider device from automatically replaying one side-effectful invocation after a crash. The adapter is a process boundary, not a sandbox or a correctness oracle.

Capability discovery uses an exact kind and optional exact principal or channel namespace. The selector is domain-separated and split across 16 shards using the capability ID, preventing every publisher of a popular kind from writing one provider key. Kademlia contains only its ordinary ephemeral provider pointers. A candidate peer answers /bnw/capability/1 with at most 32 canonical manifest IDs and 32 active offer/manifest pointers for one selector shard. A requesting node deduplicates IDs, caps each selector at 256 new manifest and offer candidates per run, retrieves the signed envelopes through /bnw/object/1, and independently validates their hashes, signatures, types, selector/shard relationship, offer expiry, and offer-to-manifest binding before indexing them. The added offer field is backward-compatible: older peers omit it and ignore it. Discovery alone does not retain the capability scope; the user must follow a selected capability before its complete history synchronizes. Nodes explicitly serve Kademlia requests but gain no authority from doing so.

The unified catalog is a bounded local projection above that discovery protocol, not a new replicated object or global index. Persistent exact-kind watches cause the node to run the existing 16-shard lookup. Catalog pages then join canonical validated manifests with locally available descriptors, active preview or followed offers, provider/protocol summaries, explicit follow state, publisher authority, and ephemeral local reachability observations. Pages are ordered by stable capability ID and use an exclusive cursor, with no more than 32 entries per response. Inspecting a row may request its public descriptor through the ordinary blob-provider network without retaining the capability scope; following and unfollowing are separate daemon mutations. Unfollowing removes scope retention and reconciles the live GossipSub subscription immediately, but does not delete already validated content-addressed objects. Missing descriptors, expired offers, and unobserved peers remain visible as absence rather than invented state. A catalog entry is therefore useful application data but never a recommendation, reputation score, or source of authority.

Reachability is evaluated when the daemon answers a catalog request and is never persisted or replicated. A syntactically valid bnw.peer endpoint is local when it names this node, connected when the libp2p swarm currently has that peer, and disconnected otherwise. Non-libp2p application endpoints are unknown; the catalog does not probe arbitrary URLs or execute MCP, inference, or tool requests. Provider aggregation prefers local, then connected, then unknown, then disconnected evidence. This signal describes a transport path only and must not be treated as application readiness, authorization, trust, or successful invocation evidence.

Schema 17 adds host-only catalog preferences. A user may favorite or hide a capability and prefer one or more providers. These records are neither content-addressed objects nor replication inputs: they carry no signature, scope, network authority, or portable reputation. Hidden rows are omitted from ordinary pages but recoverable through an explicit include-hidden query. Preferred offers sort first in detailed provider views, without overriding deterministic validation, delegation, approval, reachability, or future invocation policy.

Schema 18 adds host-only trust annotations for capability IDs and provider principals: neutral, trusted, caution, or blocked with an optional bounded private note. They are local policy inputs and never signed, content-addressed, replicated, or aggregated into a network reputation value. Catalog projections display them separately from signed publisher authority and live reachability. The schema 19 daemon invocation action enforces blocked subjects before signing.

NAT traversal stays below the BNW event boundary in bnw-network.

  • Default relay. A standalone node given no relay uses bnw_node::DEFAULT_RELAYS: BNW’s public relay as /dnsaddr/relay.cellmobs.com, pinned to its peer ID. --relay replaces it, --no-default-relay removes it, and relay servers never use it. The default is a transport hint like any relay, not an authority.
  • Advertising again. A new (non-renewal) relay reservation makes the node forget what it advertised in and asked of the DHT and refresh. It then advertises its blobs and capabilities again, covering records sent before start-up connectivity or dropped by a relay restart. Kademlia also republishes provider records hourly (PROVIDER_REPUBLISH_INTERVAL).

A node given --relay dials the relay and, once a direct connection exists, listens on <relay>/p2p-circuit so the libp2p relay client requests and renews a circuit-relay-v2 reservation over that connection. Waiting for the connection matters. The relay client’s own dial is dropped while another dial to the same relay is in flight, for example when the relay is also a bootstrap peer. Every 30 seconds, the node re-dials relays and re-requests any reservation it lacks, such as after a relay restart. It also retries immediately when a reservation listener closes. A relay may also be configured as /dnsaddr/<host>. The node resolves _dnsaddr.<host> TXT records in a background task, follows nested records within 8 lookups and 16 addresses, and keeps only addresses that end in a peer ID, optionally filtered by a trailing /p2p/<peer-id>. It retries failed lookups on the relay retry interval and after network changes. Without a trailing peer ID, the relay’s identity comes from DNS, so DNS control implies choosing the relay. That grants metadata visibility and the ability to deny relay service, but not access to end-to-end encrypted relayed streams. With a pinned peer ID, DNS is only a locator and the handshake rejects any other key. Relay addresses are grouped by relay peer and dialed one at a time, QUIC before TCP. The node keeps a single direct connection per relay, preferring QUIC, and closes redundant connections, so the reservation runs over QUIC whenever UDP is reachable. QUIC’s own keepalives hold the NAT mapping, detect a dead link in about 10 seconds, and let identify learn the node’s public UDP address for DCUtR.

Connection admission is bounded in bnw-network by libp2p’s connection-limits and memory-connection-limits behaviours. Ordinary nodes allow 32 incoming and 64 outgoing handshakes in progress, 128 established incoming connections, 256 established connections in total, and 4 per peer. The per-peer cap leaves room for a relayed connection plus direct attempts during DCUtR. Relay servers raise incoming handshakes to 128, established incoming connections to 1024, and the total to 1100. Established-connection limits are checked after the handshake, so a refused peer may briefly observe the connection before it closes. New connections are refused while the process uses more than 80% of system memory. Refusals are counted for local status only. They are not persisted and imply nothing about the refused peer.

GossipSub runs with application-level validation. The network task decodes each announcement and accepts it only if it fits in 16 KiB, names at most 64 scopes, and arrived on the topic of one of its own scopes. Honest nodes publish only on their scopes’ topics. Rejected messages are neither delivered nor forwarded and count as invalid deliveries (P4) in peer scoring. P4 is weighted at -20 per squared count and decays by 1% per second. Positive topic credit is capped at 50, so three invalid messages graylist any peer under the default thresholds. Delivery-rate penalties (P3, P3b) are disabled because scope topics are quiet, and IP colocation (P6) is disabled because honest peers commonly share a NAT address. A per-peer token bucket (burst 200, 50 per second) ignores excess messages without penalty. Validation cannot establish that an announced object exists. Retrieval and validation of the object itself remain the authority, as before.

Connection liveness is also handled in bnw-network. libp2p ping only reports failures, so the node closes a connection itself when a failed ping is reported (every 10 seconds, with a 10-second timeout). A link that died silently, for example after switching Wi-Fi networks, is therefore detected in roughly 20–30 seconds rather than after the operating system’s TCP timeout. The node keeps an in-memory, bounded set of up to 32 recently connected peers and their dialed addresses. After an unexpected disconnect it redials such a peer after 5 seconds, 30 seconds, and 2 minutes, then stops. Connections that libp2p closes for idleness are not redialed. When a local listen address expires and a new one appears, the node treats it as a network change: it retries relays and restarts redials for all tracked peers at once. Reconnection state is transport-local, never persisted or replicated, and confers no trust. A node given --relay-server serves reservations and circuits within fixed limits: 64 circuits, 10 minutes, and 16 MiB per circuit. Relays must hold an external address, asserted by the operator or confirmed by AutoNAT, before they grant reservations. DCUtR attempts to upgrade relayed connections to direct ones. AutoNAT v1 estimates public or private reachability and confirms observed external addresses. Identify-reported listen addresses of Kademlia-speaking peers are added to routing, capped at 16 per peer, so relayed addresses propagate. Relays, reachability estimates, and identify addresses are unauthenticated transport hints. They confer no authority, are not persisted beyond ordinary peer history, and do not change validation. Relayed streams keep Noise end-to-end encryption between the two peers.

Descriptors, global inverted indexes, unbounded provider sets, load streams, and application payloads never belong in the DHT. Search and inference remain consumers of the generic capability layer and must verify retrieved signed objects independently.

Local keyword search is a rebuildable SQLite FTS5 projection over validated plaintext public objects already in the store. It indexes public identity and principal names, channel records and unencrypted messages, blob names and MIME types, legacy agent advertisements, and capability kinds. Encrypted messages and blobs, direct messages, authority records, approvals, and audit details are not indexed. Queries contain at most eight terms, may filter one exact replication scope, and return no more than 32 hits.

/bnw/search/1 lets a caller send that bounded query to one selected peer, normally learned from a followed search.keyword capability offer. Providers return content references plus non-authoritative display metadata, enforce a per-caller request budget, and never push matching objects automatically. Local daemon requests and responses bind a random nonce to the expected peer.

Federated search remains a bounded client-side composition over that same protocol. Only active bnw.peer endpoints from explicitly followed search capabilities qualify. A domain-separated hash of the query and peer ID selects at most eight unique non-local peers, avoiding permanent lowest-ID preference without creating a network-wide broadcast. Requests run concurrently under one deadline. Results deduplicate by content ID, retain source-peer provenance, and merge deterministically with reciprocal-rank fusion; failures produce partial results. Provider agreement, remote metadata, and ranking remain suggestions rather than consensus or trust until a selected object is retrieved and validated.

Selected-result retrieval reuses /bnw/object/1 rather than trusting the search response or adding another object transport. The host-local request is correlated through the network by nonce, expected peer, and content ID, so background replication of the same CID cannot complete it accidentally. Hash, signature, payload, author, and scope derivation checks all run before the object enters SQLite; the CLI independently validates the stored bytes again before presenting verified fields. An explicit request may cache one plaintext public search object outside the node’s retained scopes, but it does not subscribe to that scope. This exception is type-limited to public search projections and cannot import private messages, encrypted manifests, authority records, approvals, or audit objects outside their ordinary retention rules.

Principals describe cryptographic subjects (human, organization, device, service, or agent). Delegation records grant application-neutral capability strings with optional validity windows; revocations are signed by the original issuer. The network verifies records and reconstructs trust history, while local applications retain responsibility for policy and human approval.

Milestone 4 principals may publish a domain-separated X25519 public key derived from the device’s secret key material. Private channel messages contain one XChaCha20-Poly1305 authenticated ciphertext per recipient. The encryption context binds the channel and signed author; the outer immutable envelope authenticates the complete recipient set and ciphertexts. Message content is confidential from non-recipients, while routing metadata remains public. This first protocol deliberately avoids claiming MLS-style forward secrecy, membership hiding, or post-compromise security.

Direct messages use the same sealed-payload construction without requiring a channel. Each signed record contains exactly one ciphertext for each distinct reader device (the two participants, or with accounts all of their member devices, as described below), binds sender and recipient into the authenticated-encryption context, and may reference the preceding message as an event-DAG parent. Nodes subscribe to a deterministic recipient inbox topic for live delivery; ordinary author-inventory synchronization provides eventual recovery after an offline period.

Private blobs are encrypted locally with a random XChaCha20-Poly1305 content key before the ciphertext enters the filesystem object store. A signed encrypted-blob manifest exposes only the owner, ciphertext content ID, ciphertext size, and recipient identifiers. Each recipient receives a sealed access payload containing the content key, base nonce, encryption format, original filename, media type, plaintext hash, and size. New blobs use 256 KiB plaintext chunks with a unique derived nonce and authenticated context binding the chunk index, total size, chunk size, and actual chunk length. Encryption, filesystem import, hash verification, and decryption are streaming. Retrieval writes to a temporary sibling and creates the requested output with an atomic no-overwrite hard link only after every chunk tag and the complete signed plaintext hash verify. Ciphertext transfer remains independently resumable through /bnw/blob/1. Access records without an encryption-format field decode as the Milestone 4 single-shot format, preserving legacy reads.

Capability advertisements are self-assertions. Attestations are signed third-party claims with optional evidence links and expiry. Neither is automatically trusted: local policy first requires a live, non-revoked delegation for the exact capability, then applies an allow, deny, or human-approval rule. Audit events record signed outcomes and link related evidence through event-DAG parents; they describe actions but do not prove that an external action actually occurred.

Approval requests name a requester, approver, capability, exact action ID, and expiry. Approval or denial decisions are separately signed by the named approver and parent the request. Policy evaluation accepts an approval only when the requester, capability, action, authority, validity window, and referenced request all match; this prevents a valid approval from being replayed for another action.

An account has a stable ID equal to its initial root identity, while successive account-root records can replace the active root key. Creation is self-signed. Rotation must be signed by the preceding root; recovery must be signed by a principal designated in the preceding root. Each valid transition increments the root sequence and parents the preceding root record. If concurrent transitions fork a root, the lowest content ID deterministically selects the one canonical child before later descendants are considered. Device authorizations and revocations are signed by the current root. A root transition conservatively invalidates authorizations from an earlier root, and the CLI reauthorizes the rotating or recovering device under the new root. The current implementation supports any one designated recovery principal, not a threshold policy.

Account membership needs both halves. The current root’s active authorization of a device counts only while the device’s own latest principal record claims that account; the claim is an optional account field, omitted when empty, so older records keep their bytes. Neither side can bind the other alone. Membership is evaluated at read time from locally validated records, so the two halves may arrive in either order. At most MAX_ACCOUNT_DEVICES (8) members count, the most recently authorized first.

Account addressing.

  • Recipients. An account used as a recipient stands for its member devices, and a member device stands for its whole account. Senders seal new material for the members with published encryption keys. A sender that is itself a member also seals for its account’s other members.
  • Direct messages. A direct message involving an account carries optional sender_account and recipient_account hints.
    • It may be sealed for up to 16 sorted, unique devices that include the sender and recipient. The store accepts extra readers only when a hint is present.
    • Its scopes are the inboxes of all readers, so each device keeps retaining only its own inbox.
    • The hints are bound into the ciphertexts’ associated data (bnw direct message v2). Readers still re-derive accounts themselves when filing conversations.
    • Messages without accounts keep the exact two-party form and associated data.
  • Private channel posts and private blob manifests already accept any bounded recipient set, so accounts expand into devices there without a format change.
  • Revocation. Revocation cannot erase material already delivered to a device.

Channel ownership.

  • Binding. A channel record carries the nonce its ID was derived from, and the store accepts it only if channel_id == derive(owner, nonce). The owner is therefore fixed by the ID, and nobody else can publish a record that names a different owner.
  • Who may sign. Records are kept per signer.
    • The record that counts is the lowest content ID among those signed by the owner or, for an account owner, a current member device.
    • Failing that, it is the lowest signed by a device the account once authorized, so a channel stays described after its creating device is revoked.
    • Records from anyone else are ignored.
  • Administration. Owner checks (admitting agents, channel:grant, the owned flag) accept the owner or any current member of an owning account.
  • Post attribution. Public and encrypted channel messages carry the account their device posts for. They are covered by the device’s signature, and readers show the account only after verifying the membership themselves.

Routing by data handling.

  • Declarations. A capability offer may carry handling: egress (local or remote), region, and retention days. It is validated on insert and is self-asserted, like execution claims.
  • Routing policy. Requesters keep a local, unreplicated routing policy. Automatic provider selection (the agent runtime, portable-agent resolution, the catalog’s excluded marker) skips offers whose declaration fails it.
  • Configured providers. An explicitly configured provider that fails the policy is refused.

Capability audiences.

  • The policy. A provider’s local policy for each capability it publishes names an audience:
    • grants, the default: only principals it granted capability:invoke one by one.
    • account: its own account’s devices, and agents the account operates. An agent counts when this node hosts it, or when a device of the account granted it an agent: permission. The agent’s own operator link never counts, since the agent signs it and could name anyone.
    • contacts: the account, plus contacts and their accounts’ devices.
    • anyone.
  • Grants on first use. When an invocation arrives from a requester in the audience that holds no grant, the provider signs it an ordinary capability:invoke delegation lasting 30 days, and then treats the invocation like any other. The authorization rule is unchanged: every execution is still backed by a signed delegation from the provider, and provenance bundles verify as before.
  • Narrowing. The provider records which grants it signed for an audience (schema 32). Narrowing the audience revokes those it no longer covers. A requester that left the audience, for example a removed contact, has its grant revoked at its next invocation. Individual grants are never touched.
  • The offer hint. An offer may carry access (account, contacts, or anyone), signed with the offer. Renewal re-signs an offer whose hint no longer matches the policy. Requesters treat it as a hint:
    • an agent lists a tool whose offer is open to anyone;
    • it also lists one open to the provider’s account or contacts when the provider (or its account) operates the agent or is a local contact. The provider still decides on arrival, and an unadmitted call waits unauthorized, as before.

Offer features. An offer may list up to eight features, short lowercase names of what the provider’s backend can do, validated on insert. inference-tools says the model takes native tool calls. It is recorded when an inference backend is registered, and renewal re-signs an offer whose features no longer match. Features are self-declared hints: a requester that relies on one still gets a signed rejection if the backend refuses.

Agent invites. An invite for an agent comes only from a channel’s administrator. It admits the agent (channel:reply), and in a members-only channel it signs membership. The invite then asks the agent’s operator:

  • An agent the inviter operates is granted agent:observe and agent:reply for the channel directly.
  • Anyone else’s agent. The inviter signs an approval request to the agent’s approving operator, with capability agent:observe:<channel> and details naming the channel and agent. Its action ID hashes bnw.agent-invite/1, the agent, and the channel. The operator’s node verifies all three and that the approver operates the agent. Approving signs the two grants from the deciding device and follows the channel.

A node follows every channel its hosted agents observe, and only their operators grant that.

Sharing several MCP tools. A capability still runs one pinned tool. ProviderMcpShareTools takes a capability of the local identity, and tool names from the last discovery the node ran for it. For each tool, it publishes a tool.mcp capability with the tool’s own description and input schema. It registers the tool on the same transport with the discovered schema hash pinned. The registration’s oauth_from names the capability whose OAuth credential file it uses, so one server keeps one set of tokens. Each tool then gets its policy, its audience, and a renewing offer. A tool already registered on the same transport by this node is updated instead.

Sharing a local model. ProviderModelsDetect starts a job that probes the fixed loopback addresses of Ollama, LM Studio, and llama.cpp. Each probe has a short timeout, a bounded reply, no proxy, and no redirects; nothing the network supplies is ever probed. ProviderModelShare is the provider setup steps in one call: create the inference.model capability (or reuse the one registered for the same endpoint and model), register the openai-chat backend with local egress, set the policy and audience, and offer with renewal. It accepts only an http chat endpoint on a loopback address.

History transfer.

  • The bundle. A member device may encrypt its readable direct messages and private posts for another member device, as a private payload with the application/vnd.bnw.history+json media type.
  • What is imported. Recipients import only bundles owned by a current member of their own account and naming that account. Imported entries are stored separately and shown as transferred.
  • What it proves. Their text is the sending device’s copy, not a re-verified original.

Settings sync.

  • The record. Local settings are not replicated in general. The exception is AccountSync records: a device’s snapshot of its synced settings, sealed for its account’s current member devices, and scoped to the account’s inbox. Each device keeps its own sequence chain.
  • What a snapshot holds. It is a bounded list of items, each a key, a value (or None once removed), a logical clock, and the device that set it. Snapshots hold at most 1024 items and 96 KiB of plaintext, so every member’s ciphertext fits one envelope.
  • Merging. Readers merge the latest snapshot of each current member device, and the higher (clock, device) wins for each item, so devices converge without timestamps. Snapshots from non-members are ignored.
  • Recording and publishing. Local changes are found by comparing current settings with the synced items. They are published when they change or when the account’s devices change.

Search access.

  • Who is asking. A search responder identifies the requester by the device key its connection authenticated, which the peer ID encodes.
  • Policy. It answers according to a local policy:
    • off.
    • contacts, the default: contacts and their accounts’ devices, the responder’s own account devices, and holders of capability:invoke on a search.keyword capability the responder published.
    • public: anyone, rate-limited.
  • What is answered. Answers exclude members-only channels’ records and posts.
  • What a search.keyword offer is. It is an advertisement plus an access extension, not an execution backend. Offering one makes the node selectable by followers and, under contacts, admits its invoke-grant holders. Answers still come from the node’s own index under its policy. Its readiness for offer renewal is “search is not off”.
  • Other kinds. Kinds other than tool.mcp and inference.model never run on the autopilot. Custom kinds are answered by hand through the same signed result path.
  • Whom a node asks. A requester fans out only to connected devices it trusts (contacts and its own account) and to followed search providers, so query text is never sent to arbitrary peers.

Contacts. Contacts are a local, unreplicated address book. A contact link names an ID (the only trusted part) and at most two address hints. Nodes dial hints of unconnected contacts on start and each minute, bounded to 16 contacts.

Members-only channels.

  • Recording the mode. A channel record may carry access: Members, fixed at creation.
  • Membership. Members are the owner (or an owning account’s current devices) and principals holding a live channel:post:<channel> delegation from the owner’s signers or from a channel:grant holder’s signers. A delegation to an account covers its member devices. Membership is evaluated when read, so revocation ends it.
  • Encryption. Members post encrypted channel messages, sealed for every member’s devices.
  • Filtering. Nodes refuse to sign public posts to such channels, and feeds show neither plaintext posts nor posts from non-members.
  • What stays public. The record (name and access mode) replicates with the channel scope and stays readable by anyone holding the ID.
  • Revocation. A membership grant stops counting when revoked by its issuer, or by any current administering signer (the owner’s signers or a channel:grant holder’s signers). The store keeps such third-party revocations, and membership evaluation decides whose count.
  • Agents. Agents participate as members. Triggers and context come from the encrypted posts sealed for them, and replies are encrypted posts.

Account invocations. Invocations, progress, results, cancellations, and inference claims carry an optional requester_account: the requester sets it, and the provider copies it. Their scopes add that account’s inbox, which each member device retains alongside its own. The field is a routing hint; membership is checked when a device reads. Providers encrypt outputs for every member device of the requester’s account. A provider’s check of an output before signing accepts exactly the requester, itself, and those member devices as recipients; inputs must still be sealed for exactly the requester and the provider.

Protocol 2 boundary. Envelopes carry protocol version 2, and objects of other versions fail validation. A store holding protocol 1 objects refuses to open, and bnw reset moves such a directory’s history aside while keeping its keys.

Account authority. Decisions and grants are still signed by one device each, as that device; what changes is whose signatures count.

  • Approvals. An approval request may name an account as approver. The decision that counts is the latest decision of each signer that is the approver itself or a current member of the approver’s account. A denial from any of them wins; otherwise the lowest content ID wins.
  • Operators. An agent’s operator may be an account. Operator-tier grants count from the account’s current members, and drafts are sealed for every member.
  • Capability grants. A delegation to an account supports each current member device’s actions.
  • When membership is checked. Membership is evaluated when a decision or grant is used, not when it was signed. A device that leaves or is revoked stops contributing, including to approvals that have not settled.
  • Provenance. Bundles include the account root history, the relevant device authorization, and the device’s claim, so an offline verifier can re-check membership.

These diagrams show the principal trust boundaries. Network responses remain hints until the receiving node validates the referenced signed bytes itself.

Selective replication and partition repair

Section titled “Selective replication and partition repair”
sequenceDiagram
title Selective scope synchronization
participant NodeA
participant NodeB
participant StoreB
NodeB->>NodeA: Request heads for retained scope
NodeA-->>NodeB: Per-author heads
NodeB->>NodeA: Request divergent author ranges
NodeA-->>NodeB: Fixed range hashes
NodeB->>NodeA: Request divergent range inventory
NodeA-->>NodeB: Paged content IDs
NodeB->>NodeA: GET CID via /bnw/object/1
NodeA-->>NodeB: Canonical signed bytes
NodeB->>NodeB: Verify hash, signature, payload, scopes
NodeB->>StoreB: Persist validated object

The requester initiates this exchange independently for every scope it retains. Announcement metadata cannot widen retention: NodeB derives the object’s scopes again before persistence.

Capability discovery and explicit following

Section titled “Capability discovery and explicit following”
sequenceDiagram
title Bounded capability discovery
participant ProviderCLI
participant ProviderStore
participant ProviderNode
participant DHT
participant RequesterNode
participant RequesterStore
participant RequesterCLI
ProviderCLI->>ProviderStore: Store signed capability manifest
ProviderCLI->>ProviderNode: Wake local node
ProviderNode->>DHT: Advertise selector shard
RequesterCLI->>RequesterNode: Watch exact kind through local API
RequesterNode->>RequesterStore: Persist discovery interest
RequesterNode->>DHT: Find providers by shard
DHT-->>RequesterNode: Candidate peer pointers
ProviderCLI->>ProviderStore: Store short-lived provider offer
RequesterNode->>ProviderNode: Request bounded catalog candidates
ProviderNode-->>RequesterNode: Manifest CIDs and active offer pointers
RequesterNode->>ProviderNode: GET manifests/offers via /bnw/object/1
ProviderNode-->>RequesterNode: Signed canonical bytes
RequesterNode->>RequesterStore: Validate selector, signatures, links, expiry
RequesterCLI->>RequesterNode: Inspect selected capability
RequesterNode->>DHT: Find descriptor blob providers
RequesterNode->>RequesterStore: Verify and store descriptor bytes
RequesterCLI->>RequesterNode: Follow selected capability scope

The DHT carries only bounded provider pointers. Discovery imports matching manifests but does not subscribe to their histories; following is an explicit local choice. Catalog reads join only locally validated state and never query every peer on demand.

sequenceDiagram
title Federated search with selected retrieval
participant RequesterCLI
participant RequesterNode
participant ProviderA
participant ProviderB
participant LocalStore
RequesterCLI->>RequesterNode: Submit bounded keyword query
RequesterNode->>ProviderA: Query /bnw/search/1
RequesterNode->>ProviderB: Query /bnw/search/1
ProviderA-->>RequesterNode: Unverified result references
ProviderB-->>RequesterNode: Unverified result references
RequesterNode-->>RequesterCLI: Deduplicated ranked references
RequesterCLI->>RequesterNode: Fetch selected peer and CID
RequesterNode->>ProviderA: GET CID via /bnw/object/1
ProviderA-->>RequesterNode: Canonical signed bytes
RequesterNode->>RequesterNode: Verify hash, signature, payload, scopes
RequesterNode->>LocalStore: Cache validated public object
RequesterNode-->>RequesterCLI: Report fetch completion
RequesterCLI->>LocalStore: Read and validate stored bytes
LocalStore-->>RequesterCLI: Verified object fields

Provider agreement and ranking never establish truth. Verification occurs only after retrieving the exact content-addressed object from a selected source.

sequenceDiagram
title Capability invocation with exact-action approval
participant RequesterCLI
participant RequesterStore
participant RequesterNode
participant ProviderNode
participant ProviderCLI
participant ProviderStore
RequesterCLI->>RequesterStore: Sign and store invocation
RequesterCLI->>RequesterNode: Wake local node
RequesterNode->>ProviderNode: Replicate through provider inbox scope
ProviderNode->>ProviderStore: Validate and persist invocation
ProviderCLI->>ProviderStore: Check exact provider-issued delegation
ProviderStore-->>ProviderCLI: Approval required by local policy
RequesterCLI->>RequesterStore: Sign approval request for invocation CID
RequesterNode->>ProviderNode: Replicate approval request
ProviderCLI->>ProviderStore: Sign approval decision
opt Provider reports intermediate state
ProviderCLI->>ProviderStore: Append authorized progress
ProviderNode-->>RequesterNode: Replicate signed progress
end
alt Requester asks provider to stop before a result
RequesterCLI->>RequesterStore: Sign cancellation request
RequesterNode->>ProviderNode: Replicate cancellation request
ProviderCLI->>ProviderStore: Sign rejected or failed acknowledgement
else Provider completes
ProviderCLI->>ProviderStore: Sign completed result with evidence CIDs
end
ProviderCLI->>ProviderNode: Wake local node
ProviderNode-->>RequesterNode: Replicate result through requester inbox
RequesterNode->>RequesterStore: Validate and persist result
RequesterCLI->>RequesterStore: Read deterministic terminal result

The permission is capability:invoke:<capability-id>, the delegation must be issued by the named provider, and any approval must bind the same requester, provider, permission, and invocation CID. Progress is an immutable provider chain; cancellation is a cooperative requester signal. A raced terminal result has deterministic projection precedence while all signed evidence remains stored. BNW coordinates signed intent and evidence; the provider application or optional adapter executes the tool or model and the requester still decides whether the output is credible.

sequenceDiagram
title Reference MCP adapter trust boundary
participant Requester
participant ProviderStore
participant ProviderCLI
participant LocalRegistry
participant MCPServer
Requester-->>ProviderStore: Replicated signed invocation + encrypted JSON
ProviderCLI->>ProviderStore: Verify binding, authorization, state, payload
ProviderCLI->>LocalRegistry: Resolve capability to fixed local configuration
LocalRegistry-->>ProviderCLI: Program, arguments, tool, env names, timeout
ProviderCLI->>ProviderStore: Append mcp.accepted and mcp.calling
ProviderCLI->>MCPServer: Launch stdio process and initialize
ProviderCLI->>MCPServer: tools/list; verify fixed tool
ProviderCLI->>MCPServer: tools/call with decrypted JSON object
MCPServer-->>ProviderCLI: CallToolResult
ProviderCLI->>ProviderStore: Recheck authorization, expiry, cancellation
ProviderCLI->>ProviderStore: Encrypt result for both participants
ProviderCLI->>ProviderStore: Append completed result + evidence CIDs

The requester crosses the local execution boundary only through the validated JSON argument object. It cannot select a program, add process arguments, choose a different tool, inject environment values, or extend the provider’s timeout.

Remote Streamable HTTP registrations use the same provider-local capability mapping. HTTPS is mandatory except for loopback testing. OAuth secrets are referenced by environment-variable name, authorization-code tokens are encrypted to the provider device identity, and neither is signed or replicated. Token refreshes hold a per-credential lock (an in-process mutex plus an OS file lock), so concurrent calls that share a sign-in, in the daemon or the CLI, never spend one rotating refresh token twice. Sign-in health is kept in a plaintext <credentials>.status file next to the sealed credentials; it holds no secrets, so readiness is computed without the device key. A call that the server refuses (401 after a refresh, a rejected refresh token, or 403 insufficient_scope) marks the sign-in as needed, which pauses offer renewal and makes new invocations wait before their at-most-once claim. A call already claimed when the refusal arrives is answered with a signed failure naming the sign-in problem, because its effects cannot be known. Binding snapshots the chosen tool’s input/output schema hash so a server-side schema change fails closed until the provider explicitly reviews and rebinds it. Decrypted invocation payloads may leave the device only when the provider registered the endpoint with the explicit --allow-remote-execution opt-in. Execution records the destination origin, fixed tool, and schema hash in signed progress, never follows MCP HTTP redirects, and does not log payload or credential contents.

Milestone 8 begins with bnw.inference-profile/1, a separately versioned descriptor carried by the existing generic capability manifest. It binds the inference.model kind to the fixed inference/generate operation and describes a provider-neutral model ID, optional revision, optional immutable model or model-manifest CID, bounded input/output modality sets, optional context and output token limits, and the same media-type, schema, byte-limit, and privacy contracts used by other invocations. Existing profiles remain byte-for-byte valid; the inference fields do not silently extend bnw.capability-profile/1.

Inference requests reuse capability discovery, expiring offers, exact provider-issued delegation, approval, participant-encrypted payloads, progress, cancellation, and terminal results. The descriptor is a signed provider claim, not proof that a particular model executed or that an answer is correct. Dynamic load, price, and capacity belong in expiring offer metadata rather than the stable model descriptor. Model execution and credentials stay behind a provider-local adapter; requesters must never select the provider’s runtime endpoint or replace its configured model.

sequenceDiagram
title Reference inference adapter trust boundary
participant Requester
participant ProviderStore
participant ProviderCLI
participant LocalRegistry
participant ModelEndpoint
Requester-->>ProviderStore: Replicated invocation + encrypted JSON request
ProviderCLI->>ProviderStore: Verify binding, authorization, state, payload
ProviderCLI->>LocalRegistry: Resolve capability to fixed configuration
LocalRegistry-->>ProviderCLI: Adapter, endpoint, runtime model, key env, egress, timeout
ProviderCLI->>ProviderStore: Append inference.accepted and inference.calling
ProviderCLI->>ModelEndpoint: Translate BNW request + inject fixed model
ModelEndpoint-->>ProviderCLI: Bounded non-streaming JSON response
ProviderCLI->>ProviderCLI: Normalize provider response to BNW response
ProviderCLI->>ProviderStore: Recheck authorization, expiry, cancellation
ProviderCLI->>ProviderStore: Encrypt response for both participants
ProviderCLI->>ProviderStore: Append completed result + evidence CIDs
ProviderCLI->>ProviderStore: Append signed provider-self-report claim

The provider’s local registry is private, unsigned configuration keyed by capability ID. It maps the descriptor’s stable public model claim to a fixed provider-local runtime name such as an Ollama model, hosted model identifier, deployment ID, or gateway alias. Equality is not required: the mapping is an explicit provider assertion, and neither value is requester-controlled. The encrypted application boundary uses the strict, separately versioned bnw.inference-request/1 and bnw.inference-response/1 documents described in INFERENCE.md, rather than treating any provider API as BNW’s wire format. Version 1 contains a top-level optional system prompt, user/assistant messages with text blocks, a narrow set of generation controls, normalized returned text and stop reason, optional usage, and non-authoritative provider metadata. Model selection, endpoints, credentials, streaming, tools, thinking controls, retries, and provider extension objects are excluded.

The openai-chat adapter translates that contract to an OpenAI-compatible Chat Completions call, while anthropic-messages translates it to the native Anthropic Messages request and authentication headers. Both inject the provider’s fixed runtime model, force non-streaming output, normalize the response, and enforce descriptor token and byte limits. The normalized provider metadata records the model string returned by the runtime. HTTPS is mandatory outside loopback, redirects are disabled, and credentials are read only from a named provider environment variable.

Payload egress is an explicit property independent of the immediate URL. local is valid only for a loopback endpoint, while remote acknowledges that plaintext ultimately leaves the host. This distinction matters for optional local gateways such as LiteLLM: BNW may connect to loopback while the gateway forwards the request to a hosted provider. LiteLLM remains an application-layer deployment choice, not a BNW dependency or protocol authority. Its downstream logging, caching, retries, fallbacks, aliases, and routing remain under provider control and can affect confidentiality, billing, and the meaning of a model claim.

Execution checks binding, authorization, cancellation, expiry, and terminal state before the HTTP request and again before completion. Signed progress records only the adapter, endpoint origin, public claimed model, fixed runtime model, and declared egress class—not prompts, responses, URL paths, or secrets. Provider HTTP failures include only a sanitized, bounded diagnostic message. A persistent provider-device claim is created before the external call so an ambiguous crash cannot automatically repeat it. This is an at-most-once local execution guard, not network-wide consensus.

After a successful result, the provider signs a participant-scoped InferenceExecutionClaim that links the exact invocation, canonical capability manifest and descriptor, completed result, and encrypted output. It records the descriptor model/revision/artifact, configured runtime model, runtime-reported model when valid, adapter, and the explicit provider-self-report method. Claims replicate only through the requester and provider inbox scopes. Every valid signed claim is retained and returned in content-ID order; BNW deliberately makes no canonical selection among conflicting claims. Structural link consistency can be verified, but the endpoint, gateway, and provider can still lie about which model ran. The claim is evidence of who asserted what, not proof of model execution or correctness.

The requester answers with its own evidence. An InvocationReceipt (object type 33, method requester-receipt) is signed by the invocation’s requester for every capability kind, not only inference. Its parents are [invocation, result?, settlement?].

  • Contents. It records the requester’s outcome and, when known, how many seconds it waited, a 1–5 rating, and a settlement it cites.
    • Outcomes are accepted or unusable for a completed result, declined or failed for the matching result kinds, and no-response without a result.
    • The wait is measured from signing the invocation to first holding the result, by the requester’s clock. It is left out when the result arrived more than a minute after it was signed, which means it came late through sync.
  • Validation. The store checks it without looking up other objects: the author is the requester, the requester is not the provider, the parents are exact, the method is known, a result is cited exactly when the outcome needs one, and the rating is in range.
  • Current receipt. A requester may sign several receipts. The current one has its highest signing sequence, and the lowest content ID on a tie, the same rule as offers.
  • Automatic signing. The node signs a receipt when a result for one of its local identities’ invocations arrives, and a no-response receipt ten minutes after one expires unanswered. A late result supersedes a no-response receipt. bnw invoke rate adds the requester’s verdict, a rating, or a settlement that its witness quorum approved.
  • Replication. Receipts replicate through the participants’ inboxes, like claims, so they reveal nothing the provider did not already know.
  • Consent. A receipt’s public flag says the requester lets anyone publish it. It is off unless the requester turns it on (bnw receipts public on, or bnw invoke rate --public). A newer receipt without the flag withdraws consent for later digests.
  • Published receipts. A ReceiptDigest (object type 34) carries public receipts beyond the participants, to the capability’s scope and the provider’s principal scope.
    • A follower of a capability keeps only objects in scopes it retains, so it would discard an inbox-scoped receipt fetched by ID. Each digest entry therefore embeds the canonical signed envelopes themselves: the requester’s invocation, the requester’s receipt, and the provider’s result. This follows the pattern of a settlement embedding its result.
    • The provider’s result is embedded only when it has no free-text details. That plaintext stays with the participants, and the receipt still cites the result’s ID.
    • The store checks every embedded envelope’s signature and every link:
      • the receipt is the requester’s own, it is public, and it names this invocation;
      • the invocation is for this capability and provider;
      • any result is the provider’s, for this invocation, cited by the receipt, and fits its outcome;
      • no invocation repeats.
    • The publisher is the provider or, for a requester publishing its own receipts, that requester. A provider can leave receipts out of its digests, but it cannot forge them or keep a requester from publishing its own.
    • A digest holds at most 32 entries. Each publisher’s digests for one provider and capability form a chain through previous and sequence.
    • Readers index the embedded receipts into published_receipts, not as objects of their own, so readers neither retain nor re-serve them. receipt_summaries counts only the current receipt of each invocation, whichever digest carried it.
    • Publishing reveals which requester device used which capability and when, and the content IDs of the sealed payloads, but no payloads. That is why the requester’s consent is required.
  • Publishing.
    • Providers opt in per capability (bnw receipts publishing <capability> on). This is a local setting, not replicated.
    • The node publishes a digest once 32 receipts are waiting, or at most hourly when fewer are.
    • bnw receipts publish publishes immediately, as the provider or as a requester.
  • Clients.
    • The local API has four receipt operations:
      • InvocationRate is idempotent per key and checks everything before claiming the key, so a refused rating leaves the key free;
      • ReceiptsPublic and ReceiptsPublicSet;
      • ProviderReceiptPublishingSet, which the Provider prefix refuses on companion nodes;
      • ProviderReceiptPublish, also refused on companion nodes.
    • Invocation carries the current receipt, and CatalogInspect carries each provider’s summary. Both are optional fields that older clients ignore.
    • Receipts raise invocation.update for their participants, and digests raise catalog.update.
    • The MCP server’s rate_invocation can rate but never sets public, and its allowlist excludes every publishing request. A prompt injection therefore cannot publish a user’s history.
  • What a receipt proves. It is the requester’s own report, graded as information by provenance verification. It proves who said what, not that the result was good or bad.
  • Older nodes. They do not know object types 33 and 34 and reject them, as they did settlements.

Milestone 9 starts with bnw.agent-manifest/1, another strict application descriptor carried by the generic capability layer under kind agent.portable. The outer capability manifest supplies signed publisher and subject identity. The descriptor supplies an immutable instructions CID, a sorted set of unique machine-safe roles bound to exact capability IDs, sorted immutable policy CIDs, an optional memory capability ID, and a bounded agent/run payload contract.

This first slice is a portable definition, not an executor. Exact capability IDs keep resolution deterministic and reuse the existing canonical-manifest and provider-offer machinery. Publishing the agent neither grants delegation to dependencies nor selects providers. Fuzzy requirements, substitutions, pricing, reputation, trust ranking, and workflow scheduling remain explicit later layers evaluated locally rather than global consensus state.

The second slice adds strict bnw.offer-terms/1 metadata and local exact-dependency resolution. Prices share an integer microcredit scale, but every terms document names its issuer; balances from different issuers are not globally fungible. The resolver validates locally available canonical manifests, descriptors, active offers, availability, issuer, and per-invocation ceilings. Selection is deterministic for one local snapshot and policy, not network consensus. Signed reservations and settlement remain a separate Milestone 10 protocol because receipts alone cannot prevent double spending across partitions.

Milestone 10 begins without changing invocation wire objects. After a requester publishes an invocation, its provider may sign a CapabilityQuote bound to that invocation and the exact terms CID. The requester may then sign a CapabilitySpendAuthorization bound to the quote and a ceiling no lower than its minimum. Both objects replicate only through the requester, provider, and credit issuer inbox scopes. They express price commitment and consent, not a hold or settled balance. Provider execution remains ungated until the issuer-reservation slice is implemented.

Settlement resilience uses immutable bnw.credit-policy/1 epochs rather than deriving consensus membership from each node’s live DHT view. A policy fixes the issuer, sorted witness committee, greater-than-two-thirds threshold, optional failure-domain claims, and previous epoch. The policy and transaction IDs derive a progressive contact order, excluding requester and provider, but expansion never changes the committee or threshold. If quorum is unavailable, a provider may later accept bounded pending exposure by local policy; the transaction cannot alter settled UTXOs. Witnesses receive economic hashes and signatures, not private capability payloads.

The reservation slice adds issuer-signed bootstrap UTXOs, requester-signed spend proposals bound to an exact authorization, and committee attestations. Each proposal carries a bounded signed proof bundle for the invocation, offer, quote, authorization, terms, and policy; witnesses can therefore validate the economic chain without receiving the private input or subscribing to all participant history. A proposal conserves value by locking the authorized maximum and returning only requester change; it cannot directly transfer value to an arbitrary principal. Spendable balance is derived from quorum-settled unspent outputs. Each honest witness durably refuses to approve a second proposal with an overlapping input. Greater-than-two-thirds quorums therefore intersect, while any observed pair of conflicting quorums is preserved and surfaced as a catastrophic safety violation instead of resolved by local arrival order. Epoch activation and pending-risk enforcement remain subsequent slices.

Settlement. A settlement slice closes the loop. A CreditSettlement is parented only by its reservation and splits the reserved amount into a provider output (0) and a requester refund output (1), which must sum exactly to the reservation.

  • Provider settlement. The provider signs it and embeds its signed CapabilityResult plus metering. The charge is bounded by the quote minimum, the metered cost at the signed offer rates, and the outcome: failed or rejected results cost nothing.
  • Requester release. The requester may sign a full-refund release only after the invocation expires plus CREDIT_RELEASE_GRACE_SECS.
  • Attestation. CreditSettlementAttestations are parented by the settlement, the reservation, and any conflicting settlement. Each witness approves at most one settlement per reservation, so intersecting quorums leave one settlement per reservation.
  • Conflicts and replication. Observed double quorums surface as catastrophic-conflict, and neither side’s outputs resolve. Both objects replicate through the issuer, participant, and witness inbox scopes.

This section records the intended design. Only the transcript context described first is implemented.

Inference is stateless. A bnw.inference-request/1 carries the whole conversation it is about: the optional system text and every message. Providers keep nothing between calls, and no request refers to an earlier one. This keeps providers interchangeable for routing and failover. It keeps provenance exact, because the encrypted input is precisely what the model saw. And it gives providers no conversation state to trust, retain, or delete: retention stays a signed declaration on the offer. Later request versions may add prompt-caching hints, or reference large context by content ID instead of inlining it, without making the provider hold state.

Agents own context. The runtime assembles a transcript from signed history that the agent’s own key can read: at most 32 earlier channel messages, the agent’s posts as its turns, and everyone else’s as named user turns. Context is therefore bounded by what was replicated and sealed to the agent, never by what the host happens to hold for other identities. A hosted agent’s transcript comes from its own key, not the node’s.

Context is keyed by conversation, not device. The key is the agent identity plus the conversation:

  • Channel. The channel ID, as today.
  • Direct conversation (hosted agents only). The other side’s account, or its device when it has no account. Admission and limits are in the README under “Running an agent”.

The account is the right unit because direct messages are already sealed for every member device and filed by account. A user therefore sees one conversation with an agent across their phone and laptop. Device-level separation applies only to state that lives on one device, such as unsent drafts. Agent context holds nothing of that kind.

Context never crosses conversations by default. Nothing learned in one conversation may appear in another’s prompt. This matters most once agents have memory: a hosted agent serving several channels or people must not surface what Alice said in Bob’s channel. Sharing across conversations, for example an operator’s standing notes, needs an explicit operator grant, like the existing agent:* grants.

Memory is not implemented yet. bnw.agent-manifest/1 carries an optional memory capability ID, which is validated and stored but unused. Before it is implemented, these questions need decisions:

  • Scope. Every memory entry belongs to exactly one (agent, conversation) key, and reads return only that key’s entries unless a grant says otherwise.
  • Storage. Memory is either local-only state, like other host-local registries (unsigned, not replicated, lost with the host), or encrypted, replicated objects sealed for the agent and its operators (which survive and move with a portable agent).
  • Forgetting. Replicated objects are immutable, so “forget this” cannot delete them. Replicated memory would need per-scope content keys that are destroyed on request (crypto-shredding). Local-only memory can simply be deleted.
  • Remote memory. Because memory is named as a capability, it can be served by another provider through ordinary invocations. That provider then sees the agent’s memories. As with payload egress, this must be an explicit, declared choice rather than a default.
  • Operator visibility. Operators can list and delete an agent’s memory for each conversation.

Planned order.

  1. Agents answer direct messages, with context keyed by conversation. Done for hosted agents.
  2. Budget the transcript by the model descriptor’s context_tokens instead of a fixed 32 messages, summarizing older turns when it overflows.
  3. Implement memory under the rules above.

Discovery defaults to LAN mDNS, with optional remembered bootstrap multiaddresses. Channels are public and object bodies are small and SQLite-backed. On connection, and periodically while a peer remains connected, peers exchange compact per-author summaries for each locally retained replication scope. Divergent authors then exchange deterministic hashes for occupied 256-sequence buckets inside that scope; only buckets whose count or hash differs require paginated content-ID exchange. This preserves recovery from gaps, same-sequence forks, and missed live announcements without pulling unrelated channel or inbox histories. Missing objects are deduplicated and fetched from one provider at a time, with alternate-peer fallback after unavailability, timeout, or disconnect. GossipSub provides live announcements on the same deterministic scope topics. Concurrent channel-record conflicts resolve deterministically by content ID.

On Unix platforms, the node holds an exclusive bnw-node.lock for its data directory and binds two mode-0600 endpoints. bnw-node.sock remains a small datagram path for wake notifications, live status, federated-search coordination, and selected-object retrieval. bnw-api.sock is a length-framed Unix stream API for payloads too large for the platform datagram ceiling. Its catalog surface provides bounded pages, persistent watches, detailed inspection, offer refresh, on-demand descriptor retrieval, explicit following, and local preference/trust mutations. The same API exposes bounded invocation activity and public-channel feeds plus idempotent invocation, cancellation, and public-channel signing actions. Requests and responses use CBOR, enforce 64 KiB and 1 MiB limits respectively, carry caller nonces, and have bounded client/server timeouts. Both endpoints stay on the host and confer no network authority. Most older CLI mutations still write SQLite directly and then wake the daemon; these product-facing surfaces establish the path future clients can share without reading the database.

The optional web gateway exposes the same local API to browser and desktop clients as JSON over HTTP, without a separate implementation:

  • Shared handler. The request handler is factored out of the Unix-socket framing. The gateway forwards each request into the node’s main loop, which owns the store, over a channel, and answers it with the shared handler. Validation, idempotency, trust, and grant checks therefore cannot diverge between clients.
  • Requests. POST /api/v1/<kebab-case-operation> bodies are mapped generically onto the tagged request enum, and responses drop the transport nonce.
  • Events. GET /api/v1/events streams Server-Sent Events. While a stream is open, a scanner on the main loop runs every second. It reads stored objects in arrival order (SQLite rowid), agent trigger and tool-call rows by update time, and the network status snapshot. It classifies what involves the local identity and publishes over a bounded broadcast channel. Scanning the store rather than hooking write paths catches objects from the network, the API, and CLI commands that write the database directly. Events carry IDs only, so reads keep going through the checked operations. With no stream open, the scanner just advances its cursors. A slow client gets a lagged event instead of unbounded buffering. The bearer token travels in a header, never a URL, and at most 8 streams are open at once.
  • Operator operations. Identity, peers, channels, admission, approvals, agent grants, and revocation live in one operator module in the node crate. The daemon’s API handlers and the CLI both call it. Actions that create a new object each time (channel creation, admission, grants) are claimed under an idempotency key before signing and released if signing fails. Decisions and revocations are naturally idempotent: repeating one returns the earlier object. The approval inbox re-verifies each agent request against its signed action ID before showing the decrypted draft or tool input. It returns those in full, so an operator never approves text they could not see. It holds at most 16 entries to stay within the 1 MiB response limit. An operator sees a remote agent only through replicated signed objects: the agent’s agent:control link, its grants, its approval requests, and its audit events. The agent’s trigger state stays on the agent’s node. agent-list and agent-audit project these, and the event scanner reports audit events from agents that link the local identity.
  • Web client. The gateway serves an embedded Preact client from crates/bnw-node/web without authentication, since the files hold no secrets. Every call it makes then uses the session token. The files are included in the binary at compile time. Preact and htm are vendored as one pinned ES module, so the Content Security Policy stays default-src 'self' with no inline scripts or styles. The client renders all network-supplied text as text, never HTML, which a unit test enforces. It keeps the token in per-tab session storage and reads the event stream with fetch(). The running gateway records its bound port in bnw-web.port, so bnw web open links to it. Starting without the gateway removes both runtime files, so a stale link never outlives its gateway.
  • Provider autopilot.
    • Provider execution lives in bnw_node::provider and runs in three phases:
      1. plan runs on the event loop. It checks binding, expiry, cancellation, the contract, authorization, the backend, and secrets. Then it decrypts the input, takes the claim, and signs the accepted and calling progress.
      2. call runs off the loop without touching the store.
      3. finish runs on the loop. It rechecks everything, encrypts the output, and signs the result.
    • The CLI’s invoke execute-* and the daemon’s autopilot run the same code.
    • The at-most-once claim is a row in the local provider_executions table, so the CLI and the daemon, which share the database, can never both run an invocation.
    • provider_policies holds per-capability manual/ask/auto policies and limits. Both tables are host-local, unsigned, and never replicated.
    • At start, a claimed or calling execution without a result is answered with a signed failure, because its effects are unknown, and it is not run again.
  • Provider setup.
    • bnw_node::provider_setup builds descriptors and publishes capability manifests and offers. It also registers MCP and inference backends. The CLI commands and the local API call the same functions, so the web client signs exactly what the CLI signs.
    • Backend calls run as bounded background jobs (at most 32) that report through provider-setup-status:
      • tool discovery;
      • readiness checks;
      • OAuth, where the daemon binds a loopback callback listener (in bnw-http). It answers only /callback with the flow’s state and a loopback Host, so stray requests and DNS-rebinding pages cannot end or drive a sign-in. A callback URL can also be pasted in from another device.
    • A tool can be pinned only from the node’s own most recent discovery, so the schema hash is never taken from the client.
    • Offer renewal (schema 24 adds offer_terms to provider_policies) runs on the provider scan tick. It signs a fresh bnw.peer offer once the newest one from this node is in the last fifth of its lifetime, and only while the backend is ready. Renewed offers link the same offer terms.
    • Gateway requests are tagged as coming from the web. Such a request cannot register a local program unless the operator enabled --allow-local-programs, because a program runs with the node’s privileges.
  • Agent runtime.
    • bnw_node::agent_runtime holds the agent loop. It polls for a trigger, builds the prompt, stages the inference request, creates the invocation, reads the decrypted output, and proposes a reply or tool call.
    • Every step is a LocalApiRequest, sent through a small LocalApi client. The daemon uses an in-process channel into the same request handler as the web gateway (tagged as not from the web), and bnw agent run uses the socket. The two cannot diverge, and the runtime gains no authority a local client lacks.
    • Settings and status live in the host-local agent_runtimes table (schema 25) and are reread every round. The task stops when the node’s request channel closes.
  • Hosted agents.
    • A node can hold keys for agent identities besides its own, under agents/<id>/identity.key (0600). The host-local hosted_agents table lists them; it is never replicated.
    • Every agent-scoped row (agent_cursors, agent_triggers, agent_tool_calls) is keyed by agent (schema 26), so two agents addressed by the same message each get their own trigger. Rows from before schema 26 have an empty agent until the node identity adopts them at startup.
    • The runtime supervisor starts one runtime task per active identity. Each task’s requests carry the identity they act as, and the handler builds the request context for that identity: the node’s own, or an active hosted agent’s key loaded on first use. Everything it signs, stages, or decrypts is therefore that agent’s own. Gateway and socket requests always act as the node identity, which is every hosted agent’s operator.
    • The settle tick posts approved replies and runs approved tool calls for each active identity.
    • Staging jobs remember their signer.
  • Attachments.
    • A public channel message carries its attachments inline: blob ID, size, media type, and name, at most 8, each validated on insert. The file therefore replicates with the channel’s scope rather than the global scope that standalone BlobRecords use, so no node fetches files for channels it does not keep.
    • The optional field is omitted when empty, so messages without attachments keep byte-identical signed payloads, and older nodes decode and ignore it. Attachments are not envelope parents, because the store accepts only reply_to there and older nodes would reject the message.
    • Schema 27 indexes attachments. Attachments up to 4 MiB count as referenced, so the node seeks them at once and asks the delivering peer. Larger ones become referenced only when requested (AttachmentFetch). A node announces itself as provider for every attachment it holds.
    • Private messages list encrypted blob manifests sealed for the message’s readers, reusing private payload replication and decryption.
  • Provenance bundles.
    • bnw_node::provenance exports every signed object and public blob around one invocation, as canonical bytes, for its participants.
    • Verification needs nothing but the bundle. It opens a fresh temporary store, so every object passes the same size, hash, signature, and decode checks as network input. It then checks the links: authorship, the offer binding and validity, the recipients of the payload manifests, progress chain continuity, result authorship, and whether each cited grant authorized the result at its signing time without an earlier revocation in the bundle.
    • Missing evidence makes a bundle incomplete; contradicting evidence makes it invalid. Execution claims are reported as provider statements, and receipts as requester statements.
    • A receipt fails verification if someone other than the requester signed it, if it cites a result other than the invocation’s canonical one, if its outcome contradicts that result’s kind, or if it cites a settlement that is not in the bundle. A no-response receipt next to a result is reported, not judged: the requester may never have received it.
    • Bundles carry settlements and their witness attestations with the credit reservations.
  • MCP server.
    • bnw-mcp-server implements an rmcp ServerHandler over the same LocalApi client as the agent runtime, reaching the node through its socket. It has no store access, keys, or state of its own beyond a per-session write counter.
    • permitted is the trust boundary: a fixed list of request kinds it may send, checked for every request. Approval decisions, delegations, trust, provider, profile, and channel mutations are absent, so no tool and no prompt injection can reach them. Signing requests (ChannelPost, DirectSend, PayloadStage, InvocationCreate) are reachable only through write tools that check their scope, destination allowlist, and budget first.
    • Arguments are parsed strictly (deny_unknown_fields), results are bounded, and each call is audited with a hash of its arguments rather than their content.
  • Search.
    • Federated search runs as a bounded daemon job. Provider selection, fan-out, the 7-second deadline, and reciprocal-rank merging live in bnw_node::search, shared with the CLI.
    • A job records which peer each request nonce went to and accepts only that peer’s answer. Responses are size-checked as before, and pending providers are marked timed out lazily when the job is polled. At most 16 jobs are kept.
    • Results stay unverified references. object-fetch starts the existing nonce-bound retrieval without a datagram reply address, and the client polls object-view, which reads and validates the stored object. Retrieval never follows a scope.
  • Private messaging.
    • Direct and encrypted channel messages are sealed separately for each recipient and the author, with associated data binding each one to its conversation or channel and author. A ciphertext therefore cannot be replayed into another context.
    • bnw_node::messaging holds encryption, decryption, conversation grouping, and newest-first paging, and the daemon API and the CLI share it.
    • The node decrypts only for the authenticated local client that asks. The web client receives plaintext over the loopback gateway, and nothing is decrypted for peers. Messages that cannot be decrypted are returned with an error instead of being dropped silently.
    • Sending requires the recipient’s published encryption key.
  • Transfers. Staging and decryption work on local paths, which a browser cannot supply, so the gateway adds a transport layer rather than new logic:
    • Uploads stream to a mode-0700 web-uploads directory under random 128-bit IDs. payload-stage refers to an upload by ID, and the gateway substitutes its path.
    • payload-download maps onto the daemon’s PayloadOpen, with an output path the gateway chooses in web-downloads. Browsers therefore cannot name arbitrary output paths, and payload-open is not exposed.
    • A download is unlinked as soon as it is opened for streaming. It is sent only as an application/octet-stream attachment with nosniff, so a payload can never render as a page on the gateway’s origin.
    • Uploads accept only application/octet-stream, which forces a CORS preflight that the gateway never grants.
    • Transfers are bounded in size (256 MiB) and count (16 each), expire after an hour, and are cleared at start.
  • Encoding. ID types serialize as hex strings for human-readable formats only. Their CBOR encoding, and so every signed object’s bytes and hash, is unchanged.
  • Security. The gateway binds 127.0.0.1 only and requires a bearer token regenerated at each start and stored with mode 0600. It refuses foreign Host headers against DNS rebinding and foreign Origin headers against cross-site calls, sends no CORS headers, accepts only application/json, and caps bodies at the local API’s 64 KiB. Every response carries a strict Content Security Policy with frame-ancestors 'none', no-referrer, and no-store. Malware running as the same user can already read the node’s keys, so the gateway adds no exposure beyond that.

Encrypted invocation payloads are retrieved through the same API without moving file bytes over it. A single-invocation lookup projects one lifecycle for a local participant. PayloadStatus reports whether an encrypted payload’s signed manifest has replicated, how many ciphertext bytes are local, and the sender’s suggested name reduced to one safe path component. When the manifest is local but its content is incomplete, the node asks the network for providers of that ciphertext. PayloadOpen validates an absolute output path that must not exist yet, and loads and size-checks the manifest, the access record sealed to the local identity, and the ciphertext file on the event loop. It then decrypts on a blocking thread, so large payloads never stall networking, and the caller polls the job with PayloadJob. At most 4 jobs run at once and 64 finished jobs are remembered. Decryption verifies every chunk and the complete plaintext hash against the signed metadata. It writes through a temporary sibling that is hard-linked into place, so a failed or conflicting write leaves nothing behind. The CLI’s private-blob commands and provider adapters use the same decryption code. The daemon runs as the invoking user and its socket is private to that user, so accepting an output path grants nothing beyond the caller’s own file access.

Input staging is the reverse path. PayloadStage names a capability, a provider, and an absolute source path under an idempotency key. On the event loop, the daemon:

  • hashes the request together with the source file’s size and modification time, so reusing a key after the file changed is a conflict
  • returns the original input for a completed key
  • checks local trust annotations
  • requires the capability’s descriptor locally, since staging always needs the input contract (unlike invocation requests)
  • rejects public contracts
  • resolves the media type and enforces the contract’s size limit
  • reads the provider’s published encryption key
  • claims the key

A blocking thread then hashes the file, encrypts it in authenticated chunks under a fresh content key, and imports the ciphertext through a thread-safe blob-directory handle that never touches the database. It also seals the access record for exactly the requester and provider. Back on the loop, the daemon signs and stores the manifest and completes the claim. A job that fails before signing releases its claim. Staging claims are only unfinished while their job runs, so any found at startup are released too. The CLI’s direct encryption commands use the same encryption code.

Milestones 4 through 8 are complete. Milestone 7 includes schema 14 signed capability invocation requests, authorized immutable progress chains, cooperative requester cancellation, terminal provider results, deterministic fork and race projection, participant-inbox retention, provider-issued exact-capability delegation checks, exact-action approvals, signed authorization evidence, strict optional v1 descriptor profiles, followed-descriptor retrieval, exact-participant encrypted payload preparation, the CLI lifecycle, and provider-local stdio and remote OAuth MCP adapters. Milestone 8 adds strict request-level inference profiles, provider-neutral request/response documents, OpenAI-compatible and native Anthropic adapters, explicit ultimate payload-egress classification, optional LiteLLM gateway compatibility, and schema 15 participant-scoped non-authoritative execution claims. Milestone 9 includes strict portable agent manifests, issuer-scoped offer terms, and local exact-dependency resolution. Milestone 10 includes optional provider quotes, requester spend authorization, fixed resilient witness-policy epochs, and schema 16 witnessed UTXO reservations; further settlement work is paused until realistic multi-peer witness testing is available. Milestone 11 adds the daemon-backed catalog, shared local API, reachability projection, schema 17 local preferences, immediate follow/unfollow reconciliation, bounded invocation activity tracking, schema 18 local trust annotations, schema 19 idempotent trust-enforced invocation submission, and schema 20 durable public-channel feeds with idempotent posting and reply linkage. Non-authoritative relays, CRDTs, MLS, mobile transports, and moderation remain separate higher layers.