Brainy Brainy
Docs Brainy

brainy serve

In this section

brainy serve is the engine answering on an address. It is a Rust binary that links the engine's core as a library — not a JavaScript server in front of the native addon, and not a wrapper around a Node process. The same code the .node addon exposes is called directly, in one process, with no napi crossing on any request path.

Two ways to run

A library mount. A Rust host process embeds the crate and mounts the doors on a listener it owns. The host keeps its own runtime, its own address, and its own shutdown.

A thin CLI. brainy serve, standalone, for a host that just wants the engine to answer on an address.

brainy serve                                  # binds 127.0.0.1:8300
brainy serve --listen 10.0.0.5:8300,h2        # one listener, HTTP/2
brainy serve --listen 10.0.0.5:8300,h2 \
             --listen 0.0.0.0:8443,h3         # LAN h2 + an edge h3 listener

The starter. With no arguments at all, the binary opens ./brain, serves it on a port the operating system says is free — loopback, never a wildcard — and prints the snippet an MCP client is configured with. Nothing is asked and nothing is configured. It does not CREATE a store: a store's identity, manifest and counts ledger are written by the engine's own open, and a directory merely shaped like one is the empty success this engine will not produce, so an absent store is a refusal naming the one command that makes one.

brainy                                        # the starter
brainy mcp                                    # the stdio MCP server over ./brain

The door surface is a law

Every public engine verb reaches every transport, and no transport keeps its own list. The route table, the SSE endpoints, the WebSocket method set, the MCP tool list and the gRPC service are all generated from one source: api-contract.json, the emitted manifest of the versioned API contract whose prose is api-contract.md.

A door is never quietly absent. Every door either answers or refuses by name:

  • NotYetNativeError — the door's implementation is moving into the engine's core and is not there yet. The refusal carries the door's name and the release it arrives in. Never a no-op, never an empty success.

  • InProcessOnlyError — the door hands back a live in-process object (a plugin, a storage adapter). A wire can carry what such an object reports; it cannot carry the object. The refusal names what to call instead.

Both appear in the door's MCP tool description as well, so an agent reads the refusal before it writes the call.

Every door's response, answer or refusal alike, also carries the server-side measured wall of that call — Server-Timing: brainy;dur=<ms> on HTTP (and _meta.ms merged into a JSON body), brainy-ms in a gRPC response's metadata, and meta.ms on a WebSocket answer or an SSE stream's closing caught-up frame — one clock, taken once in the dispatcher, never a second timer per door or per transport.

The contract door

GET /contract.json serves a generated envelope — {service, version, builtAt, contractVersion, layers} — describing every door on every transport, every refusal class, and the server's own doors. It follows the fleet's contract-door standard: generated from the table that enforces it, stamped with the build that serves it, and pinned by the server's own suite against a committed copy.

Consumers vendor it at build time, commit the copy, and diff it in CI. A difference is a red build, never silent drift.

GET /agent and GET /llms.txt sit beside it. They are onboarding prose for an arriving agent — how to get a key, where the contract door is, how to connect over MCP. They are not the contract, and they never substitute for it.

Names and defaults

Contract service name

brainy

Contract envelope

GET /contract.json (stable path, short cache)

Onboarding

GET /agent, GET /llms.txt

Engine doors

/v1/…

SSE change feed

GET /v1/brains/{id}/changes?since=

WebSocket

/v1/ws

MCP streamable HTTP

POST /mcp (JSON-RPC 2.0, revision 2025-06-18)

MCP stdio

brainy serve as brainy-serve mcp, key from $BRAINY_KEY

MCP instructions

/_brainy/mcp/instructions.md in the store, else a generated default ≤ 700 tokens

MCP resources

the store's /skills/ documents, as brainy://skills/<path>

The starter

brainy-serve with no arguments — ./brain on a free loopback port, connect snippet printed

gRPC

--listen host:port,grpc; the artifact is docs/brainy.proto

Default bind

127.0.0.1:8300 when nothing is named — never 0.0.0.0

Listener flag

--listen host:port[,h2\|grpc\|h3], repeatable

h3 certificate

--tls-cert, --tls-key — required by any h3 listener

Brains

--brain name=path, or --brains-dir path (named by directory)

Keys file

~/.config/soulcraft/brainy-serve-keys, mode 0600, placed by the host

Service key

the admin doors (health, diagnostics, repair, snapshot, warm, stats)

First start, no keys file

mints one key and prints it once

Issuers file

~/.config/soulcraft/brainy-serve-issuers (or $BRAINY_SERVE_ISSUERS), mode 0600, placed by the host

Minted key

bk1.<payload>.<signature> — an issuer-signed Authorization: Bearer, verified against the issuers file; see Minted keys

Client resolution order. The fixed LAN name brainy.example.internal (a hosts line, DNS once there is more than one), then the public https://brainy.soulcraft.com, then BRAINY_URL last.

Transports

The order these landed in, and why.

1 · HTTP/1.1 and HTTP/2, from day one. Both on the same listener. TLS is optional and the host terminates it at the edge; the server speaks cleartext h2 on a private address, which is what a LAN wants and what removes a certificate from the fleet's critical path.

2 · MessagePack, negotiated per request. JSON is the default. Accept: application/msgpack returns MessagePack and Content-Type: application/msgpack accepts it — the same documents, one serde model, two encodings. Vectors and bulk pages are the reason: a 1,024-element vector of doubles is 9,227 bytes of MessagePack against 16,256 of JSON — a 1.76x saving (MEASURED, the codec's own a_vector_is_smaller_in_msgpack pin), because JSON spells every float in decimal and MessagePack does not. A page of a thousand rows carries that difference a thousand times, and the segment log already stores its records as MessagePack, so wire and store share a codec.

The saving is smaller than the arithmetic on a raw f32 vector suggests, and the reason is stated rather than rounded away: the document model holds numbers as f64, so a vector element costs nine bytes on the wire where the engine stores four. Serving vectors as f32 roughly halves that again; it lands with the doors that return vectors, where the width is part of the answer's shape.

Every door round-trips identically in both encodings, and a pin holds that. A body whose bytes do not decode cleanly — including a JSON body sent to a MessagePack door, which would otherwise read as the integer 123 and discard the rest — is refused by name rather than half-read.

3 · gRPC, generated from the contract. --listen host:port,grpc serves it. The .proto is generated at build time from the contract — one rpc per door, named by the door, with the service implementation generated beside it — so there is no list of methods anyone can forget to update, and a pin holds the gRPC surface equal to the HTTP surface door for door. docs/brainy.proto is the committed artifact a consumer runs protoc over to get a typed stub in Python, Go, Java or C#.

A door is typed by its name, not by per-door fields. The contract carries TypeScript signatures, not field schemas, and deriving protobuf fields from a signature string would be a guess a generated client's compiler would happily bless — so one request and one answer message carry the brain, the id and the door's document as the same JSON everything else speaks. Typed messages arrive when the contract grows real parameter schemas.

A door's refusal is carried in the answer, never as a gRPC status. A status is a transport verdict and a refusal is an engine answer; a caller that must tell "the network failed" from "the write path is not native yet" cannot if the second is folded into the first. Only the transport's own failures are statuses — no credential, and a body the server cannot read.

Every rpc is unary for now, for the same reason the change feed does not hold a connection: a server-streaming rpc would promise an event that cannot occur until a commit can happen in this process.

4 · HTTP/3 over QUIC. --listen host:port,h3, with --tls-cert and --tls-key. The same doors, the same encodings, the same pipeline — HTTP/1.1, HTTP/2 and HTTP/3 differ in how they frame a request and not at all in what a request is, so no transport carries its own copy of the policy.

The certificate is the host's. QUIC has no cleartext mode: TLS 1.3 is part of the protocol, not a layer over it. This server mints nothing — one it generated itself would be one no client could verify and every client would be taught to skip verifying, which is worse than no TLS because it looks like TLS. Naming an h3 listener without both files refuses at startup, and deliberately does not fall back to h2 on that address: a host that asked for h3 and silently got h2 would advertise h3 to its clients and be wrong. Every certificate problem is named separately — missing, unreadable, a PEM with no CERTIFICATE block, a PEM with no PRIVATE KEY block, a key belonging to a different certificate — because "TLS failed" is the least useful thing a server can say at startup.

ALPN offers exactly h3; offering more would let a client negotiate a protocol the listener does not serve. Alt-Svc is advertised from the h1/h2 answers only when an h3 listener actually exists, since advertising an alternative that is not there teaches a client to spend a round trip failing. Shutdown closes the endpoint and waits for idle rather than cutting streams, which to a client would look exactly like a network failure.

Protocols are per-listener, and that is the only knob. A host enables h3 only where it helps — a public edge serving browsers and mobile — while the LAN runs h2 with MessagePack. One server, one door table, a listener list the host names.

MCP

Every door is a tool. MCP is a transport here, not a curated selection, so a client that speaks only MCP reaches exactly the engine an HTTP client reaches — and the two land on the same dispatcher, so neither can serve a method the other does not.

tools/list is the generated table, whole. A tool's input schema is generated from its binding: the path parameters it names are required strings, and a door whose contract signature declares parameters also takes body. It does not claim to know the body's fields — the contract carries signatures, not schemas, and naming fields from a type name would be a guess wearing a specification's clothes. The signature travels verbatim in the description instead.

A door's refusal is a result, not a protocol error. A write that is not native yet, or a key that does not reach a brain, comes back as tool content the model reads, with isError: true and the machine-readable class in structuredContent. A protocol error is invisible to a model, and a refusal an agent cannot read is a refusal it will retry forever. Protocol errors are kept for what they are: a malformed request, an unknown method, a missing argument.

Resources are records with addresses: a brain's census, an entity, an entity's history. Every template names the door that reads it, and reading one calls that door through the same path everything else uses. A resource whose read has no implementation is not offered — which is why there is no relation resource: the contract has no door that reads one relation by id, and related is a query, not an address.

Over stdio the process runs AS a principal: BRAINY_KEY names the key, read from the environment and never from an argument, because an argument is visible to every process on the machine. Nothing but JSON-RPC goes to stdout.

The server teaches its own use. Each tool description carries the contract's summary and signature AND one generated line about when to reach for that door; initialize.instructions carries the store's own ceremony from /_brainy/mcp/instructions.md, or a compact default measured against a 700-token budget; and the store's /skills/ documents are served as resources at brainy://skills/<path>, so improving a skill is a write rather than a release. The protocol revision is 2025-06-18, one revision and not a range, refused by name at the MCP-Protocol-Version header. The whole model — connect snippets, the title and description rules, what refuses today — is MCP.

Measured

Conditions. Job serve-measure2, exclusive lane on a 32-core box, load average 2.7 at start, release build, loopback, a sparse-preserving copy of the the venue store (7.1 GB allocated, 158 GB apparent) — never the archive. 600 requests per configuration (640 at c=64). Percentiles are nearest-rank. Every figure is served latency from a caller's socket: the whole cost of an answer including framing, not the engine's time in isolation.

door

transport

enc

c=1 p50

c=8 p50

c=64 p50

c=64 p95

best answers/s

bytes/answer

get

h1

json

0.068 ms

0.087 ms

0.536 ms

1.042 ms

96,629

156

get

h2

json

0.028 ms

0.070 ms

0.418 ms

0.535 ms

145,693

29

get

h2

msgpack

0.027 ms

0.066 ms

0.410 ms

0.551 ms

147,478

19

counts

h1

json

0.079 ms

0.131 ms

0.596 ms

0.843 ms

95,645

687

counts

h2

json

0.040 ms

0.103 ms

0.854 ms

1.207 ms

75,190

559

counts

websocket

json

0.022 ms

0.204 ms

1.616 ms

1.929 ms

44,548

608

paged read

h1

json

2.713 ms

2.765 ms

9.287 ms

13.736 ms

6,375

48,273

paged read

h2

json

2.372 ms

3.019 ms

9.853 ms

14.823 ms

6,219

48,143

paged read

h2

msgpack

2.355 ms

3.003 ms

9.476 ms

13.720 ms

6,227

39,661

The gRPC row is withheld pending re-measurement: the run that produced this table still carried a transport defect on that path (tonic's tcp_nodelay applies only to a listener tonic itself binds, never to an incoming it is handed), and a number taken with a known defect in it is not a measurement. The fix is in and pinned; the row returns with the first sweep taken on the exclusive lane after it, and not before — a figure measured beside other work is not a receipt.

What MessagePack actually buys, plainly: bytes, not latency. A get answer is 19 bytes against 29, and counts 551 against 559 — and at those sizes there is no latency gain to speak of (0.027 ms against 0.028 ms at c=1). The saving is real only where the payload is: the 48 KB paged answer drops to 39.7 KB, about 18%, with p50 within noise of JSON. Choose it for bandwidth on large or bulk answers, not for speed on small ones.

Two defects this measurement found, both invisible to a green unit suite and both now fixed and pinned:

  • Every h2 and gRPC answer carried a 40 ms delayed-ACK stallTCP_NODELAY was unset on accepted sockets. HTTP/1.1 escaped it because its writes coalesced past the interaction, which is exactly why latency has to be measured per transport rather than inferred from one.

  • The WebSocket answered nothing — 600 of 640 requests lost. The HTTP/1.1 upgrade was never performed, so every handshake succeeded and every frame after it went nowhere.

Sovereignty

  • It binds to the address the host names. 127.0.0.1:8300 when nothing is named, and never 0.0.0.0 by default.

  • It never contacts any host. Not for weights, not for licence validation, not for telemetry.

  • Keys are files the host places, mode 0600. The engine mints nothing — except a first start with no keys file, which mints one key and prints it once.

  • A minted bk1 token is verified, never minted, by this server: the issuer — Soulcraft Identity, or a self-hoster's own minting service, itself run through brainy-serve issuer keygen / token mint as the reference implementation — signs it; this server only checks the signature against a key in the issuers file, another file the host places at mode 0600. See Minted keys.

  • There is no UI, no login federation, and no generative model in this binary. The language seam is configured to the host's provider.

The language provider is resolved from the process's environment at startup, in the same precedence a bare new Brainy() uses on the Node side — BRAINY_LANGUAGE_PROVIDER, then exactly one recognized vendor key, then a local model server on a well-known port, then none. A misconfiguration (two vendor keys with no explicit choice, or an endpoint the enterprise outbound gate refuses) stops the server before it binds anything. See docs/language-providers.md for the full matrix, every environment variable, and every refusal.

Keys and principals

A key names a principal — a person, a business, or a service — and the brains that principal may reach. The service key is separate: it reaches the engine mechanics (health, diagnostics, repair, snapshot, warm, stats) on every brain and reaches no principal's rows.

Every request carries a principal or the service key. Refusals are typed and named.

Minted keys

A keys-file line is one durable secret every holder of it shares — right for a service acting as itself, wrong for an arriving AGENT: anima, an outside provider's own agent, a third party, each of whom should carry their OWN credential, scoped to one brain and to read or read/write, revoked by expiry rather than by editing a file everyone else's key also lives in. A minted key is that credential: a short-lived, signed claim an ISSUER hands the agent, which this server VERIFIES rather than looks up. The engine still mints no bearer credential of its own for anyone but the stranger at first start — an issuer is always an external identity.

The format

bk1.<base64url(payload JSON)>.<base64url(ed25519 signature over the payload bytes)>TEXT

Presented the same way a static key is: Authorization: Bearer bk1.eyJ…. The payload:

Field

Meaning

iss

The issuer id — a line in the issuers file.

kid

Optional. Which of that issuer's keys signed this, for rotation.

sub

The agent or user id this token names. Opaque to this server.

brain

The one store this token opens. One path segment.

scope

["read"], ["read","write"], or ["read","write","owner"] (any order) — a closed set. See The owner scope.

iat

Issued-at, Unix seconds.

exp

Expiry, Unix seconds. Enforced with a 60-second clock-skew allowance.

jti

Optional. Checked against the host-placed revoked list when present; otherwise carried, never tracked for replay by this server.

The signature covers the exact bytes of the decoded payload segment — never a re-serialised or canonicalised form — so a verifier hashes what it was handed and nothing else.

The issuers file

~/.config/soulcraft/brainy-serve-issuers (or $BRAINY_SERVE_ISSUERS), mode 0600, placed by the host — refused before it is read if the group or the world can read it, the same law the keys file already holds. One line per key:

# <issuer-id>        ed25519:<base64 public key>          [kid]
soulcraft-identity    ed25519:MCowBQYDK2VwAyEA…            2026-09
self-hosted-issuer    ed25519:MCowBQYDK2VwAyEA…TEXT

The optional third field names the key for rotation: a token whose kid matches a line is checked only against that line; a token with no kid is checked against every key its issuer has, so a fleet can add a new key before retiring the old one instead of every held token failing at the moment of rotation.

A missing issuers file is not a refusal to start — it means this server trusts no issuer, so every bk1 token is answered BRAINY_KEY_UNKNOWN_ISSUER, the same "not placed yet" story an empty keys file already tells. The file is re-read on its own mtime change, checked at most once every 5 seconds — never per request, never a filesystem watcher (SIGHUP, inotify): a rotated kid is honoured within that window, with no restart. A file that exists but fails to parse refuses this server's START the first time; once running, a later edit that fails to parse is logged and the last good table is kept rather than dropped.

The owner scope

The owner triple — ["read","write","owner"], in any order — is the OWNER's own credential: minted by a human sign-in only, never by an agent's key. It implies the full read/write grant and additionally may call the three doors that RULE on policy rather than merely mutate a brain's own rows: policy.ratify, policy.unlock and policy.freeze (activating a proposed rule, lifting a lock, freezing every tier-1 rule). Every other policy door — policy.check, policy.declare, policy.propose, policy.retire, policy.audit — is unaffected: an agent may tighten its own leash, propose anything else, and read what the rules stopped, from any credential.

Calling one of the three ruling doors with anything short of the owner triple — a plain ["read","write"] token, a ["read"] token, or a keys-file static key (whose scope is unconditionally None, since the keys file carries no owner marker of its own) — refuses PolicyOwnerScopeRequiredError (403), named by the door and checked before that door's own disposition, the same "authorisation before disposition" order every other refusal in this section already keeps. There is no narrower "owner, read-only" shape: a ruling either carries the full triple or the array is not this scope.

The revoked list

A minted token is a bearer credential good until its own exp — this server contacts nothing outside itself to learn anything sooner (see Sovereignty above). To revoke one early — a leaked token, an offboarded agent — the host places revoked.json beside the issuers file (--revoked <path>, or the issuers directory's brainy-serve-revoked by default), copied VERBATIM from auth's own public feed by the deployment; this server never fetches it itself:

{ "revoked": [ { "jti": "3f9a…", "exp": 1799999999 } ] }JSON

exp is the revoked token's own expiry, carried so a stale entry can be dropped once the token would have expired on its own terms anyway — it is not a deadline on the revocation. A token whose jti is listed, and still inside that entry's own exp, refuses BRAINY_KEY_REVOKED (401) at the same ladder position BRAINY_KEY_BAD_SIGNATURE holds: revocation is asked only of a payload already proven the issuer's own.

Like the issuers file, this one is re-read on its own mtime change, checked at most once every 5 seconds, no restart. A missing file means no jti this server is shown is ever revoked — a legitimate, documented state, not a refusal. A present file that fails to parse refuses this server's START the first time; on a later reload it is logged and the last good list is kept.

The revocation's latency of record, end to end, is the sum of three numbers, none of them this server's alone: the feed's own max-age (however long auth is willing to serve a stale answer), the host's own pull cadence (however often the deployment copies the feed into revoked.json), and up to 5 seconds for this server's own check window. A self-hosted deployment that runs no such pipeline places its own revoked.json by hand, or none at all — an absent file is a legitimate choice, not a missing feature.

The refusals

Eight classes, none of them a contract door's own — the same family UnauthenticatedError, ForbiddenError and the transport's other local refusals already belong to, documented here because this is where a caller learns to expect them rather than in the generated engine contract, which knows nothing about bearer credentials:

Class

Status

When

BRAINY_KEY_MALFORMED

401

The bearer does not parse as bk1.<payload>.<signature> — wrong segment count, illegal base64url, undecodable JSON, an illegal scope, or a brain that is not one legal path segment.

BRAINY_KEY_UNKNOWN_ISSUER

401

The token's iss (or iss/kid pair) names nobody in the issuers file.

BRAINY_KEY_BAD_SIGNATURE

401

No key this issuer is trusted for verifies the signature.

BRAINY_KEY_REVOKED

401

The token's jti is on the host-placed revoked list, still inside that entry's own exp.

BRAINY_KEY_EXPIRED

401

Past exp plus the 60-second clock-skew allowance.

BRAINY_KEY_SCOPE

403

The door mutates the store and the token's scope is read only.

BRAINY_KEY_BRAIN

403

The door addresses a brain other than the one the token names.

PolicyOwnerScopeRequiredError

403

The door rules on policy (policy.ratify/policy.unlock/policy.freeze) and this bearer's scope is not the owner triple.

A missing or unrecognised bearer of ANY shape (no header, a non-bearer scheme, an unknown static key) still answers the existing generic UnauthenticatedError — deliberately: a static key is a secret an attacker can probe near-misses against, and a specific refusal there would be a hint. A bk1 token is signed, not secret, so there is nothing to be gained by hiding which of the checks it failed and real operator value in being told which.

Every transport answers the same way: HTTP/1.1, h2 and h3 in the JSON or MessagePack body (error.name); gRPC as UNAUTHENTICATED for the five identity-time classes — BRAINY_KEY_MALFORMED, BRAINY_KEY_UNKNOWN_ISSUER, BRAINY_KEY_BAD_SIGNATURE, BRAINY_KEY_REVOKED, BRAINY_KEY_EXPIRED — the message text names which, since gRPC's status is coarser than HTTP's, and as the ANSWER's own refusal — the same shape ForbiddenError already uses — for BRAINY_KEY_SCOPE, BRAINY_KEY_BRAIN and PolicyOwnerScopeRequiredError, which are known-identity refusals, not credential failures; MCP and the WebSocket session both ride the same HTTP-shaped body their other refusals do. Authorization: Bearer bk1.… works identically over MCP, including stdio via $BRAINY_KEY.

The operator commands

Reference implementations — a self-hoster's own issuer, or a test rig; Identity mints its own keypairs and tokens the same way, never by shelling out to this binary:

# Generate a keypair. Prints the issuers-file line; writes the PRIVATE key
# ONLY to --out, mode 0600 — never to stdout.
brainy-serve issuer keygen --issuer soulcraft-identity --out ./issuer.key

# Mint a token. Reads the issuer id (and kid, if any) from the file above.
brainy-serve token mint \
  --issuer-key ./issuer.key \
  --brain wnw \
  --sub agent:anima \
  --scope read,write \
  --ttl 3600BASH

A worked example

$ brainy-serve issuer keygen --issuer example --out /tmp/example.key
[brainy serve] private key written to /tmp/example.key at mode 0600 — never printed.
[brainy serve] append this line to the issuers file:

example  ed25519:8f2c1a9e4b7d0f3c6a5e8b1d4c7f0a3e6b9c2d5f8a1e4b7c0d3f6a9e2b5c8d1f

$ echo 'example  ed25519:8f2c1a9e4b7d0f3c6a5e8b1d4c7f0a3e6b9c2d5f8a1e4b7c0d3f6a9e2b5c8d1f' \
    >> ~/.config/soulcraft/brainy-serve-issuers
$ chmod 600 ~/.config/soulcraft/brainy-serve-issuers

$ brainy-serve token mint --issuer-key /tmp/example.key --brain wnw \
    --sub agent:anima --scope read --ttl 600
bk1.eyJpc3MiOiJleGFtcGxlIiwic3ViIjoiYWdlbnQ6YW5pbWEiLCJicmFpbiI6IndudyIsInNjb3BlIjpbInJlYWQiXSwiaWF0IjoxNzU3MjAwMDAwLCJleHAiOjE3NTcyMDA2MDB9.MEUCIQ…

$ curl -H 'Authorization: Bearer bk1.eyJ…' http://127.0.0.1:8300/v1/brains/wnw/countsBASH

(Keys are throwaway; the payload above is illustrative, not a real signature.) A running brainy-serve serve picks up the new issuers-file line on its own — within 5 seconds of the file's mtime changing, no restart needed.

The writer model, and the pool

One writer per store, and the lock decides. The writer of a store is whoever opens it read-write; the store's lease admits exactly one; every other attach is read-only. That is one law for the whole engine, not a server policy: the lease is locks/_writer.lock, its clean-close record is locks/_writer.close, and the claim, the staleness verdict and the fence are native/src/brain/writer.rs — the same module Brainy.init() reaches across a napi boundary in a Node process and this server links directly as a library. Two implementations of one lock is how a store ends up with two live holders, which is a class of incident this engine has already paid for once.

One holder per brain inside this process. The server is the holder, and every door goes through the pool. There is no second way to open a store here: a reader opened beside the pool would be a second holder of a single-writer store.

The writer lease, and which doors move it

POST /v1/brains/{brain}/open — the init door — is the read-write open, and the only door that takes a lease. It is service-key only. It answers with a writer block that says which role this server got:

  • It won the lease. role: "writer", the lease's own pid, hostname, startedAt and heartbeat, and claimPath — how it came by it: fresh (no lease was there), leftover-after-clean-close (bookkeeping an orderly shutdown could not remove — nothing to recover), stale-after-crash (the previous writer left no close record, so this open owes recovery and says so), forced (an operator's {"force": true}, never an inference).

  • Another live process holds it. role: "reader", the holder named by pid and host, and refused: "BRAINY_WRITER_LOCKED" — the engine's own code. The store is still served for reads. This server never queues behind a holder, never polls for one to leave, and never downgrades silently: the refusal is in the answer, by name, and a second warning says the open went read-only.

A read never claims. The pool opens a store for whichever door needs it first, and almost every door is a read; a pool that claimed on admission would make this server the writer of every store it was asked to read from.

POST /v1/brains/{brain}/close gives the lease back and records the orderly close, then drops the pool's handle — the canonical reader, its descriptors and its mmap regions. A brain another door is still answering from stays alive until that door finishes. The recorded close is what lets the NEXT open know the store was closed cleanly as a fact rather than infer it from a pid, so an orderly stop owes it: the server also releases every lease it holds on shutdown, after the listeners drain, and narrates each one.

The lease outlives a residency, deliberately. Eviction is a memory decision; the writer role is not. A store this server is the writer of stays a store this server is the writer of across an eviction and a readmission — otherwise the arbiter would be handing the writer role to whatever process claimed next.

What a lease costs on this build

This build answers no write door on the wire. Every one of them refuses by name with the release it arrives in, because the core's executing write path has not landed. So a store whose lease this server holds is a store nothing can write until it is closed. That is not left for an operator to discover from a failed write:

  • init's answer carries a writer-serves-no-writes warning naming the release.

  • The per-brain health door carries it as a write-doors check at warn, and the store's writer as a writer-lease check — pass when this server holds it and the fence still holds, warn when another live process does (the ordinary fleet shape, not a fault), fail when the lease exists and cannot be parsed (this server will not judge a lease it cannot read) or when this server's own fence has been lost.

  • GET /v1/health reports pool.writers and pool.writerBrains — how many of this population's stores this process is the writer of, and which.

Brains are named at startup (--brain <name>=<path>, repeatable) and none is opened until a door needs it, so a server holding a thousand names starts in milliseconds and pays for the stores it actually serves.

The budget is derived, never configured. The control-group limit when the process runs under one, otherwise total memory; the kernel's availability figure; and this process's own resident set, which is not the population's. A fifth of the reachable limit is held back, floored, because an arbiter that budgets to the last byte evicts on a schedule set by the kernel's out-of-memory killer. A box that publishes neither a control-group limit nor a memory total is floored and says so. There is one environment override and reading it is narrated every time, naming the number it replaced.

Eviction is a clean close; readmission is an attach. The whole handle goes and everything it held is released; the next door on that brain reopens it and waits — it is never refused. A brain is closed when it has been quiet for sixteen of its own measured access intervals (floored at thirty seconds so it is never closed between two phases of one request, capped at thirty minutes), or when the population is over budget, largest measured footprint first.

A brain with a door in flight is never evicted — excluded, not ranked last. A brain answering a request is not idle whatever it costs.

GET /v1/health reports the arbiter: what it may hold, what it holds, how many brains it knows, and whether the budget was floored or overridden.

Read-only attach

The file doors — vfs.readFile, vfs.readdir, vfs.stat, and through them the MCP skills and the store's own instructions — read through a read-only attach. It is worth saying exactly what that is, because it is not a read-only mode flag:

  • Nothing is claimed and nothing is written. No writer lock is taken by the attach itself — the lease is init's business and nothing else's, so a server that was never asked to open a store read-write never touches its lock directory. No directory is created, no log is grown or rotated, no archive is swept, no torn artifact is quarantined, no census is written, and no background job starts. Dropping the handle releases every map; there is nothing to close, because nothing was claimed.

  • It judges servability by the OWNING open's law, not by its own. A reader with a second opinion about whether a store may be served is a second truth.

  • It refuses; it never repairs. Every write the owning open makes is correct because that process owns the store. A reader that quarantined a torn SSTable would be deleting evidence from a store another process is writing, and one that created a missing id mapper would turn a lost mapper into "this store is empty". So a store that cannot be served exactly as it stands refuses by name, carrying the reason: a writer handoff in flight, a torn SSTable, a record profile from a newer writer, an unreadable generation stamp, a metadata projection nothing attested.

  • The unflushed tail is part of the read. Mapping only the sealed SSTables would answer "no such file" for everything written since the last flush.

One generation per attach

An attach is frozen at the generation it resolved, for the whole life of the brain's residency. A file written after it is invisible to it — not stale for a while, invisible.

Re-attach is how a reader moves forward, and it happens exactly where the pool's lifecycle already happens: a brain that is evicted and readmitted attaches again, at whatever generation is serving then, and a restart does the same. There is no refresh timer and there will not be one. A reader that re-resolved its generation under a caller would answer two requests of one session from two states of the store, and nothing in either answer would say so.

The attach itself is paid by the FIRST file door on a residency, not by admission: a read by id needs the canonical reader and nothing else, so opening a brain does not map a metadata read set that only the path doors read through.

What the file doors answer

readFile resolves the path through the store's own path posting — never a directory walk — reads the content-addressed blob, and verifies the bytes against their own address before returning them. A blob whose bytes do not hash to their own address is corruption and is reported as such, never served. utf-8 is the default encoding and it REFUSES rather than decode lossily, naming the offset where text stops and base64 as the cure.

readdir reads the path field's value space out of the attached read set and answers every node under the prefix, at any depth. Each text file's bytes are carried inline up to a megabyte, so a client building a resource index resolves titles and descriptions from front matter in one call rather than one call per document; a file over that cap, or one whose media type is not text, is listed with its size and its media type and says why its bytes are not there.

stat answers one node's recorded type, media type and size without opening a blob at all.

The accords reads

The accords profile's five READ doors are served natively, through the same read-only attach the file doors use and under the same laws. They are reached under a brain, namespaced so they cannot collide with an engine door of the same name — read, search and resolve exist in both families:

Door

HTTP

MCP tool

Session

accords.inbox

POST /v1/brains/{brain}/accords/inbox

accord_inbox

accords.inbox

accords.dashboard

GET /v1/brains/{brain}/accords/dashboard

accord_dashboard

accords.dashboard

accords.search

POST /v1/brains/{brain}/accords/search

accord_search

accords.search

accords.read

POST /v1/brains/{brain}/accords/records

accord_read

accords.read

accords.timeline

POST /v1/brains/{brain}/accords/timeline

accord_timeline

accords.timeline

Every one of them is BrainPrincipal: holding a key for the board is enough, and none of them is an engine mechanic. The eight accords WRITES are present on every transport and refuse by name with the release they arrive in — never a no-op and never an empty success.

The in-process brain.accords.* facade forwards these same five reads. It reaches them through the SAME Stage 3 Phase A Brain facade this server runs on (native/src/brain.rs, native/src/brain_napi.rs's NativeBrain.accordsInbox() / …, wrapped by NativeBrainDoors in src/native/brain.ts) — reused rather than a second napi surface, so this door and this server answer from one implementation. The handle opens once per brain and is held at the generation it attached — a row written after the first read is invisible to that facade until the brain is closed and reopened, the same law this server's own residency pool lives under. brain.accords.inbox() (and dashboard/search/read/timeline) therefore answer for real in a TypeScript process with no socket in the loop; the eight writes still throw NotYetNativeError there too.

One read({ id }) for all nine kinds. A record id carries its own kind prefix (thread:ACC-X, decision:a-slug, round:thread:ACC-X:3), so the id names the row and there is no kind parameter to get wrong: a wrong-kind read cannot exist. A BARE protocol id is probed under every prefix in one batch — exactly one hit answers, several refuse as ambiguous carrying both ids, none refuses as not found carrying what was probed.

timeline({ id }) is one door for both families. A thread's rounds and an action's log entries are the same row kind under the same parent law, so a caller never chooses between them. The delta form (afterSeq) is one more CLAUSE, not a slice after the read, so a reader that has seen forty of nine hundred turns pays for the ones it has not seen. A timeline whose positions are not the exact contiguous range refuses as corrupt rather than answering a negotiation with a turn missing.

search is kind-agnostic, and an exact id cannot be outranked. A query containing a record's protocol id — bare or prefixed — returns that record as hit one whatever kind it is: its score is set one above the highest a word count can reach, so the property is arithmetic rather than a sort that happens to hold. The rest is the engine's own text leg over the same word postings find ranks by, inside the candidate set the caller's narrowings produced. With no query there is no relevance AND no re-sort: the search view is one find() with no order key, so the rows come back in the store's own order — the order the reference engine answers that view in, which the cross-language pin holds this one to.

inbox answers in protocol order — the mandate, the decisions awaiting the user's word, this participant's open actions, their live threads — each an ordered set of native segments, clipped at the caller's limit with the pre-truncation counts in totals so a reader knows what was cut. dashboard is the board's whole shape, and its lab lens is a predicate on a nested provenance key applied in the query rather than after the fetch.

Two halves these reads do not serve, said in the answer

  • The semantic leg of search. A vector search needs the vector index and an embedding provider, and an attach maps the metadata read set and the canonical tree — neither of those. So the answer carries semantic.served: false with the reason, and the text leg it did serve is named. A door that answered a hybrid query from half its legs without saying so would be the quiet wrong answer this server exists to refuse.

  • The presence stamp. A participant's own read of their own board moves their durable last-read stamp (the profile's rule R16). That is a WRITE, and a read-only attach takes no lock and writes nothing, so inbox answers presenceStamped.stamped: false with the release the stamp arrives in. The read half is whole.

Every answer also carries what it cost — how many segments ran, how many candidates their postings matched, how many rows were read. An accord view orders by a metadata key and an attach has no column store, so ordering a segment reads the rows it ordered: honest work, visible in the counters, and bounded by the segment rather than by the store.

The refusals these doors can hand a caller, by name

Class

HTTP

When

NoSuchAccordRecordError

404

The board holds no record under that id. Carries every id that was probed.

AccordIdAmbiguousError

409

A bare id names a record under more than one kind prefix. Carries both.

NotAnAccordRecordError

409

The store holds that row and its (type, subtype) pair is no kind the profile declares.

MandateAmbiguousError

409

The board does not hold exactly one active mandate (the profile's rule S7). Carries the count.

TimelineCorruptError

500

A timeline's positions are not the contiguous range they must be (S5). Carries expected and found.

UnindexedAccordFieldError

400

A narrowing or an ordering on a field the profile declares unindexed. Carries the field.

UnknownAccordKindError

400

A kind or lens narrowing outside the set the read serves. Carries the set.

AccordParameterError

400

A required parameter is absent, or a cursor cannot name a position.

AccordPostingUnreadableError

500

A posting, a value space, or an entity int the index named could not be read.

AccordRowUnreadableError

500

A record the index named exists and its identity record could not be read.

StoreNotAttachableError

409

The store cannot be attached for reading as it stands — the attach's own reason, by name. The cure is an action by the store's owner, never a retry.

MandateAmbiguousError and TimelineCorruptError are the accord PROFILE's own classes, published in the API contract with the rule each enforces; the rest are this server's and are declared in the generated contract at /contract.json.

What serves, and what refuses

Served natively today by the MODEL rather than by a store: embed, embedBatch and similarity. The engine's own all-MiniLM-L6-v2 is loaded from files on the box — the same weights the Node package maps, found beside the binary or at BRAINY_EMBED_MODEL_DIR — and nothing is fetched. A server with no model REFUSES those three doors by name and lists every directory it looked in; it never answers a zero vector, because a vector is what a row MEANS to a store and a wrong one makes every search plausible and wrong.

Served natively from the store: the canonical reader, get, batchGet, counts, explain, paged reads, aggregates, now/generation/asOf/diff/history, export, the lifecycle pair init and close — the read-write open and the lease's release — the per-brain health verdict, diagnostics, repair, snapshot, the change feed, the VFS reads (readFile, readdir, stat), the accords profile's five reads (inbox, dashboard, search, read, timeline), and the two query doors —

  • find's metadata legs: where (every operator the conformance table freezes), type, subtype, service, excludeVFS, the visibility law, orderBy/order, and limit/offset. The filter is evaluated by the SAME evaluator a Node process runs, so a where cannot mean one thing here and something else in process.

  • related: one hop, from the GRAPH ADJACENCY projection attached read-only — which is where an edge's endpoints actually live. { node } is undirected, { from }/{ to } are directed, type/subtype/service narrow the edge set, and the page is edge id ascending.

Four things are said here rather than discovered. An orderBy reads one value per MATCHED row (there is no column store in a read-only attach) and therefore carries the engine's own bounded-fallback budget of 1000 matches, refusing by name above it with the cure. A stored record with no timestamps keeps none here, where the in-process door fills them with the clock. A directed related page is edge id ascending here and comes back in the adjacency's own set-iteration order in process — the same edges, and only one of those two orders survives a rebuild. And a { node } page with an OFFSET is cut from the whole ordered incidence here, where the in-process door fetches offset + limit from each side, sorts that prefix and slices — its own documented best-effort for a high-degree node.

related is worth a sentence about where it reads, because the obvious reading was wrong. The in-process door builds a filter over verb records (fromsourceId, totargetId), and a stored relation record keys bare — so one hop looks like one posting read. MEASURED on a store this engine wrote: the metadata index posts an edge's verb, its subtype and its whole user bag, and posts neither sourceId nor targetId — a filter on either answers zero rows on every store. The endpoints are in the graph adjacency projection, so that is what this server maps, gated on the projection's own stamp: it answers when the stamp says the persisted trees cover the generation the store is committed at, and REFUSES by name with both numbers when they are behind. It never runs the recovery walk an in-process index runs to close that gap — that would turn one hop into a scan of every verb — and never answers the empty page a posting-driven implementation would have returned for every anchor, which reads exactly like "this node has no edges".

Refusing by name because a read-only attach maps no such index, each naming what is absent and where the same query is served: a semantic legquery text, an explicit vector, or a searchMode of vector/semantic/hybrid/text — and similar (no vector index in the read set); a graph legfind({ connected }), searchMode: 'graph' and graph traversal (the multi-hop walk, which the one-hop attach above does not serve); and the ranked and aggregated shapespurpose, rerank: true, aggregate, fusion, near, passages and a ranked cursor. None of them is approximated: a semantic find answered from the word postings alone would rank plausibly and wrongly, and that is worse than a refusal a client can route around.

Refusing by name until the core owns the write path: add, update, remove, addMany, updateMany, removeMany, relate, unrelate, updateRelation, relateMany, transact, clear, import, the VFS writes, the two aggregate-definition doors, and the accords profile's eight writes (postRound, postUpdate, file, decide, vote, record, resolve, escalate).

Refusing as in-process only: use (a plugin is code) and storageAdapter (a live object holding this process's file descriptors and its writer lock). Each names what to call instead.