Releases: markfietje/brain-server
Release list
v1.27.25
[1.27.25] — 2026-08-19
Release notes
Security fixes
- The graph-PPR third recall leg is now scoped like the vector and FTS
legs. It applies the domain label,access_scope, owner, memory-kind and
retention predicates via the same shared SQL builder (push_gate_filters),
and carriesk.piiinto the hit so the read seam redacts graph hits
exactly like the other legs. Before this, the leg (unreleased default-on)
ignored every filter and hardcodedpii: false— a cross-domain,
cross-owner, unredacted side door on/recall,/search, and
/ump/recallin shim mode (pass-3 S3-01, CRITICAL). Pinned by
graph_leg_scopes_domain_and_owner_s3_01+
graph_leg_empty_permit_and_pii_carry_s3_01(two-domain shared-entity
fixture — the exact collision shape of the finding). /verifybinds theX-Brain-Domainlabel in SQL + the record gate
(the/get/{id}idiom): a foreign-domain chunk id now reads as not-found
instead of answering "supported" as a cross-domain content-confirmation
oracle (S2-09). Pinned byverify_cannot_cross_domain.GET /ump/memory/{id}binds the domain label + record gate — the
MCP-reachable (ump.get) surface no longer renders any row by bare id
under a global read grant (S2-10). Pinned byump_get_memory_cannot_cross_domain.GET /procedure/{id}/stepsbinds the domain label + record gate (S2-30).GET /domains/{name}/exportrequires Admin in shim mode — the snapshot
resolves to the ONE shared pool there (every tenant's chunks, owners, the
audit chain), which a per-name Read grant must never cover. Multi-db keeps
Read (the file IS the domain). TheVACUUM INTOpath now goes through the
shared quote-escaping primitive (S2-08/S2-24).- The rate limiter moved OUTSIDE the auth layers. An unauthenticated
flood is now 429-throttled before any token work — previously it
401-rejected before ever consuming a bucket, and each free 401 performed a
synchronous audit write on a fresh connection (unthrottled
DB-write-per-request amplification). The deny-path audit writes now run on
spawn_blocking(S3-03). Pinned byrate_limit_layer_is_outside_auth_layers. GET /graph/relationships/{id}/historygates onAction::Admin,
matching what every doc surface (CHANGELOG §1.27.22, openapi.yaml,
docs/api.md, its own doc comments) already claimed — the retired
PII-bearing entity labels it returns are operator evidence. The read-audit
failure is no longer silent (S3-02)./addwrites the quarantine flag IN-TX, before the commit — a failed
flag write now rolls the whole chunk back (the/ingest/memoryposture)
instead of leaving the injection chunk durably storedflagged = 0while
telling the caller it failed (S3-06)./suggestapplies the v1.14 scope filter + v1.23 role gate like
/recall— an owner-restricted role no longer sees other owners' private
rows as suggestions (S2-29).- Smaller hardening:
X-Forwarded-Fortrusts the RIGHTMOST entry under
BRAIN_TRUST_PROXY=1(leftmost is client-spoofable; S2-39); the rate
limiter fails CLOSED on a poisoned lock (S2-50); the dead
"developer mode"blocklist entry now matches (whitespace is stripped
pre-match; S2-44); the audit-chain BEGIN-failure path bumps
audit_commit_failures(it was silent; S3-09); the two boot-time
VACUUM INTOliterals go through the escaped primitive (S3-11).
Security fixes (wave 2 — the deferred items)
- The audit retention prune now VERIFIES before it prunes and records a
retentionevidence row for what it deleted — previously the re-anchor
would have re-blessed a tampered chain into a freshly-verifying one
(evidence laundering), and the deletion of audit evidence was itself
unevidenced. A failed re-anchor UPDATE now rolls the whole prune back
instead of committing a half-rewritten chain (S2-16 + S2-35). verify_chainenforces the NULL-prefix rule (F-03, the no-hash-change
half): a NULLprev_hashis legal only before the chain starts. Legitimate
writers always chain from the tip once one exists, so a mid-chain NULL is
tamper — previously it was skipped silently at any position. No stored hash
changes.brain restorere-applies ACTIVE legal holds from the pre-restore DB
and loudly discloses tombstoned content the backup resurrected — a pre-hold
backup no longer silently unfreezes litigation-held ids, and an undone
DSAR purge is on the record (S2-28).- The open-edge invariant is structural:
idx_rels_open_unique(partial
UNIQUE on the tripleWHERE superseded_at IS NULL, after a deterministic
newest-wins dedup of legacy double-open rows) — a racing double-insert now
fails at the DB and rolls back the ingest instead of corrupting the
lineage (S3-08; schema → 1.27.25). - The remaining shim-mode reads are scoped:
/decayed+/quarantine
bind theX-Brain-Domainlabel in SQL;/statscounts by domain label
(entities/relationships via their chunk linkage);/consolidate/propose
requires Admin in shim mode (its five detection scans are corpus-wide);
the domain-registrydomain_invaliderror no longer embeds the
known_domainsinventory (S2-31/43/32). - Ingest auto-routing re-authorizes on the ACTUAL target — a
write:<t>/global-only principal can no longer contaminate another
tenant's domain through centroid routing (S2-33). /clientsdenies empty-grant auditors at the gate (403, not a silent
200-empty — "Some([]) denies all" now means the surface too; S2-15).- The DSAR certificate's remanence claim follows the pragma attempt — on
a failedsecure_delete=ONit downgrades to the disclosed logical posture
instead of certifying an overwrite that never ran (S2-18). - Chunker fidelity: an UNTERMINATED oversized fenced block no longer
duplicates its final code line into every stored piece (the last line was
treated as a closer it wasn't); degenerate over-cap lines inside fences end
with a newline so re-attached closers sit at line starts; prose pieces stay
strict verbatim (S2-19/S2-20). - Evidence self-links are skipped in the batched enrichment (a
from == torow satisfied bothIN (…)groups and duplicated into API
responses; S2-38). Domain delete now archives tombstones +
evidence_links into the pre-delete segment alongside the audit rows — the
deletion registry is evidence and no longer dies with the domain (S2-21).
Bug fixes
- Plugin:
autoRecallGraph: falsedisables the graph leg again. The
flag previously OMITTED thegraphparam when false, so the server's
default-on change silently enabled the leg for every plugin user. The
flag is now always sent explicitly; the plugin's documented default stays
opt-in.
Improvements
openapi.yaml/health+/health/dbschemas now match the shipped
shapes (the public probe is{status, version}; the detailed body is
Read-gated on/health/db) — the contract previously documented the full
fingerprint body on the public route.SECURITY.mdegress inventory is
truthful (three enumerated, bounded, opt-in/gated paths — not "exactly
one").
Full Changelog: v1.27.24...v1.27.25
v1.27.24
[1.27.24] — 2026-08-18
Release notes
Security fixes
- A corrupt breach
jurisdictionscell now fails the row read instead of
silently becoming an empty list. If the stored JSON on a breach was
corrupted, the breach previously read back with zero affected jurisdictions —
hiding from the DPO every affected-law notification deadline that the breach
carries. That read now errors loudly (fail-closed, the repo's D-1 "never
certify silence" invariant) rather than presenting an empty scope.
Bug fixes
- Removed the blanket
#![allow(dead_code)]+#![allow(unused_imports)]
on the handlers module and deleted the real dead code they were hiding
(unused imports inauth,recall,ump,govern; the never-used
authorize_read_domain; the never-readProposalRow.created_at; the UMP
recallranking_hintsrequest field, now_ranking_hintswith its wire key
preserved). No behavior change — clippy-D warningsis now the dead-code
watchdog instead of a blanket allow.
Full Changelog: v1.27.23...v1.27.24
v1.27.23 — Medicate
[1.27.23] — 2026-08-18
Server-only release (server Cargo.toml/lock 1.27.22 → 1.27.23; client +
plugin unchanged). "Medicate" — the three security findings the adversarial
pass surfaced as still-open, delivered as small, behavior-gated hardening: no
new schema, no new endpoints, no wire change, no telemetry. Two landed here
(health surface reduction + fail-closed embed errors); the third (the bounded
outbound client) was already shipped in v1.27.21 (M9: 5 s connect / 15 s total
egress bound) and is re-verified, not re-built.
Security fixes
- Public
/healthis now the minimal probe shape (A-02). The load-balancer
probe (status+version) stays public; every deployment-fingerprinting
field —model,otel.endpoint,pool,backup,webhook,hardening,
compliance.dpo_contact,integrity— moved behind the existing Read gate on
/health/db. An unauthenticated network probe can no longer fingerprint a
regulated BPO deployment. Intentional surface reduction (same class as the
v1.20.2 F2 carve-out): an operator monitor reading the detailed fields must
switch to the gated/health/db. - Dependency hardening: h2 0.4.15 → 0.4.16 (RUSTSEC-2026-0258). The HTTP/2
dependency (reached via the reqwest/hyper client) was bumped to clear the
"unbounded empty DATA frames" advisory.cargo auditreturns exit 0 on both
the server and client trees; the two remaining findings areunmaintained
warnings (paste, number_prefix) deep in the HF tokenizers/model2vec stack —
not vulnerabilities, and not clearable without a major bump.
Bug fixes
- Embed failures are no longer silent (A-03). The feature-gated neural
embedders (bge-m3/gte-base-en-v1.5) logged nothing when the model
failed, returning an empty vector the callers silently skipped. Every failure
branch now emits awarn!(the D-1 "never certify silence" invariant the repo
enforces on the audit settle, quarantine flag, and purge residues). Behavior
is otherwise unchanged: callers already skip the row on an empty vector, so no
corrupt zero-length embedding was ever written — this closes only the missing
signal, not the guard.
v1.27.22 — Cascade
[1.27.22] — 2026-08-18
Server-only release (server Cargo.toml/lock 1.27.21 → 1.27.22; client +
plugin unchanged). "Cascade" — a bug-fix release closing two
documented-but-unimplemented behaviors in the graph edge layer: edge
supersession was write-once (nothing ever closed an old edge's invalid_at when
reality changed) and traversal claimed to skip superseded edges but never did.
This release makes the code true to its own documentation, reusing the
bi-temporal columns + hash-chained audit + quarantine machinery already shipped.
No new storage, no new schema columns/tables, no wire change, no telemetry; the
schema stamp advances to 1.27.22 for the added relationships.superseded_at
column + index swap.
Bug fixes
- Edge supersession is now wired (BUG-1). The ingest path replaced its
write-onceINSERT OR IGNOREwith a pure bi-temporal resolver
(resolve_edge_insert). Re-ingesting an unchanged relation is still an
idempotent no-op (no history churn); re-ingesting a relation with a changed
window/interval now retires the old edge version (superseded_at= the
transaction-time end, old row preserved verbatim) and inserts the corrected
version as the new current belief. The handoff is exact:
old.superseded_at == new.created_at. - Traversal now skips superseded edges (BUG-2), matching its own doc. The
recursive walk filters edges to current beliefs: live (superseded_at IS NULL) and the newest live version of their(from, to, relation_type)
triple. This is a no-op on well-formed/legacy DBs (a lone edge has no newer
live peer), so default recall/traversal output is byte-identical; it corrects
the case where a backdated supersession previously returned two edges claiming
the same triple at one instant. /graph/relationships/{id}/history(Admin, audited). A new read surface
reconstructs the full version history of an edge triple — every version in
order with its four timestamps (valid_at,invalid_at,created_at,
superseded_at) + acurrentflag — given any one version id, so a
superseded belief can always be recovered (supersession never deletes).- Superseded edges are hidden from graph + adjacency reads.
GET /graph/relations,entity_relations,relations_for, the UMP relation
fan-out, and the graph-PPR adjacency aggregation all filter to current
beliefs, so a retired edge no longer surfaces as a live relation.
Improvements
- Supersession events ride the existing hash-chained audit log
(AuditKind::Ingest, detailcreated:<id>/superseded:<old_id>->:<new_id>)
and the history-surface read is itself recorded (AuditKind::GraphRead). - Fail-closed: an inability to resolve an edge insert declines the ingest
transaction (never a silent half-write); an unresolvable history id returns
404 Relationship not found.
Security fixes
- None (no new trust boundary; the graph-label read seam posture is unchanged
from v1.27.21).
v1.27.21
[1.27.21] — 2026-08-18
Release notes
Security fixes
- Legal-hold fence closed on two erasure paths (S2-03 CRIT / S2-04). A held
chunk was frozen against/purge, DSAR andforget— butPOST /ump/forget {"hard":true}(reachable at Write scope via the MCPump.forget
tool) and the ingest-replace/vault sweep bypassed the fence and could erase
it. Both now runrefuse_if_heldin-tx →409 legal_hold_active, all-or-
nothing. - Fence-forgeability close (S2-02). A stored body containing the literal
=== BRAIN_UNTRUSTED_CONTEXT END ===(or BEGIN) would close the untrusted
region early. The sharedstrip_sentinelsprimitive now removes both
literals before wrapping on every seam (MCPtool_result_payload+
format_response, and the plugin's recall banner), ordered invisible-strip
first so a zero-width split cannot re-heal a marker into the fence. - Backup v3 header bound as GCM AAD + KDF bounds (S2-13 / S2-14). The v2
header was not covered by the GCM tag — any header bit could be flipped
without failing authentication. v3 (same byte layout,brain backupnow
defaults tov3) binds the exact header bytes as GCM AAD, and
validate_kdf_paramsbounds attacker-controlled Argon2id params before any
allocation (m 8 MiB..1 GiB, t 1..=64, p 1..=8) so a craftedm = u32::MAX
errors (kdf_params_out_of_range) instead of OOMing.brain backupaccepts
v1|v2|v3; legacy v1/v2 files keep their read paths. - Auth fail-closed (F-27 class). A single-team wildcard
read:<team>/*now
grants only the sharedglobalpool, never every tenant's named domain (a
flat domain namespace means the team field can never narrow a*domain
grant — naming a domain requires naming it); and a token with no roles
passesrequire_dpo_roleonly when the deployment defines no roles at all,
closing the single-token shape that could ride a bare admin scope.
Bug fixes
- Empty reconcile is an explicit decision (S2/N1). An empty
live_uris
previously retired every active vault source and swept its chunks,
indistinguishable from a failed listing. It now 400slive_set_emptyunless
the caller setsallow_empty: true; the client panel waives it only through
the shared two-step confirm. - Client offline-queue integrity (N5–N8). Retry-park (a persisted counter
parks an auto-replay after 5 failures instead of refiring forever;
destructive actions always park); idempotency key normalizes the volatile
fields out so a re-enqueue collapses onto its twin; the persisted DSAR
subject hash is nowSHA-256(salt ‖ subject)with a per-install salt
(defeats precomputed/rainbow tables, legacy items decode via the empty-salt
form); and the purge owner is persisted so an owner-scoped purge no
longer replays as an empty no-op body that silently erased nothing. - Replay drift (N9/N13). Char-boundary-safe
hash_prefix(a corrupt stored
hash truncates on char boundaries) andkept_setdrift detection vs the
parent catch same-length row swaps. - Fence sentinel in the plugin (M7). The plugin resolves its bearer via the
env ladderBRAIN_TOKEN_FILE→BRAIN_TOKEN→ config, never writes a token,
and its per-turn abstention log logs the query length only (a recall query
is user text and openclaw's log is persistent) — see the plugin 0.4.5
CHANGELOG. - Webhook egress bound. The egress client now enforces a 5 s connect / 15 s
total timeout so a hung sink cannot stall the request path.
Full Changelog: v1.27.20...v1.27.21
v1.27.20
[1.27.20] — 2026-08-17
Release notes
Improvements
- Every label in the app now resolves through the translation layer.
The five locale bundles (en/de/fr/es/nl) expose one identical key set, and
every render surface — main chrome, command palette, review queue, recall,
security, health, register, graph, subjects, ops, audit, data, system, ump,
ingest, procedures, consolidate, the shared confirm — resolves its labels
throught()/t_fmt()instead of hard-coded strings. A new source-scan
test gates future work so a raw string can't silently leak back into the
UI. The keyboard-shortcuts help also gained the missingE(edit) key. - A hung backend can no longer spin a panel forever. The client's shared
HTTP client carries the CLI's socket discipline (5s handshake / 15s total),
so a backend that stops answering surfaces as a network error instead of an
endlessly-loading panel. (The browser/wasm build keeps its own fetch
timeouts.) - The CLI's
--jsonenvelope mode is here.query,explain,get,
ingest-dir,suggest,suggest-metrics,retention,snapshot-status,
connector-status,status, andevalall emit a machine-parseable
{"ok":…,"cmd":…,"data":…}envelope with documented exit codes (0 ok ·
1 runtime · 2 usage). - Flag parsing is honest. Boolean flags (
--dry-run,--yes,--force,
--json, …) never swallow the next token, soingest-dir --dry-run ~/vaultfinally works. Unknown flags exit 2 instead of being silently
swallowed,--ends flag parsing, and a bad value like--k abcexits 2
with a clear message instead of silently becoming 5.ingest-direxits
non-zero when every file failed.statusrenders absent counters asn/a. brain --helpcannot drift. Help is generated from the same subcommand
table the dispatcher uses — the orphanedbrain client addline is gone,
brain token rotateandbrain ump …are now listed, and a
flags:/exit codes:section documents the contract.brain suggest
output also runs the same cleanup chain as recall/get.
Bug fixes
brain ingest-dir --dry-run <path>previously swallowed the path as the
flag's value and ingested nothing.--k abcsilently coerced to5; unknown--flagvalues were swallowed
instead of refused.brain statusprinted-1for absent counters.brain client addrendered flush-left in help output.
Full Changelog: v1.27.19...v1.27.20
v1.27.19
[1.27.19] — 2026-08-16
Release notes
Security fixes
- A failed logout/revoke no longer says 204 "done".
POST /auth/logout
andPOST /auth/revokewrote the token to the revocation denylist
best-effort and returned success regardless — an operator logging out
believed the token was dead when a failed INSERT left it live for its full
15-minute shelf life (and a revoked token could be refreshed). Both now
surface a denylist write failure as500 revoke_failed; success still
means the token is really dead. - Purge residue deletes propagate (were
let _ =). A chunk purge deleted
the tombstoned row's relationships / vec0 embedding / evidence links /
traces in silence — one failing DELETE while the rest succeeded left a
partial erasure that the purge then certified complete. Every residue
delete now participates in the purge transaction: a failure rolls the whole
purge back instead of certifying a lie. - The prompt-injection blocklist screen runs once per hit, not per
consumer. Recall constructed eachSearchResultwith raw bytes, then the
PRF query-expansion extractors re-normalized each hit's content against the
blocklist per query. The screen now runs once at construction and rides as
an internalblocklist_hitflag (never serialized); both extractors read
the flag. Behavior-identical, one scan saved per hit per query. - Erasure hygiene warns instead of certifying silence. The DSAR/shared
purge previously swallowed a failedPRAGMA secure_delete=ONor a failed
wal_checkpoint(TRUNCATE)— the two operations that ensure erased page
images don't survive in the WAL or freelist. Failures are now logged loudly
instead of whispering "erased".
Improvements
- Audit-settle failures are visible. The best-effort audit-chain settle
(COMMIT/ROLLBACK of the chained row) could fail under a busy writer — the
caller still got a row id, and nothing said the chain might have missed it.
/health'shardeningblock now carries a monotonicaudit_commit_failures
counter (0 = green; >0 = rows possibly off the durable chain). - Every other write-path
let _ =residue propagated (23 further sites):
chunk stored without its evidence links, stale vec0 rows surviving reindex,
webhook seen-writes, retention prunes, refresh failures, orphaned PII
residues, secure_delete/TRUNCATE on purge — each now either fails the
operation or warns with context. - Client decisions announce their outcome. A failed approve/reject in the
Operations queue, a failed quartine release/delete in Security, and failed
decayed/tombstone loads in the Data panel were silently dropped — each now
renders anaria-livestatus line (waslet _ =on the result, orif let Okon the load). - A single-record ingest lost its last panic. The singleton UMP path
lowered a one-element batch with.next().unwrap()behind a length guard;
it is now apop()+?— no panic fallback left on the write path.
Bug fixes
- Dead "reserved" trace vocabulary removed.
trace.rsshipped an
#[allow(dead_code)]update:/supersedes:/contradicts:/causes:
prefix vocabulary "reserved for v1.6 Reconcile"; v1.6 shipped and closed
without consuming it. The dead constants and their tests are gone — the
used surface (MAX_HOPS/MAX_VISITEDtraversal caps) is unchanged.
Full Changelog: v1.27.18...v1.27.19
v1.27.18
[1.27.18] — 2026-08-16
Release notes
Improvements
- PRF corpus weighting now really runs. The recall query-expansion path
extracts terms via the FTS5 vocabulary — corpus document-frequency weighting
was the design since v0.9.1, but the vocab query never executed against the
bundled SQLite (wrong column names), degrading every expansion to the
unweighted fallback. The queries now target the real schema, the df
round-trip is capped (MAX_DF_TERMS, adversarial-vocab bound), and the
expanded term lists are pinned by tests. Because the weighting now applies,
expansion output CHANGES versus 1.27.17 (corpus-idf re-ranking) — recall
eval rows will shift. - Release binary tuned for speed (
opt-level"z" → 2; LTO/strip/
codegen-units unchanged). The server is an in-process vector store, not a
download; "z" traded measurable recall-latency headroom for binary size. - Evidence enrichment batched (one links lookup per result set, was one
probe + one query per hit) — and the batched query's placeholder-pair bug
(one of twoINgroups never bound → silent empty links) is fixed and
regression-pinned. - Read-seam fast path:
sanitize_read_cowreturns the input borrowed —
zero copies — when every transform is provably a no-op (clean rows dominate). - Search filters become
Arc(cheap clones across per-domain recall
loops), and a process-localVEC0_READYflag replaces the per-query
"does vec0 exist" probe. /domains/{name}/importdial 1 GiB (was capped by the global 1 MiB
limit — the route's dedicated layer now sits before the global one; every
other route keeps the 1 MiB cap).
Bug fixes
/ingest/memorycould store an oversized entry or silently report
"Empty content" for invalid UTF-8. Both now hard-reject: per-entry content
overMAX_CONTENT→400 entry_too_large(all-or-nothing, before any
write), non-UTF-8 body →400 invalid_utf8. Every legacy wire shape is
unchanged.- Entity-mention dedup was quadratic (O(m²) containment scan per
sentence); now a linear running-scan with the old result pinned as a test
oracle on randomized fixtures. - The retention read-gate used
strftime('%s', …)TEXT math; the exact
same predicate now usesunixepoch(COALESCE(…))— value-identical (pinned
SQL-side) and index-friendly.
Security fixes
- Connection-tracker slot leak on ingest timeout. An
/ingest/memory
that exceeded the 60 s bound (and panics) kept its single-connection slot
until the next sweep; the slot is now an RAII guard released on every exit. - Reserved index slots vacuumed:
idx_knowledge_domain,
idx_knowledge_owner,idx_knowledge_title_headingadded (domain delete,
DSAR subject resolution, proposal write-gate dedup);idx_tombstones_kid,
idx_entities_name,idx_evidence_links_fromdropped (each a strict
duplicate of a UNIQUE autoindex or newer sibling). Schema → 1.27.18.
Full Changelog: v1.27.17...v1.27.18
v1.27.17
[1.27.17] — 2026-08-16
Release notes
Security fixes
- Per-backup random keys (was: deterministic nonce). A v1 backup derived
its AES-GCM nonce fromSHA-256(passphrase || created_at)— two backups
within the same second reused the identical nonce (catastrophic in GCM).
Backups now use argon2id key derivation with a random 16-byte salt and a
random 12-byte nonce sourced per backup from the RNG (new format; legacy
v1 files still restore). - Argon2id key derivation (was: SHA-256). v1 derived the 32-byte key with
a single SHA-256 of the passphrase — offline dictionary attacks at trivial
cost. New backups use argon2id (64 MiB / 3 passes / 1 lane, tuned to stay
under ~2 s on dev hardware). - Plaintext snapshot is 0600 at birth (was: umask-dependent). The
safety-snapshot / backupVACUUM INTOfile was created with umask-derived
permissions and chmod'd only after success — a crash inside the window left
readable plaintext. Snapshot files are now created 0600 viacreate_new
(a pre-existing file at the path aborts, never overwrites) and are removed
on every failure path. - Restore refuses to clobber the previous safety snapshot. Restoring over
an existing target already preserved the pre-restore state as<db>.bak;
a second restore silently failed on that file with a cryptic SQL error. It
now fails-closed with a clear message before touching the disk.
Improvements
brain backupgains--format v1|v2(default v2); restore and
brain doctor --backupauto-detect both formats.- Backup refuses to run while a stale
brain.bakexists (a swapped/truncated
source DB was previously enshrined as the "safety snapshot").
Full Changelog: v1.27.16...v1.27.17
v1.27.16
[1.27.16] — 2026-08-16
Release notes
Security fixes
- Auth degrades closed, never open. A poisoned token-store lock was an
empty set → "auth disabled" → allow-all; it is now fail-closed
500 auth_store_unavailable. A configured-but-empty token store (file or
env set, zero tokens) denied everything; it now returns 401 instead of
reading as "no auth". The JWT revocation check (v1.2.0) skipped itself on
ANY pool/SQL error (if let Ok(conn)+unwrap_or(false)); any store
failure now denies. The role-retrieval gate (v1.23.0) degraded to "no
narrowing" (read everything) on a pool/role-store error; it now degrades to
the empty permit (read nothing) with awarn!./auth/logoutis no longer
a public route: the presented access token is verified by the middleware
first — an unauthenticated "logout" could only ever succeed at revoking
nothing. - The multi-db domain registry is now registered-only and capped. In
BRAIN_MULTI_DB=true,pool_forNEVER opens a file for an unregistered
name (previously any probeable read createdbrain-<name>.dblazily —
unbounded disk fill).POST /domainsis the one creation path, bounded by
BRAIN_MAX_DOMAIN_DBS(default 256; 507insufficient_storagebeyond it);
every resolution read of an unknown name returns the probe-blind 404
domain_unknown(indistinguishable from an empty-but-real domain). The
clients-register boot seed keeps client domains resolvable if their file
vanished between boots (recreated on first access, still cap-bounded). - JWT principals are domain-scoped on reads.
/searchnow authorizes
against the domain it actually queries (was alwaysglobal)./get/{id}
and/multi-getbind the header'sX-Brain-Domainlabel in SQL — an id
can never cross domains in shim mode — re-authorize on the row's own
domain, and run the same record gate (v1.14 scopes + v1.23 roles) recall
enforces; foreign rows read as 404 / are dropped, never loud. Recall
federation and graph traversal drop foreign-domain targets before any
search runs; shim-mode graph edges scope by their chunk's provenance label
(an unlinked edge is invisible to scoped readers). - Trust labels are closed vocabularies at the write boundary.
/ingest
rejects an unknown/mixed-casememory_kind(400invalid_memory_kind—
no silent fallback tofact) and aconfidenceoutside0.0..=1.0(400
invalid_confidence— no silent clamping, a clamped lie hides the liar);
the proposal path (/proposals) enforces the same strict kind round-trip.
A JWT (agent) principal on/addmay only use the closedsource
vocabulary (ingest kinds + connector family kinds) —manual, the
origin:humanmarker, is excluded so a token-authenticated agent cannot
forge human authorship. The UMP L3 operator signing key now fails closed to
L2 on a group/world-readable seed file (same 0600 enforcement the other
secrets get). - The per-IP rate limiter actually was not per-IP. The serve wiring never
injected the peerSocketAddrextension, so every client shared ONE
"unknown" bucket — a global rate limit in practice. The server now serves
withinto_make_service_with_connect_info, buckets are keyed by remote
address (production-behavior pinned by a source-inspection test), and the
bounded key set (RATE_LIMIT_MAX_KEYS) evicts the oldest 25% rather than
growing unbounded.
Improvements
None.
Bug fixes
None.
Full Changelog: v1.27.15...v1.27.16