Skip to content

Releases: markfietje/brain-server

v1.27.25

Choose a tag to compare

@github-actions github-actions released this 19 Aug 13:17

[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 carries k.pii into 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 hardcoded pii: false — a cross-domain,
    cross-owner, unredacted side door on /recall, /search, and
    /ump/recall in 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).
  • /verify binds the X-Brain-Domain label 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 by verify_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 by ump_get_memory_cannot_cross_domain.
  • GET /procedure/{id}/steps binds the domain label + record gate (S2-30).
  • GET /domains/{name}/export requires 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). The VACUUM INTO path 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 by rate_limit_layer_is_outside_auth_layers.
  • GET /graph/relationships/{id}/history gates on Action::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).
  • /add writes the quarantine flag IN-TX, before the commit — a failed
    flag write now rolls the whole chunk back (the /ingest/memory posture)
    instead of leaving the injection chunk durably stored flagged = 0 while
    telling the caller it failed (S3-06).
  • /suggest applies 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-For trusts 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 INTO literals 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
    retention evidence 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_chain enforces the NULL-prefix rule (F-03, the no-hash-change
    half): a NULL prev_hash is 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 restore re-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 triple WHERE 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 the X-Brain-Domain label in SQL; /stats counts 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-registry domain_invalid error no longer embeds the
    known_domains inventory (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).
  • /clients denies 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 failed secure_delete=ON it 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 == to row satisfied both IN (…) 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: false disables the graph leg again. The
    flag previously OMITTED the graph param 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/db schemas 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.md egress inventory is
    truthful (three enumerated, bounded, opt-in/gated paths — not "exactly
    one").

Full Changelog: v1.27.24...v1.27.25

v1.27.24

Choose a tag to compare

@github-actions github-actions released this 18 Aug 12:45

[1.27.24] — 2026-08-18

Release notes

Security fixes

  • A corrupt breach jurisdictions cell 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 in auth, recall, ump, govern; the never-used
    authorize_read_domain; the never-read ProposalRow.created_at; the UMP
    recall ranking_hints request field, now _ranking_hints with its wire key
    preserved). No behavior change — clippy -D warnings is now the dead-code
    watchdog instead of a blanket allow.

Full Changelog: v1.27.23...v1.27.24

v1.27.23 — Medicate

Choose a tag to compare

@markfietje markfietje released this 18 Aug 10:34

[1.27.23] — 2026-08-18

Server-only release (server Cargo.toml/lock 1.27.221.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 /health is 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 audit returns exit 0 on both
    the server and client trees; the two remaining findings are unmaintained
    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 a warn! (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

Choose a tag to compare

@markfietje markfietje released this 18 Aug 09:41

[1.27.22] — 2026-08-18

Server-only release (server Cargo.toml/lock 1.27.211.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-once INSERT OR IGNORE with 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) + a current flag — 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, detail created:<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

Choose a tag to compare

@github-actions github-actions released this 18 Aug 03:48

[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 and forget — but POST /ump/forget {"hard":true} (reachable at Write scope via the MCP ump.forget
    tool) and the ingest-replace/vault sweep bypassed the fence and could erase
    it. Both now run refuse_if_held in-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 shared strip_sentinels primitive now removes both
    literals before wrapping on every seam (MCP tool_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 backup now
    defaults to v3) binds the exact header bytes as GCM AAD, and
    validate_kdf_params bounds attacker-controlled Argon2id params before any
    allocation (m 8 MiB..1 GiB, t 1..=64, p 1..=8) so a crafted m = u32::MAX
    errors (kdf_params_out_of_range) instead of OOMing. brain backup accepts
    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 shared global pool, 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
    passes require_dpo_role only 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 400s live_set_empty unless
    the caller sets allow_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 now SHA-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) and kept_set drift detection vs the
    parent catch same-length row swaps.
  • Fence sentinel in the plugin (M7). The plugin resolves its bearer via the
    env ladder BRAIN_TOKEN_FILEBRAIN_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

Choose a tag to compare

@github-actions github-actions released this 17 Aug 02:16

[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
    through t()/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 missing E (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 --json envelope mode is here. query, explain, get,
    ingest-dir, suggest, suggest-metrics, retention, snapshot-status,
    connector-status, status, and eval all 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, so ingest-dir --dry-run ~/vault finally works. Unknown flags exit 2 instead of being silently
    swallowed, -- ends flag parsing, and a bad value like --k abc exits 2
    with a clear message instead of silently becoming 5. ingest-dir exits
    non-zero when every file failed. status renders absent counters as n/a.
  • brain --help cannot drift. Help is generated from the same subcommand
    table the dispatcher uses — the orphaned brain client add line is gone,
    brain token rotate and brain 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 abc silently coerced to 5; unknown --flag values were swallowed
    instead of refused.
  • brain status printed -1 for absent counters.
  • brain client add rendered flush-left in help output.

Full Changelog: v1.27.19...v1.27.20

v1.27.19

Choose a tag to compare

@github-actions github-actions released this 16 Aug 12:28

[1.27.19] — 2026-08-16

Release notes

Security fixes

  • A failed logout/revoke no longer says 204 "done". POST /auth/logout
    and POST /auth/revoke wrote 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 as 500 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 each SearchResult with 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 internal blocklist_hit flag (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 failed PRAGMA secure_delete=ON or 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's hardening block now carries a monotonic audit_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 an aria-live status line (was let _ = on the result, or if let Ok on 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 a pop() + ? — no panic fallback left on the write path.

Bug fixes

  • Dead "reserved" trace vocabulary removed. trace.rs shipped 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_VISITED traversal caps) is unchanged.

Full Changelog: v1.27.18...v1.27.19

v1.27.18

Choose a tag to compare

@github-actions github-actions released this 16 Aug 10:45

[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 two IN groups never bound → silent empty links) is fixed and
    regression-pinned.
  • Read-seam fast path: sanitize_read_cow returns 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-local VEC0_READY flag replaces the per-query
    "does vec0 exist" probe.
  • /domains/{name}/import dial 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/memory could store an oversized entry or silently report
    "Empty content" for invalid UTF-8.
    Both now hard-reject: per-entry content
    over MAX_CONTENT400 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 uses unixepoch(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_heading added (domain delete,
    DSAR subject resolution, proposal write-gate dedup); idx_tombstones_kid,
    idx_entities_name, idx_evidence_links_from dropped (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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 08:44

[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 from SHA-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 / backup VACUUM INTO file 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 via create_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 backup gains --format v1|v2 (default v2); restore and
    brain doctor --backup auto-detect both formats.
  • Backup refuses to run while a stale brain.bak exists (a swapped/truncated
    source DB was previously enshrined as the "safety snapshot").

Full Changelog: v1.27.16...v1.27.17

v1.27.16

Choose a tag to compare

@github-actions github-actions released this 16 Aug 07:53

[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 a warn!. /auth/logout is 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_for NEVER opens a file for an unregistered
    name (previously any probeable read created brain-<name>.db lazily —
    unbounded disk fill). POST /domains is the one creation path, bounded by
    BRAIN_MAX_DOMAIN_DBS (default 256; 507 insufficient_storage beyond 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. /search now authorizes
    against the domain it actually queries (was always global). /get/{id}
    and /multi-get bind the header's X-Brain-Domain label 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-case memory_kind (400 invalid_memory_kind
    no silent fallback to fact) and a confidence outside 0.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 /add may only use the closed source
    vocabulary (ingest kinds + connector family kinds) — manual, the
    origin:human marker, 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 peer SocketAddr extension, so every client shared ONE
    "unknown" bucket — a global rate limit in practice. The server now serves
    with into_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