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=andping=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.shwraps 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’smemory-corekeeps 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 auditadvisory ID sets are identical old-lock vs new-lock — zero new advisories. presage+presage-store-sqlitepinrev = f74b96e0…(wasbranch = "main"). Upstream main had moved past the committed stack (newer libsignal-service pastbb43e81); 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-gateandsignal-gateway-gaterun clippy and test with--locked.cargo fmtcannot carry the flag (it rejects--locked; it resolves via--no-depsmetadata, which is also why staleness probes must use the full form). - The verification sweep gains
lock-freshness— a full-formcargo metadata --lockedlane over every TRACKED lockfile (tracked, not on-disk:fuzz/Cargo.lockis a gitignored local artifact no checkout ever sees). Local-only coverage; CI’s teeth are the--lockedflags. - 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 threesrc/comments shipped by the two preceding rounds (audit-id labels insrc/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 ACIINFO log lines are deleted. PII law on the module: no operand rides any log lane.POST /v1/cache/seedis audited at WARN with sha256 digests — loud and PII-lawful. - S9-03:
config.yaml(carriesauth_token) refuses group/world bits at load — the 0600 law the other secret files already enforce. - S9-04:
BrainClientfollows 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-migrationVACUUM INTObackup 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 inresolveConfig: a stringagents(which turned allowlists into substring matching), a stringautoRecallTopK, 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: unknownuntrustedOrigins/captureModeenum 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 thememory_recalltool 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_commentsis IMPOSSIBLE and was not attempted. It ispub fn, butspire_inventoryis#[cfg(test)] pub mod(src/lib.rs:346), so it does not exist in the lib an integration test links against — thecfgis the blocker, not visibility. The F8-04 pin therefore lives intests/main_suite.rsand reuses the two existing test-side house lexers (strip_line_comments/strip_cfg_test_regions). No secondsrc/stripper was written;dup_guardis untouched. - F8-09’s reachability claim was wrong in the direction that matters. The note
predicted the row-mapping arm unreachable because
TEXTaffinity coerces every storage class. Measured against SQLite: true forINTEGERandREAL, false forBLOB. A BLOBroster_jsonIS 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 suggested42/1.5fixtures used — the pin would have been green before the fix while proving the arm that was not changed. The pin assertstypeof(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:
| # | Planted | Caught |
|---|---|---|
| 1 | revert recall.rs to the raw interpolation | guard fires naming recall.rs:575 |
| 2 | plant impl From<&str> for LogValue | constructor pin fires |
| 3 | register /webhooks/noverify in the real router | declaration pin fires |
| 4 | revert the ::/96 normalisation | ::169.254.169.254 not refused |
| 5 | delete the two v4 rows | 224.0.0.1 not refused |
| 6 | plant BIND_PORT=abc → Ok(8765) | boot-refusal pin fires |
| 7 | restore .flatten() | Ok(SweepReport { crew_rows: 0, .. }) where a refusal was required |
| 8 | move 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
VACUUMhalf — 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_blockingwrite handlers insrc/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.
::/96is 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:
| Row | The audit said | Measured |
|---|---|---|
| F8-04 | two unsanitised log sites | eight |
| F8-06 | replace a prefix rule | that 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-09 | the arm is unreachable | reachable — a BLOB survives TEXT affinity, so a shape pin would have been green before the fix |
| F8-03 | one finding | two; 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 byexit 1, andgit blameshows the guard introduced it. - S8-06’s file:line was wrong (
client/src/download.rs:35, notpanels/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_noticekeeps its seven fields, because adisclosure_timingfield 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:
| Mode | Cost | What it does |
|---|---|---|
--selfcheck | ~0.1 s | version↔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.lockfiles” was false, and the audit’s replacement number (eight) is also wrong: there are 8 on disk / 7 tracked, becausefuzz/Cargo.lockis 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.pydoes walkdocs/; 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.mdis gone; the line now names--verify-countand 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, becauseplugin/package.jsonhas noscriptsblock and depends onworkspace:*— 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-04backed 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.knowledgegains nothing, so every FK-children map of theknowledgeparent delete stays accurate. NULL means the approval promoted nothing.record_promoted_chunkwrites the edge beside the shared decision CAS, as a SEPARATE write rather than a new CAS parameter:cas_proposal_approvedhas 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 forKIND_LINK_ONLY, which reuses an existing article).purge_promoted_proposalserases 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. Thecontent LIKEarm 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 LIKEarm 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
contentwould 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.tsis already stale againstopenapi.yaml(§6.1), and a certificate field nothing consumes is a field no one verifies. audit_eventsstill 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_enforcednow runs a second, STRUCTURAL counter. The keyword counter recognised exactly four statement openers (select/insert/update/delete … from) and was blind toPRAGMA,VACUUM,BEGIN/COMMIT/ROLLBACK,REPLACE INTO, and the entire rusqlite method surface — while ten production violations were live undersrc/handlers/and the guard reportedok. The new counter matches CALL SHAPES (Connection::open(,.execute_batch(,.query_row(, …), which is what makes it see those shapes without false-firing onh.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-deleteVACUUM— which also stops discarding its error withlet _ =, forbidden by the fail-closed law — the UMP consent-denial audit open (ump_ops:: record_forbidden_scope_at_db), the snapshot probe (new core), andshifts’ hand-rolled transaction. shiftstransaction: three defects closed at once. The hand-rolledBEGIN IMMEDIATE/COMMIT/ROLLBACKbecame the RAIIWorkflowTx, which discards the ROLLBACK error, returns an open transaction to the pool when a panic unwinds past the rollback, and bypassesnote_busy_errorcontention telemetry.- The snapshot probe now opens READ-ONLY.
Connection::opendoes not setSQLITE_OPEN_READ_ONLY, so a surface whose own doc comment said “Read-only — it never creates or mutates a snapshot” was opening every.bakread-write. It is nowSQLITE_OPEN_READ_ONLY | SQLITE_OPEN_URI, and a probe can no longer alter the evidence it reports on. Measured: thePRAGMA integrity_checkworks on the read-only handle, so the rollback contingency in the round’s §9 was not needed. CRATE_TEST_FLOORis 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 (still2 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 hardcodesrequired_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_actionusedgate_foras 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 theAUTHZ_GATEStable literal.
Ceilings recorded, not hidden
DenyReason::MethodNotPermittedandCapabilityDenyOnlyare unreachable in production (gates.rs:163MethodPolicy::Any,:167required_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/explainreportsrequired_actionnext to a verdict the action never influenced. The endpoint does not disclose this. Deferred — the shell’sschema.d.tsis already stale againstopenapi.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 insrc/to migrate them to (measured:pub test_db/test_connreturn 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
- The out-of-sample false-promotion rate is not yet measured. No long-run figure has been published for a deterministic gate by anyone.
- The promotion route is disabled, in every configuration.
- Gap generation has no reliable published detection method — the loop generates rather than detects because the techniques that exist are unsafe as detectors.
- 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
sourcenor achecksum, socargo auditover 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.jsonschemaandschemarsremain declined: the schema is typed Rust plus SQLCHECKconstraints, 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=WALdoes not fail when it cannot be applied — SQLite returns the prior mode and the statement succeeds — and the pragma was issued inside anexecute_batchthat 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 thatbrain standbyandbrain shredare 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
systemdunit (NoNewPrivileges,PrivateTmp,ProtectSystem=strictwith oneReadWritePaths, all capabilities dropped and none added back),install.shthat refuses to overwrite an existing store,uninstall.shthat never removes the data, and a morningclean-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, becauseBRAIN_DB_PATHlives 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.shmatches an absolute binary path. brain standby shipruns exactly one cycle and exits with its status.standby startis 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 12in the measuring script, and a “1,056 ms” boot came from asleep 1poll loop. Re-measured: 31–65 ms stop, 344–349 ms boot, and awal_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
systemdunit was never started undersystemdon 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
authzmodule ships a closed, deterministic, pure(Principal?, Gate, Method) -> Verdictoracle (Allow/Defer(reason)/Deny(reason)) and aroute_layermiddleware that runs it on every matched, non-exempt route. The concrete win is coverage: a matched, non-public route with no row in theAUTHZ_GATEStable 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 aroute_layerso unmatched paths keep their probe-blind 404s. - Every refusal writes one hash-chained
audit_eventsrow carrying the closed reason, the method, the matched route pattern, themask_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, defaultpass) selects how a principal with an EMPTYrolesclaim is treated. Unknown values refuse boot; the resolved value is printed at boot and echoed onexplain. The middleware itself has no off switch.
Corrections to the record (found and measured, not assumed)
CAN_ACTIONSdoes nameworkflow, and the shippedworkflow-operatorpreset grants exactlycan:["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 holdscan:["read","write","reject"].- The
route_guardsmodule doc claimed the module was “compiled nowhere outside test builds”. It is production data and always has been (pubatserver/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"), butpublishis not inCAN_ACTIONSandrole::validaterejects it, so no role row can hold it. KCS article publication is therefore impossible for every role-bearing principal, including theadminpreset; only role-less JWT principals and the unconfigured superuser can publish. The fix is mintingpublishintoCAN_ACTIONS, which this round’s frozen-vocabulary rule forbids. The capability is declared in a namedDENY_ONLY_CAPABILITIESclass 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_roleremain the inner gate. - The agent principal class is refused by the handlers, not by this middleware:
measured against the authz matrix,
/reindexis anAdminrow 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_idis 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.lockbyte-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_linkis 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-appendsubcommand (bench-gated, off by default). - Zero new dependency edges; the Clopper-Pearson bound is hand-rolled from
f64::ln_gammaand 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 a400, never a silent clamp); the derivation is deterministic for (window, now). Every metric carries a typed state —computedwith a value, orinsufficientwith a closed reason — so an absent metric is never rendered0and 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
findingsrow with the CLOSED source vocabulary (source LIKE 'delivery:%') narrowed by the typed confidence column (0.0is the mismatch arm; the match arm writes1.0). The claim text is never read:findings.claimis 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 thecontradictionstable 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_atin-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_shajoins to a recorded vcs commit-time fact (a typed-evidence row whose machine-written evidence slot carriescommit_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 isinsufficient(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_timeanddeployment_rework_ratedeclareinsufficientwith 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 inPRE_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.yamlcarries 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/releasesfiles 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.revisionis Release Candidate — cited by name, never claimed stable).POST /workflow/delivery/releases/{id}/approverecords 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 fromapproved_atand is evaluated inside the promote transaction.POST /workflow/delivery/releases/{id}/promotere-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, landspromoted, and mints the dispatch intents. Promotion IS the outbox write; nothing here touches the network.POST /workflow/delivery/dueis 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,remainingreported 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
promptdisposition now writes a bounded, screened pending question the/answerroute 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_radiusis 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_aton a match (promoted → verified via the crate’s transition law); the crank never writes it.
Bug fixes
resolve_bindingselectsdelivery_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::Workflowinaudit_events, and the audit-retention prune is kind-blind: promotion evidence ages exactly like every other audit row, per the operator’sBRAIN_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 theworkflowrole.- Two read-only adapters (
vcsfor repository commits/statuses,cifor 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.
/metricsgainsbrain_delivery_intents_pendingandbrain_delivery_untrusted_rows_pending, per domain.
Security fixes
- The exact-host refusal (
https://api.github.comonly) 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 — underredirect::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_digestcovers the endpoint, the target ref, and the secret’s FILE NAME — never the secret and never its path. delivery_bindingsis domain-scoped end to end, and thetarget_kindCHECK is enforced by the database. There is no write route: consent is given by configuring a binding at boot and withdrawn withactive = 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_reservedbefore the topic charset is checked, so a forged reserved topic is answered with the refusal that actually applies rather than a misleadingtopic_invalid. Itsdeniedaudit 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
pendingwith no reader. A non-zero intent gauge is the expected steady state, not an alarm. registry/deploy/pm/incidentare 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
LinknextURL 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}/attestationsreturns 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
modelbinding; 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_tracesgainsseqwith aUNIQUE(run_id, seq)index, allocated asMAX(seq)+1in 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-verifyandGET /workflow/delivery/runs/{id}/trace, givedelivery_tracesits 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 anorderdiff. Models are never re-run: the comparator lives in a crate whose entire dependency set isserde/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
workflowrole 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-host409 delivery_attestation_refusedis 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-verifyandGET .../tracequerieddelivery_tracesdirectly and did not check the run’s kind, while every write path resolves its run through the kind-filtered head. Becauseworkflow_runsis shared with the GDL, account, and valet engines, a principal withReadon a domain could pass a non-delivery run id and receive a structurally-valid delivery payload — answering200where every write answers404, 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 sharedagent_session_eventstable with only thecontrol:*family excluded, so it could returnuser,assistant, andtool_resultrows — the model transcript — to any principal withReadon the run’s domain. The read now filters positively to theddl_*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 atrace_idacross this upgrade must re-read it. Four published response schemas carrytrace_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/predicatemirror 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, andbudget_spendstay 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-diffis 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 thedelivery_traces.seqordinal; stamp1.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 shippedump_integritystack — 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 COLUMNforseqdefaults every existing row to0, so a run with three trace rows held three(run_id, 0)pairs and theCREATE UNIQUE INDEXthat 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.mdin thebrain-steward-ipplanning 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.shcan 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 warningson four shapes, fmt on two targets, lipstyk-gate with zero diagnostics, eightcargo audits,cargo machete,env-truth,badges --selfcheck, andrepo-briefall 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_LEDGERcarries a row for1.29(14) and this release adds no debt, so opening1.30would faildebt_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 indocs/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
400while a quarantined one is a409. - 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:
apply_steeringwas a silent no-op — it discarded itskindargument (let _ = kind;), returnedOk(agg.clone()), and could neverErr, while carrying notodo!/unimplemented!/FIXMEmarker. Its existing test passed identically with the stub and with a real implementation. All sixSteeringKindvalues are reserved vocabulary with no defined semantics against a two-fieldAggregate, and no caller needs a mutation — so the function is now an explicitly declared no-op with an infallible signature. AResultit 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.- The critic ceiling tripped one verdict late. The design owner states
“5 → pause”; the code compared
> 5against a bare inline literal, so the sixth non-okay verdict paused the run. The ceiling is now a namedCRITIC_CEILINGconst 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. "replayExempt"was an accepted QA key with noExecutorQafield. With nodeny_unknown_fields, a nestedexecutorQa.replayExemptvalidated and was then silently dropped, leaving the gate’s ownreplay_exemptfalse — 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.stage_writerdropped artifacts silently. It paired artifacts with kinds throughzip, 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 aResultso 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.shblocks 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 warningson four shapes, fmt on two targets, lipstyk-gate with zero diagnostics, andbrain-migrate-rehearseall 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_LEDGERholds rows for1.28(15) and1.29(14) only, anddebt_ledger_reflects_reality_and_burns_down_per_linerefuses a build whose line has no strictly-smaller row. This release adds no debt, so opening1.30would fail that guard unless an unrelatedTODO(unify)pair were unified first. The house precedent settles it: patch lines carry additive work —1.28.62shipped therevoked_principalstable and a schema stamp,1.28.77shipped the erasure line,1.28.84shipped 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, so1.29.1is 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) anddelivery_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 withkind='delivery': no second engine, noworkflow_runsorworkflow_stepsmigration, 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 …/gatesevaluates 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-corecarries 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-addressedtrc_<32 hex>id over the row’s facts and its ordinal in the run, closedCHECKvocabularies onstage/phase/status/tier, the(run_id)and(run_id, created_at)replay indexes) anddelivery_budgets(composite(run_id, kind)PK). Both land in oneexecute_batchwith their indexes; no FK, no down-migration, additiveCREATE TABLE IF NOT EXISTSonly. 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 kindCHECKbecause 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
§7non-goal is stale and is superseded here.§7reads “no new trace table” — written to stop exactly this table. ADDENDUM 2 §2 decides thatdelivery_traceslands in this round with the1.32.15stamp, its own schema, first writer, indexes, and a replay-read contract;§1.6was rewritten to say so and ADDENDUM 1 item 3 carries an inline supersession marker, but§7itself 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 variantssnake_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_versionstays 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 ridespolicy_digest+pipeline_version, both of which the trace row does write. Piping the engine version into the law column would fabricate alaw_version_mismatchagainst 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 rootCargo.lockgains exactly one[[package]]entry (509 → 510) and zero third-party entries — the crate depends only onserde,serde_json, andsha2, all already locked. One resolve without--locked, its entire diff inspected before anything else ran,--lockedfor every command after.crates/Cargo.lockgains 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_radiustrips 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 warningsclean on four shapes,fmtclean on two targets,lipstyk-gatewith zero diagnostics on changed lines,cargo macheteclean, all eightcargo auditruns at exit 0,env-truthandbadgesself-checks clean, andbrain-migrate-rehearsereportingALL CHECKS PASSEDagainst 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_questionis 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; the1.32.15–1.32.18stamps 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.mdthen 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 thepending_questiongap 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, sopromotereads 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}/gdlaccepts the bounded{ticket}body only. Callers that sendbase_url,model,secret_file, or timeout/response fields receive400 gdl_request_migrated; configure the server-ownedBRAIN_GDL_PROVIDER_BASE_URL,BRAIN_GDL_PROVIDER_MODEL,BRAIN_GDL_PROVIDER_SECRET_FILE, andBRAIN_GDL_PROVIDER_SECRET_ROOTprofile instead. Readiness reportsgdl_provider: disabled|configured|invalid; partial or invalid configuration refuses bootstrap. - GDL launch integrity. Provider failures after admission become a durable, non-retryable
gdl_provider_failedterminal: 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
workflowrole before profile, secret, DNS, or provider work. The new least-privilegeworkflow-operatorrole 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 commit23cc116. 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.13and1.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-execprofiles 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-auditbinary (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 explicitGDL_R10_PACK_DIRis 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_raterolls 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/decisionandPOST /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 prereg8ab2906e…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, andcargo-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.lockbyte-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/verifycensuses evidence rows;/verifychecks 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.
- row counts) the operator records OFF-HOST and later recomputes with
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=ONwith readback asserted,wal_checkpoint(TRUNCATE),VACUUM, a second TRUNCATE checkpoint,integrity_check— and evidences the act with one hash-chainedforgetrow. Freelist reads back zero. Filesystem copies,<db>.baksnapshots, 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’simplemented()was a bare substring match, so a comment, doc-string, log line, or fixture string naming aBRAIN_*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 anenv::var/var_os/set_var/remove_varread line; the three runtime-derived/external-consumer stragglers ride an explicit printed PINNED_CALLSITES inventory (the secrets-ladderresolve("case_status")derive ×2, andBRAIN_SERVER_AUTH_TOKEN= openclaw-host substitution), andBRAIN_MODEL_PROFILEis a declared non-knob (the docs say so themselves).--selfcheckbuilds 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”;
verifyjoins 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) andsrc/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
forgetrow 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_rebindfamily) 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
dirsbump +cargo update -pper package,--precise 6.0.3pinning 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 warningson bench / default / otel / engine-crates / steward-harness / client (incl. the desktop feature); fullcargo 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 undersrc/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 pushbypassed 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-lockfilepasses; 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-orderVecDeque; 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_boundedRED→GREEN (red: “cache grew to 4608 entries — unbounded”). Ceilings (honest): the LIVE twin —signal/worker.rs:31’sRecipientCache, 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-kernelfeature’s only serde_yaml consumer wasloader.rs(the declarative plugin-mount manifest parser): ZERO callers across the workspace and zero doc references (thecordis.ymlin 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, soharness-kernel = ["dep:serde_json"]now; serde_json stays (workflow_state.rs). serde_yaml + unsafe-libyaml are out ofCargo.lock,crates/Cargo.lock, ANDtools/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 ispublish = 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)insanitizeForBlock(the sibling discipline at the card-ensure and pause catches); the C0/DEL-collapse regex moves to escaped\u0000-\u001F\u007Fform (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-levelpermissions: contents: write→contents: read; the release job carries the onlycontents: 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 thegit tag && git push --tagsbypass. - M5 (K7-03) — the sync script is the fork’s writer.
scripts/sync-plugin.shgains: (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-lockfilepasses (the K7-03 acceptance test), fork vitest 71/71, fork tsc clean; fork commit58767515d46= sync outputs only (package.json, team-bridge.ts, plugin CHANGELOG). - Pins:
signal_gateway_cache_is_bounded(RED→GREEN);extension_manifest_matches_lock_specifierlives 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 ridestools/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-versionmoves 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); fork58767515d46.
[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 |
|---|---|---|
| 1 | DNS-rebind of the pinned host | Loop (accepted-risk disclosure, v1.28.79) |
| 2 | First-use tool flagging | Loop (accepted-risk disclosure, v1.28.79) |
| 3 | Shim tenancy | Loop (accepted-risk disclosure, v1.28.79) |
| 4 | Writable pins file | Loop (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 undersrc/service/dsar/andsrc/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 inlifecycle/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.shas 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.mdrow 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_FINALsplit into the incident date and the fix-anchored vulnerability formula). - M2 (R7-09) — the recursive walk.
collect_service_rs_filesextracted and made recursive (theno_sql_in_handlers_enforcedidiom); the guard’s production-region split and message unchanged.transport_free_guard_walks_recursivelycounts 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 planteduse axum::inlifecycle/FAILED the guard naming the file (plant never landed); (3) the census direction was red-proofed the same way with a plantedp256dependency (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: plantedp256→ 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-versiontops at 1.5, and (found during execution) it reads NO config file — env/CLI only (verified in its source; the.cargo/cyclonedx.tomlroute the plan guessed does not exist). Shipped:--spec-version 1.5pinned inscripts/sbom.shwith the ceiling comment;sbom/brain-server-1.28.88.cdx.jsonregenerated (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_caughtextended 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-versionunchanged (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_guardscans 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 fixedloopbacklabel when no principal resolved (opaque-token superuser). The locate query keys onknowledge.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_feedbackkeeps the principal-sub-or-NULL shape (the sweep’s feedback arm is unchanged). - The
/ops/crewroster and the/ops/skillsfeed emit their stored strings through the read seam:principal/current_case_refwere already invisible-stripped at the roster core;roles,skills, and the Watchbillsitenow ridesanitize_readtoo. 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/auditlisting (theactorsub is the row’s one non-hash string) pass a deep string-leaf composition ofsanitize_readat the emission boundary. No digest impact — none of these fields bindreview_digest. Idempotent on clean content. - The read-seam wiring guard reads code, not prose: the site table’s
handler_bodyextractor 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:
ownerbecomes a total attribution ledger. One helper (content_owner_stamp, besideprincipal_to_owner) + the fixedLOOPBACK_OPERATOR_OWNERlabel; 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/proposalkeeps 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_refand PASSED (the core already strips them) — the shipped pin plants hostileroles_json, aprincipal_skillsskill, and a hostile site shift so the guard has teeth against the actual gap. - M3 (F7-06) — one sweep, one helper (
sanitize_value_stringsinhandlers/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_bodyreturns 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 →
/dsarexport forloopback→ 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-versionunchanged (no wire contract move — the hardening is content-level at existing surfaces). Ceilings (honest): historical rows stay stamp-blind;suggest_feedbackowner 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 passsanitize_readverbatim — the drill demonstrated all five classes riding raw on v1.28.85 recall output. The tier is scheme-hostile, not attribute-hostile: benignhttp(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}/historyemitted 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 newedges_skippedfield (plus one audit note), and no entity row is created. The structured path 400s on anentity_typeoutside[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 /procedurestored caller content with no audit row; structured/ingestaudited only graph edges;/addand/ingest/markdownrecorded their audit AFTER the commit (the crash window the audit-per-write law closed). All three holes closed: aprocedureaudit 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, fork5b64e7a). The hostile-element mirror (exported since 0.6.8, never called) is now invoked insidesanitizeForBlockat 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-triggersourcePromptis 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 sanctionedformat.test.tsdelta 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_skippedso declined graph edges are visible to callers (proof0d797ba). - 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(proof5145f4b).
Engineering record
- Range: 10 commits on main (
713748aM1 attribute tier,0d797baM2 graph seam,48fef68M3 audit law,15a7c99/7bcbecb/8830209/e2cc810M4 plugin wiring incl. the sync-script-mandated oxfmt pass and the S7-04 delta elimination, this commit M5) + fork commit5b64e7a(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_readverbatim; the hostile heading emitted raw through/graph/traverse; the procedure write carried zero audit rows; the source-order lock proved both legacy handlers recorded aftertx.commit(); the plugin img canary survivedsanitizeForBlockverbatim; 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 → 409conflict→ re-review 200; hostile-heading ingest 200edges_skipped:2, zero hostile entity rows, traverse clean; procedure write →procedureaudit row on the chain;/ump/audit/verifyok:true(6/6 signed). - Gates: full
cargo test --features bench,migrategreen per milestone; clippy-D warningsbench + 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 kindingest, so kind-filtered audit consumers missed erasures. Both rows (the erasure itself and the per-proposal scrub row) now write kindforget. Historicalingest-kind forget rows keep their meaning; new rows are labeled what they are. - The deployed fork extension carries the hostile-element mirror (proof
ace4f986in 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 carriesretained_proposal_copies+scrubbed_count; webhook-without-secret refuses boot; live DB untouched. - Ceilings: full
cargo testgate 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 everyBRAIN_SSE_REAUTH_SECS(default 30; fail-closed on parse), kills the stream with a{revoked:true}frame, and the reconnect gets 403. Setting=0restores 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.=0disables 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 —mathandstylenow opaque-strip (tag AND inner content; a demonstratedmathinner-content leak was the red-first proof), withdetails,body,button,select,marquee,dialog,animate,picture,noscriptadded plus 30 MathML child fallbacks. Storage stays verbatim;review_digestmoves 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; andscripts/env-truth.shstands 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-safeDisplayimpls is pinned bytests/error_taxonomy.rs— error strings an operator sees can no longer leak internals by drift;tests/singularity_pins.rsadds 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 existingset_or_remove_envafter 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 houseallow(dead_code)reservation — the mirror’s non-test caller is the wasm read seam, still pending; CI clippy-D warningscaught 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 testgreen at the release commit; clippy-D warningsclean (bench/migrate, default, otel); fmt clean; engine-crates + steward-harness green; badges--selfcheckclean. 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;=0is 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/readyshape 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 untaggedaacee4d).POST /ops/agents/revokealways writes: revoking an identity the deployment has never seen returns 200 withknown:falseplus a warning namingagent@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, andallow_unknownis accepted-and-ignored for wire compatibility. Verified by revoking an unseen identity, re-revoking it (second call reportsknown: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 400principal_malformedrather than a silent trim onto an identity the operator did not type. The response carrieswedged_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 whileDELETE /memory/{id}answered bare{"deleted":true}. The response now namesretained_proposal_copies, and?scrub_proposals=1replaces 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, extendsb36a603). 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 aretained_truncatedflag (correlation is exact bytes — documented at the seam), andscrubbed_countreports 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.9324d88converges/multi-getonto the same shape: the batch projection carries the ingest-kind label and each row emits it through the same sanitization;created_atstays by-id-only. - Fail-closed injection thresholds (proof
bc326df). MisconfiguredBRAIN_INJECTION_THRESHOLD_HIGH/LOWvalues 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/applenow get the strict API policy. Covered by near-miss probes. - Secret-parent directories are owner-only (proof
bc326df).install-service.shrestricts 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 (proofd63ddcb). 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
60fb64b6aeain 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 todocs/compliance.md: subject-wide purge for erasure demands,?scrub_proposals=1for 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 indocs/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 askind_updateor 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 fullcargo testinvocation 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/outenvelopes and handover pings. Every predicate now scopes by the authenticated pair. - Read-seam gaps closed.
/get/{id}sanitizes the storedsourcelabel (the/quarantinesibling posture);/procedure/{id}/stepspasses 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_exactarms 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
provenanceobject fail closed asTampered— 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.
sourceis capped at 64 bytes on both write seams;/auth/revokecapsjti/iss(128/256). - CodeQL: all 26 open alerts cleared — every literal HMAC secret in
test fixtures replaced with generated key material (
testkeyshelper; 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=1for one run — see the openclaw-fork changelog); the plugin (0.6.5) refuses multi-lineBRAIN_TOKENenv values. - The
/apppublic seat matches the exact segment; the hostcalls dormancy pin walkssrc/recursively (the docs claim is now true at every depth). - Docs truth: THREAT_MODEL §5 names the
/exportverbatim + 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-refURNs 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 unlessBRAIN_ALLOW_WILDCARD_GRANT=1is 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=1refuses 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=2requires two distinct principals before a proposal promotes: the first approval records a hash-chained audit row and returnspending_second; a repeat approval by the same principal is refused withquorum_same_principal. Default remains single approval; the publish/remedy decision branches keep their own semantics.
Improvements
/recallresponses carryincluded_global, always present, so mixing of the global corpus into a domain-routed query is visible to every consumer./health/dbcarries anauthnobject (enabled,required) and anallow_policy_bypassestripwire counting ingests that bypassed screening underINJECTION_POLICY=allow.- Provenance verify output carries
authentication(operator-pinnedvsself-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_DBis 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_feedbackrows 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 isdefaulton 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 matchestenant_id = subject OR owner = subjectin 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 fora_b%over-matchedaxb, 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) withESCAPE '\'. - Restore verifies BEFORE the live DB is overwritten (SP-C1 — MED):
the chainless/chain-verify refusals used to fire AFTER
write_atomichad 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>.bakplaceholder (wire-invisible: error strings + logs). - Valet brief
whatpasses the read seam (SP-W12): the one unsanitized text field in the handler now routes throughsanitize_storedwith the same posture as its siblings — pinned byte-for-byte with a hostile fixture.
Improvements
- By-id reads carry the
flaggedmarker (SP-S3b):GET /get/{id}and/multi-getreturn quarantined rows withflagged: 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_bundlestream-builds with a running byte counter and refuses past the ceiling with the named 507export_too_large(carrying the byte count + the chunked DSAR pointer) BEFORE the rest of the DB is materialized. Default 1 GiB;BRAIN_EXPORT_MAX_BYTESoverrides, 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 reportsremaining(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) + theschema_versionstamp → 1.28.77 (SCHEMA_VERSION_V1_28_77); contract test extended (version + column probe).record_feedbackgains the owner param; both call sites (/suggest/feedback,/ump/feedback) capture the JWTsub; 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 ridesdependent_rows(the .76 discipline) AND the new namedfeedback_rowscounter 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 survivedrun_poolpurge; the .76purge_removes_suggest_feedback_for_purged_chunkpin 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_exactbranches stay exact.dsar_pattern_fencing_percent_underscorered at 2 matched runs (unfenced_swallowedaxb), green at exactly 1. - M3 (SP-S3b, red→green):
ChunkRecordcarriesflaggedon both projections (by-id + batch); both handlers emit it; openapiChunkschema 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_veconce 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 507export_too_largenaming the chunked DSAR path.config::export_max_bytes(defaultDEFAULT_EXPORT_MAX_BYTES= 1 GiB) +validate_export_max_bytesat 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_pinsurvives 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_overwritealso 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 asdue, counted without the batch truncation, bounded by MAX_DUE_SCAN) reports the additiveremainingfield; non-zero remainder audited viarecord_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
whatroutes throughsanitize_stored(&what, false, &None)— the exact sibling posture;valet_brief_what_passes_read_seampins byte-for-byte equality withsanitize_readon 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[ c](outer-url)re-emitted as a live auto-fetchimage 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 byhostile_element_strip_does_not_heal_nested_tag(incl. the 65-level overflow construction) andstrip_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
whatcan no longer be rewritten past the screen (SP-W4 — HIGH; completes the X-W4 closure): the fence held at run-open only, whilePUT /workflow/runs/{id}/staterewrote 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). Pinput_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 401identity_revokedcode. Pinrefresh_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. Pinconsole_actor_revoked_refused. - Private
valet/duelabels no longer stream unfiltered on the live SSE feed (SP-A7 — MED): the reconnect-replay path gated bothworkflowandvalet/duekinds with opt-in + per-domain Read, but the live stream gated onlyworkflow— an unfiltered Read-on-global subscriber received every private reminder label across all domains. Both kinds share the gate now. Pinvalet_due_requires_optin_and_domain_authz. - Egress validation covers the IPv6 embed families (SP-E1 — MED):
IPv4-mapped IPv6 (
::ffff:169.254.169.254passed as “public v6” while the kernel routes to the embedded link-local v4), NAT6464:ff9b::/96, 6to42002::/16, Teredo2001::/32, and discard-only100::/64are denied; mapped PUBLIC v4 stays admitted (pinned complement). Edge- literal pins extendprivate_ranges_refused_table. BRAIN_MCP_SCOPE=readnow deniesump.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 annotatedx-brain-scope: read-deniedwith 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.shcrashed at HEAD (grep exit-1 on zero route sites in the thin main.rs underset -e); it now counts router registrations and runs clean — the one-shot briefing tool works again.- Corrected false in-code claims:
review_digestbinds the READ-CANONICAL form, not stored bytes (anysanitize_readwidening 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 shipsdefault-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_dropasserted a source string whose only occurrence was the assertion itself — it could never fail — and the exec spawn isstd::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 blockwait()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_readoutput exactly for rows whose stored text welds nested constructs — those rows’review_digestmoves, 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 coresrole::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 insideprivate_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_dropis 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.shwritesBRAIN_WRITE_POSTURE=reviewfor installs whose plist carries NO explicit posture yet — an operator-set value (including a deliberateopenopt-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 staysopen— 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 +
.baksnapshots 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 whensbom/brain-server-<version>.cdx.jsonis 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.mdcarries 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/outforge 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 indocs/AUDIT.md. - CI caught what macOS could not (merged-usr): the first CI run on
the release commit went RED on Ubuntu —
/binis a symlink to/usr/binthere, so canonicalizing only the argv0 turned every honest textual allowlist entry (/bin/ls) into a refusal; two exec tests failed andrelease.shREFUSED 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 --selfcheckgreen WITH the new SBOM gate;bash -non the installer (shellcheck not installed locally — noted ceiling); released as tagv1.28.75only 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
.bakbreaks 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 /ingestandPOST /ingest/proposalacceptorigin_context: "owner"|"channel"(absent = owner, byte-compat; anything else is a 400 — closed vocabulary). A channel capture stores the row with originchannel-capture; under the review posture the proposal’s SOURCE is stampedchannel-captureso 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 newuntrustedOrigins: "label"|"exclude"config (defaultlabel) drops channel-captured hits from AUTO-INJECT entirely underexclude; thememory_recallTOOL path always labels (tools return what was asked). autoCapture sendsorigin_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 (
domainlabels at the recall + gate spans);query_hashis 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_incompleterow on the hash-chained audit trail naming the remainder.
Security fixes
- The operator signing key is DETERMINISTIC (X-C4): the fixed
filename
operator.ed25519inside 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.prevexists (a third generation would orphan the middle one — pinned). NO scheduling, NO background anything.- Cards carry
signing_epoch(additive column):verify_cardpicks 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_eventstable REFUSES withchainless_image_refusedunless 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 markedlegacy_unkeyed_chain: forgeable: trueon the completion line + a disclosure evidence row naming--re-auditas 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_rotationlineage 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 suggestionsstops writing unguarded (X-W6): the KCS evidence side-effect (abstention + SIR rows) now requires Write on the run’s domain AND theworkflowrole capability. Read-only principals get the suggestions body unchanged with the additiveevidence_recorded: false. The endpoint is NOT split or moved — the KCS double loop’s capture is intact for writers.- A denied
/eventssubscriber 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::Statuspath) — 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_digestbinds 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:
/eventsdenial 401/403 replaces the legacy 200+SSE-error shape; the authz matrix moved/eventsout ofSSE_SOFT(/ump/subscribekeeps the in-band denial). openapi documents both wire deltas additively;x-api-versionunchanged. - Fast-path integrity: the borrow-preserving
sanitize_read_cowfast 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 throughsanitize_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=offopts 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/dbechoes the tri-stateinjection_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.shscaffolds 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/untrustedsegregation, 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_boundedpins 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 pluginformat.tsinvisible 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_valuenow maps\rto 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/hexwere 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, subagent@loopback— instead of another superuser bearer. The scope set iswrite:*/global(write implies read down; the shared pool only) and the role set is the ship-withagentpreset (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/revokeforagent@loopbackand the next agent bearer dies401 identity_revokedat the middleware. Agent 403s are audited at that boundary (agent_forbiddenrows) so the denials are evidence, not silence. A leaked (group/world-readable) or emptyAGENT_TOKEN_FILErefuses the boot. - The observability family stops narrating every tenant (X-A5). The
full
/health/dbbody (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/healthcontent plus the pool-liveness bit; a credential with neither Read nor Admin is 403 (openapi documents the new shape)./metricsper-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 samecan_read_domainpredicate the read paths use; out-of-scope domains collapse into one SUMMEDdomain="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)orauth: single token (LEGACY SUPERUSER — second line recommended). AUTH_TOKENenv 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/dbbody 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/dbfull 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 seeother). - 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
workflowengine capability is not grantable to ANY ship-with role (role::validaterestrictscantoCAN_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’srejectcapability 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@ loopbackprincipal; 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 + theagent_forbiddenaudit 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 extensionauthz_matrix_agent_loopback_class(every AUTHZ_GATES row × the agent class, role-gated rows tabulated from the handler sources) and the M2 sethealth_db_admin_full_read_reduced,public_health_unchanged,admin_sees_domain_labels,tenant_reader_sees_other_not_domain_names(pure pin overscoped_domain_label— shim-mode/metricscan only enumerateglobal, which every/metricsreader 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/dbdescription + 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 ONEaudit_eventsrow withstatus='denied'anddetail_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 dies401 {"code":"identity_revoked"}at the middleware while the operator bearer still passes/stats200 — Blackout’s kill-switch binds the agent by name, class-blind; (4) shapes: the operator’s/health/dbis 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/metricsscrape 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 andauth: 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 realIpAddrs — string encodings (hex/octal/dword) are canonicalized by the URL parser before the table ever sees them. The metadata hostnamesmetadata.amazonaws.com/metadata.google.internalrefuse before resolution. The pinned client forces every send to the validated address set (reqwestresolve_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_binno 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 absoluteBRAIN_STEWARD_BINoverride (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. Thebrain workflow crankCLI 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 failedtry_wait) now kills + reaps before returning. - The console pending listing role-checks (X-M6).
POST /webhooks/channel/{kind}/consolewithaction: "pending"(proposal bodies + digests) now requires the mapped actor’sreadcapability through the samechannel_user_map+ role-store machinery every other console action uses (empty grants nothing).decide,due,crankunchanged. - 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=1is the ONE egress opt-out (fail-closed parse: any other value refuses the boot — theBRAIN_WRITE_POSTUREpattern). 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_unresolvedlabel.
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-harnessmust setBRAIN_STEWARD_BINto an ABSOLUTE path or install the binary besidebrain-server. A relativeBRAIN_STEWARD_BINnow 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.invalidname — 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-canarykill -0reap 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
UDRILLholdingread+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/hookwithBRAIN_EGRESS_ALLOW_PRIVATE=1→ boots AND logsegress: PRIVATE sink address admitted by BRAIN_EGRESS_ALLOW_PRIVATE=1followed byegress 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 aBRAIN_STEWARD_BINsleep-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 sTimeoutLayer(408) drops the handler future first, andkill_on_dropreaps on THAT drop too — the child now dies on every abandonment path (previously it survived all of them); (4) a shadowingsteward-harnessplanted 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 rowworkflow/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_addrsis 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 toenqueue_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 crankkeeps 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_URLat a loopback listener must now setBRAIN_EGRESS_ALLOW_PRIVATE=1for 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
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’sgateway.controlUi.remoteImageHostsallowlist (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: trueAND 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: nosrc, 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_refsfrom Cordon, the two default flips, the data-URI budget) + the standing ceilings stated honestly — bare URLs in prose remain linkified-but-inert (the documentedgate.rsceiling, 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: trueand curategateway.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 carryingplus a barehttps://attacker.example/canaryURL 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.rsceiling; 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
mcpbinary acceptsBRAIN_MCP_SCOPE=read: the four write verbs (brain_ingest,ump.remember,ump.revise,ump.forget) refuse at dispatch withtool_out_of_scope, andtools/listannotates them"x-brain-scope": "read-denied"so recall-only hosts can render or hide them. Defaultfullis byte-identical compat; an unknown value refuses to start (fail-closed parse, WRITE_POSTURE pattern); the scope logs at startup. /ump/audit/verifyresponses carry the additiveintegritycensus{verified, signed, hash_only}— the UMP record population under the current serve posture — plusnote: "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_requiredwhen missing) and, with a local operator key, refuses anexpected_signeraliasing 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 surfacessigned_byso self-assertion is visible.
Breaking changes (migration)
POST /parcels/import:expected_signeris REQUIRED. Clients that imported without naming a counterparty now get400 signer_required. The migration is one line — name your counterparty: pass the did:key of the publisher you expect inexpected_signer. Reverting restores the default-empty signer and REOPENS X-C1.
Engineering record
- M2 (X-M1, brain):
McpScopefail-closed parse; dispatch-time scope gate BEFORE any network seam; the gate reads a boot-onceOnceLock(the stdio single-parent model makes process-lifetime scope correct); startup logsmcp: 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=bogusexits 1 with the hex-escaped value;readboots and logs the scope.ponytail:per-tool allowlists are YAGNI — two scopes match the two real consumers;BRAIN_MCP_SCOPEis 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_jsonis 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 insideprovenance_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). Thenotefires ONLY for the transitional combination (key exists AND hash-only seen). Serve behavior unchanged — visibility, not gating. The fixture lives inservice::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-toolsha256(name + \0 + description + \0 + stableStringify(schema)), per-server digest over name-ordered fingerprints; pins filemcp-catalog-pins.jsonbeside the agent bundle (agentDir discipline, 0644, not a secret); colliding display renames feed the ORIGINAL server-side name into the fingerprint.materializeBundleMcpToolsForRunreconciles 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-scannamed 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, sosigned == verifiedwhenever the key resolves; thenoteis 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 installreconciles 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 asdetail); the protocol schema (TypeBox, closed object) gates the field, and the generated Swift/Kotlin models are regenerated in-commit. The plugin’stitle/descriptionstay — 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 dsarrequires an explicit--action. The old silentpurgedefault — an irreversible multi-domain erasure on a bare invocation — is gone. Omission and unknown values error naming the choices (purge | export | both;bothis 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 likesource-delete. Migration: scripted purge adds--action purge --yes. Export and--dry-runstay prompt-free.brain restorealways prompts unless--yes.--forcenow 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.baksafety snapshot is unchanged.
Improvements
resolve_passphraserefuses 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_ACTIONSclosed vocab +dsar_action_from_flags(omission/unknown both error with the choice list),dsar_needs_confirmation(purge|both,--yesseam, 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(liveGET /clients/{name}resolve; fail-loud — a purge prompt that cannot name its blast radius refuses),restore_needs_confirmation(forcecarried in the signature so the pin asserts –force ≠ –yes),restore_target_summary+read_target_chain_head(read-only connection;audit::read_head_pindisplay), andcheck_secret_file_modeextracted from the rotator and shared withresolve_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 exportedtruncatePluginApprovalArgs(code-point-safe, exact-count marker),buildApprovalArgsin 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 likedetail, then cap). Protocol models regenerated (protocol:gen,:gen:swift,:gen:kotlin);protocol:check:swiftgreen. - 9 new CLI tests (red-first): action-required shape, unknown-action
choices, purge/both prompt matrix,
--yesseam, prompt content (digest + domain count + IRREVERSIBLE), pinned sha256 vector, restore prompts-even-with-force,--yesseam, target summary (resolved absolute path + size + chain head, pinned via a seededschema_metapin 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.lengthis 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--yesagainst 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-lockfileinstall silently failed on committed drift —extensions/brain-servertypebox 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
argsas 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
/suggestjoins the untrusted contract (X-R1). Every hit now carriesuntrusted: true— recall/search parity. Suggested content is data, never instructions. Additive JSON field; openapi.yaml schema entry added additively;docs/api.mdone-liner. Content itself already passedsanitize_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 ownUNTRUSTED_BEGIN/ENDfence 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
wrapExternalContentenvelope web_fetch uses, with the newMCP Tool Resultsource label — theuntrustedMcpOutputflag finally renders as prompt framing instead of a non-rendering metadata detail. - Plugin strip set synced to the Rust canonical set (X-R5, plugin).
sanitizeForBlockgains 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 asINVISIBLE_CLASSES; plugin 0.5.0 → 0.5.1. Behavior change is invisible-class-only prompt bytes.
Improvements
- The openclaw host’s
stripInvisibleUnicodewidened 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. wrapExternalContentrefactored onto an exportedcreateExternalContentEnvelopeSegments(byte-identical output) so the multi-block MCP envelope shares the exact marker/metadata family.
Engineering record
- M1 (brain):
SuggestionHitgainspub untrusted: bool(plan-verbatim doc comment), serializedtrueat the single construction site (handlers/suggest.rs:213region). 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 + theuntrusted: truelabel). 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—sanitizePluginContextapplied 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 bymcp-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.tscomment 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=falseopt-out remains the stronger kill switch and nostripContextescape 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’spnpm-lock.yamltypebox 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.rsclaimed “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 refused401 identity_revokedon 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 (theissis 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 minregardless of the token’s realexp— 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 tokenexp(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-
kidalgorithm pinning (X-A3a). A key record’s declared alg is compared strictly against the JOSE header’s alg BEFORE any signature work; a mismatch refuses401 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; theNoneescape 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 singleroute_guards::PUBLIC_PATHS+is_public_pathdecision living beside the tables it feeds;/.well-known/security.txtjoined both guard tables (the one row gap), markedpublic(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 inOPENAPI_ROUTESand — unless public or explicitly allowlisted — inAUTHZ_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=allowis 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_blockingverify block (after the jti denylist read, one more indexed SELECT — the sameworkflow::mesh::is_revokedthe mesh surfaces consult, so cost is the proven dispatch-path cost) and in a sharedensure_cap_principal_alivehelper 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 bareTokenStoretoOpaqueAuthState {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 extensionauthz_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 theOption::Noneescape keeping the operator-revoke default; the verifiedexprides request extensions asAccessTokenExp(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_kidwired 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’/privatestubs never pollute the wire scans;collect_registrations; the purereverse_guard_failures).authz_gates_cover_every_non_public_routewas rebuilt on the method-keyed scan (rows markedpublicskip 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_warningcalled once from the bootstrap (a counting-subscriber pin proves exactly-once forallowand never for quarantine/reject);config::injection_policy_echofeeds the/health/dbhardening 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 (actoruser:drill-operator, status ok) and twodeniedrows keyed path-only (target =/stats,/recallhashes; identical detail hash — the path-only, token-never law) chained into the live-format hash chain./health/dbechoedinjection_policy: "quarantine"(default posture). - Wire: openapi.yaml additive (the
IdentityRevoked401 response component, theinjection_policyhealth 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);
/metricslabel 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 inenqueue_child(the shared function, not a per-caller check) behindRESERVED_OUTBOX_TOPICS— onepub constinworkflow::outbox— with apub(crate)-constructorKernelOrigintoken held by exactly four kernel writers (enqueue_out,enqueue_ping, the steering inbox write, the valet crank). The events route now refuses reserved topics with400 topic_reserved+ adeniedaudit 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}/stateaccepts onlyactive | 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 refuse400 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): avalet/%run opened over HTTP with a screen-Reject or Quarantine label refuses400 screen_rejected; a state that is not a readable valet envelope refuses400 valet_state_invalid. Both screen verdicts refuse — an operator-channel label has no quarantine destination. - The alert bus authenticates the
valet/duekind. Aworkflow/valet*row publishes under the trustedvalet/duekind only when its idempotency key carries the crank’svalet-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.yamldocuments the two new 400 shapes and the statusenum;docs/api.mdnotes the reserved-topic and closed-status contracts. No route additions (route-coverage / route-authz tables unchanged); no schema change;x-api-versionunchanged.
Behavior-change ledger (previously-accepted requests that now refuse — documented, not silent)
| Change | Before → 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 status | accepted → 400 unknown_status + audit row |
POST /workflow/runs with kind=valet/% + screen-Reject/Quarantine what | stored unscreened → 400 screen_rejected |
POST /workflow/runs with kind=valet/% and non-envelope state | stored (inert, drifted) → 400 valet_state_invalid |
alert-bus valet/due kind | any 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): forgedchannel/out→ row landed → the real HMAC drain (POST /webhooks/channel/signal/drain) delivered the forged envelope to the bridge; forgedchannel/ping→ claimed+delivered;steeringwith injection text → landed in the inbox read;status="zzz_arbitrary"→ written to the run row; forgedworkflow/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; fivedeniedaudit rows on the workflow chain; the drain returns an empty batch (nothing forged exists to deliver);/ump/audit/verifyok; 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-pinreserved_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 warningson default/bench/otel; CI dry-run set green (default-features build, engine-crates, steward-harness, otel); lipstyk diff-strict green;cargo fmt --checkclean. - Ceilings (honest):
steeringis reserved EXACTLY — a hypotheticalsteering/xsub-topic is not reserved (no consumer exists; extend the const only with a kernel writer that owns the gate).KernelOriginis apub(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 onglobal). Every card use, delegation dispatch, and result submission re-checks the newrevoked_principalstable BEFORE signature verification and refuses403 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 markHUMANwith 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) carriesreview_independence_risk(0|1),approval_uniformity_ratio(integer ten-thousandths), andreview_decisions_window— over the same window and sample cap the client fetch uses, pinned verdict-identical to the client detector byscoreboard_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 (theauth/jwt.rs::ALLOWED_ALGSwhitelist 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/revokedlineage event observed,/audit/verifyok. - 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_jsoninsidewrite_artifact— the puremanifest_jsondigest 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_watchai_act_art50_marking_watchflipped 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 additiveprovenanceproperty (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_62in 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-chainedauthaudit row + a bounded sweep of active runs owning in-flight delegations, cancelled viaworkflow::state::cas_update(the EXISTING pathPUT /workflow/runs/{id}/stateserves) with per-run audit rows anddelegation/revokedlineage 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_recordedgreen. - 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_watchflipped 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 warningsclean 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 sharedmeasure_capacity, the/health/dbdetail 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 inBRAIN_DATA_ROOTnow fails layout resolution and inBRAIN_DB_PATHfalls 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, andstatusfails closed on any tamper or torn cycle.promote-checkis 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_subcommandsparses 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,ropahad 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 handlerStateextraction (route registration → handler body → thestateparameter entering the guard) into the three flaggedfs::metadata(&state.db_path)size probes (guard_capacity, the sharedmeasure_capacity, and the/health/dbdetail 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 newcapacity::db_size_bytes), so no path argument exists on the capacity surfaces at all;measure_capacitylost its&Pathparameter and the/health/db+/metricshandlers no longer clonestate.db_path. The handlers-side twin got the same fix (its alert had been operator-dismissed earlier — same shape). (2) The env reads instorage_layoutgained fail-closed traversal refusal anyway (a..component inBRAIN_DATA_ROOT/BRAIN_DB_PATHnow falls back to the layout default — real hardening against a hostile env knob, independent of the analyzer):resolve_rootreturnsStorageLayoutError::InvalidRootfor a traversal-carrying data root (the same shape as the existing non-absolute refusal), andlegacy_dbmoved onto a pure env-independent core (legacy_db_from) so the fallback is unit-pinned without process-env mutation. Behavior change, deliberate: aBRAIN_DB_PATHlike/data/../evil/brain.dbnow resolves to the layout default instead of being honored. - Log injection (
rust/log-injection, CWE-117): the flagged sink is the centroid-refresh failureeprintln!in the markdown ingest handler; the source is the payload-supplieddomain(the siblingdocument_idlog is server-generated and untouched).sanitize_log_value(inserver/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 wereassert!/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, featureloom): the batch ingest embed stage (UMP?format=ump/ump-mdmulti-record batches) and the consolidate near-dup scan’s pure-CPU preprocessing (dequantize + serialize; the KNN loop stays serial on the shared&Connectionby 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/dbechoes the boot decision (loom: active (N threads)|off:no-feature/off:jetson/off:env). - Determinism, proven at three levels: unit pins (
loom_preserves_fused_ranksover a frozen gold corpus through the real cosine/eval paths,loom_batch_order_invariantas 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 resolvedLoomState; 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_blockingvialoom::fan_outwhen active;ingest_onegainsprecomputed_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_duplicatescollects raw int8 blobs, then fans the dequantize + little-endian serialize pass out; row order ==ORDER BY k.idpreserved by the ordered collect. The KNN loop stays serial: rusqliteConnectionis!Syncand 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-tierbuilds again:server::bootstrapnamedsearch::rerank::warmup()without thesearchmodule 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 besidebusy_timeout— previously onlybusy_timeoutwas per-connection andsynchronoussilently reset to the compile default (FULL) on every reconnect whileNORMALfrom 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 (theBRAIN_WRITE_POSTUREpattern). The applied policy is echoed by/health/dbunderdurability. - 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_lockfast path costs zero clock reads). Two new/metricsgauges,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 throughWorkflowTx::beginor edit the baseline deliberately. - Lock-bounds audit: every production
Mutex/RwLocksite (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 onworkflow/tx.rs, a fence pinning the whole-file-test exclusion (src/search/tests.rs), and a RED-PROOF: a plantedconn.transaction()in productionconfig.rsfailed 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:
CapacityEnvelopegainssynchronous_mode: SynchronousMode(Full|Normal) +wal_autocheckpoint_pages: u32;capacity::Durabilitycarries 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 reportedsynchronous=2(FULL) andwal_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 throughconcurrency::{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) inconcurrency.rs;LockWaitHistogram::quantile_edge_usis 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 throughreload_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 inBENCHMARKS.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/dbdurability.*keys; docs/api.md/health/dbrow; 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_greenasserts 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_SEEDprinted; 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_MSis an envelope-free ship gate;BENCH_ENVELOPEgains 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).
- ~2.5× margin, docs/THROUGHPUT_PROOF_20260905.md); jetson 150 ms
marked unmeasured (no ARM runner). The merge is pinned deterministic
(
- Contention becomes visible (
src/concurrency.rs): process-local counters (the audit-static precedent) surfaced on/metricsand/health/db, wired ONLY at existing error arms — zero added cost on success paths.brain_pool_timeouts_totalcounts r2d2 checkout failures at the handler error seam (HandlerError::db_down, the sharedpool.get().map_errarm — 92 call sites collapsed onto it, wire-identical) and the workflow lane’s checkout arm;brain_busy_errors_totalcounts SQLITE_BUSY-family errors at the governed-write BEGIN sites (WorkflowTx::begin+ the lane’sBEGIN IMMEDIATE);brain_pool_in_use{domain}/brain_pool_idle {domain}come fromr2d2::Statesnapshots 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/dbJSON gains additiveconcurrency.*keys. A proptest pins counter monotonicity under Relaxed ordering (2 cases). - The metrics dictionary gains its ops twin — every
/metricsseries (the tenbrain_*names) now has a docs/metrics.md dictionary row, pinned by the newmetrics_series_have_dictionary_rowsmeta-test (the scoreboard parity discipline applied to telemetry); docs/api.md’s/health/dbrow 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 withBENCH_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
/metricshandler issrc/server/router/core.rs::metrics(was main.rs ~2092); the r2d2 pool builder issrc/server/bootstrap.rs(was main.rs ~5393);resolve_domain_poollives insrc/handlers/mod.rsand resolves REGISTRIES, not connections — its error arms are domain-resolution errors, so the checkout-timeout counter wires at the actual checkout arms (the 92-siteHandlerError::db_downseam + 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_totalandbrain_db_busy_totalare 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
/addAddResponse arms, anyhow-context sites in search/domain-router internals) do not bumpbrain_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;
/metricsbefore/during/after a 6 400-search burst showsbrain_pool_in_use0 → 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 warningson 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 theinclude_str!anchors (nowCARGO_MANIFEST_DIR-absolute) and the root use-block that traveled with the region souse 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/ outsidesrc/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; andbootstrap_stays_protocol_free— no axum types insrc/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_FLOORretired per its own relocation convention (its 109 pins moved this release);ROUTE_CALL_SITESretired early (main.rs routes pinned to 0);TOTAL_SRC_TEST_FLOOR→CRATE_TEST_FLOORover src/ + tests/ (re-measured 1,196 at the move; 1,198 at close — the gates added two);ROUTER_SITES_FLOOR199 and guard-table rows 161/145 survive. src/route_guards.rsre-homed tosrc/server/router/route_guards.rsbeside the registrations it tables (decl moves; content unchanged — 100% rename).spire_inventory.rsstays 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):
- 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.
- 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. - THE RE-HOME — route_guards beside the families; consumers re-pathed (spire_inventory, tests/authz_matrix.rs, tests/main_suite.rs).
- 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):
- Middleware stack + auth middlewares staged into
server/router/{mod, auth}.rs(C1a). app(state)composition lifted out of main_inner; the middleware inputs (token store, JWT state, CORS) moved ontoAppStateso the composition is a pure function of state; the three middleware oneshot suites moved intoserver/router/auth.rswith their subjects (C1b).server/bootstrap.rsreceives 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,AppStateconstruction + the four alert watchers + multi-db seed, webhook drain worker, bind resolution + loopback-bind guard + unsigned-egress warnings.boot.rsfolds in whole (ct_eq, argv, worker threads, bind predicates — pins travel).main_inneris now the serve loop only (C2).app(state)moves toserver/router/mod.rs; the six family builders land — core (17 routes), memory (56 + the 3-route deprecated legacy fragment + the 1 GiBimport_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).- THE LIB FLIP: lib.rs declares the whole server tree with
pub mod serveras the only named surface; main.rs consumes it viabrain_server::server::...; the law-9 matrix moved totests/authz_matrix.rsdrivingbrain_server::server::router::appfrom outside the crate — the lib seam earns its keep (C4/C5). - Law-13 gauges:
brain_db_busy_total(SQLITE_BUSY surfaced at the audit seam) on/metrics,db_busy_hitsin the/healthhardening 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-IPRateLimiter(with the bounded-bucket eviction), theConnectionTracker+ RAIITrackerEntry, the connection and RSS watchdogs, andprocess_rss_mib— promoted frommain.rswith all nine of its unit pins (tracker ×3 + Drop/panic + timeout-slot, limiter ×3, RSS ×1).src/screen.rsgains the layer-1 blocklist:contains_suspicious_patternmoved besideis_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 tocrate::screen::contains_suspicious_pattern.src/screen.rsgains the quarantine read-seam pair:flag_if_quarantined(the Quarantine verdict’s persistence) andsuppress_flagged_evidence(the verdict’s read-seam enforcement) with the snippet pin. Service-layer callers (procedure, recall, ingest) repoint tocrate::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 inmain.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_THREADSresolution, the loopback-bind fail-closed predicates + guard, and the constant-timect_eq— with the ct_eq and bind pins. Deliberately NOTsrc/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 → 109across 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:
a1480e7http_limit (fn family + 9 pins),5a19760blocklist → screen (fn + 7 pins),19d3de8fence kin → screen (2 fns + snippet pin),1f26978graph_read (3 fns + 2 pins),c9ae723boot (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 thescreen_foldspin 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 returnResult<_, AppError>, andAppErrorimplementsIntoResponse(transport-shaped), so the family fails the selection rule and rides with Vaulting’s memory family; the three source-scan pins stay pointed atmain.rs, where their subjects still live, and their verdicts are unchanged. (2)html_escape+parse_annotationsstayed: their consumers are the axum ingest handlers, which the prompt’s scope gate excludes. (3)measure_capacitystayed (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 theirtest_db()fixture, per the stays list. TrackerEntry::countis now#[cfg(test)](it was already test-only); the router-level budget pin reads the newRateLimiter::WINDOW_BUDGET_PROBEconst instead of the privatemax_requestsfield. 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 — ceilingsMAIN_RS_LINES ≤ 19_282,TEST_REGION_LINES ≤ 12_712,ROUTE_CALL_SITES ≤ 234; floorsMAIN_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.rsinstead 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):
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.test(spire)— green: ceilings set to measured truth (19,909 / 13,342 / 234; the +3 ledger decl lines honestly included).refactor(spire)— guard tables →src/route_guards.rsas data; ceilings 19,467 / 12,897; docs_truth’s test-file-skip preserved by declaring the module from main.rs (a#[cfg(test)] pub modinside handlers/mod.rs would have skipped it from the comment guard).refactor(spire)— the handlers-family pins (authorize ×3, audit_scope ×2, typed-edge) relocate intohandlers/mod.rs.refactor(spire)— config/temporal/trace/eval pins relocate.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.- 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
domainagainst 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'+ nullableproposals.title+ theidx_proposals_status_domainindex. Existing rows keep'global'forever — provenance beats guessing. The schema-contract test gains the missingexpected_proposals_colsblock. 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 itsdomainand optionaltitle(the autocapture source title, the parcel row title);POST /ingest/proposalaccepts the optional bounded+screenedtitle.- 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): theproposalsaggregate’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 fromservice::gate, which keeps the KCS/promotion/export machinery. Pinned byreview_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’slist_proposals_pageis the review core’spending_page(the Cornerstone name kept); the plan’ssql_inventory_baselinegate.rs-row check is SUPERSEDED — the enforcing flip deleted the baseline machinery, andno_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 proposalssites 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 —
/proposalsgains thedomainquery param + description;/ingest/proposalgainstitle(maxLength 500); theProposalViewcomponent 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 warningsgreen; 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 knowledgeglobal— 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}/proposalsstays 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 +sincewindow + the LIMIT ceiling) with the deadline/SLA derivation and the supervisor owner filter; the creation insert (theproposal_pendingaudit 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) sopublic_slug_takenkeeps its frozen 409, the translation CAS with its verbatimdatetime('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 asexport_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_enforcedwalkssrc/handlers/recursively and fails on ANY counted statement; a ≥30-file sanity refuses the vacuous pass, andsql_statement_counter_still_firesproves 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.mdstates 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/proposalmaxLength 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).
- 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
Security fixes
- None. The review wire (digest binding, sanitize_read, PII masking), the
approve-role gate, and the
public_slug_taken409 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, Δ 0and 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 warningsgreen 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_id1), kcs worklist read, webhook HMAC posture (missing signature → 401),/audit/verify okat start and finish,/health+/versiongreen (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
phccrate 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-columnoversight_evidencewhile 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 nowcfg(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_stepedges 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 offHandlerError— and the grouped outcome counts),service::compliance(the best-effort oversight write, the six evidence counts withunwrap_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::staterun-row reads +open_run,workflow::outboxsteering 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), plusrole::defined_count,capacity::knowledge_docs(fail-open),legal_hold::first_missing_id(the all-or-nothing fence), andservice::recall::chunk_for_verify(the domain-bound verify read).
- the whole scoreboard test module —
- 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 ontoservice::register’s test module, which already carried the identical fixtures from Terrace; the valet brief tests moved ontoworkflow::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.rswas 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_updateexactly as before; theread_state_and_revisioncore 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
Deniedaudit 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, thekcs_state_invalidvocabulary, 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’screate(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_seamscans unchanged handler bodies;authz_gates_cover_every_non_public_routestill 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_chunknow 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 reusesworkflow::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 warningsgreen on bench, default, otel, and compliance-pack (clippy only — see ceilings); full suite--features bench1316 passed / 7 ignored; CI dry-run green (engine-crates tests + clippy + fmt, steward-harness tests + clippy, default-features test –all-targets withRUSTFLAGS=-D warnings); lipstyk diff-strict clean vs v1.28.50 after one finding fixed (record_feedbackborrows 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 okthroughout. (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 noaudit_eventsrow (the tombstone is the evidence — the erasure-family convergence follow-up). The Signal digest-mismatchDeniedaudit still rolls back with its tx (evidence of the refusal is lost — the audit-adjacency follow-up).put_run_state’s 200 body still carriescas_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 settingBRAIN_CONNECTOR_CONFIG_DIRwithout 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 returnsNoneAND 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.2ump_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, thenbrain eval --floor r5=0.85 --floor r10=0.85 --floor mrr=0.85against 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_blockingkeeps 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_hitsstays 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_vocabularyrepointed 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-localDeref<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 warningsgreen on bench, default, and otel; full suite--features bench1301 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}/tracereplay round-trip,include_flaggedreview 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 okon 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
getper domain (byte-identical posture). The graph-leg SearchFilters boundary pins and the PRF occurrence-schema pins stayed attached tosearch/graph_ppr.rsand 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 wireHitSourcelabels; the service owns the WRITE, not the response shaping).RecallRequest/IngestRequestand 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): theclientsrows (insert with canonical-lowercase storage, the WORM-lite archive flip,list/by_namereads), the Art-28 DPA-terms round-trip (blank/ oversize fenced byMAX_DPA_FIELD— the fence now holds of the FUNCTION, re-asserted inset_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 bycoach_audits_inside_the_tx+ its rollback twin), andtermination_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): aclient-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-domainauthorize) stays in front, byte-identical. - The domain-admin core (
service/domains_admin.rs): the shim-mode census (DISTINCTdomainlabels + 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 → thedomain_deletedevidence row INSIDE the caller’s tx — pre-move that audit rode after the commit with alet _ =, the exact certified-silence form the error-propagation sweep forbids; the erasure and its evidence now commit or roll back together, pinned bydomain_delete_rolls_back_with_its_audit),vacuum,export_snapshot(through the sharedbackup::vacuum_intoescaper — the quote-escaping and symlink-containment pins stay attached to that primitive verbatim;domain_export_routes_through_shared_ vacuum_escaperpins 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.rs64 → 0 SQL,handlers/clients.rs44 → 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.rsis 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_coreadds 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_activeenvelope with reasons;domain_delete_refuses_while_holds_activemoved 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; thesrc/clients.rsunit 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 ofHandlerErrorfields); 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 todomain_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 inhandlers/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.rsheader:evidence_linksboth arms (NO ACTION — explicit first),relationships(SET NULL — explicit first so entities don’t orphan), the orphan-entitiessweep (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 thecase_articles/kcs_translationsNO 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 sharedlegal_hold_activeenvelope 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 plainDomainRowthe adapter maps 1:1. - Error-conversion notes (the honest diff): the pre-move client
resolution ran
transfers::listBEFORE the archived refusal in the DSAR seam; the typedrequire_active_clientrefusal now precedes the mechanism lookup (a read-order change with no wire effect — the 409 body is identical and the lookup was read-only). Thedomain_deletedaudit 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 +/healthalert, never forge) is unchanged. - Gates: fmt clean; clippy
--all-targets --features bench -D warningszero warnings (the fn-pointer signature aliases in the new type-level pin factor the complexity); full suite--features benchgreen (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 took_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 okon 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 refused409 legal_hold_activewhile held (rows + file intact), then after hold release proceeded: FK-ordered sweep, 0600 pre-deletion archive segment (NULLprev_hashserialized, tombstones appended, nodomain_deletedinside), 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.rsretains 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/DpaTermskeep their legacy serde derives (they ARE the wire/storage forms — the retention exemplar’s ceiling);relabel_chunkskeeps 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-softcontinueat 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
/decayedaggregate 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 bysql_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
/purgeby-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 shared409 legal_hold_activeenvelope), the strict-posture remanence pragmas (secure_delete=ONbefore,WAL TRUNCATEcheckpoint 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 bylifecycle_purge_audits_inside_the_tx). The negative-reach invalidation (therecall_tracesdeletes — no stale trace may keep “proving” erased content was returned — plus the tombstone row) already rode the same tx inside the primitive; re-asserted bylifecycle_purge_evidence_and_trace_invalidation_ride_the_same_tx. - The by-id/batch read projections move (
service/lifecycle/fetch.rs):/get/{id}and/multi-getrow 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 sharedKNOWLEDGE_ROW_COLS/knowledge_row_to_json/load_knowledge_rowprojection (one source of truth for the export and the/ump/*record paths) out of the gate handler.MAX_MULTI_GET/MAX_PURGE_IDSare re-asserted at the storage boundary (the routes keep their identical wire fences in front). gate.rs83 → 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_idsretyped torusqlite::Error(the Quarryactive_reasonsconvention — 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_chunkandmulti_getjoinstored_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 frommain.rs— into the three planned submodules. The proposal family and/exportstay 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-txrecall_tracesinvalidation + 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/purgestorage and untouched. - Pins (count delta ≥ 0): the three
/decayedunit 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 acrossservice/lifecycle.rs+ everylifecycle/*.rssubmodule never names a handler/transport type or a pool handle — and walks the subtree, closing the general grep’s non-recursive blind spot fordsar/sweep.rstoo),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), anddecayed_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- versionstamp 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 sharedlegal_hold_activeenvelope 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_DECAYEDclamp + 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 toconfig.rsso the service can share it without naming a handler module), andLIMIT 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
knowledgehard-delete whose FK-children map (incl. thecase_articles/kcs_translationsNO ACTION ceilings) is theservice/purge.rsmodule header; the residue rows-affected checks (`if n0
→ 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 warningszero warnings; full suite--features benchgreen (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=500byte-identical;/decayedpagination (limit=1&offset=0/1) byte-identical; a legal hold hides the held id from/decayedon both;/purgeof the held id →409 legal_hold_activewith 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-getbyte-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::Valueshapes (byte-for-byte wire pins outrank the domain-type aspiration — same ceiling as the retention exemplar);DecayedQuery/PurgeRequeststay 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) andchannel_threads.case_run_id(Switchboard) are declared NOT NULL foreign keys onworkflow_runswith 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 bydsar_sweep_takes_the_run_fk_children_delegations_and_channel_threads.
Improvements
- The rights surface converges onto the service layer:
src/service/dsar.rsowns 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.rsis the single home for “what erasure reaches” in the governed-workflow tables (folded in fromworkflow/erasure.rs).src/service/purge.rstakes 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.rs103 → 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 shared409 legal_hold_activeenvelope.
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-levellegal_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 intorun_pooland 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 acrossservice/dsar.rs,service/dsar/sweep.rs, andservice/purge.rsnever namescrate::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/PurgeErrorwithFrom<rusqlite::Error>preserving messages verbatim; the handlerFromimpls render the exact frozen bodies — internal-error text unchanged, the certificate route’s 404 unchanged, the shared409 legal_hold_activeenvelope unchanged),?-propagation via thoseFromimpls replacing per-sitemap_errnoise, the bundle/ledger digest now computed bycrate::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), andrun_dsar_poolbecoming 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 (embeddingsCASCADE;relationshipsSET NULL + explicit;evidence_links/proposals/recall_tracessoft refs, explicit;tombstonesa soft ref BY DESIGN),workflow_runs’ map in the sweep header (steps/findings/contradictions/outbox/handover_offers/case_notes deleted first;case_status_refspurged or revoked;crm_casesUNLINKED; 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 revertof 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(actionboth, jurisdictioneu, mechanismscc-eu-2021) purged the free root only, LISTED the held chunk + reason underheld_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 underowner:<subject>.
Honest ceilings
case_articles.knowledge_idandkcs_translations.knowledge_idare 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_poolowns 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_idis 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_jsonmaps) — 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 /retentionpolicy 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 ofsrc/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, andspawn_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_txand 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 undersrc/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), andretention_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::Databasecarries 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 ofGET/POST /retentionandGET /retention/reportare 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 revertof 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_overridesgets bounds enforcement, not full charset validation. - The guard watches
src/handlers/*.rsonly — 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-bridgegains aslackkind that binds NO listener — the bridge DIALS Slack over the Socket-Mode WebSocket (apps.connections.open → wss, capped-backoff reconnects).messageevents in mapped channels become screened case notes via the ordinary inbound seam (thread map or[case N]); the sender’s OPAQUE user id rides asactor_ref. Pinned bysocket_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.Submitreturning 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 mapsactor_refthrough 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-mapFILES achannel/user_mapproposal (crew_skills_update-style); approval is the ONLY writer of the newchannel_user_maptable — 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/pingoutbox 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_threadsmap, 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
| Change | Nature | Compat |
|---|---|---|
Slack (Socket Mode) + Teams (Bot Framework) adapters in tools/channel-bridge | additive edge processes | config-off default; absent config = channel dark |
POST /webhooks/channel/{kind}/console (pending/decide/due/crank) | additive route, openapi + coverage + guard tables in step | HMAC self-authenticating; bearer surface untouched |
channel_user_map table + channel/user_map proposal kind + /workflow/channel/user-map | additive schema bump to 1.28.45 + additive route | approval is the only table writer |
Envelope actor_ref, drained pings[], activity kind channel | additive wire fields/vocabulary | absent = prior behavior byte-for-byte |
| Proposal renderers (Blocks / Adaptive Cards) with digest fields | bridge-side | server 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-endconsole_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
crankconsole 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 tochannel/templatedispatch). 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 duelists 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 (
channelkind); 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 againstX-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); drainschannel/outon an internal tick crank and delivers to the Cloud API — approvedchannel/templateacts 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/templateacts 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_statusoutbox 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_credentialsnow scans BOTH bridge crates (signal-gateway AND channel-bridge).
Behavior-change ledger
| Change | Nature | Compat |
|---|---|---|
WhatsApp edge in tools/channel-bridge (handshake + hub-sig verify + Cloud-API sender + tier state) | additive edge process | config-off default |
channel/template proposal kind + approve-dispatch wiring | additive | existing proposal gates reused; memory kinds untouched |
Envelope projections: optional attachment_digests[], status, quality | additive wire fields | absent = Switchboard behavior byte-for-byte |
case/channel_status outbox topic | additive topic (case/% family) | drains ride existing Read-gated SSE fan-out |
| Tier backoff table (kernel + edge mirror) | edge-enforced pacing; kernel-side pin | no schema change, no route change |
| Media quarantine | edge-side bytes; digests recorded on notes kernel-side | no 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_outre-reads status+kind from the database inside the tx; nothing caller-declared is trusted), and the edge crate carrieshub_signature_verified_constant_timeplus 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 warningsclean 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_versionat 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_outdoes 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 againstchannel-{kind}-{tenant}.jsonconfigs (0600 fail-closed), replay-caps on(bridge, external_id), flood-bounds, then in ONE transaction:channel::screen_contentsanitize + blocklist + invisible-strip BEFORE any state → thread resolution viachannel_threads→ unknown conversations AUTO-OPEN acare/caserun 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/outcarries content PRECISELY BECAUSE it is gated —enqueue_outtype-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 SHAREDconsent_registryunder purposeswitchboard_channel. The SSE/alert drainers exclude the topic by family; delivery is pull-model viaPOST /webhooks/channel/{kind}/drain, batch marked delivered atomically, senders dedupe onevent_id. - Registration:
POST /workflow/plugins/mountgains 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-gatewayupgraded 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 optionalbrain: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
| Change | Nature | Compat |
|---|---|---|
POST /webhooks/channel/{kind} + /drain | additive routes, openapi + coverage + guard tables in step | HMAC self-authenticating like /webhooks/* |
channel_threads table (UNIQUE on channel+tenant+conversation_ref) | additive schema bump to 1.28.43 | pragma-checked by contract test |
channel/out outbox topic | additive topic, EXCLUDED from workflow/% + case/% drains | content reaches only the authenticated drain |
Tokenless bridge mount mode on /workflow/plugins/mount | additive authn on existing route | bearer path byte-compatible |
Consent reads under purpose switchboard_channel | additive registry rows | Outreach purposes untouched |
tools/signal-gateway (presage native, edition 2024) | new edge binary alongside valet-relay | valet-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/) · plusenvelope_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 benchclean; 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 existingsla_deadlineconvention.brain valet duefires due envelopes (idempotency keyvalet-{run}-{due_at}— a double cron never double-fires),repeatre-arms a NEW envelope via CAS, and overdue ranks reminders before digests then earliest-deadline-first.scripts/import-content-plan.tscreates 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 exactlyvalet/dueenvelopes as Signal messages (metadata-only by construction). Inbound Signal →POST /webhooks/signal(Standard-Webhooks HMAC, replay-capped, flood-bounded):[case N] textbecomes 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 briefcomposes 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
| Change | Nature | Compat |
|---|---|---|
valet/* worktypes + brain valet due/add/brief/consent CLI | additive (FirstLight’s run routes) | no schema change beyond runs |
POST /workflow/valet/due, GET /workflow/valet/brief, PUT /workflow/valet/consent | additive routes, openapi + guard tables in step | Write/Read + workflow role |
POST /webhooks/signal inbound kind | additive, always HMAC-gated | same machinery as kb-feedback kind |
tools/valet-relay + signal-relay.json config | new edge process, cron/launchd-kept | server unchanged; no brain tokens in relay |
valet::style_check pure module + lint_json on draft proposals | additive | advisory only, never a gate |
kind='draft' proposal vocabulary + ALERT_KIND_VALET bus kind | additive | promote lands drafts as fact (forward-compat default) |
Outreach-lite: one-subject consent registry (valet_consents) | scoped pull-forward of v1.28.35 | full 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 asenqueue_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/dueenvelope 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
personalview 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.mdnow 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_markedpins 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/shiftsandGET /ops/skillsnow stamp every response withschema_version: "wfm/1"under a written additive-only change policy (docs/wfm-seam.mdcarries the field declaration and change log). A newbrain 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/workloadcomputes 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/coveragejoins 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 indocs/wfm-seam.mdexactly, 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(thehttp.rsinclude 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 addsubcommands over/ropa(Admin-gated, audited upsert server-side), plus a reviewed seed draft atdocs/examples/ropa-seed.jsonand populate instructions indocs/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-1renamed to the v4-canonicalinset-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/@applyat-rules. - Validation: full gate green (server main bin 942 passed / 6 ignored,
+5); clippy
-D warningsclean; fmt clean.
Honest ceilings
- Gate-backlog attribution rides only onto principals the domain’s own
lineage already surfaced (
proposalshas 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.mdis 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 underar; untranslated leftovers bounded to technical vocabulary),pseudolocale_elongation_renders_without_truncation(fluent-pseudotransform of everyenstring 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.mdgains 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.mdrefreshed 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 warningsclean 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 plannedcustomer_effort_eventsCES 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 fromscoreboard_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_ deterministicalready 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_eventsremains a defined-but-unwired proxy (scorer integration next release);gap_rate_unitsstill 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.rsnow pins one row per release line with the liveTODO(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-statusrefuses loudly when no policy is published rather than hosting links that lead nowhere. -
Acknowledgment is its own audited step:
POST /workflow/runs/{id}/complaint/acklands the legalreceived → acknowledgedtransition with a dedicated audit marker (ISO 10002 posture: within the hour).POST /workflow/complaints/ack-sweepsweeps every active complaint past its ack deadline — exactly oneworkflow/complaint/ ack_overduealert 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 —
closedrefuses 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/signrow over a trailing 31-day window — continual improvement with zero new machinery. -
Repo hygiene gates (folded from the parallel gate pass):
src/dup_guard.rsflags any top-level helper defined in more than one file ofsrc/, with a categorized allowlist whose entries must carry files + a reason and die when the duplication disappears;scripts/repo-brief.shis 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 legsigned_row_carries_the_complaints_extract),complaint_policy_is_published_and_linked_from_status_pages; full gate green (fmt, clippy-D warningsbench + 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 +approverole) 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-statusemitsstatus/<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 inkb_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/translatefiles a pendingkcs_translateproposal); approval is the ONLY writer of an approvedkcs_translationsrow, pinned tobased_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,nlemits{locale}/{slug}.htmlpages 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_awayrows 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: areasknote kind on the case channel orbrain workflow note <run> <text> --reask. The derived heuristic files acase_merge_suggestedproposal for OPEN cases sharing an exact hashed subject withinBRAIN_REASK_WINDOW_DAYS(default 3 days) — propose, never write; approval is the human CRM merge. The metrics dictionary gainsreask_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_refcolumn. 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/translatewith 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) +PublicStatusvocabulary 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 warningsclean (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).
[1.28.35] — 2026-08-26 — “Outreach”: proactive care, consent-first
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_consentproposals; 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/followupschedules 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, andvoc_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.rsowns 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.rsis 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_consentbranch 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_subjectgains the exact-hash arm (consent_rowson 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 passessanitize_read; OptPrincipal everywhere. - Scoreboard: three additive VoC fields + parity-test extension; docs/metrics.md normative.
- Install:
scripts/install-service.shbuilds + installsbrain-connector-crmbest-effort (the same optional-bin loop asbrain-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_registrytable (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_rcainto 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.rsowns the deterministic policy once:RemedyKind(+ legal anchors), theApprovalLevelladder +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::ComplaintRcavariant added — additive on a#[non_exhaustive]enum), andODR_DISCONTINUATION_BASIS. Pins:approval_caps_escalate_deterministically,complaint_lifecycle_is_a_closed_chain,complaint_clusters_outrank_incident_repeaters_in_capture_priority. workflow/complaint.rsis 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_closereads the run kind + 30-day complaint window; complaint runs capturecomplaint_rcaatcapture_salience-computed salience. Pin:complaint_capture_outranks_repeater_capture. The gate’s approve path handlescomplaint_remedy(cap branch) andcomplaint_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_entriesextended; 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 ONEintegrationjob — a singlecargo build --releaseserves 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.shnow 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.
- deploys in a single job; stale matrices supersede via concurrency
cancel-in-progress. Release path:
- 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-coregainsdisposition.rs(closed basis table,FraudSignals.score()clamped arithmetic,FRAUD_REVIEW_THRESHOLD_UNITS,HARD_ESCALATION_UNITS): plan pinsdisposition_ranking_is_deterministic_and_cites_basis,returnless_refund_requires_fraud_review_over_threshold.- SDK gains
pure/aftersales.rs(AftersalesKindmaps the workflow kinds;aftersales_kpisowns all six formulas): pinftfr_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): pinsserial_batch_query_backs_traceability,recall_campaign_is_a_blast_proposal_with_safety_gate_refs. File-backed integration tests.- Scoreboard wiring:
GET /workflow/scoreboardderives 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 thescoreboard_fields_have_dictionary_entriesmeta-test extended together. EntitlementRecordgrows an optionalbatchfield (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_flaggedstate 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, andRetentionOutreachjoin 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
IntentClassextended inworkflow/frontdoor.rswith the closedWORKTYPE_TABLE(9 policy rows) +worktype_policy/worktype_skills; SDKpolicy::Worktypeowns the SLA clock table (single owner across the ABI). Tests added: bin 898 / 6 ignored (+7 over v1.28.31: plan-named pinsintent_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, plusmemory_kind_round_tripsextended 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 (+1worktype_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 ashandover/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 theen-XApseudolocale join the shipped locale set under the existing key-parity wall; mirroring is pinned by a render-smoke test. - WFM seam completed:
GET /ops/skillsjoins 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;Envelopegains additiveack_deadline(non-complaint stamps keep one clock);relay::record_dispute_escalationreuses the offer machinery with audit detailhandover/dispute. Tests: bin 891 / 6 ignored (+8 over v1.28.30: plan-named pinscomplaint_class_gets_acknowledgment_sla,complaint_escalation_is_audited_as_dispute, plus SDKcomplaint_envelope_ack_leads_response), lib 194 / 1 unchanged. - G2:
config::fcr_window_days(); derivation consumes the window via an optional recorded recurrence age; testsfcr_window_is_configurable_and_ deterministic(shared-lock env posture) +scoreboard_fields_have_dictionary_entries(two-way docs↔code parity). - G3: client
a11ytest module parses docs/trust/wcag22-aa-checklist.md (PASS/CEILING verdicts only; CEILING must cite the ACR) +acr_lists_known_ceilings_honestly. - G4:
SUPPORTED_LOCALES5 → 7;dir_for_localeextracted pure (the shell effect consumes it); client suite 232 passed (+3). - G5:
workflow::crew::list_skills(bounded 1000-row ordered read) + handlerget_ops_skills(Read on domain, strip-seam on emitted principals); route + openapi + docs/api.md + guard tables in the same change; testwfm_feed_round_trips_shifts_and_skills. - G10: new
src/docs_truth.rsmeta-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 warningsclean; live smoke on a DB COPY green (brain doctorclean,/audit/verifyok: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_contactflag 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_ledgertable 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 insrc/handlers/parcels.rs. Ledger writes chain viarecord_tenant(SAVEPOINT-nested) inside the caller’s transaction. Content screening reuses the two-layerscreenat 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 warningsclean. 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
proposalstable predates domains: imported rows are GLOBAL pending proposals until approval, distinguishable by theirparcel:{domain}:{signer}source label only — no per-domain review queue yet. Planned as v1.28.53 “Triage” (additiveproposals.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_signercheck + 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
sincecursor.
[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 (additiveagent_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:/@principalmentions into swarm invites the invitee accepts into the channel (same accept discipline as Relay). - invite pings flow over the existing
/eventsSSE 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, plusmention_storm_refuses_over_the_cap, the erasure pindsar_sweep_erases_channel_rows_and_fk_children_of_the_run(offers + notes + the CRM-link unlink against one run), the SSE-drain pinchannel_notes_drain_to_the_sse_bus, and the four third-pass hardening pinsoversized_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 (additivecase_notes). Second-pass hardening: the POST receipt echoes the STORED row’s clock (one read per request — previously a secondUtc::now()could drift from the persistedcreated_at), retention resolution moved off the async reactor into the read’s blocking task, the lineage-append tip-read deduped into one sharedoutbox::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/verifyok; 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 viaBRAIN_RETENTION_KIND_DAYSor a bound profile block; absent policy = notes persist with their run./retention/reportdoes not yet include acase-noterow (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.
@principalresolution 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 pinsboard_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 (additivehandover_offers). Live smoke on a DB COPY of the live DB: migration stamps 1.28.27, doctor clean,/audit/verifyok 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 (404via the storage backstop) now refuses400 reason_requiredat 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_principalfails closed on control/invisible characters (a stripped id could collide with a different real principal at accept time); the board skips a corrupt-state_jsonrun LOUDLY — warn log pluscorrupt_state_rows_skippedon 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_stepkeys + aworkflow_stepsrow 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_jsonper row (no index-served ranking); fine on loopback SQLite. overlap_minuteson 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 (additivepresence/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/verifyok 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_refis 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
globaltenant 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 (additiveshiftstable + 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 doctorintegrity 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).
rosterholds principal ids = personal data; the DSAR erasure sweep does NOT cover theshiftstable 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_shiftpairs 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.shover the tarball) — no minisign integration insidebrain kb build. revisionrenders the articlecontent_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_TOKENnow 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-attestedOriginheaders must be loopback (DNS-rebinding posture, IPv6-literal safe); request bodies are capped DURING the read (DefaultBodyLimit+to_bytesbound → 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_lineguard fired only after buffering the whole line; reads are now chunked and stop atMAX_LINE_BYTES— a multi-GB newline-free stream produces bounded-32700refusals, not an OOM. - Rewind role gate judges the right store: the
approvecapability is now checked against the RUN’S DOMAIN pool, not the global one; CAS conflicts surface as409 cas_staleinstead of a 500. - Handoff packet read-seam parity:
intent,is_seed,is_not_seed, andpending_questionpasssanitize_readlike 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 firstcrm/case/closedevent delivery, not on engine-run Done directly (a closed case without a CRM binding captures nothing); pre-Evolve knowledge rows keepkcs_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 acrm/case/updatedorcrm/case/closedevent. - 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 INTOcopy → migration stamped 1.28.22 →brain doctor✓ @ 1.28.22 →/audit/verify ok:true→ support-case run opened +crm/case/closedevent accepted end-to-end on the wire).
- fmt clean on server (default/bench/connector-crm) + sdk + client; live smoke on
a COPY of the real DB green (
Honest ceilings
- Delivery rides the UMP
/ingestpath 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 newestSystemModstamp. - 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, flaggedtruncated. 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/checkpointfires 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
Vecslicing — 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-splitis green..cargo/config.tomlswaps-C strip=symbols→strip=debuginfo+-C link-arg=--emit-relocs(the splitter needs relocations + function names; DWARF-only stripping);bundle-budget.shmeasures 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_readseam).
Engineering record
- M1 (cadence):
resolve_checkpoint_every(Option<u32>)(default 25, clamp 1..=100) besideresolve_budget; the crank tracksevents_since_ckptand 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 bycheckpoints_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)+ conveniencederive_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 inhandlers/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 pincontext_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_eventsgains theLast-Event-IDheader; the app-level stream driver threads the max workflow event id across reconnects. Server replay lives inalert.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 desktopcompiles a native window shell from the same tree;scripts/build-desktop.sh [macos|nsis|appimage|all]wraps the documenteddx bundle --desktopcommand set with fail-on-error discipline (the dx CLI stays an operator install — that line was already honest, it stays honest). Themobilefeature 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_DIRon 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/timelineroute 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
/scoreboardpanel (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.tomlgains the Dioxus feature triad (web/desktop/mobile, defaultweb);[desktop.window]lands in Dioxus.toml; CI’s client-gate addslibwebkit2gtkheaders +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 sharedsrc/download.rsseam (native path writes toBRAIN_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 beyondsync. Size posture:.cargo/config.tomlapplies-C opt-level=z -C strip=symbolsto 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=zrustflags 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
FrameGatepolicy 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_FIELDSsilently 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
/eventsnow also carries drainedworkflow/*outbox events under kindworkflowwith 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
/eventsstream for the whole app (survives route changes): capped exponential backoff (1 s → 30 s), deduped per coordinate space (alertseq, 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_idconversation panel (deep-linkable): the run’s stream events fold through the conversation assembler into keyed chat nodes —review-jobrenders 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), andworkflow-runrenders 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/mountper mounted plugin, carrying the bundle SHA-256 read from the Anchor-signed/app/boot.json(.wasmentry preferred). Fire-and-forget with a console warning — evidence loss is visible, never fatal. - MCP over HTTP: the
mcpbinary now serves its full JSON-RPC surface over Streamable HTTP (POST /mcp, SSE-framed when the client’sAcceptasks) in addition to stdio — opt-in viaMCP_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
PluginHostwhile consumers read it asSignal<PluginHost>, so mounting the Overview approval dock panicked. The provider now wraps the host in a signal. - Fixed an aborted-
git-stashhazard 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_TOKENbearer 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_workerinsrc/alert.rs— every 2 s, per registered domain (webhook drainer’s cadence + fail-soft discipline), pendingtopic LIKE 'workflow/%'rows advance via the existingworkflow::outbox::deliver(audit row commits in the same tx; non-workflow topics likesteeringare 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 pureworkflow_event_admissible(kinds, authorized): opt-in required AND domain Read granted (default-off for old consumers). Pinned byworkflow_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. Newclient/src/panels/conversation.rskeyed off the shared slot registry (ui_renderer::chat_node_viewdispatch + generic-card fallback); answer binds SHA-256 of the exactpending_questionbytes (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:.wasmpreferred, absent manifest → metadata-only evidence — an unverifiable digest is never invented). - MCP HTTP transport:
src/bin/mcp.rsreuses the existing JSON-RPC core (handle_line) behind an axum router driven bytower::ServiceExt::oneshotin 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’sAccept; 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-repetitionfires 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 doctorclean, 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
initializeper connection because nothing sticks between requests. Non-loopback binds withoutMCP_HTTP_TOKENare 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}/rewindrestores the state snapshot from aworkflow/checkpointevent (or the run root) in one transaction, appending abranches[]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/checkpointcarrying the full state snapshot (≤256 KiB guard — oversized states error loudly, never truncate). - The I-PASS handoff packet exists:
GET /workflow/runs/{id}/handoffassembles 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_completederives 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,
approverole, 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
mainfound while wiring this release:exec_allowlist()called a non-existentparse_word_listhelper (a leftover from the previous lipstyk cleanup pass); it now uses the siblingword_listlike 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_childmirrorsenqueue’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;enqueuenow 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 byverify_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 toenqueueand reports the0sentinel id, so every existing impl (server host, remote host, test doubles) compiles unchanged.SqliteWorkflowHostoverrides with the real thing through the same lane discipline. - M3 (engine + routes): the crank threads
last_eventinto every emission (host path and mediated Effects door — the events hostcall body gained optionalparent_event_id, its receipt is nowenqueued:<created>:<event_id>); the cursor seeds from the LASTstate.branches[].from_event, which is what makes rewind work without a server push./eventsPOST gainsparent_event_id→{first, event_id}; new GET/events?branch=, POST/rewind, GET/handoffhandlers live insrc/handlers/workflow_lineage.rswith 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/eventspath 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_lineageis 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 dedicatedsteward-harness-gatejob (fmt + clippy + test) for the engine’s tokio conformance. - Cooperative cancel is real: new
crank_cancellableobserves a sharedCancellationTokenat every step boundary and settles the run asStoppedAt::Cancelledexactly 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 andBudgetExceededcould 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 bysigterm_settle_then_resume_exactartifacts-equal-control plus the no-half-step twin audit). CancellationToken::clonesnapshotted 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 overcompleted|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, andCancellationTokengained 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 itsrun-{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 asCancelled— 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, InMemHostoutbox_of/audit_logtest accessors; steward-harness tokio gains thetime/rt-multi-threadfeatures (feature-add, no new dependency). - M3 (CI):
engine-cratesjob runs the SDK settlement gate explicitly; newsteward-harness-gatejob 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_stepproves the driver settles without waiting out a stuck child and that the report carriescancelled; 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
Budgetis 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), andui(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/scoreboardno longer 500s: the audited-run linkage queried a plain-textaudit_events.targetcolumn that the migrated DDL never had (same dead-code class as the removed executor INSERTs). The set now reconstructs viahash("run:{id}")membership overtarget_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_ALLOWLISTempty/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 sharedfirst_tokenhelper inbin_common/http.rs(pinned), which also fixes MCPbrain_search/ump.*calls against rotation-slot files. - M1 (server):
src/workflow/hostcalls.rs::build()registers all seven kinds via the extractedregister_handlers.production_policy(engine)grants the per-engineexecallow ONLY whenBRAIN_ENGINE_EXEC_ALLOWLISTresolves 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_mediationrefusal table,BRAIN_ENGINE_WORKDIRpin (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_urlscheme 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 idempotentenqueued:falsereceipt. Every refusal path auditsworkflow/hostcall/{kind}/deniedthrough the host chain. - M2 (SDK):
HostCallContextgains an append-onlyBTreeMap<(label, kind), u64>behind acounters()accessor — incremented for every canonicalized dispatch INCLUDING denials; plushas_handler(kind)(the exhaustiveness pin’s read seam). - M3 (engine): steward-harness
effects::Effectsis the ONE effect door —exec/http/event/suggest/logserialize the exact mediated body shapes and ridedispatch; crank event emissions route through it when provided (crank_full, additive — existing signatures unchanged) with the per-call tally landing inCrankReport.hostcalls. The reqwest transport stays solely inremote_host.rs, pinned by the include_str! self-grepengine_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, SDKdispatch_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>andtenant_for_targetresolves arun:reference ANYWHERE in a target — handler audit rows land on the run’s domain tenant instead ofglobal(pinned byhostcall_audits_resolve_the_run_domain_tenant);knowledge_suggestagainst 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 == Denieduntil Witness wires the GUI consent path;uirefuses with its named reason even where policy would admit it.- Exec timeout is the fixed 30 s
Budgetdefault — the per-op budget seam (Budget::op_secswired into the handler) lands with the GUI crank; workdir defaults to the process cwd whenBRAIN_ENGINE_WORKDIRis 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}/answerdigest-binds the answer to the livepending_question(SHA-256), appendsanswers[], 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 theworkflowrole; answer carriesapprove. brain workflowis real:open/status/answer/approve/crank(spawns the harness binary beside the CLI or viaBRAIN_STEWARD_BIN; usage string updated).
Bug fixes
- Dead code removed:
src/workflow/executor.rs+consensus.rsINSERTed 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 infallibleOptPrincipalextractor (None= loopback superuser posture unchanged); pinned over real HTTP in the smoke path. - M1 (SDK): the four state keys are now NORMATIVE ABI —
Decision+decidemoved tobrain-engine-sdk::workflow_state(behindharness-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). Serverdriver.rsre-exports; its pins pass unchanged. New pindecision_keys_are_frozen_abi(fixture round-trip over all four keys + precedence). - M3 (engine):
steward-harnessrestructured lib+bin:RemoteWorkflowHost(loopback-http-only transport law, bearer ladderBRAIN_TOKEN_FILE→BRAIN_TOKEN→default install path, journaling tx) implements the SDK seam;crankloopsdecide→gate waterfall (over DECLARED constraints:required_evidence[],mutations,supporting_lines,needs_approval)→CAS persist (one reload-retry on stale, then REPORT)→outbox log;Donefolds scoreboard keys (handoff_complete = status=="completed", never upgrading a recorded false) + finalworkflow/endevent. Gate rejections becomeDI_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_atwalks 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}/stateis deliberately NOT read-seam sanitized (engines CAS against exact stored bytes) — it requires the same domain Read grant PLUS theworkflowengine 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_digestis now400 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 (409on mismatch/unknown digest). - New
BRAIN_WRITE_POSTURE=open|review(defaultopen; installer setsreview). Under review,/add,/ingest,/ingest/memory,/ingest/markdown,/ump/remember,/ump/reviseroute through the existing proposal pipeline and return202 proposal_pending— agents propose, operators dispose. Origin labels corrected (/ingest/memoryderives; 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 serializeorigin/flagged/authority; UMP recall records carryuntrusted: true;/exportgains a top-leveluntrustedmarker with content verbatim. - Boot chain means something: symlink containment (canonical, fail-closed), Ed25519-signed manifest (
sig+kid) withGET /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;
baseUrlscheme gate (https or loopback);originprovenance 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/openandproposal/decidedconversation 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.jsonplus awindow.__BRAIN_BOOT__script seat list everypkg/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 underpkg/, 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 sharedSlotRegistry; 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 (theslots/changedpayload). The approval dock now consumes the shared host instead of building an ad-hoc registry. - M2: server-side pure producer (
src/proposal_events.rs: brandedProposalIdwire formp<id>, open/decided builders) published on the/eventsfeed under a new fixedproposalalert 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 viabuild_view_node. - M3:
frontend.rsgains pureboot_manifest(dist)(sorted, SHA-256 per bundle) +inject_boot_script(idempotent, head-anchored); routes/app/boot.json+/app/boot.js; clientplugins/boot.rsvalidates manifests fail-closed with acertifies()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 auditclean (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/updatedprogress 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-setsfeature — 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, withpanic = "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_atonPOST /breachoverflowed 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.protocolVersionwas hex-escaped inerror.messagebut echoed RAW inerror.data.requested— same injection carrier. Both are escaped now. - CLI hardening:
brain domains-recomputeno longer panics on an unexpected response shape;client *subcommands percent-encode{name}path segments;brain restorerefuses 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/exporttags 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; RoPAretention_daysis 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-setscrate (publish = false): seven embedded gold cases (gold/qc_report.json, fivegold/gdl_cases/*.json), each freezingsystem_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
calibrationmodule (Cohen’s κ in integer ten-thousandths, weekly/monthly cadence gates,CalibrationRecordwhose detail string ridesAuditKind::Workflow) re-exported besidescoreboard;policy::LAW_VERSIONS+stamp_envelope_for_jurisdiction; optionalgold-setsfeature 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.rsowns the cadence/baseline stamps inschema_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/scoreboardgained an additivecalibration_report_emittedfield; the sign endpoint is Admin + DPO-role gated, wire input validated (reviewer 1..=128 chars, κ sentinel −1 or 0..=10000), 409already_signed_this_monthwhen 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 auditclean (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_decayedon/recalland/searchwere 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’strueis honored; everyone else is clamped tofalse. - 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;
subidentifiers in local logs are masked to hash prefixes; duplicate JWTkids refuse key-store load instead of silently collapsing; model artifacts support fail-closed SHA-256 pinning viaBRAIN_MODEL_MANIFEST; the snapshot path uses the one sharedVACUUM INTOescaper.
Improvements
- DSAR residue sweeps accept
subject_exact: truefor exact matching alongside the erasure-safe substring default. /ingest/memoryenforces an explicit entry-count cap (too_many_entries, 500).- Release binaries are minisign-signable (
scripts/release-sign.sh) andinstall-service.shverifies signatures whenever the operator configuresBRAIN_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.digestfield, 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 auditclean (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
jtistay expiry-only until re-minted; legacy queued approvals without a stored digest replay digest-less; model pinning activates only when the operator setsBRAIN_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. WhenBRAIN_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), andGET /compliance/inventorychecks 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/reportnow 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_FILEis 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.tomlexempts tests), so request-data paths there are structurally panic-free;unsafe_op_in_unsafe_fnandmissing_safety_docare denied crate-wide (zero current sites — the first futureunsafe fninherits block-scoped safety).
Bug fixes
- Fixed a boot-blocking router panic introduced in 1.28.4:
/appwas registered twice (the static SPA seat handlers AND a historicalnest_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 CIserver-boot/recall eval gatejobs. 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. benchno longer fails against servers ≥ 1.27.23: it readscapacity.rss_mibfrom the Read-gated/health/db(with the operator token) instead of the shrunken public/health, falling back to legacy shapes for older servers.BENCH_SCALESenv 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 viaBRAIN_AUDIT_SIGNING_KEY/_FILE(0600 check; absent key ⇒ NULL signature, disclosed on export). Every record ALSO extends the existingaudit_eventschain (AuditKind::Decision). The recorder lives on the host write path (WorkflowHost::audit) — engines cannot write their own evidence; pinned byhost_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_evidencetable +record_oversightwired 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/inventoryflags 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_registrytable + routes; disclosure notices continue via the existing/.well-known/ai-noticesurface. 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-packthe 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 benchclean 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}/importis 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 denyclippy::unwrap_used(clippy.tomlexempts 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, anddataset_hashmust 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_digestof 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_DISToverrides 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;ApiClientremains 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 benchclean on server, client, crates; lipstyk diff watchdog exit 0 (one SLOP finding fixed:ls | headparsing replaced with a newest-mtime glob loop inbundle-budget.sh; heuristic warns cleared via table-driven matching, tokenized CSS values, and test-shape variation);cargo auditclean 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
completedsurfaces 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_okis 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.scoringre-export the pure reducer/scorer; host owns the wire shape (SDK stays dependency-free); endpoint derivation defaults absent scorer fields honestly and deriveshandoff_completefrom 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 auditclean (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 closedHostCallKind→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. PlusBudget::effective_timeout(manager ∩ per-op intersection), cooperativeCancellationToken, RAIIExtensionRegion(drop cancels within the 5 s cleanup budget), pureexec_mediationdestructive-command table, and aManagerProbeWeak-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 (RunStateSourceoverworkflow_runs+ReadViewSanitizer=sanitize_readunder 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-mediatedtools/log; handlers are log (structured emit), session (sanitized view viaWorkflowHost::load_state),secret_status(broker resolves host-side, publishes{configured}only, name-shape validated), andmediated_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_idactive-hold convention — chunk ids are positive, so no collision) are deferred and certificate-listed beside held chunks; dry-run countsworkflow_rows(matched runs incl. frozen + dependents) into the additiveFootprintfield (openapi updated). - Secrets broker (
src/secrets.rs):BRAIN_<NAME>_KEY_FILE(mode-checked via the existingcheck_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 warningsclean 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_idcolumn 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-sdkcrate — 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-sdkjoins the engine-crate workspace node — zero dependencies,unsafe_code = "forbid", clippyunwrap_used/expect_used/panic = deny(tests excepted via scoped cfg).sdk::pure::{evidence,qa_score}moved verbatim fromsrc/workflowaspubAPI; output types are#[non_exhaustive]; oracle tests travel with the code. sdk::policynow owns the P-class SLA TTL table andDEFAULT_RETENTION_KIND_DAYS; the server’s front-door and config modules facade re-export them — policy truth lives once. Server behavior unchanged.WorkflowHosttrait (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.HostTxis an RAII unit-of-work guard: commit on call, rollback on drop.- First host adapter: SQLite pool lane in
src/workflow/host.rs— singleBEGIN IMMEDIATEwrite lane, fail-fastBusyon 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). Traitaudit()resolves tenant fromrun:<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-sdkonly; 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 --offlinebuilds 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-leakedHostTxholds 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_scorefor pure functions; existingfuzz_chunker/fuzz_validatorretained. Corpus committed;cargo +nightly fuzz runentry 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-coreengine (diagnostics pipeline) with kernel/gates/advisor/evidence/subagents.
Engineering record
- Crates workspace +
src/workflowwiring; 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 2024across all manifests;gen→generationin recall debounce (client/src/panels/recall.rs:64) andreview.rstemporary-borrow fix;client/serverclippy harden (collapsible_if/let_and_return) viacargo clippy --fix.
Engineering record
src/backup.rs:1#![allow(deprecated)]for upstreamaes-gcm→generic-array0.14 deprecation;src/config.rs/src/capacity.rs/src/storage_layout.rs/src/main.rs/src/connector/auth/store.rsstd::env::set_var/remove_varwrapped inunsafe(Rust 1.98).cargo clippy --all-targets --features bench -- -D warnings+cargo clippy --manifest-path client/Cargo.toml -- -D warnings+cargo fmtclean.
[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-corecrate: 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:1wired viasrc/workflow/mod.rs:25.cargo test --features bench --lib147 passed;cargo clippy --all-targets --features bench -- -D warnings+cargo fmtclean.
[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_returnfailures blocking CI (client/src/main.rs:2014).
Improvements
- New
brain-interview-corecrate: 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:1wired viasrc/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
(
hmac256epoch) 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 0600audit-chain.keybeside 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 inschema_metain 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/verifyeven though the surviving prefix walks clean. - Restore attestation.
restoreverifies the restored chain before certifying the restore (a backup whose chain does not verify is refused — the.bakkeeps the pre-restore state) and compares pre/post head pins: a restore that ROLLS BACK the evidence chain is disclosed at error level and therestore complete (head=…)row records where the chain landed. - Multi-domain chain coverage.
/audit/verify,/audit,/metrics,/ump/audit/verifyand the retention prune now cover EVERY registered domain’s chain, not just the global pool —okis 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 ananchorevidence row on the NEW chain per domain. Idempotent; per-domain failures fail the run. Fresh (row-less) DBs bootstrap straight tohmac256when a key resolves — existing chains stay legacy until the operator re-anchors.- Fixed
--re-embedexiting 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-auditor the fresh-DB bootstrap writes the stamp. Writes to anhmac256DB without its key fail closed (row refused,/healthcounter 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_chainon 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-auditexercised end-to-end on a real DB: key file generated 0600, epoch + head pin stamped,anchorrows 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’schain_okstill watches the global chain only (/audit/verifyis the authoritative multi-domain surface); thehmac256key is part of the backup baseline — a restore on a host without it refuses certification (copyaudit-chain.keywith 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 warningsclean with zero dependencies. The workspace builds green now and fills crate-by-crate in the upcoming engine ports; the driver harness stays intools/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
*-coreengine 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::Workflowrows — audit-per-write holds of the FUNCTION, not - Idempotent event delivery by key, not retry count. The outbox enqueues
INSERT OR IGNOREagainst aUNIQUE idempotency_keyand delivers via a singleUPDATE … 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;normalizestays 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 inlineoutbox.idempotency_key UNIQUE);test_migration_schema_contractextended to pin tables + the ingest→FTS→vec0 roundtrip unchanged. - M4/M5 —
src/workflow/{tx,outbox,state,evidence}.rs; 11 tests includingaudit_rolls_back_with_the_transitionandoutbox_enqueue_audits_once_not_on_replay.deliverusesUPDATE … RETURNING run_id(no second lookup);cas_updatedistinguishesStale { actual_revision }fromGonefor the engines’DI_*_CONFLICTmapping. - 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
normalizeis 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_CONNECTORsat behind a comment reserving it for a connector split that shipped years ago and never used it. It is gone, andclippy -D warningsnow 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 insrc/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_domainmatch reducible tothen/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 thev1.27.x "name" M# (F-##)label), collapsing duplicate re-assertions to one authoritative site;src/migration.rskept 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_idssource-scan guard (the repo’sno_raw_strings_in_rsx-style test pattern). - M5 — verification gate: fmt, clippy
-D warnings(default + bench + otel), full suite, lipstyk strict-diff,badges.sh --selfcheckall clean in one pass. CI follow-up (9662584): theforced_domaintwo-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. thelipstykjob). - 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/reportno 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 as500 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 consolidatedpoisoned_lock_denies_every_gatepin 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
lipstykjob): 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 acrosssrc/,client/,plugin/.
Engineering record
- M1 (fail-closed extension): the sweep over every
unwrap_or_default()/pool.get().ok()?/RwLockread 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, registryPoisoned, webhook-secret fail-closed,guard_capacity’s availability fail-open is documented + out of authz scope). The one genuine residual wasgovern.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 ofrole_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(realcatch_unwindpoisoning), andpoisoned_lock_denies_every_gate(source-shape pin across the five seams). - M2 (S2-03): verified shipped —
/ump/forget {"hard":true}runsrefuse_if_heldin-tx (v1.27.21) ANDpurge_chunk_idscarries the structural backstop fence, so the property holds of the function, not of call-site discipline. Added the plan’s soft-branch pinump_forget_soft_flags_but_not_held_chunks. - M3 (F-61 + S2-44):
contains_suspicious_patternrewritten — 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/overrideas single-token entries, tier-2 line-anchored markers unchanged. The four pre-existingsuspicious_pattern_*tests pass unchanged; new:blocklist_matches_multi_word_phrases,normalization_does_not_kill_phrase_entries. NOTE: the matcher feedsSearchResult::raw()’sblocklist_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_heldin-tx (main.rsingest_markdown/write_markdown_ingest), and domain delete archives tombstones + evidence_links (v1.27.25 wave 2, pinned bydomain_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 intospawn_blockingmoves, wire-shapeOptionhandling, 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-localretrieval profiles) now usesmixedbread-ai/mxbai-rerank-large-v1as its primary model, withBAAI/bge-reranker-v2-m3as 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 defaultmodels/mxbai-rerank-large-v1/dir (then setBRAIN_RERANK_MODEL_DIR).
Engineering record
src/audit.rs:record_tenantreturnsNoneonBEGIN IMMEDIATE/SAVEPOINTfailure instead of proceeding unterminated +record_commit_failurebump; the fork-window comment documents the F-23 rationale (drop > fork).src/search/rerank.rs:Reranker::newtries the mxbai user-defined seam first (new_mxbai_user_defined), warns + falls back toBGERerankerV2M3on 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
multilingualretrieval profile was mislabeled —minishlab/potion-base-2Mis an English model (distilled fromBAAI/bge-base-en-v1.5), not multilingual. Renamed tocompact(PROFILE_COMPACT); the oldPROFILE_MULTILINGUAL/MODEL_PROFILE=multilingualremains as a deprecated alias resolving to the same profile (no behavior change). Also correctedmxbai-rerank-large-v1to DeBERTa-v3-large (~435M, was misstated as v2) andgte-base-en-v1.5to ~137M (was 149M). - Model binaries are gitignored (downloaded per the plan, never committed).
- Docs aligned to source truth:
docs/configuration.mdgains the retrieval-profiles model matrix +BRAIN_RERANK_MODEL_DIR/BRAIN_RERANK_TOP_N;docs/SPECS.md§7.5 current-state rewritten;docs/BENCHMARKS.mdv1.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.
- the reranker;
- 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 carriesk.piiinto the hit so the read seam redacts graph hits exactly like the other legs. Before this, the leg (unreleased default-on) ignored every filter and hardcodedpii: false— a cross-domain, cross-owner, unredacted side door on/recall,/search, and/ump/recallin shim mode (pass-3 S3-01, CRITICAL). Pinned bygraph_leg_scopes_domain_and_owner_s3_01+graph_leg_empty_permit_and_pii_carry_s3_01(two-domain shared-entity fixture — the exact collision shape of the finding). /verifybinds theX-Brain-Domainlabel in SQL + the record gate (the/get/{id}idiom): a foreign-domain chunk id now reads as not-found instead of answering “supported” as a cross-domain content-confirmation oracle (S2-09). Pinned byverify_cannot_cross_domain.GET /ump/memory/{id}binds the domain label + record gate — the MCP-reachable (ump.get) surface no longer renders any row by bare id under a global read grant (S2-10). Pinned byump_get_memory_cannot_cross_domain.GET /procedure/{id}/stepsbinds the domain label + record gate (S2-30).GET /domains/{name}/exportrequires Admin in shim mode — the snapshot resolves to the ONE shared pool there (every tenant's chunks, owners, the audit chain), which a per-name Read grant must never cover. Multi-db keeps Read (the file IS the domain). TheVACUUM INTOpath now goes through the shared quote-escaping primitive (S2-08/S2-24).- The rate limiter moved OUTSIDE the auth layers. An unauthenticated
flood is now 429-throttled before any token work — previously it
401-rejected before ever consuming a bucket, and each free 401 performed a
synchronous audit write on a fresh connection (unthrottled
DB-write-per-request amplification). The deny-path audit writes now run on
spawn_blocking(S3-03). Pinned byrate_limit_layer_is_outside_auth_layers. GET /graph/relationships/{id}/historygates onAction::Admin, matching what every doc surface (CHANGELOG §1.27.22, openapi.yaml, docs/api.md, its own doc comments) already claimed — the retired PII-bearing entity labels it returns are operator evidence. The read-audit failure is no longer silent (S3-02)./addwrites the quarantine flag IN-TX, before the commit — a failed flag write now rolls the whole chunk back (the/ingest/memoryposture) instead of leaving the injection chunk durably storedflagged = 0while telling the caller it failed (S3-06)./suggestapplies the v1.14 scope filter + v1.23 role gate like/recall— an owner-restricted role no longer sees other owners' private rows as suggestions (S2-29).- Smaller hardening:
X-Forwarded-Fortrusts the RIGHTMOST entry underBRAIN_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 bumpsaudit_commit_failures(it was silent; S3-09); the two boot-timeVACUUM INTOliterals go through the escaped primitive (S3-11). - The audit retention prune now VERIFIES before it prunes and records a
retentionevidence row for what it deleted — previously the re-anchor would have re-blessed a tampered chain into a freshly-verifying one (evidence laundering), and the deletion of audit evidence was itself unevidenced. A failed re-anchor UPDATE now rolls the whole prune back instead of committing a half-rewritten chain (S2-16 + S2-35). verify_chainenforces the NULL-prefix rule (F-03, the no-hash-change half): a NULLprev_hashis legal only before the chain starts. Legitimate writers always chain from the tip once one exists, so a mid-chain NULL is tamper — previously it was skipped silently at any position. No stored hash changes.brain restorere-applies ACTIVE legal holds from the pre-restore DB and loudly discloses tombstoned content the backup resurrected — a pre-hold backup no longer silently unfreezes litigation-held ids, and an undone DSAR purge is on the record (S2-28).- The open-edge invariant is structural:
idx_rels_open_unique(partial UNIQUE on the tripleWHERE superseded_at IS NULL, after a deterministic newest-wins dedup of legacy double-open rows) — a racing double-insert now fails at the DB and rolls back the ingest instead of corrupting the lineage (S3-08; schema → 1.27.25). - The remaining shim-mode reads are scoped:
/decayed+/quarantinebind theX-Brain-Domainlabel in SQL;/statscounts by domain label (entities/relationships via their chunk linkage);/consolidate/proposerequires Admin in shim mode (its five detection scans are corpus-wide); the domain-registrydomain_invaliderror no longer embeds theknown_domainsinventory (S2-31/43/32). - Ingest auto-routing re-authorizes on the ACTUAL target — a
write:<t>/global-only principal can no longer contaminate another tenant’s domain through centroid routing (S2-33). /clientsdenies empty-grant auditors at the gate (403, not a silent 200-empty — “Some([]) denies all” now means the surface too; S2-15).- The DSAR certificate’s remanence claim follows the pragma attempt — on
a failed
secure_delete=ONit downgrades to the disclosed logical posture instead of certifying an overwrite that never ran (S2-18). - Chunker fidelity: an UNTERMINATED oversized fenced block no longer duplicates its final code line into every stored piece (the last line was treated as a closer it wasn’t); degenerate over-cap lines inside fences end with a newline so re-attached closers sit at line starts; prose pieces stay strict verbatim (S2-19/S2-20).
- Evidence self-links are skipped in the batched enrichment (a
from == torow satisfied bothIN (…)groups and duplicated into API responses; S2-38). Domain delete now archives tombstones + evidence_links into the pre-delete segment alongside the audit rows — the deletion registry is evidence and no longer dies with the domain (S2-21). - Plugin:
autoRecallGraph: falsedisables the graph leg again. The flag previously OMITTED thegraphparam when false, so the server's default-on change silently enabled the leg for every plugin user. The flag is now always sent explicitly; the plugin's documented default stays opt-in.
Improvements
openapi.yaml/health+/health/dbschemas now match the shipped shapes (the public probe is{status, version}; the detailed body is Read-gated on/health/db) — the contract previously documented the full fingerprint body on the public route.SECURITY.mdegress inventory is truthful (three enumerated, bounded, opt-in/gated paths — not “exactly one”).
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
jurisdictionscell now fails the row read instead of silently becoming an empty list. If the stored JSON on a breach was corrupted, the breach previously read back with zero affected jurisdictions — hiding from the DPO every affected-law notification deadline that the breach carries. That read now errors loudly (fail-closed, the repo’s D-1 “never certify silence” invariant) rather than presenting an empty scope.
Bug fixes
- Removed the blanket
#![allow(dead_code)]+#![allow(unused_imports)]on the handlers module and deleted the real dead code they were hiding (unused imports inauth,recall,ump,govern; the never-usedauthorize_read_domain; the never-readProposalRow.created_at; the UMP recallranking_hintsrequest field, now_ranking_hintswith its wire key preserved). No behavior change — clippy-D warningsis now the dead-code watchdog instead of a blanket allow.
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
/healthis now the minimal probe shape. The unauthenticated load-balancer probe shows onlystatus+version; every deployment-fingerprinting field (model,otel.endpoint,pool,backup,webhook,hardening,compliance.dpo_contact,integrity) moved behind the authenticated/health/dbdetail. 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 auditis 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
/healthis now the minimal probe shape (A-02). The load-balancer probe (status+version) stays public; every deployment-fingerprinting field —model,otel.endpoint,pool,backup,webhook,hardening,compliance.dpo_contact,integrity— moved behind the existing Read gate on/health/db. An unauthenticated network probe can no longer fingerprint a regulated BPO deployment. Intentional surface reduction (same class as the v1.20.2 F2 carve-out): an operator monitor reading the detailed fields must switch to the gated/health/db. - Dependency hardening: h2 0.4.15 → 0.4.16 (RUSTSEC-2026-0258). The HTTP/2
dependency (reached via the reqwest/hyper client) was bumped to clear the
“unbounded empty DATA frames” advisory.
cargo auditreturns exit 0 on both the server and client trees; the two remaining findings areunmaintainedwarnings (paste, number_prefix) deep in the HF tokenizers/model2vec stack — not vulnerabilities, and not clearable without a major bump.
Bug fixes
- Embed failures are no longer silent (A-03). The feature-gated neural
embedders (
bge-m3/gte-base-en-v1.5) logged nothing when the model failed, returning an empty vector the callers silently skipped. Every failure branch now emits awarn!(the D-1 “never certify silence” invariant the repo enforces on the audit settle, quarantine flag, and purge residues). Behavior is otherwise unchanged: callers already skip the row on an empty vector, so no corrupt zero-length embedding was ever written — this closes only the missing signal, not the guard.
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 IGNOREwith a pure bi-temporal resolver (resolve_edge_insert). Re-ingesting an unchanged relation is still an idempotent no-op (no history churn); re-ingesting a relation with a changed window/interval now retires the old edge version (superseded_at= the transaction-time end, old row preserved verbatim) and inserts the corrected version as the new current belief. The handoff is exact:old.superseded_at == new.created_at. - Traversal now skips superseded edges (BUG-2), matching its own doc. The
recursive walk filters edges to current beliefs: live (
superseded_at IS NULL) and the newest live version of their(from, to, relation_type)triple. This is a no-op on well-formed/legacy DBs (a lone edge has no newer live peer), so default recall/traversal output is byte-identical; it corrects the case where a backdated supersession previously returned two edges claiming the same triple at one instant. /graph/relationships/{id}/history(Admin, audited). A new read surface reconstructs the full version history of an edge triple — every version in order with its four timestamps (valid_at,invalid_at,created_at,superseded_at) + acurrentflag — given any one version id, so a superseded belief can always be recovered (supersession never deletes).- Superseded edges are hidden from graph + adjacency reads.
GET /graph/relations,entity_relations,relations_for, the UMP relation fan-out, and the graph-PPR adjacency aggregation all filter to current beliefs, so a retired edge no longer surfaces as a live relation.
Improvements
- Supersession events ride the existing hash-chained audit log
(
AuditKind::Ingest, detailcreated:<id>/superseded:<old_id>->:<new_id>) and the history-surface read is itself recorded (AuditKind::GraphRead). - Fail-closed: an inability to resolve an edge insert declines the ingest
transaction (never a silent half-write); an unresolvable history id returns
404 Relationship not found.
Security fixes
- None (no new trust boundary; the graph-label read seam posture is unchanged from v1.27.21).
Engineering record
- New lib module
graph_supersede(pureresolve_edge_insert+EdgeAction::{SameWindow, Created, Superseded}, unit-tested with a bareConnection), wired fromingest.rs; migration addssuperseded_atand swaps the write-once UNIQUE index for the plainidx_rels_bt(schema 1.27.22). - Tests: server bin 686 / 6 ignored (was 685; +1
edge_history), lib 133 (incl. 5graph_supersede), graphsuperseded_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.85over 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 (seeBENCHMARKS.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 setssuperseded_at(never destructively mutates), so reverting M1/M2 restores the old no-op write path; leftoversuperseded:audit rows are harmless evidence. Verifybrain doctorpost-install (first boot since v1.27.21 runs the idempotent migration). SeeIMPLEMENTATION_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 andforget— butPOST /ump/forget {"hard":true}(reachable at Write scope via the MCPump.forgettool) and the ingest-replace/vault sweep bypassed the fence and could erase it. Both now runrefuse_if_heldin-tx →409 legal_hold_active, all-or- nothing. - Fence-forgeability close (S2-02). A stored body containing the literal
=== BRAIN_UNTRUSTED_CONTEXT END ===(or BEGIN) would close the untrusted region early. The sharedstrip_sentinelsprimitive now removes both literals before wrapping on every seam (MCPtool_result_payload+format_response, and the plugin’s recall banner), ordered invisible-strip first so a zero-width split cannot re-heal a marker into the fence. - Backup v3 header bound as GCM AAD + KDF bounds (S2-13 / S2-14). The v2
header was not covered by the GCM tag — any header bit could be flipped
without failing authentication. v3 (same byte layout,
brain backupnow defaults tov3) binds the exact header bytes as GCM AAD, andvalidate_kdf_paramsbounds attacker-controlled Argon2id params before any allocation (m 8 MiB..1 GiB, t 1..=64, p 1..=8) so a craftedm = u32::MAXerrors (kdf_params_out_of_range) instead of OOMing.brain backupacceptsv1|v2|v3; legacy v1/v2 files keep their read paths. - Auth fail-closed (F-27 class). A single-team wildcard
read:<team>/*now grants only the sharedglobalpool, never every tenant’s named domain (a flat domain namespace means the team field can never narrow a*domain grant — naming a domain requires naming it); and a token with no roles passesrequire_dpo_roleonly when the deployment defines no roles at all, closing the single-token shape that could ride a bare admin scope. - Empty reconcile is an explicit decision (S2/N1). An empty
live_urispreviously retired every active vault source and swept its chunks, indistinguishable from a failed listing. It now 400slive_set_emptyunless the caller setsallow_empty: true; the client panel waives it only through the shared two-step confirm. - Client offline-queue integrity (N5–N8). Retry-park (a persisted counter
parks an auto-replay after 5 failures instead of refiring forever;
destructive actions always park); idempotency key normalizes the volatile
fields out so a re-enqueue collapses onto its twin; the persisted DSAR
subject hash is 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) andkept_setdrift detection vs the parent catch same-length row swaps. - Fence sentinel in the plugin (M7). The plugin resolves its bearer via the
env 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 abcsilently coerced to 5; unknown--flagwas swallowed instead of refused;brain statusprinted-1for absent counters;brain client addrendered 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 missingE(edit) key. - A hung backend can no longer spin a panel forever. The client’s shared HTTP client carries the CLI’s socket discipline (5s handshake / 15s total), so a backend that stops answering surfaces as a network error instead of an endlessly-loading panel. (The browser/wasm build keeps its own fetch timeouts.)
- The CLI’s
--jsonenvelope mode is here.query,explain,get,ingest-dir,suggest,suggest-metrics,retention,snapshot-status,connector-status,status, andevalall emit a machine-parseable{"ok":…,"cmd":…,"data":…}envelope with documented exit codes (0 ok · 1 runtime · 2 usage). - Flag parsing is honest. Boolean flags (
--dry-run,--yes,--force,--json, …) never swallow the next token, soingest-dir --dry-run ~/vaultfinally works. Unknown flags exit 2 instead of being silently swallowed,--ends flag parsing, and a bad value like--k abcexits 2 with a clear message instead of silently becoming 5.ingest-direxits non-zero when every file failed.statusrenders absent counters asn/a. brain --helpcannot drift. Help is generated from the same subcommand table the dispatcher uses — the orphanedbrain client addline is gone,brain token rotateandbrain ump …are now listed, and aflags:/exit codes:section documents the contract.brain suggestoutput also runs the same cleanup chain as recall/get.
Bug fixes
brain ingest-dir --dry-run <path>previously swallowed the path as the flag’s value and ingested nothing.--k abcsilently coerced to5; unknown--flagvalues were swallowed instead of refused.brain statusprinted-1for absent counters.brain client addrendered flush-left in help output.
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/logoutandPOST /auth/revokewrote the token to the revocation denylist best-effort and returned success regardless — an operator logging out believed the token was dead when a failed INSERT left it live for its full 15-minute shelf life (and a revoked token could be refreshed). Both now surface a denylist write failure as500 revoke_failed; success still means the token is really dead. - Purge residue deletes propagate (were
let _ =). A chunk purge deleted the tombstoned row’s relationships / vec0 embedding / evidence links / traces in silence — one failing DELETE while the rest succeeded left a partial erasure that the purge then certified complete. Every residue delete now participates in the purge transaction: a failure rolls the whole purge back instead of certifying a lie.
Security fixes
- The prompt-injection blocklist screen runs once per hit, not per
consumer. Recall constructed each
SearchResultwith raw bytes, then the PRF query-expansion extractors re-normalized each hit’s content against the blocklist per query. The screen now runs once at construction and rides as an internalblocklist_hitflag (never serialized); both extractors read the flag. Behavior-identical, one scan saved per hit per query. - Erasure hygiene warns instead of certifying silence. The DSAR/shared
purge previously swallowed a failed
PRAGMA secure_delete=ONor a failedwal_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’shardeningblock now carries a monotonicaudit_commit_failurescounter (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-livestatus line (waslet _ =on the result, orif let Okon the load). - A single-record ingest lost its last panic. The singleton UMP path
lowered a one-element batch with
.next().unwrap()behind a length guard; it is now apop()+?— no panic fallback left on the write path. - Dead “reserved” trace vocabulary removed.
trace.rsshipped an#[allow(dead_code)]update:/supersedes:/contradicts:/causes:prefix vocabulary “reserved for v1.6 Reconcile”; v1.6 shipped and closed without consuming it. The dead constants and their tests are gone — the used surface (MAX_HOPS/MAX_VISITEDtraversal caps) is unchanged.
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_contentre-routed throughraw()so the negative-feedback guardrail exercises the production construction seam. - F-54 pinned:
revoke_reports_failureproves a failing denylist write surfaces500 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 --selfcheckclean. - Honest ceilings:
audit_commit_failuresreports, 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. Seedocs/AGENTS_HISTORY.mdfor 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
fts5vocabinstance table exposes(term, doc, col, offset)— one row per occurrence — while the query referenced the pre-3.40cnt/rowidcolumns, 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
INgroups never bound → silent empty links) is fixed and regression-pinned. - Read-seam fast path:
sanitize_read_cowreturns the input borrowed — zero copies — when every transform is provably a no-op (clean rows dominate). - Search filters become
Arc(cheap clones across per-domain recall loops), and a process-localVEC0_READYflag replaces the per-query “does vec0 exist” probe. /domains/{name}/importdial 1 GiB (was capped by the global 1 MiB limit — the route’s dedicated layer now sits before the global one; every other route keeps the 1 MiB cap).
Bug fixes
/ingest/memorycould store an oversized entry or silently report “Empty content” for invalid UTF-8. Both now hard-reject: per-entry content overMAX_CONTENT→400 entry_too_large(all-or-nothing, before any write), non-UTF-8 body →400 invalid_utf8. Every legacy wire shape is unchanged.- Entity-mention dedup was quadratic (O(m²) containment scan per sentence); now a linear running-scan with the old result pinned as a test oracle on randomized fixtures.
- The retention read-gate used
strftime('%s', …)TEXT math; the exact same predicate now usesunixepoch(COALESCE(…))— value-identical (pinned SQL-side) and index-friendly. - Connection-tracker slot leak on ingest timeout. An
/ingest/memorythat exceeded the 60 s bound (and panics) kept its single-connection slot until the next sweep; the slot is now an RAII guard released on every exit. - Reserved index slots vacuumed:
idx_knowledge_domain,idx_knowledge_owner,idx_knowledge_title_headingadded (domain delete, DSAR subject resolution, proposal write-gate dedup);idx_tombstones_kid,idx_entities_name,idx_evidence_links_fromdropped (each a strict duplicate of a UNIQUE autoindex or newer sibling). Schema → 1.27.18.
Engineering record
- The E-1 finding, documented:
prf_df_matches_legacy_corpus_scan+prf_vocab_schema_is_occurrence_shapedfreeze 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_corpusnow asserts the stemmed vocab shapes (“microbiom”/“inflamm”) it quietly couldn’t before. - F-44 layer-order meta-test:
layer_semantics::import_route_accepts_large_bodyother_routes_still_capped_at_1mibrebuild 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_TERMSonly 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 TEXTstrftimeform (value-consistent single format); schema bumps once (the 1.27.18 migration drops three indexes on the first boot after upgrade). Seedocs/AGENTS_HISTORY.mdfor 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 INTOfile was created with umask-derived permissions and chmod’d only after success — a crash inside the window left readable plaintext. Snapshot files are now created 0600 viacreate_new(a pre-existing file at the path aborts, never overwrites) and are removed on every failure path. - Restore refuses to clobber the previous safety snapshot. Restoring over
an existing target already preserved the pre-restore state as
<db>.bak; a second restore silently failed on that file with a cryptic SQL error. It now fails-closed with a clear message before touching the disk.
Improvements
brain backupgains--format v1|v2(default v2); restore andbrain doctor --backupauto-detect both formats.- Backup refuses to run while a stale
brain.bakexists (a swapped/truncated source DB was previously enshrined as the “safety snapshot”).
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 awarn!./auth/logoutis no longer a public route: the presented access token is verified by the middleware first — an unauthenticated “logout” could only ever succeed at revoking nothing. - The multi-db domain registry is now registered-only and capped. In
BRAIN_MULTI_DB=true,pool_forNEVER opens a file for an unregistered name (previously any probeable read createdbrain-<name>.dblazily — unbounded disk fill).POST /domainsis the one creation path, bounded byBRAIN_MAX_DOMAIN_DBS(default 256; 507insufficient_storagebeyond it); every resolution read of an unknown name returns the probe-blind 404domain_unknown(indistinguishable from an empty-but-real domain). The clients-register boot seed keeps client domains resolvable if their file vanished between boots (recreated on first access, still cap-bounded). - JWT principals are domain-scoped on reads.
/searchnow authorizes against the domain it actually queries (was alwaysglobal)./get/{id}and/multi-getbind the header’sX-Brain-Domainlabel in SQL — an id can never cross domains in shim mode — re-authorize on the row’s own domain, and run the same record gate (v1.14 scopes + v1.23 roles) recall enforces; foreign rows read as 404 / are dropped, never loud. Recall federation and graph traversal drop foreign-domain targets before any search runs; shim-mode graph edges scope by their chunk’s provenance label (an unlinked edge is invisible to scoped readers). - Trust labels are closed vocabularies at the write boundary.
/ingestrejects an unknown/mixed-casememory_kind(400invalid_memory_kind— no silent fallback tofact) and aconfidenceoutside0.0..=1.0(400invalid_confidence— no silent clamping, a clamped lie hides the liar); the proposal path (/proposals) enforces the same strict kind round-trip. A JWT (agent) principal on/addmay only use the closedsourcevocabulary (ingest kinds + connector family kinds) —manual, theorigin:humanmarker, is excluded so a token-authenticated agent cannot forge human authorship. The UMP L3 operator signing key now fails closed to L2 on a group/world-readable seed file (same 0600 enforcement the other secrets get). - The per-IP rate limiter actually was not per-IP. The serve wiring never
injected the peer
SocketAddrextension, so every client shared ONE “unknown” bucket — a global rate limit in practice. The server now serves withinto_make_service_with_connect_info, buckets are keyed by remote address (production-behavior pinned by a source-inspection test), and the bounded key set (RATE_LIMIT_MAX_KEYS) evicts the oldest 25% rather than growing unbounded.
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_poolflattened ontomap_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?domainclauses in shim mode. - M2 (F-07) — per-IP rate limiting.
into_make_service_with_connect_info::<SocketAddr>; source-pin test that the wiring survives; boundedRateLimiterkey set + eviction tests. - M3 — fail-closed identity. M3.1/F-26
auth::TokenRead(NotConfigured|Active|ReadFailed) + configured-but-empty denies; M3.2/F-27role_retrieval_gateempty-permit degradation (+AND 1 = 0predicate guards for empty sets — SQLite has noIN ()); M3.3/F-28 revocation check fails closed on store errors; M3.4/F-13/auth/logoutbehind 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/addclosedsourcevocabulary for JWT principals (ADD_SOURCES_FOR_JWT;manualexcluded). - M5 (F-41) — the domain-registration cap.
MAX_DOMAIN_DBS= 256 (BRAIN_MAX_DOMAIN_DBSoverride),DomainRegistry::register(the ONE creation path) /seed_registered(boot-time, no eager pools) / registeredpool_for(refusesUnknown, never creates); clients-table boot seed;map_domain_errorseam: 400domain_invalid/ 404domain_unknown/ 507insufficient_storage/ 500 internal. Allpool_forcall sites and test helpers migrated toregister. - Contract: openapi.yaml —
/auth/logoutdescribed behind the bearer middleware;/addsourcevocabulary;/ingestmemory_kind+confidencefields + 400 codes;POST /domains507; NotFound note ondomain_unknown. Thex-api-versionstamp stays"1.21.0"(no wire-shape change; the runtime header followsCARGO_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 --selfcheckclean. 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/ownerare 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 whoseknowledge_idis 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 thewarn!. 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
/purgeand DSAR:DELETE /memory/{id},DELETE /sources/{id},/sources/reconcilesweeps,DELETE /quarantine/{id}andDELETE /domains/{name}all refuse with the same409 legal_hold_activeenvelope 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
/purgewrites — 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-fileaudit_eventssurvive, and adomain_deletedevent is appended to the surviving chain. - Strict-posture domains erase with teeth: DSAR purges and memory deletes run
PRAGMA secure_delete=ON+ awal_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 intoforget.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_eventspreserved +domain_deletedevent. - M2/M2.1/M2.2 (F-24):
secured_remanencethreaded throughrun_dsar_pool/run_dsar_subject+ the forget path;physical_purgecertificate 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.rssplit/replay rework (parked subset,queued_at,subject_hash,take_replayable),replay.rsrestored-queue row component + banner, sharedconfirm.rs::ConfirmDestructive, DSAR preview gate insubjects.rs, quarantine/system/data wipe confirms,sha2dep (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 --selfcheckclean (809 passed, UMP L3); clippy-D warnings(default, bench, otel), fmt clean,cargo auditclean (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–ENDboundary 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 soU+FEFFis removed rather than widened to a space. - The recall
snippetfield 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,/quarantinereview 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
brainCLI recall/get prints gain the same strip parity.
Security fixes
- The quarantine flag write now fails closed:
flag_if_quarantinedreturns aResult, 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/memorynow flags aRejectverdict (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):
sanitizeForBlockorder changed from strip-sentinels-first to strip-last; the\s-collapse now runs after theU+E0000–U+E007F-inclusive invisible strip soU+FEFF(which JS\streats 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_storedinsrc/gate.rs; UMP reads sanitize a clone of the row (integrity stays self-consistent); fixes the borrow-lifetime fallout of the ownedrow_ownercopy inump_ops.rs. - MCP/CLI (F-20/F-63): shared
FENCE_BEGIN/END+strip_markdown_refsstrip_control_charsin the newsrc/fence.rs;tool_result_payloadwraps results,format_response+brainprints gain parity.
- Quarantine fail-closed (F-15):
flag_if_quarantined→rusqlite::Result<bool>propagated throughhandlers/ingest.rs,handlers/procedure.rs, and themain.rs/add+/ingest/memorypaths. - Tests: server bin 627 passed / 6 ignored, lib 113 / 1 ignored, brain
12, mcp 17 (
--features bench); client 124 unchanged; plugin 142 extension tests (openclawvitest); clippy-D warnings+ fmt clean;badges.sh --selfcheckclean; UMP L3. - Honest ceilings: the fence is transport-layer data/instruction separation,
not a CaMeL/FIDES capability lattice; the restore in
main.rsrollback path drops the uncommitted tx (chunk never stored) rather than re-flagring; thesnippetstrip is a single point, not a re-run of the full screen; plugin is validated via the openclawvitestsuite +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_digestlike 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 theUNTRUSTED_*fence now run throughsanitizeForBlocklike 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 every200/201endpoint: 51 previously description-only responses carry wire-exact examples, and/auth/logoutis corrected to its real contract (204 on success, 401 when no principal is presented). - Docs: the endpoint inventory in
docs/api.mdand 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.rsDetailActionsnow passesSome(&digest)(previouslyNone), matching the queue quick-approve and batch paths. The key-accelerator quick-approve, ops panel, and offline replay still deliberately passNone(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.yamlexamples were extracted from the handler sources (BreachView, Transfer, TiaTemplate, DpaTerms, Client, LegalHoldRow, DsarResponse, DsarLedgerRow, AuditRow, capabilities, recall trace, ProposalView), not guessed; YAML validated andtest_openapi_covers_routes+authz_gates_cover_every_non_public_routere-pinned. Thex-api-version: "1.21.0"contract stamp is unchanged (the wire contract did not move; the runtimeX-Api-Versionheader followsCARGO_PKG_VERSIONas 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 auditclean (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:
/proposalsreturns the read-canonical review form + a stablecontent_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_proposalsserves the read-canonicalcontent(sanitize_read: PII redaction → markdown-ref strip → invisible-Unicode strip) alongside a stable, principal-independentreview_digestover the stripped form (PII kept out of the fingerprint so admin and non-admin readers see the same digest).approve_proposalaccepts an optionaldigest(backward-compatible:None= legacy quick-approve / offline-replay) and returns409on any drift. - Rotate (brain CLI):
token rotategenerates a fresh 32-byte hex token, atomically rewrites the token file (0600; fail-closed on group/world-readable secrets) and prints the operator-sideBRAYN/BRAIN_SERVER_AUTH_TOKENcoordination 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 storedsource(ingest kind),node_kind(memory kind),lawful_basis,regionare now selected by the vec0 + FTS retrievers, threaded through fusion, and serialized onRecallHit(allOption<String>, absent when null). The plugin renders a deterministic per-hit[src: · mk: · lb: · reg:]line inside theUNTRUSTED_...fence;brain-client.tshit/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 rotatecoordinates 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-auditorgets their own single-client dashboard (read-only, domain-scoped), andbpo-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-auditorgrant: the operatorglobalroot domain is never a valid auditor target (the min-necessary wedge cannot widen to the operator pool), and the/clientslist 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 abpo-ops(the all-clients operations read). Both seed as editable rows. - Domain-scoped client views — a
client-auditor’sGET /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 QAqa_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}/endruns the per-client termination clause: it erases (purge) or exports-and-freezes (return) the client’s active memory per its DPAretention_on_termination— a purge DPA is the common posture, and the flag--purge/--returnoverrides the policy — honors per-domain legal holds (deferred on the certificate, never purged), then archives the client + its domain (status='archived',archived_atstamped; the audit chain is never deleted). Returns aTerminationCertificate(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
Server — “Holds” (per-client legal-hold isolation)
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}/holdfreezes knowledge ids in that client’s isolation domain, never another’s — the proof + the ergonomics the v1.22 holds already promised (each domain’slegal_holdstable keys its own ids). The client’sdomainresolves 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 (certificateheld_ids) until explicitly released. Admin + audited (kind ‘client’).brain client hold add <name> <id> ... --reason Rplaces holds;brain client hold list <name>shows a client’s holds.
Engineering record
src/handlers/holds.rsextractspost_legal_hold’s body into the one sharedpost_legal_hold_for_domain(state, principal, domain, ids, reason); the/legal-holdroute (withglobal/ its?domain=) and the new/clients/{name}/holdboth compose it — no second hold implementation.src/handlers/clients.rsgainsclient_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 sharedMAX_HOLD_IDS, all-or-nothing). The authz-gate delegation scan learnspost_legal_hold_for_domain((therun_recall/ingest_oneseam). Bodyreasonis required non-blank (the sharedlegal_hold::validate);idsmust exist in the client’s domain. Routed + route-coverage + route-authz guard tables + openapi.yaml path insrc/main.rs.src/bin/brain.rsextendscmd_clientwithhold add|list.- Panic/unsafe sweep: zero
unwrap()/unsafeoutside#[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; theactive_hold_idssets 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;brainrelease build clean.
- authz + openapi audits green; clippy
- 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.0milestone and its decomposition into v1.27.1 … v1.27.10. No separate binaries were cut forv1.27.0; the first shipped code isv1.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}/dsarruns 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’sdomain+jurisdictionresolve 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_runpreviews 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 seamrun_dsar_subjectcomposes a single domain-pool DSAR into a fullDsarResponse(certificate or dry-run footprint), jurisdiction-stamped — authorizedsar_export, runrun_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 inlinePOST /dsarsubject/action validation is extracted intonormalize_dsar_subject(used by both — one trust boundary, behavior- preserving, pin testdsar_dry_run_footprint_counts_and_writes_nothingstays green).src/handlers/clients.rsgainsclient_dsar(Admin + audited) +ClientDsarRequest; it resolves the client row + its transfer mechanism (transfers::listby the client’s jurisdiction,Nonewhen none) then delegates. The certificate JSON shape is shared viacertificate_json(bothpost_dsar’s cross-pool aggregate andrun_dsar_subject’s single run build the identical contract).src/bin/brain.rsextendscmd_clientwithdsar. Routed + route-coverage + route-authz guard tables + openapi.yaml path insrc/main.rs.- Panic/unsafe sweep: zero
unwrap()/unsafeoutside#[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 +objectionright),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 atmax_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;brainrelease 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}/dpastores 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}/dpareads them back (nulluntil set). This is the evidence a client’s controller checks before authorizing the BPO. All six fields are free-text, required, and bounded (<= 2000chars; a blank field is400 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:DpaTermsstruct (sixStringfields,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(scopedWHERE name = ?UPDATE returning the affected-row count → handler 404 without a second query), anddpa_terms_of(None-preserving JSON read).Clientgains#[serde(skip_serializing_if = "Option::is_none")] dpa_termsparsed in the one row mapper;CLIENT_SELECTadds the column.src/handlers/clients.rsgainsset_client_dpa(Admin +AuditKind::Client, detaildpa_terms_set) +get_client_dpa(distinguishes unknown-client 404 from unsetnull).src/bin/brain.rsextendscmd_clientwithdpa get|set(thecmd_client_addHTTP-shape model;setrequires all six--fields). Routed + route-coverage- route-authz guard tables + openapi.yaml (
DpaTermsschema, two paths) insrc/main.rs.
- route-authz guard tables + openapi.yaml (
- Panic/unsafe sweep: zero
unwrap()/unsafeoutside#[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;brainrelease build clean. - Honest ceilings: terms are config + evidence, name-checked by a human —
not a signed contract and not enforcement;
sub_sub_processor_listis 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 /clientsnow creates + migrates the client’s isolation domain, optionally binds its law-tuned profile, and registers theclientsrow (from v1.27.1).--domaindefaults to the client name (one domain per client);--jurisdictionis required; an absent--profileruns the preset pick list;--yesskips confirm. Idempotent — re-running for an existing client is a safe no-op.
Engineering record
src/handlers/clients.rsregister_clientnow composes through a single testable seamscaffold_and_registerinsrc/clients.rs:pool_for(creates/migrates the domain, the one creation seam) →profile::bind(v1.21 seam; unknown profile fails CLOSED400 profile_not_found) →register(the v1.27.1 row write). All three steps run in onespawn_blocking; the profile bind is inside the register transaction, so a failed bind leaves neither aclientsrow nor adomain_profilesbind (atomicity). The compose short-circuits viaby_name, making the CLI re-run idempotent.src/bin/brain.rsgainsclientdispatch +cmd_client_add(thecmd_umpmodel; preset pick reuses thecmd_setuplist/probe), wired intomain+print_usage.- Panic/unsafe sweep: zero
unwrap()/unsafeoutside#[cfg(test)]in the new code; no new tables or schema bump; no/clientsDELETE (termination is a later release’send, 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;brainrelease build clean. The CLI itself is thin (HTTP call); its shape is pinned byparse_flags/postalready 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_forstill 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 thetransfersregister it mirrors.name+domainreuse the existing path-safe domain validator;jurisdictionreuses the cross-border code gate (the same400 jurisdiction_invalidas DSAR / transfers). Duplicatename→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::Clientadded (exhaustiveas_str). Migration adds theclientstable + domain index, schema_version →'1.27.0';SCHEMA_VERSION_V1_27_0added. 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()/unsafeoutside#[cfg(test)]; every SQL statement parameterized (INSERT OR IGNORE+ row-count check for the 409, noON CONFLICTchurn); name/domain path-safety via the shared validator; jurisdiction gate reused fromtransfers(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 /transfersrejectsexpires_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
mechanismis 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_registergains the signed/expiry ordering check (+2 assertions:expires < signedrejected,signed == expiryaccepted); the DSAR certificatemech_for_certismap(|m| m.trim().to_string()). openapi 400 description updated. Panic/unsafe sweep re-verified: zerounwrap()/unsafeoutside#[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 /transfersrows and the DPA artifact now serialize an unrecordedlawful_basisasnullrather 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_basisbecomesOption<String>— the None-vs-empty distinction survivestransfer_rowinstead ofunwrap_or_default();registerstoresb.trim().to_ascii_lowercase()(wasstr::trimonly). New regressionlawful_basis_stored_canonical_and_null_semantics_preserved(lowercase storage + NULL→null in row and DPA). Panic/unsafe sweep over the new modules: zerounwrap()/unsafeoutside#[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 /transfersnow validatessigned_at/expires_atepoch bounds in the same shared validator as the rest of the payload (previouslyexpires_atwas checked in the handler andsigned_atnot at all). Invalid negative epochs →400transfer_timestamp_invalid. - Consistent register response —
POST /transfersreturnsid(wastransfer_id) to match theGET /transfersrows and the/transfers/{id}artifact routes. Samejurisdiction_invalidcode + message as the DSAR jurisdiction gate. - OpenAPI schema drift —
/dsarnow documentsjurisdiction/mechanism(request) +jurisdiction/rights(response) and/ingestdocumentslawful_basis/purpose+ thecompliance.lawful_basis_missingflag — fields already returned since v1.25.0/v1.26.0 but absent from the contract file.
Engineering record
validate_registergains thesigned_at/expires_atbounds (+3 assertions invalidate_register_bounds_fields); deadMAX_LIMIT*10pre-clamp removed fromGET /transfers(listis the single bound);dsar_deadline_forcollapses two identical fallback branches viaand_thenondeadline_days; module-internal types tightenedpub→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 /transfersrecords a cross- border data flow (dataset,origin_jurisdiction,destination_jurisdiction,mechanism,counterparty,lawful_basis?,purpose,signed_at?,expires_at?),GET /transferslists it newest-first with exact-match filters (mechanism/jurisdiction/dataset).mechanismis 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 /dsarnow accepts ajurisdiction(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 /ingestaccepts apurposelabel (alongside the v1.25lawful_basis); both are stored on the record and surfaced on the/export+ DSAR bundle. A strict-posture domain storing a record with no documentedlawful_basisflags 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}/tiapre-fills the Schrems II Transfer Impact Assessment (transfer, destination law, destination-surveillance posture, supplementary-measures + sign-off prompts) andGET /transfers/{id}/dpapre-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+ thetransferstable in every domain DB (additive, schema → 1.26.0, guarded by the schema-contract test) +src/handlers/transfers.rs(POST/GET /transfers); validatedMECHANISMS- free-text-supported
is_jurisdiction_code(any short lowercase code, so a future law adds without a release).
- free-text-supported
- M2
JurisdictionRule— a curated, code-versioned table (JURISDICTIONS: eu/uk/us/au/sg/ca/ph → law + deadline_days + rights).dsar_deadline_foris pure (the law’s fixed days, else PH/“reasonable” → the operatorBRAIN_DSAR_WINDOW_DAYS); wired intohandlers/observe.rsfor the deadline, certificatejurisdiction/mechanismfields, and the responserightslist. - M3
IngestRequest.purpose+knowledge.lawful_basis/purposecolumns +idx_knowledge_purpose;lawful_basis_flag(strict_domain, basis)is pure and surfaced ascompliance.lawful_basis_missingon strict-posture ingests. - M4
tia_from+dpa_fields— the pre-filled, reviewed-not-rendered artifacts;SurveillancePosturetable (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 thedpa_fields_resolve_any_row_by_idregression (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.mdmaps 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 /breachopens an incident (DPO/admin role-gated, 72h PH-DPA + EU-Art-33 deadlines computed per affected jurisdiction),POST /breach/{id}/eventappends an append-only notification/assessment log,POST /breach/{id}/closecloses it, andGET /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_basisis 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.mddraws 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_CONTACTsurfaces 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.rsships the pure decision logic: theDPA_CONTROLScross-reference map +scrape_posture(scrape-family sources need a boundedlawful_basisor they quarantine) +notification_deadlines(ph NPC 72h / eu authority 72h / subject-notification, de-duplicated, fromdiscovered_at).COMPLIANCE_PH.mddocuments 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 viacan_act_on_breach, audited);AuditKind::Breach; migration adds thebreaches+breach_eventstables (schema → 1.25.0); wired into the router, the route-coverage + route-authz guard tables, and openapi.yaml. - M3 — PIA + scraping.
PIA_TEMPLATE.md;IngestRequestgainssource+lawful_basis;ingest_onequarantines a no-basis scrape via the existing flag seam. - DPO contact —
config::dpo_contact()(BRAIN_DPO_CONTACT) surfaced onhealth_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 with403 connector_not_in_profileany kind a domain’s bound profile does not grant. Ahealth-hipaadomain can registerehr-readonlybut notslack; asales-teamdomain registers anycrm-*. 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 toprivateaccess 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 syncandbrain connector-statusnow 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.rspins the shipped vocabulary (CONNECTOR_KINDS),is_connector_kind(), andfamily();src/profile.rsaddsProfile::connector_allowed()— the pure gate (connectors_allowedabsent → allow; explicit empty → deny-all, the air-gap posture; otherwise exact match or bare-family grant fora-bsub- kinds).src/handlers/connectors.rsgains thePOST /connectors/registerAdmin+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, plustranslate_*for crm/slack/issue/structured-fact) is the pure core every connector feeds; source/revision linkage and kind-scoped reconcile reuse the existingsourceslayer. 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):
kindvocabulary/unknown-reject/family;Profile::connector_allowedgating (hipaa/sales/air-gap);pipelinetranslate + source-kind + live-uri linkage (thecrm_backfill_links_source_and_revisioncontract);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 withlast_sync); its connector-status card is unchanged. Schema stays 1.23.0 — M1 adds no DDL (theconnectorstable already carriedkind 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 (
serverroot 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 —
serveralways grants all panels (incumbent-equivalent), the JWTrolesclaim grants the delegated set, and an absent token is unrestricted loopback-incumbent (today’s status quo).
Security fixes
- A
qaoragenttoken can no longer rubber-stamp an approval from the Review queue —role_allowsgates approve/reject/edit before any write.
Engineering record
- M3 —
src/role.rs+api.rs(client). A purerole_can_see(roles, panel)mapping table resolvesserver/delegated role names → panels and actions.ApiClient::roles()reads the claim set once per token: theserverrole → all panels; any non-serverrole → the JWTrolessubset the server stamped (delegated).api().roles()is hoisted once inapp()and read by both the desktop rail and mobile tab bar; the/panels/review.rsaction handlers consultcrate::role::role_allowsto gate approve/reject/edit, with approve requiringrole_can_see("dpo")unlessserver-root. Test changes: everyTokenClaimsliteral gainsroles;role.rshas 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 — “Regulated” (legal hold + retention classes + region pin)
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,
/purgeand 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_REGIONstamps 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:
/purgeand DSAR refuse it (409 legal_hold_activewith 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). Newlegal_holdstable(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 byMAX_HOLD_IDS),POST /legal-hold/{id}/release(404 on unknown / already-released),GET /legal-holds(filterable, Admin) — every action audited. Enforcement:page_decayedfilters held ids out of/decayed;purgereturns409 legal_hold_active(+ the per-id reasons) via the newHandlerError::conflict_with;run_dsar_poollocates held targets, defers (never purges) them, and lists{id, reasons}on the certificate’sheld_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.regioncolumn + anAFTER INSERTtrigger).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_fromresolver). The migration contract test now pins schema_version 1.22.0 and the route-authz audit learned theholdsmodule. Clippy-D warnings+ fmt clean. The new integration test is written idiomatically (Result<_, Box<dyn Error>>+?, no bareunwrap()— only.expect()with a message and safeunwrap_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_daysfield (expiry in days from now) alongside the absoluteexpires_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: strictprofile 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’sdomain.profileFK — domains are labels, so the binding is its own keyed row); schema_version → 1.21.0 (additive; no column changes). At ingest:pii_mode: strictmasks title+content at the write boundary via the existingscreen_source_promptmaskers ([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_scopefills only an ABSENT value;kindsis a constraint (an out-of-vocabulary effective kind → 400kind_not_allowed). Unreadable bound profile fails CLOSED (a strict-posture domain must not silently ingest raw PII). New friendlyttl_daysingest field (days-from-now →expires_at; an explicit absolute always wins). At retrieval: a bound profile’sretentionblock REPLACES the server-wide policy for that domain (explicit JSONnull= that kind never decays; an empty block = nothing decays — the smb-simple posture);/decayedjudges each row by ITS domain’s policy (the SQL superset unions kinds + the least-restrictive cutoff, so the superset property holds);audit_leveldrives/recallread-events whenBRAIN_AUDIT_READ_EVENTSis 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 viaPOST /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;nullunbinds — the back-compat escape hatch), documented inopenapi.yaml(+ theProfile/ProfileUpsertschemas, aNotFoundresponse 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):
enterprisetier — BGE-M3 embeddings (1024-d).desktoptier — 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
Embedderabstraction (src/embed.rs, new lib module). The embedding model moves behind an object-safe trait (encode/encode_one/store_dim/model_id);AppState.modelbecomesArc<dyn Embedder>; all ~13 encode call sites (recall/ingest/proposals/ procedure/suggest/embeddings/reindex) are profile-agnostic. The defaultStaticEmbedderdelegates 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_migrationstays as the 512-d wrapper so every existing caller (tests, migrate-rehearse, domain_registry) is unchanged. A newembedding_dimstamp inschema_metais 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-m3via FastEmbedTextRerank(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 reservedrerank_score/rerank_truncatedprovenance slots after fusion+PRF inperform_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 503recall 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 + legacyembeddingscleared — those f32 rows are the OLD dim and re-backfilling them would be cross-dim corruption), then re-embeds every chunk (the/reindexloop 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:385treats aNoneprincipal as superuser (the loopback back-compat posture); the symmetric gap was that a non-loopback bind with noAUTH_TOKEN/JWT configured would expose an unauthenticated superuser API. Newenforce_loopback_bind_guard(two pure predicatesbind_is_loopback/auth_configured, reusingconfig::auth_tokensAuthMode) 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, corpussearch,memory_recalltool) shared no guard, so one turn could fan out N recalls. A closure-scopedMap<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 Xcasts (no narrowing guard) are replaced by acheckedParams()helper backed by typeboxCheck(avalue is Static<S>type predicate — on schema failure params collapse to{}and existing?? defaultbranches take over, fail-closed).memory_recall.maxContextTokensschema max 32000 → 8000 to matchconfig.ts:55. Per-hitcontentis clamped toMAX_HIT_CHARS = 1000beforeformatRecallContext(caller-side, soformat.tsstays 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
untrustedmarker 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). Theapprove_proposalINSERT (L624) omitted theflaggedcolumn (default0), 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 setsflaggedfrom the verdict (Quarantine/Reject→ 1,Clean→ 0), and the audit detail carries the verdict label (proposal_approved:screen_quarantineetc.). The human’s decision stays final (mantra #3) —flaggedis provenance, NOT a recall deny; recall segregation unchanged.+2 tests. - Plugin: the
untrustedtag is now enforced, behind an unforgeable fence (plugin/src/format.ts).MEMORY_BANNERwas an advisory preamble with no closing delimiter andhit.untrustedwas carried but never read (decorative; the plugin admitted this atformat.ts:76-78). NewUNTRUSTED_BEGIN/UNTRUSTED_ENDsentinels wrap the block;sanitizeForBlockstrips any literal sentinel from hit bodies so a recalled chunk cannot forge the close.formatRecallContextnow filters tountrusted === true(drops the rest; fail-safe → empty injection if none qualify).sanitizeForBlockalso gains theU+E0000–U+E007Ftag block (the one set the prior regex omitted — requires theuflag +\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):
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_refsneutralizes the EchoLeak / CVE-2025-32711 class at the source.sanitize_readpreviously stripped invisible Unicode only;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+ themask_phone-style byte walk) rewrites→[label]and[text](url)→text. Bare URLs in prose are intentionally left intact (see example.comis not rewritten — false-positive trap). Composed intosanitize_readin 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_optinherits it via delegation. Storage stays verbatim (render-only, thestrip_invisiblestorage 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_clientis the one outbound HTTP client now used by both webhook sinks (alert.rs::sinkandhandlers/observe.rs::notify_art19). Both previously builtreqwest::Client::new(), which follows up to 10 redirects with no IP validation — so a misconfigured operatorBRAIN_*_WEBHOOK_URLthat 302s tohttp://169.254.169.254/...(cloud metadata) orhttp://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 theTcpListener302-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-hexxxh3_64fingerprint 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_hashand the storedquery_hashderive from low-entropy inputs (an SSN, a name, a short recall query) that a fast non-cryptographic fingerprint would expose.recall.rs’s tracequery_hashandotel.rs::query_hashnow delegate to the sameaudit::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). Newgate::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 onGET /chunk/{id}andPOST /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 referencedentities.knowledge_id— a column that does not exist — so the subquery raised “no such column” and silently aborted the wholeDELETE, leaving relationships (and the PII-bearing entity names they anchor) behind on every purge. The clause is removed;purge_chunk_idsnow 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 sweepsproposalsby 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). Awebhook_secret_paththat 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: thememory_storedefault-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
/decayedhas 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_invisibleclass: C0/C1 controls, zero-width marks, bidi overrides/ isolates). Now a shared lib modulesrc/strip_invisible.rs(screen.rs re-exports it, socrate::screen::*paths are untouched), applied at the MCP tool-result envelope +format_responseseam (src/bin/mcp.rs), the CLIbrain recall/brain getprints (src/bin/brain.rs), and the openclaw plugin (format.ts::sanitizeForBlocknow 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}andPOST /chunk/multi-getnow select + maskpiirows for non-admin principals,POST /searchmasks after the flagged-evidence suppression, andGET /proposalsmasks proposal content via the same read-timescan_piileg. Reveal stays a separate, audited principal leg. - G3 — auth fails closed on a leaked secret file.
AUTH_TOKEN_FILEthat exists with group/world bits (mode & 0o077 != 0) or that can’t yield tokens with noAUTH_TOKENenv fallback now refuses to start (config::auth_token_misconfigured+auth::check_secret_permissionsenforced 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 arun_dsar_poolper domain (registry.known_domains(); shim mode = exactly the oneglobalpool, 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 —
/decayedscans narrowed, not full-table (gate.rs+migration.rs): index-served superset WHERE (exactexpires_at < ?+ kind-policy branch at the least restrictive cutoff — min days — so no Rust-expired row is excluded;page_decayedstays the arbiter), served by newidx_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 issha256_hextoo. Knowledge-dedupcontent_hashstays xxh3 on purpose — that row still exists, so the hash is worthless. - Found bug —
/decayedreturned[]since v1.14. Thestrftime('%s', ...)column is TEXT, soget::<_, 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_permissionsmode ladder,auth_token_misconfiguredfail-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:%SCURRENT_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 /proposalsaccepts 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). Thelist_proposalsSELECT now carriesdecided_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. Extractedlist_proposals_page(thepage_decayed/list_dsar_pageidiom) so the projection is unit-testable with a bare&Connection— no HTTP stack, no model. - M1.2 —
sincewindow param.GET /proposals?status=&limit=gains?since=<unix ts>—WHERE status = ?1 AND created_at >= ?3when present, byte-identical legacy query when absent. Parameterized (the repo’s SQL discipline). Asincewindow still stops atLIMIT(200), so the stats fetch passeslimit=200explicitly or it samples only the 50 default. - M2 — client calibration core + strip (
client/src/panels/review.rs). PureCalibration+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_sincefetches the two windowed pages atlimit=200. A dismissable strip above the queue renders the four figures + a rubber-stamp warning (approve-rate > 0.9 over ≥ 20 decisions →warntier + “review the last by hand”); fetch-failed → renders nothing (the v1.20.0 offline posture).role="status"+aria-live="polite"(WCAG).cal_*i18n keys inenonly (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.yamldocumentsProposalView.decided_at+ thesinceparam.
- fmt clean; wasm + all 5 server binaries build clean.
- 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_ratekeys on the v1.20.3 read-timescreen_verdictrecomputation, 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). Thewarnthreshold (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 —
DsarResponsedeadline (src/handlers/observe.rs+src/config.rs). Puredsar_deadline(created_at)=created_at + dsar_window_secs();configgainsDEFAULT_DSAR_WINDOW_DAYS = 30(Art 17)BRAIN_DSAR_WINDOW_DAYSoverride (theBRAIN_PROPOSAL_TTL_SECSresolution pattern).DsarResponsegainscreated_at+deadline(computed, the client’s source of truth — theexpires_at/warn_secsdiscipline). No schema change.
- M1.2 —
GET /dsarledger list (Admin). Bounded (limitdefault 100, clamped1..=MAX_MULTI_GET), newest-first (ORDER BY id DESC), the audit pagination idiom.{ requests: [{id, subject, action, status, created_at, deadline, completed_at}], total }—deadlineis server-computed on the rows, so the client ticks against the same number the POST response carries (no client mirror of the window). Extractedlist_dsar_page(thepage_decayedidiom) 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). FetchesGET /dsar; per open row the deadline clock runs through the v1.20.15time_budget::{remaining, tier, format_remaining}core (day-scale bands:<3dwarn,<1ddanger), re-rendered by one ~30s on-load ticker. - M2.2 — Data panel: next expiries (
client). Purenext_expiriescore — sort by expiry, take 10, skip already-expired (the server excludes them anyway; the core is the boundary) — rendered withformat_remaininglabels, 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 /dsaris an Admin-only operator registry (not subject-facing; DSARs keep flowing through POST + certificate). The/decayedendpoint 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_runonPOST /dsar(src/handlers/observe.rs). TheDsarRequestgains#[serde(default)] dry_run: bool; theDsarResponsegainsfootprint(skip-if-none). The handler runs locate + bundle build, then adry_runbranch reports the footprint and drops the read-only tx — no purge, no residue sweep, no ledger row, no certificate.Footprintcarriesroots/derived/export_rows/tombstones(prior deletions for this subject, matching the purge’sowner:<subject>/derivedreasons)/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) issuesPOST /dsar {subject, action: both, dry_run: true}viaApiClient::dsar_preview, renders the counts with arole="status"“preview only — nothing deleted” note, and has no purge button (seeing and erasing stay one click apart). Pure parse coreparse_footprint+dsar_preview_bodypinned by wire tests.dsar_preview_*i18n keys inenonly.
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::RecallTracealready delegates totrace_panel; theTraceCardrenderer now reads the stored shape —query_hash(notquery, v1.20.17 M3), decision, actor,domains_searched, and the appliedscopearray — and runs every displayed string through the v1.20.3strip_invisiblerender 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 purereplay_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::evalblob seam (no new helper). Newreplay_*keys inenonly (de/fr/es/nl fall back per theops_titleconvention):replay_title“Decision replay”,replay_audit_link“open audit row”,replay_export“export evidence”.RecallTracestays 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_mapread path removed (src/handlers/gate.rs).ExportQuerydropsinclude_pii_map(a request carrying?include_pii_map=trueis simply ignored — serde drops the unknown field), thepii_mapSELECT is gone, and the/exportenvelope no longer carries apii_mapkey.export_format_versionstays 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 holdspii: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_maperases any legacy placeholder rows and the table at migration (the oldCREATE TABLE IF NOT EXISTSwas removed in the same release, so a fresh DB never recreates it). Schema version → 1.20.19 (SCHEMA_VERSION_V1_20_19); guarded bytest_migration_schema_contract+migration_drops_pii_map_and_empty_table. - M2 — configuration contract.
BRAIN_REDACT_PIIhad noconfig.rsgetter (it was a documentation-only claim); removed from all live docs.openapi.yaml/exportno longer documentsinclude_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}andGET /graph/relationswere 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=(defaultMAX_GRAPH_EDGES= 500, clamped1..=500) and runORDER 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). SharedGraphLimitquery struct +clamp_graph_limithelper; extractedentity_relations/relations_forso the LIMIT contract is unit-tested. - M2 —
find_subject_conflictsis 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 readWHERE 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 —
/decayedis paged (src/handlers/gate.rs).list_decayedreturned every expired chunk (full-table scan on the Rust-sideeffective_expiryfilter). New?limit=(defaultMAX_DECAYED= 500) +?offset=page the Rust-filtered result — the page split never lands on the “is it actually expired?” decision. Extractedpage_decayedfor 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). Thedsar_requestsside-table persisted the full exportedbundleJSON — a retained copy of the very data a DSAR just erased. Now persistsbundle_hash(xxh3 of the export body) only. Mature DSAR ledger rows are pruned on the existing read-event prune cadence:purge_stale_dsar_ledgerdeletesstatus='completed'rows older thanBRAIN_DSAR_LEDGER_DAYS(default 30). Also hardened the purge transaction’s atomicity (M5): the ledger row + certificate are committed with the erase, and the certificatesigned_atis backfilled after commit. - M2 — cross-owner export redaction (
src/handlers/gate.rs).GET /export(and/export?format=ump) gained an optionalredact_ownerquery param: any row whoseownerdoesn’t match is exported withcontentredacted to[redacted]. A sharedshould_redacthelper 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). Therecall_tracesside-table stored the rawquerytext. Now storesquery_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). Aump.rememberwhose declaredscope.ownerdoesn’t match the authenticated principal was silently dropped. It is now recorded as adeniedauth audit row via the sharedrecord_forbidden_scopehelper; 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 warningsclean.cargo fmt --checkclean.test_openapi_covers_routes+authz_gates_cover_every_non_public_route+test_migration_schema_contractgreen (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
contentonly; 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_ledgerruns on the read-event prune cadence, not a dedicated boot timer; retention is per whole-ledger, not per-subject.query_hash/bundle_hashare 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_invisiblewidened (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), andU+2066–U+2069(LRI/RLI/FSI/PDI isolates). No new codepath, no new dep, no abstraction — the existing predicate now covers the full UnicodeBidi_Controlset. Becausestrip_invisibleis 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 serializesuntrusted: trueon 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 onProposalViewvia the new puregate::proposal_deadline(created_at)—expires_at(created_at + proposal_ttl_secs(), the alert watcher’s own math),warn_secs/critical_secs(the exactALERT_WARN_SECS/ALERT_CRITICAL_SECSconstants, so client badge and server alert share one boundary). No schema change, no new route.openapi.yamldocuments 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;Expiredrows 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 purereview::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_unixdelegates 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_ordersort test, thetime_budgettier/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 exactingest_proposalpath (noveltyvec0 KNN,find_conflict,salience), runs the v1.20.3 two-layer injection screen (Reject→ 400;Quarantine→ allowed + stored, the read-timescreen_verdictbadge recomputes it), and stampsedited_at. Same stale/expiry + CAS discipline as approve/reject (v1.20.2 A3/A4): TTL check + expiry audit before the tx,BEGIN IMMEDIATEtx withstatus='pending're-check,n==0→ clean409rollback 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.7gate.editotel 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_forsignal wired through the panel +card()(an Edit button), anEditEditordialog (Escape-close, cancel, re-scored-on-save, inlinefeedbackerror),Ekeyboard mapping, and the?help table row. Awarnedited badge (edited_atset) renders on the card + detail header so a reviewer/auditor sees the content shown is not the original capture. Offline: a newQueuedAction::Edit(payload-keyed, replay via the existing offline queue). New i18n keysedit/review_key_editinen(other locales fall back via the established convention). - M3 — wire contract:
ProposalView.edited_at(server) ↔Proposal.edited_at(#[serde(default)], client);openapi.yamldocuments/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_editstrings areen-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 --envelopeoperator 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 itsdocs/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.mdlink 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 thedocs/location (../trust/→./trust/). - M3 — cross-links:
docs/product-site/index.mdlinks the blog + media kit; README Documentation table +docs/README.mddocs-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 livecurl/braincommand that proves it, plus the owned-ceilings list — andreproduce.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.mdgain the three-tier links; README version badge regenerated from the real build viascripts/badges.sh(server + client now both 1.20.12);openapi.yaml+CLIENT_ROADMAP+client/README.mdre-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 fromCargo.toml(server) +client/Cargo.toml(client), test count from an actualcargo test --features bench,migraterun (parses the “N passed” lines), UMP level from the shipped self-attested L3 (asserted every push by theump-conformanceCI job), and an SBOM-present flag from the on-disk CycloneDX JSON. Prints the badge block for the human to paste;--selfcheckverifies 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 viabadges.sh, AGENTS.md) with the verifying commands and the gates that must stay green. Documents the docs-only exception (noCargo.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 —
/proofintegrity panel: NOT built (optional, off by default). The v1.20.10 integrity signal already lives in the queue-headerBadge; 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
/proofpanel is optional and off by default. - The release checklist is a doc, not automation; a
release.shthat 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_watcherre-runs the existing full/audit/verifychain check on a cadence (BRAIN_CHAIN_CHECK_SECS, default 60s) and raises anintegrityalert on ok↔broken transitions (purechain_transitioncore: no per-tick spam, a broken boot raises instantly, a recovery raisesok)./healthgainsintegrity:{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.mdintodist/cra-kit/with aCRA_MANIFEST.jsonSHA-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 existingGET /get/{id}(chunkorigin/owner/evidence span) +GET /audit?kind=reconcile(proposal-gate trail) into a per-decisionADMT_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 —
/healthintegrityobject documented; version stamp → 1.20.10.
Changed
health_bodynow takesintegrityand emits it;AppStatecarries the watcher’sChainWatchState.
[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 theknowledgebody ofGET /exportand partitions rows into the three origin tiers (human/model/imported) with live counts, plus an All tab. Pureregister_filternarrows by owner/source/memory-kind; each row renders id · bounded excerpt · provenance badges · UTC date. - M2 — shared evidence viewer (
EvidenceModal) — one reusablerole="dialog"opened from any register row; fetches the existingGET /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 RadixDialogRoot). - Wiring —
Route::Register, rail + mobile tab + command palette (nav 13 → 14, guard test updated), i18nnav_registerinen(other locales fall back per the established convention). - Tests — client 99 passed (6 new:
register_filter,origin_group,register_excerptincl. 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_rowsyields zero rows from any non-/exportbody, so the ledger can’t be fed a mutation’s response. - Recall hits still open the existing shared drawer (
DrawerContent::Hit); the register’sEvidenceModalispubfor a future recall entry (the plan’s recall wiring was deferred — rewiring would orphan a drawer variant). highlightsandsource_promptare server proposal-only and are not rendered (the plan’s client-side claims to them were wrong;/get/{id}has no such fields).format_epochis a dependency-free UTCYYYY-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 /eventsSSE 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; SSEretryhint; Read-gated. Payloads carry ids/seq only — content and PII never leave the server (AlertKindis a fixed enum, so the wire type can’t grow arbitrary fields).- Publishing points —
verify_audit_chain(chain),ingest_proposal(pending+screenon quarantine), the v1.20.4 proposal-TTL expiry (expiry). Emitted via a tokio broadcast onAppState. - Optional outbound alert webhook (
src/alert.rs::sink+src/webhook.rs::sign_standard_signature) — whenBRAIN_ALERT_WEBHOOK_URL(+ optionalBRAIN_ALERT_WEBHOOK_SECRET) is set, each alert is enqueued and delivered with the Standard-Webhooksv1,HMAC-SHA256 signature (the same scheme as v1.20.4), 3 retries, fail-soft. - Client
/opssubscribes —region_for(kind)maps an alert to a console region (pending/screen/chain→ queue/flagged refresh,expiry→ SLA clock reset), a monotonicseqguard (should_apply) drops replays, and anaria-live="polite"line announces each alert (i18nalert_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_applyflood guard,parse_alert_eventkind+seq only).
Honest ceilings
GET /eventsis server-push over SSE; the client polls with a bounded read (a browserEventSourcecan’t carry the bearer token, sofetch+bytes_streamis 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). seqis 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_otelbuilds theSdkTracerProvider+ an OTLP HTTP exporter toBRAIN_OTEL_ENDPOINT(defaulthttp://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→screenspan, recordsverdict.recall::run_recall→recallspan (decision,graph_rescued,hits,domain,principal,query_hash).gate::ingest_proposal/approve_proposal/reject_proposal→gate.{propose,approve,reject}spans withoutcome.
main.rs:init_tracingwiresEnvFilter(its own layer — the fmt layer has nowith_env_filtermethod) + the otel layer behindBRAIN_OTEL_ENDPOINT;provider.tracer("brain-server")viaTracerProvider::tracer.- Cargo.toml:
otelfeature (tracing,tracing-subscriber/env-filter,opentelemetry,opentelemetry_sdk,opentelemetry-otlp,tracing-opentelemetry).tracing-subscriber’sregistryfeature is enabled only underotel(the OTLP layer needs it). - Tests (
screen::tests::otel_tests, cfg-gated):screen_emits_verdict_spanproves via a hand-rolled capturingLayer<Registry>that the seam emits ascreenspan with exactly[("verdict", "clean")];verdict_span_label_covers_all_verdictspins all three label mappings.
Honest ceilings
- The default build has no telemetry; an operator must rebuild with
--features otel+ run a collector (seesrc/config.rs/BRAIN_OTEL_ENDPOINT). query_hashis 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_spanlabels 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::Opsat/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 existingdecidepath), flagged & quarantined (recallinclude_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 fromcreated_at+ the mirroredDEFAULT_PROPOSAL_TTL_SECS,Noneonce past deadline),sla_tier(critical< 5 min /warn< 1 hr /ok),gate_health, andqueue_priority(expired first, then nearest-expiry, stable tie-break by id). A once-on-mount loop re-renders all countdowns from a freshnow_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
flaggedbadge and strip invisible smuggling chars at display only (raw bytes never rewritten). - M4 — wrap —
ops_*/sla_*/gate_*i18n keys inen(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_SECSmirrors the server default; an operator override ofBRAIN_PROPOSAL_TTL_SECSmakes the displayed clock drift until the server 400 (documented in the core; the server’s expiry is authoritative).Proposal.screen_verdictis not yet on the client wire type (server-side in v1.20.3), so the queue rows carrysource_promptbut 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) orCeiling 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 viaBRAIN_INJECTION_THRESHOLD_HIGH/ LOW, retrain trigger,sha256summodel-artifact hash-pin).
Fixed / Changed
ROADMAP.mdreleased-version header → 1.20.5 + released row for the docs capstone;COMPLIANCE.md+SECURITY.md+docs/deployment.mdcross-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
untrustedsegregation + 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 thev1,<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-idfeeds the existingwebhook_seenidempotency. The spec path accepts any kind — the flag is an explicit operator opt-in for their own trusted senders. /healthwebhook posture (M2).webhook.replay_secs(300),webhook.timestamp_required, andwebhook.scheme(standard-webhooks|legacy) exposed at a glance (mirrors thehardeningobject pattern).- Documentation stance for GitHub (M3, the real deliverable). GitHub’s
replay protection is
x-github-deliveryidempotency (its sender is a trusted third party), not a timestamp window — documented inSECURITY.md§webhooks,COMPLIANCE.md§webhooks, anddocs/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_SECSwindow 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’swebhook-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-classifierfeature +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/untrustedsegregation remain the always-on defense. When enabled, loads the model atBRAIN_INJECTION_CLASSIFIER+ tokenizer atBRAIN_INJECTION_TOKENIZER(Fastly-lineage BERT-tiny INT8, ~4.3 MB) once via aLazyLock, off the request path. Banding: score ≥BRAIN_INJECTION_THRESHOLD_HIGH(0.9) → HTTP 400; ≥BRAIN_INJECTION_THRESHOLD_LOW(0.7) → stored flagged; else clean. UnderAllowpolicy 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_quarantinednow 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).rejectis never persisted (the proposal path 400s on Reject at write time); the badge is recomputed deterministically at read time. /healthhardening 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_invisiblestrips 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/untrustedsegregation (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 --envelopebefore 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_promptremains PII-scanned, not semantically safe. screen_verdictis 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 asquarantine.strip_invisibleruns 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 emittedout[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 byredact_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_tenantwrapped read-tip + INSERT in aSAVEPOINT, which on an autocommit caller isBEGIN DEFERRED— two concurrent writers both read the same tip and both INSERT the sameprev_hash(chain forks). Now branches onconn.is_autocommit(): autocommit →BEGIN IMMEDIATEso the read-modify-write serializes at BEGIN; inside a caller tx (autocommit false) → keepSAVEPOINT(outer tx already holds the write lock). Mirrors the provenrecord_and_rotatepattern. Pinned byaudit_chain_survives_concurrent_autocommit_writers(two threads + Barrier +verify_chain). - A2 [M]
prune_audit_retentionre-anchor now usesTransactionBehavior::Immediate(wasunchecked_transaction), same root cause as A1. - A3 [H]
approve_proposalUPDATE lackedAND status='pending'(src/handlers/gate.rs). Two concurrent approves raced; the loser surfaced a generic 500 viaidx_knowledge_hashUNIQUE. Now CAS’s the row, checksn > 0, returns409 proposal_already_decidedotherwise, and the whole SELECT-INSERT-UPDATE promote runs inBEGIN IMMEDIATE. - A4 [H]
expire_if_staleaudit visibility depended on caller tx state.approve_proposalran 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&Connectionand was correct.
Fixed — GhostJacking-audit G1 hole on /procedure (first-pass M1)
- B1
/procedurewrite core now screens injection like its siblings (src/handlers/procedure.rs). The Shield release’s “shared write core” claim had a hole:/procedureINSERTed intoknowledgedirectly. Now mirrorsingest_one— screens root content+title AND every step (contains_suspicious_pattern), honors Reject policy → 400input_rejected, callsflag_if_quarantinedper-chunk under Quarantine (default), and skipsnext_stepKG edges for a quarantined procedure. Pinned by the model-backed#[ignore]dprocedure_screens_injection_like_its_siblings.
Fixed — PII redaction missed 16–19 digit Luhn cards (first-pass M2)
- C1
mask_phoneupper bound was 15; cards are 13–19 (src/gate.rs). A 16-digit Visa/Mastercard was flaggedpii=1but never masked → leaked verbatim viaredact_contentandscreen_source_prompt. Newmask_cardLuhn-checks 13–19 digit runs (single source of truth reusing thescan_piidetector), called from bothredact_contentandscreen_source_prompt."4111 1111 1111 1111"→[redacted:card]. Pinned byredaction_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-Foris now trusted only whenBRAIN_TRUST_PROXY=1(default: socket addr — a direct-connection attacker can’t cycle the header). TheRateLimiterHashMap is capped atRATE_LIMIT_MAX_KEYS = 10_000with LRU eviction of the oldest 25% when full (bounded memory, no new dep). Pinned byrate_limiter_caps_tracked_ips_and_evicts_oldest. - D2 [H] linker quadratic blowup on adversarial content (
src/linker.rs).extract_vocabularyis now capped atMAX_VOCAB_ENTITIES = 500(one guard at entity insertion; the O(mentions²) loops inherit the bound). Pinned byextract_vocabulary_caps_at_max_vocab_entities. - D3 [M]
/exportbuffered 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/embeddingsunbounded batch amplification (src/main.rs).inputs.len()is now capped atMAX_EMBEDDING_BATCH = 64→ 400.
Fixed — AuthZ completeness + tenant isolation
- E1 [H]
/tombstones+/dsar/{id}/certificatelacked tenant scoping (src/handlers/observe.rs). Both are Admin-gated but didn’t callaudit_scope; a team-scoped admin saw every tenant’s tombstones (reason = owner:<subject>) + certificates. Now filtered against the principal’ssubat the SQL layer (cross-tenant → empty result / 404, no existence leak); superuser (Noneprincipal) unconstrained. - E2 wiring-guard test blind to chained routes +
cap_gate— the capability gate remains exercised bycap_gate_enforces_verbs_scope_and_never_admincapability_accepted_only_on_ump_surface_with_operator_key; the contract table + comment updated.
- E3 [M]
/adddid not enforceMAX_CONTENT(src/main.rs) — now checks the same boundingest_oneuses → 400.
Fixed — Input validation + data hygiene
- F1 [M]
source_promptunbounded + not injection-screened (src/handlers/gate.rs).MAX_SOURCE_PROMPT = 2048(plugin sends ≤2000) → reject longer; screened viascreen_source_promptso a tripped prompt persists only as the[redacted:…]form (reviewer sees the warning). - F2 [L]
/health/dbwas 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_getN+1 queries (src/main.rs) — collapsed to a singleSELECT ... WHERE id IN (...)respectingMAX_MULTI_GET. - F4 [L]
/metricstenant 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 — noinitializehandshake; every modern request validates the mandatory per-request_meta(io.modelcontextprotocol/protocolVersion+io.modelcontextprotocol/clientCapabilities);server/discoverreplacesinitializefor modern clients (supportedVersions: ["2026-07-28", "2025-11-25"]); every result carriesresultType: "complete"+_meta.io.modelcontextprotocol/serverInfo;tools/list+server/discoveradvertisettlMs/cacheScopecaching hints (SEP-2549). Error surface per the new spec: missing_meta/fields → -32602, unsupported version → -32022 withdata.{supported,requested}, unknown tool → -32602, parse error → -32700 (null id), null id → -32600. Dual-era: a legacy client’sinitializeselects 2025-11-25 semantics scoped to the stdio process.pingkept 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_lineunbounded → OOM — capped atMAX_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 viaerror.message. Pinned bysanitize_echo_destroys_injection_structure+ the updatedunknown_tool_is_a_protocol_error. - G4 [I]
legacyflag 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.
/exportstreaming uses a bounded guard, not a server-sent stream (v2.x nicety);RateLimiterLRU 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 —
/ingestnow screens injection like its siblings (src/handlers/ingest.rs): the sharedingest_onecore (plain + single-UMP + batch-UMP + the plugin’smemory_store/autoCapture) mirrors/addand/ingest/memory—Rejectpolicy → HTTP 400input_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):
captureModeon the plugin (proposaldefault |direct).proposalPOSTs/ingest/proposalvia the newBrainClient.submitProposal()— nothing from an untrusted turn becomes memory until a reviewer approves.directkeeps the old behavior (still screened server-side).proposals.source_promptcolumn (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 + auditedproposal_expired; approve/reject on a stale proposal refuse with 400. source_promptround-trips through/proposals(ProposalView), the client wire type, and the Review panel’s “sourcing prompt” block.
- M3 — docs:
SECURITY.mdnames/ingestas screened + the auto-capture gate;docs/MEMGHOST_MITIGATION.mddocumentscaptureMode.
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’ssource_prompt_is_pii_screened_and_rendered). Plugin: +3 (submitProposal wire; captureMode default routes to/ingest/proposal; config default). schema_versioncontract → 1.20.1;authz_gates_cover_every_non_public_routetest_openapi_covers_routesunchanged (no new routes).
Security
- G1 closed:
/ingestno 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_storestays 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;systemresolves viaprefers-color-scheme(pick_themeextended to a tri-state overTHEME_MODES; the existing theme effect setsdata-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.shbuilds 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 theclient-gateCI 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::Queuedrather than collapsing to a generic pending state. Packageidempotency 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 bundlewasm/JSCSS sizes + FPS profiling are operator steps (no Dioxus CLI here); the plan’s <50 KB initial / <5 MB mobile budgets are tracked inBENCHMARKS.mdas measured-success criteria, the CI budget guards the dominant term (release wasm). systemtheme 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: theAuditroute now carriessince+principalquery params (Route::Audit { since, principal }), threaded intoaudit::paneland seeded into the existing client-sideAuditFiltervia a new purefilter_from_query. A reviewer can share a filtered audit view (e.g./audit?principal=alice) and it opens pre-filtered. Pure core + test; all sixRoute::Auditconstruction 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-configurationadvertises emptyauthorization_endpoint/token_endpoint. A real authorization-code + PKCE flow needs a new/auth/authorizeproxy 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.origincolumn (migration):TEXT NOT NULL DEFAULT 'imported'+idx_knowledge_originindex + idempotent backfill by source kind (manual→human,memory→model, elseimported). Write-time tagging wired into the interactive/assistant paths:/addand the propose→ approve promote setoriginfrom the resolved source kind via the puregate::origin_for_sourcehelper;/ingest/memorywritesmodel; procedures writehuman.markdown/structuredbulk imports keep the safeimporteddefault — never claim human authorship for an unknown path. - M1 —
/exportprovenance block: per-rowsource+originalready emitted; now addsexport_format_version: 2+ aprovenance_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-noticeorigin_metadatanow listsoriginalongsidesource/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: onlyredact_for_history-clean lines are written to weblocalStoragevia the existingi18n::pref_save/pref_loadseam, capped at the last 100. A line whose request body was non-JSON (line_is_secret, i.e. an opaque token-like payloadredact_for_historycannot redact) is flaggedsecretand held in-memory only — never persisted. Purepersist_historydrops secret/empty lines and caps. Thecredentials_stay_in_memorygrep 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 fromdx 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/subscribeendpoint 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 isdocument::eval-based, both unverifiable in this environment (no Android SDK / browser harness). The accessibleRefreshButtonand existing focus trap remain.
Verification
cargo test(client): 76 passed (was 74; +2line_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. Purekeyboard_help()core + i18n keys (review_help_*,ensource; other locales fall back viaresolve). The?mapping respects the existing WCAG 2.1.4 shortcuts-off toggle.- Client CI gate (M2). New
client-gatejob in.github/workflows/ci.yml:cargo fmt --check+cargo clippy --all-targets -- -D warnings+cargo test+ thewasm32-unknown-unknownbuild. 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 inclient/a11y-checklist.md. - Native screen-reader pass (M1.7) — the human gate; tracked as the
existing
client/a11y-checklist.mdmatrix (VoiceOver/NVDA/TalkBack), an operator step.
Verification
cargo test(client): 74 passed (was 73; +1question_mark_opens_help_and_table_covers_all_keys). Clippy-D warnings- fmt clean; wasm build clean.
ci.ymlparses (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.Commandis 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_loadseam (non-secret label list, last 8, dedup + cap). - Keyboard:
↑/↓navigate the flattened list (group headers are labels, not items),Enterruns,Esccloses,/re-focuses the input,Tab/Shift+Tabcycle via the existing hand-rolledfocus_trap. - Destructive confirm: selecting a destructive
Runaction (Reindex —destructive_action) swaps the list to a single “Press Enter to confirm”aria-liverow;Escaborts. - Screen-reader labels on every row (
aria-label=command_label). - M1.5 single source of truth:
palette_commands+ thepalette_navigate_covers_every_non_detail_routeguard ensure every non-detail route is reachable. TheLookup/Runrow 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+conformanceL2/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/proposecounts) + 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_alertscore + 3 tests (empty case, severity ordering, only-nonzero-sources).
- Status row (≤4 cards): Health (conn dot + status/version), Snapshot
integrity (
- api.rs: 6 new
ApiClientmethods (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/;Connectmoved 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 (viaNavLink/TabLink) and to the palette. - i18n: new Overview + palette keys in all five locales
(
en/de/fr/es/nl), locale-awareformat_numberon 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_commandsreplaced by the groupedpalette_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/Runcommand 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 /purgeby comma/space/newline-separated ids or an owner), portable export (GET /exportas JSON / UMP / UMP-Markdown via the existingdocument::evaldownload seam), a per-kind retention editor (GET /retention→retention_to_editssorted overrides; set a kind+days override, one-click×clear per kind), the/decayedreview list, and the/tombstonesdeletion-registry. Status region isrole="status" aria-live="polite". - M6 — UMP panel (
src/panels/ump.rs): the v1.17.3 wire surface — capabilities card (UmpCapabilities+ pureump_integrity_badgebadge/label from theconformanceline),POST /ump/remember(JSON body →{ok,id}),POST /ump/recallwith kind filter +max_recallclamped to 1..100 (renders theresultsenvelope), andPOST /ump/auditload + verify-chain (ump_audit/ump_recall/ump_remember+UmpRecallResult/UmpAudittyped 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_requestrequest-line builderredact_for_historyso 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); newdata_*/ump_*/sys_*/nav_*keys in all five locales (each locale now 50 keys, en-completeness test green). api.rs:Cloneadded to the 10 typed wire structs soSignal<T>()call-syntax reads work (root cause of the call-syntax failures; consolidate.rs’sItemalready had it),post_rawmadepub, pureparse_purge_result/retention_to_edits/parse_ump_record/parse_ump_recall/ump_integrity_badge/serialize_request/redact_for_historycores + 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):
letstatements as direct rsx children ofif letbodies (hoisted all signal reads + label computations beforersx!);t()/placeholders with literal braces inside rsx format strings (hoisted to locals, simplifiedr#"{"query":...}"#placeholders to plain strings);Signal<T>()call syntax needsT: Clone;onkeydowncomparesKey::Enternot"Enter"; namedmove |_|closures can’t coerce toListenerCallback(wrapped asmove |_| 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 viaGET /graph/entity/{name}→ typedEntityView(traits + relations withfrom/to/relation_type); a traverse card issuingGET /graph/traverse?start=&depth=&kind=&at=&cross_domain=true→ typedTraverseResponsewithpaths(structured hop chains rendered by the purerender_pathcore,A --relation--> B --relation--> C) and the flattraversalrows collapsed in a<details>table.kindfilter validated by the purekind_is_valid(exact orprefix:-style, matching the v1.7 server contract);parse_entitycore + tests. - M4 — Create workspace (
src/panels/create.rshub →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 viaparse_ingest_result/IngestOutcome(Created / Duplicate / Error). - Procedures (
procedures.rs): a step builder (title/body/optional is-decision, add-step list) →POST /procedure→ typedProcedureResponse; lists ordered steps via/procedure/{id}/steps→Vec<StepView>; plus the two deterministic helpers:POST /classify(typedClassifyResponse→ category + confidence + matched keywords) andPOST /decision/{id}/evaluate(typedDecisionOutcome, vars parsed by the pureparse_decision_varscore — lenient, non-numeric dropped). - Consolidate (
consolidate.rs):POST /consolidate/propose→ typedConsolidateProposal; unresolved contradictions + near-duplicates rendered as list items; one-clickPOST /consolidate/apply(supersedes link) andPOST /consolidate/undo, both refresh the proposal list.
- Ingest (
- Routes/nav/i18n:
Route::Graph{}at/graphandRoute::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 ApiClientmethods + pure cores (render_path,kind_is_valid,parse_entity,parse_ingest_result,parse_decision_vars) + wire-contract tests.
Fixed (client)
- The palette’s
render_pathcore emitted a doubled--separator between hop chains (A --e--> B -- --c--> C) — one--was pushed twice; the separator is now emitted exactly once, pinningrender_path_renders_faithful_chainstoA --employs--> 2 --ceo_of--> carol. - The Create hub’s three panels render under ONE focusable
<h1>(the hub owns thePageTitle; the nested panels drop theirs) — no duplicate-h1 a11y regression. - Dioxus rsx hazards fixed during the build pass: inline
ifin rsx can’t hold a nestedrsx!(switched the ingest tab body to amatchontab().as_str());#[component]fn can’t be called positionally as a plain fn in braces (thetab_btnhelper is a plainfnnow); 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
pathsintermediate 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/Runcommand 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 evalnever 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 evalwas dead on arrival — every run returned 405.run_evalsentGET /recall?query=…&k=10, but/recallis a POST-only JSON route ({query, limit}); the v1.17.1 M3 ship gate andBENCH_RECALL_FLOORcould never have computed a score. Now POSTs the correct body on/recalland keepsGET /search?q=…&k=10on the search leg (src/bin/brain.rs). - Fixed: judged-index mapping was hash-order arbitrary.
results_to_doc_indicesmapped result content → DOCS index through aHashSet, 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:
/recallresponse parsing — the parser only read theresultswrapper (/searchshape) while/recallreturnshits; both shapes now parse (pinned by a new brain-bin test). - CI (round-21 gaps): two new jobs —
ump-conformanceboots a scratch keyed instance and asserts the reference suite’sUMP 1.0 / L3badge line (the runner exits 0 for any level ≥ L1, so the gate checks the text);recall-gateseeds the frozen 10-doc corpus and enforces--floor r5=0.85 --floor r10=0.85 --floor mrr=0.85withpipefail. - SBOM: the tag release workflow now generates a CycloneDX SBOM via the
existing
scripts/sbom.sh(cargo-cyclonedx from Cargo.lock) and ships it indist/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 stayPENDINGper 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_ed25519used a 33-byte bare-0xedmulticodec prefix; the referencedidKeyFromPublicKeyprefixes the two-byte0xed 0x01varint (34 bytes), andpublicKeyFromDidKeyrejects anything else. Old outputdid:key:z2De…; correct formdid: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 minusintegrityonly (idstays inside), computed with the reference’s JS-flavor canonicalization (integral floats serialize as1, not1.0; U+2028/U+2029 escaped) so the referenceverify()byte- matches; the signature is Ed25519 over BLAKE3 of thecontent_hashSTRING.verify_recorddual-reads the legacy v1.17.3 shape. Fix found by the live reference run: the emitted signature initially carried bare base64 — the referenceverifyHashrequires theed25519:prefix (/^ed25519:(.+)$/), soL3.signedfailed until the emit gained the prefix (verify accepts both forms). Pinned by assertions inemit_record_signed_and_verified_with_ operator_key+ump_suite_parity_l1_to_l3. from_umpversion gate lenient — op requests carry noumpfield (the suite sends none); absent now defaults to1.0(only an explicit unknown major is rejected).provenance+consentcarried — stored inUmpMeta, re-emitted on every record (the suite’s remember includesprovenance; it previously round-tripped nowhere).superseded_byon the prior record —GET /ump/memory/{id}and/ump/recallnow resolvesupersedesevidence links and emit the successor’s content-addressed urn; the revised record drops the carriedoriginso its own id resolves to a fresh urn (L2 bi-temporal: prior hastime.valid_to+ a non-emptysuperseded_bypointing at the revision).- id resolution by urn —
/ump/memory/{id},/ump/revise,/ump/forget,/ump/feedbackaccept the content-addressedurn:ump:…form (resolved via theump_idcolumn, whichKNOWLEDGE_ROW_COLSnow loads; it was previously missing so ids fell back to the xxh3-shapedurn:ump:<content_hash>form and urn lookups 404’d). /ump/feedback→{ok: true}(the suite asserts it);sessionaccepted and persisted; unknown ids 404./ump/forgetreportserasedfor the hard path,tombstonedfor the soft path.- Ops — the launchd plist gains
BRAIN_UMP_KEY_DIR; wiki + keygen docs use the correctdid:keyform;COMPLIANCE.mdcites 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;--ignoredsuite-parity test green. clippy-D warnings+ fmt clean.- External reference run (live):
@universalmemoryprotocol/core1.0.0ump-conformanceagainst a throwaway keyed instance (fresh DB + operator key +AUTH_TOKEN): 13/13 checks,UMP 1.0 / L3— L1 capabilities (ump 1.0, 5 kinds), remembercreated, get, recall (urn id +signals), L2 revise + bi-temporalvalid_to+ superseded, forgettombstoned, validation 400invalid_record, L3 discovery, signed (referenceverify()byte-matches + Ed25519 verifies), feedback{ok:true}, capability tokens (no-token 401, token 200), subscribe SSE. Reruns against a persistent DB reportmergedon L1.remember by design (content dedup) — the suite assumes a fresh store, same as the referenceump-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) — newsrc/handlers/ump_ops.rs(the codec stays inump.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 — declaredscope.ownermust 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 sharedrun_recallcore — 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:falsesoft-flags,hard:truetakes the v1.14purge_chunk_idserase path, both tombstoned + audited);POST /ump/feedback(outcomefollowed|overridden|ignored|contradicted→ the suggest-feedback last-wins upsert with the granularump_outcomepersisted);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 overlist_audit+verify_chain,capabilities.audit: true). Batch ingest —POST /ingest?format=umpaccepts 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.rsmirrors 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 viaBRAIN_TOKEN_FILE/BRAIN_TOKEN). - M4 — File binding (
*.ump.md/*.ump.json, spec §4.3) —GET /export?format=ump-mdrenders the portable export as the §6.3 markdown projection (front-matterump/id/kind/scope/time/provenance+ body; parse via thevault.rsparsers, round-trip lossless);POST /ingest?format=ump-mdparses the same projection back through the shared lowering.brain ump export|importCLI carries both wire forms with--output/--inputfile paths. Fix: the v1.17.1/exportdrop on DBs with emptyknowledge(a fatal row-mapping bug) —observed_secsis nowpub(crate)andknowledge_row_to_jsonreadsOption<String>timestamps; pinned byexport_mapping_survives_real_timestamp_rows. - M5 — Identity + capability tokens (spec §5) — new pure lib module
src/ump_integrity.rs(#![deny(unsafe_code)], thebrain_server::evalprecedent):did_key_from_ed25519(multicodec0xed+ base58btc →did:key:z6Mk…), RFC 8785 JCS canonicalization (BTreeMap), blake3 → base32 content hashes, ed25519-dalek sign/verify (§2.8integritysignatures), 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 toBRAIN_UMP_KEY_DIR(default~/.config/brain-server/ump/operator.key, 0600, refuses overwrite) and prints the DID. Enforcement: a capability token presented asAuthorization: Beareron/ump/*+/exportis verified (key, signature, expiry) at the auth middleware, then verbs × scope are enforced per handler (cap_gateafterauthorize— reads needread, writeswriteorderive, export pathsexport; scope must be absent/empty orglobal;audit/audit/verifydeny 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 inAPI_CONTRACT.md+SECURITY.md. - Docs —
API_CONTRACT.mdgains a §UMP binding (levels, routes, tokens, redact semantics, §5.3 note);COMPLIANCE.mdmaps the UMP integrity + consent controls;SECURITY.mdcovers UMP key storage (same 0600/0700 posture asBRAIN_JWT_KEY_DIR) + injection-resistant rehydration;openapi.yaml→ 1.17.3 (10/ump/*routes + 2 well-known docs + batch/ump-mdformatvalues +UmpRecord/UmpCapabilities/UmpRecallResponse/UmpFeedbackRequest/UmpBatchRequest/Integrityschemas). 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/subscribeis 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:keyemission 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 inventeddeclarativemapping is gone;decisionlowers tosemantic); ids are content-addressed per §6.2 (urn:ump:<content_hash>, fallbackurn:ump:brain:<domain>:<id>for hashless legacy rows);time.*is RFC 3339 (§2.3 REQUIRED string form, round-tripped from brain naive-UTC); top-levelrelationsuse the §2.5{type, target}shape (about= from-entity, typed link = to-entity) while the lossless graph stays inbody.structured; and §8 is honored — import rejects an unknownumpmajor 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 evalruns 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 —
/ingestnow seedsownerfrom the principal consistently (gate::principal_to_ownerispuband 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:730days, overridable viaBRAIN_RETENTION_KIND_DAYS) enforced at query time inpush_gate_filters(per-kindexpires_atdisjunction), never by a sweeper./decayednow reportseffective_expiry/memory_kind/reason(per_chunkvskind_policy). Additiveretention_policytable; schema stamp 1.17.1.
- audited): kind-default expiry (
- M3 recall ship-gate CLI —
brain evalruns the frozen 32-query fixture (tests/fixtures/eval_queries.md) against/recalland asserts floors (--floor r5=0.85 …orBENCH_RECALL_FLOOR);brain benchgains the same floor gate.brain_server::evalmetric fns shared by both. - M4 UMP wire adapter —
GET /export?format=umpre-renders the portable export as UMP records with a name-based per-chunk graph;POST /ingest?format=umplowers 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_NAMEnames 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: perVACUUM 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 fromCOMPLIANCE.md§6.4 andREADME.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, andopenapi.yaml; pinned by a unit test.- COMPLIANCE.md — §7 now references the live
/.well-known/ai-noticedisclosure (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+Homequick 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 existingrefreshsignal (re-fetch). Works on every renderer; the native pull-to-refresh gesture remains a documented v1.18.0 ceiling (needs touch events — untestable withoutdx serve). - M3.3 deep-link intent filters (
Dioxus.toml) — iOSurl_schemes = ["brain"]- an Android
VIEW/BROWSABLEintent filter for thebrain://scheme, so a custom-scheme link opens the app into the existingRoutablerouter. Full https universal-link parity is v1.19.0.
- an Android
- M3.4 offline connect pre-fill (
main.rs) — the connect screen persists the last successful base URL (non-secret UI pref via the existingi18nlocalStorage seam; the token stays in the OS keyring only) and pre-fills the URL field on a returning/offline connect. The specific/healthfailure was already shown (no crash); the field now comes pre-populated too. Pureprefill_if_emptyguard + test. - M3.1 store-readiness (
client/STORE_READINESS.mdnew) — 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 inDioxus.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/nlbundles are compiled in at build time viainclude_str!and parsed once.t()resolves current-locale →en→ the key itself (visible fallback, never blank), so a partial locale degrades to English. Alocales/<code>/main.ftlfile is added per language; RTL-ready viais_rtl.fluent/fluent-langnegare 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.
diron<html>flips tortlforar/he/fa/urlocales (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.cssswaps 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_numbergroups per locale (en→,,de/fr/es/nl→.), wired into the shell pending/flags counts. Deviates from the plan’sIntl.NumberFormat-via-document::evalbecause 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_memoryguard 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 thersx!text-node interpolation never holds a nestedt("…")call). deploy-web.shnow compiles Tailwind.dx bundledoes not recompile Tailwind in build mode (the[tailwind] inputhere isstyles/input.css, not a roottailwind.css, so dx’s auto-watch never fires) — it copies+hashes the pre-builtassets/tailwind.css, so CSS edits silently never reached the bundle (the stale-CSS class of bug Agent 50 fixed). The script now runsnpx @tailwindcss/cli -i styles/input.css -o assets/tailwind.cssfirst, per the Dioxus 0.7 docs. Verified: the fresh bundle carriesdata-theme/data-density.
Client — Tests
- 48 passed (was 43; +5 i18n tests):
resolvefallback chain, per-localegroup_digits, RTL detection, persisted-pref sanitizers, and a guard that every locale’s keys exist inen(the.ftlfiles actually load). Pure cores are signal-free so the unit tests need no Dioxus runtime.
Fixed
- Dioxus global signals exposed as accessor
fns (notstatics) — astatic Signalcan’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).
fluentis the upgrade path. frdigit 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-schemeflips correctly. - The
.ftlfiles 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
limitparameter 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-readable0644); each is a plaintext copy of the whole store. All threeVACUUM INTOsites now chmod the resulting.bakto0600. /healthnever leaks content. Extracted the response into a purehealth_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, andopenapi.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 fromdocs/README.md.
Server — Fixed
GET /tombstones?limit=was silently ignored. The query struct had nolimitfield, so the param was accepted and dropped, returning all rows. Now honored (default 100, clamped toMAX_TOMBSTONES)./exportomitted thesourcecolumn COMPLIANCE.md §7 claims it emits. Addedsourceto the export SELECT + per-row JSON (back-compat additive).- Test isolation.
v1_export_import_roundtrip_preserves_dataranrun_migration(which builds thevec0index) withoutregister_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 helperslocate_proposal/subject_ofpinned by tests. - M2 — PWA.
client/pwa/manifest.webmanifest(standalone,#0b0d10theme) +client/pwa/sw.js(offline shell: caches only/app/index.html/app/assets/*, never the API; navigation falls back to the shell).deploy-web.shships both intodist/and injects the manifest link, theme-color, and service-worker registration intoindex.html.
- M4 — Paginated audit.
GET /audit?offset=(server,OFFSETin the SQL) + a client Load-more button with a boundary-id dedup guard. The serverrecent_tenantnow 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_labelpinned 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_commitpinned by a test.
Client — Hardened
- M7.3 — Drawer focus trap. Tab / Shift+Tab now cycle focus inside the
dialog (hand-rolled
document::eval; thedx components add dialogroute 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 warningsclean,cargo fmt --checkclean,cargo build --target wasm32-unknown-unknownclean. - Server: 436 lib + audit/integration green (
cargo test --features bench,migrate); the only server change is the additiveoffsetparam on/audit. - Live
/app: 200;/app/manifest.webmanifest+/app/sw.js200; 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 dialogcan’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 JWTsubclaim;derive_principal()distinguishes opaque loopback tokens (None) from JWT-shaped ones. - Principal display (M2) — the top bar shows
acting as <sub>for JWT tokens,loopbackfor opaque ones (replaces the hardcodedremote-userplaceholder in Connect). The Intent-Based-Auditing identity pillar. - Refresh-on-401 (M3) + pre-emptive refresh (M5.1) — a
request_with_refreshwrapper silently refreshes once on 401 and retries the original request;needs_refresh()refreshes proactively when the access token’sexpis 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 mintor an IdP). - Revocation-aware errors (M6) —
error_message()mapsrefresh_reuse_ detected→ “session revoked”, 401 → “session may have expired” with a reconnect path.
Fixed
request()no longer holds theRwLockguard across an await (clippyawait_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/authorizeor 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 +inertattribute) — 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 (keyring3.6.3:apple-native→ Keychain,windows-native→ Credential Manager,sync-secret-service→ Secret Service; Android Keystore viaandroid-native-keyring-storeis the documenteddx-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); ause_resourceon launch silently probes/healthwith 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, sameRoutabletargets → identical a11y nav); pure@media (min/max-width: 640px)swaps them with no viewport JS..tab-linkenforces ≥44px touch targets (iOS HIG / Material)..tab-barand the drawer consumeenv(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 --checkclean, desktop +wasm32-unknown-unknownbuilds clean, Tailwind v4.3.3 compilesstyles/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 —
AppShellmoved from a top nav rail to a fixed left sidebar (brand mark + groupednav-linkpills 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 acard. No layout semantics changed — every nav target stays a real<Link>, every action a real<button>(theinteractive_elements_are_buttonsgate 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 huesok/warn/danger/info/neutralkept 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, andcargo fmt --checkstay green (31 tests). deploy-web.shstale-CSS fix — the script’sls | head -1glob picked the alphabetically-first (stale) hashedtailwind-*.cssintarget/between rebuilds, so a restyle could deploy the old stylesheet while index.html pointed at the new one. Nowls -t | head -1picks 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.tomlgainsbase_path = "app"so asset URLs are/app/assets/…(not/assets/…, which 401’d against the API CSP/auth);client/README.mddocuments the dev/serve/deploy workflow;package.json+tailwind.cssbuild tooling added. - Client CSP blocked WASM instantiation (
'unsafe-eval'live fix) — the wasm-bindgen glue callsnew Function()for module instantiation;'wasm-unsafe-eval'alone permits WASM compile/instantiate but not JSeval(), so the/appbundle threw “call to Function() blocked by CSP” and the client never rendered. Added'unsafe-eval'toCLIENT_CSPscript-src (API CSP staysdefault-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.shstale-asset race — the script globbedtarget/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-*.csslink → copy toclient/dist(what the server serves at/app). Concrete filenames instead of globs.
Security
- API CSP stays strict (
default-src 'none'); only the/appstatic 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)atconfig::client_dir()(envBRAIN_CLIENT_DIR, defaultclient/dist) with anot_found_service(ServeFile(index.html))SPA fallback so deep-links route client-side./redirects to/app/. TheCompressionLayerbrotli-compresses the WASM bundle. API unaffected if the dir is absent. - Path-aware Content-Security-Policy —
security_headers_middlewarenow reads the request path:/app+/getCLIENT_CSP(allows'wasm-unsafe-eval'and'unsafe-eval'for the WASM runtime +connect-src 'self'), every other route gets the strictAPI_CSP. Both/appand/are in the auth-public path set in bothjwt_auth_middlewareandauth_middleware(the static bundle needs no bearer). Live fix:'unsafe-eval'was added toCLIENT_CSPafter the first/appsmoke —'wasm-unsafe-eval'alone permits WASM compile/instantiate but the wasm-bindgen glue’snew Function()is JS eval, so the bundle threw “call to Function() blocked by CSP”. The API CSP stays strict (default-src 'none'). ErrorBoundaryaround 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()mapsApiError(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_outcomepure 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 touchuse_persistent(localStorage is XSS-readable).
“Accessible” (client WCAG 2.2 AA pass)
- SPA focus management (M1) — every panel’s
<h1>is a sharedPageTitlecomponent: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 viadocument::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_buttonsgrep 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.mdrecords 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 —
dxCLI not available in this environment, sodx components add dialogand thedx bundle --platform webaxe gate can’t run. The drawer already hasrole="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.0creates a check run and the default GITHUB_TOKEN lackedchecks: write(“Resource not accessible by integration” — an infra failure, not a code one). Added the permission on the audit job + bumpedactions/checkoutv4 → v5 (Node 24, clears the Node 20 deprecation).
Fixed
/tombstonesdeletion registry under-reporting (Round 11 finding). Pre-v1.14 tombstone rows only setdeleted_at;purged_atwas NULL, and the handler read it as a non-nulli64, soflatten()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 ofdeleted_at) + handler readsOption<i64>and surfaces remaining NULLs asnull. Registry now shows the full deletion history.- Purge/DSAR cascade to
recall_traces(Round 11 finding).purge_chunk_idsnow 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 betweenrecall_tracesandaudit_events). - Retention prune sweeps orphaned traces.
prune_audit_retentionnow deletesrecall_tracesrows 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.rstests.
[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//healthheader 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_futureprobe 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). Pureprobe_state(failures, ok). - Dependency-free sleep via
document::eval+setTimeout— notokiodep (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 sharedwrites_enabledsignal derives from conn state. - Chain-verify-before-writes recovery: on a recovery 200, conn goes green
but writes stay frozen until
GET /audit/verifyreturns{"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: Ntop-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
RowOutcometracking (Pending/Done/AlreadyDone/Failed): a failed call in a batch is surfaced inline, never silently dropped.404-no-pending→AlreadyDone(success — non-idempotent contract). BatchGuardDropGuard: clearsPendingrows from the selection on cancel (DESIGN §6 cancel-safety).A/S/R/J/Kkeyboard 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/supersededtags. Monospace + tabular-nums on ids/scores. min_relevanceslider (high/medium/low) with puredrop_low_relevance— the live post-fusion tier filter.?trace=trueartifact: the recall response carries atrace_id;/recall/:trace_id(deep-linkable) fetchesGET /recall/{id}/traceand 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 viaGET /dsar/{id}/certificate, not the cert-time head). TypedDsarCertificate::from_value. - Deferred: the DESIGN §4.3 expandable locate tree (subject roots →
derived_fromdescendants, PII masked as[redacted:…]withoutpii:read) is NOT in this release — the currentPOST /dsarresponse carries no located records, so it needs a server wire change. Tracked inCLIENT_ROADMAP.mdunder v1.17.0. - Trace toggle read-control fix: the Recall
?trace=truecheckbox is a read control but was gated onwrites_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=authfiltered tostatus == "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) + purefilter_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.cssfiles, disabling the genericvscode-css-language-serverthat 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_valuetyped card fields.
Honest ceilings (carried forward)
- Connection is web-first. The
onfocus/visibilitychangeinstant-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=onGET /auditis 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/exportserver route. dx serveis 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-getemit a read event into the existing append-only SHA-256 hash chain (newAuditKind::Recall/Search/Get;record/record_tenantnow 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 /recallacceptstrace: trueand returns thetrace_id(the audit row id;recall_tracesside 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 (ownerrows + transitivederived_fromdescendants, bounded depth 8) → export bundle (portable JSON) → purge in one transaction (knowledge + vec0 + relationships + evidence_links + proposals refs) → tombstone (reasonowner:<subject>/derived,origin_idfor derived) → audit → deletion certificate{subject, action, found_count, purged_ids, tombstone_root, certified_at, chain_head}→ ledger row indsar_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 livechain_verifiesrecomputation 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/purgeand 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.tombstonesgainsreason 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,/decayedunchanged except tombstone rows now also carryreason='explicit'.- OpenAPI:
/recallgainstrace/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 noknowledgerow. 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>callsresolve_supersessionin the same tx (old fact expires atomically).POST /proposals/{id}/rejectcreates nothing.GET /proposalslists the queue. Newproposalstable (append-only review ledger, audited viaAuditKind::Ingest/Reconcile). - M2 — Decay + GDPR lifecycle. Per-chunk
expires_atwith strict<query-time filtering (default excludes decayed chunks;?include_decayed=truereturns them taggeddecayed). Nothing decays autonomously.GET /decayedis the operator review list.GET /exportis portable JSON (live rows + graph + proposals ledger;pii_mapexcluded by default).POST /purgeis a hard, explicit, audited delete across knowledge + vec0 + relationships + proposals references in one tx, leaving a tombstone +/auditevent, by id list or owner anchor. Newtombstonescolumns (content_hash,purged_at). - M3 — Confidence + stated-vs-inferred + relevance tier.
confidence(deterministic, stored-rule factors: source authority + conflict presence + assertion) andassertion_kind(stated/observed/inferred) surface on every chunk and everyRecallHit;derived_fromchunks readinferred.min_relevance(high/medium) filters low-tier hits at query time. - M4 — Access scope, owner, PII. Record-level
access_scope(private/domain/team/public; defaultprivate= 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 apiiflag at ingest; recall redacts output to[redacted:email]/[redacted:phone]unless the principal is loopback orAdmin. Opt-in write-time placeholder mode (BRAIN_REDACT_PII=1) stores[pii:email]inknowledge.contentwith the real value only inpii_map;pii:readresolves it,/exportexcludes 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 thepii_maptable is dropped.) - M5 —
episodicmemory_kind +?memory_kind=filter (legacy rows defaultfact), wired through the sharedpush_gate_filtersSQL 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(viaBRAIN_SECURITY_CONTACT; omitted when unset),Expires(now + 1 year, never stale),Preferred-Languages, andCanonical(whenBRAIN_PUBLIC_BASE_URLis set). Procurement + EU Cyber Resilience Act look for this before features.scripts/sbom.sh— generates a CycloneDX SBOM per release viacargo-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/memorydrops entries matching aBRAIN_INGEST_SKIP_PATTERNSprefix (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);
/metricsand/healthnow 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, perps), so Prometheus consumers of the capacity story were misled and the 320 MiB envelope was invisible in metrics. It now calls the sameprocess_rss_mib()used by the/healthcapacity envelope (main.rs), so/metricsand/healthagree on the same number. - Added
process_rss_mib_reports_plausible_process_footprintregression 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
sourcevalues 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 /recallnow honors and validates a query-string?source=, matchingGET /search. Previously the handler readsourcefrom the JSON body only (noQuery<>extractor), so?source=was silently ignored —?source=webreturned 200 unfiltered instead of 422, and a caller could get unfiltered results thinking they had filtered. Bodysourcestill 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 sharedresolve_source_filterparser (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
sourcevalue 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_searchedis 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
sourceretrieval filter is fixed for every documented value.POST /recalland legacyGET /searchnow honorsourceas documented: ingest kinds (memory|markdown|structured|manual|vault) filter in SQL before ranking; retrieval legs (vector|fts|graph) filter post-fusion on theSearchSourcetag;bothis 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, andbothis 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/memoryreturns real chunk ids. The response used to lie:entry_idwas the count of entries added, not a chunk id. It now reportschunk_id(first real inserted rowid,nullwhen nothing added),chunk_ids(all inserted rowids),entries_added, andduplicates_skipped.entry_idis kept as a deprecated alias ofchunk_id(src/main.rs). - P2 —
domains_searchedis present on every/recallresponse (empty array when no hits), no longer gated onprovenance. 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 repairedsourcecontract.
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.
/recallacceptsexplainas an alias forprovenance; graph traverse acceptsname/entityaliases forstart— no more per-endpoint spelling quirks.
Engineering record
Hardening pass (post-1.13.1 review).
PRAGMA busy_timeout=5000on every pool init (src/main.rsmain pool,src/domain_registry.rsopen_with_migration,src/migration.rspragma batch). Previously onlyauth/revocation.rsset a busy timeout, so concurrent writers againstPOOL_MAX_SIZE=20connections could fail immediately withSQLITE_BUSYinstead of waiting. Write contention now queues up to 5 s.POST /recallacceptsexplainas an alias forprovenance(src/handlers/recall.rs).GET /searchhad always gated telemetry onexplain;/recallusedprovenance, so the same intent needed two flag names depending on the endpoint. Both spellings now work on/recall.GET /graph/traverseacceptsname/entityas aliases forstart(src/main.rsTraverseQuery). Docs canon isstart(openapi.yaml, README), but the response field isentityand sibling routes usename/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
globalrescue leg (the real working-memory corpus). An un-routed query (belowDOMAIN_CONFIDENCE_THRESHOLD) scopes toglobaland never federates into a bulk domain — so a 90%-of-rows domain can no longer swamp working-memory queries. Pure helpershim_routing_targets(). - Kill switch
BRAIN_RECALL_ROUTING_ENABLED(default on). Set tofalseto 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
gutmindsynergyrows (domains_searched: ['global','gutmindsynergy']); working-memory queries stay inglobal; 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-slimruntime, non-rootbrainuser,read_onlyrootfs + tmpfs,cap_drop: ALL,no-new-privileges,/healthhealthcheck. 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 viaHF_COMMITbuild arg for reproducibility. Loopback-safe default preserved (BIND_HOST=127.0.0.1;BIND_PUBLIC=1required for public binding).docker-compose.yml—brain-serverservice (loopback-published127.0.0.1:8765,./datavolume for DB/keys/token, healthcheck, read-only + hardened) and anoauth2-proxyservice behind thessoprofile (OIDC, Entra/Okta/Keycloak/Auth0-ready).docker compose up -d= pilot online in minutes;docker compose --profile sso up -dadds the SSO edge.docs/docker.md— image facts, build, run, compose, web-client mount, container backup/restore via the in-imagebrainCLI.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): newread_domain_vectorsreadsvec_knowledge(matchingfind_near_duplicates) joined toknowledgewithvalid_to IS NULL(superseded chunks excluded), dequantized viadecode_embedding.recompute_centroiduses it. The old code read the frozenembeddingstable, 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 toglobalwith no confident match. Zero extra embedding work; deterministic (sameroute()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:tomay not beglobal; drainingglobalrequires?confirm=global(typo-replay); every id must exist; bounded byMAX_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_COUNTknob (default 1 — a no-op unless raised) suppresses sub-N domains.brain domains-recomputeCLI. - Deployment runbook (order matters): deploy → run
domains-recomputeimmediately →domain-movekeyword passes → verifydomains_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/refreshcheck-then-act race fixed (src/auth/revocation.rs):record_refresh_use+rotate_chainran as two separate steps, so two concurrent presentations of the SAME refresh token could both readcurrent_jti == presented, both pass, and both mint — silently defeating reuse detection. Newrecord_and_rotateruns the check + rotation underBEGIN 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 byconcurrent_refresh_serializes_exactly_one_winner(removing theBEGIN IMMEDIATEmakes 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_initFFI unchanged. - CI
cargo auditjob 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 toSECURITY.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.tomlcreated.
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(allX-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)
- Read:
- Two actions upgraded to the matrix:
POST /reindexandDELETE /memory/{id}were Write; §3.3 puts both on the Admin surface. /audittenant scoping: newhandlers::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 (Noneprincipal, 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 (mirrorstest_openapi_covers_routes) whose source-scan asserts every handler body callsauthorize()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 (newtowerdev-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)
Noneprincipal = 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=trueon/graph/traversegates 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 byrelation_type; the weighted sums feedbuild_graphunchanged);SparseGraph::dampen_hubs(θ)— per-source-nodew_ij · min(1, θ/deg(i)), θ = 50, applied to the reachable-bounded graph before PPR. Both deterministic, bounded by the existingMAX_VISITED/MAX_PPR_ITERcaps,#![deny(unsafe_code)].- Complexity-gated graph rescue (
src/search/mod.rs+src/handlers/recall.rs): when the calibrated estimator saysClarifyQueryand the caller did not enablegraph, one bounded graph-augmented pass runs and fuses via the shared RRF two-pass fuse; abstention is re-scoped to the final outcome (low_confidenceonly whenClarifyQueryAND zero hits). Strictly additive — the rescued path previously returned empty hits. should_attempt_graph_rescue()— pure gate (recommendation, explicitgraph, kill switch);config::brain_graph_rescue_enabled()behindBRAIN_GRAPH_RESCUE_ENABLED(default true;falserestores exact v1.11.0 abstention).RetrievalStrategy::HybridGraph+SearchTelemetry.graph_rescuedfor observability;brain querytelemetry prints it.fuse_pass_lists()— the two-pass RRF fuse extracted fromfuse_prf_passes(which is now a thin wrapper addingprf_expanded); the graph rescue reuses it without claiming PRF expansion.
Changed
recall.rsabstention_decision(recommendation, hits_empty): abstains only onClarifyQuerywith an empty final hit list (v1.5.0 contract preserved on the non-rescue path).- OpenAPI → 1.12.0 (
graph_rescuedonSearchTelemetry); README, ROADMAP, AGENTS updated.
Fixed
- Nothing regressed: the v1.11.0 unweighted graph ranked the
tagged_withcloud above semantic neighbors on mixed hubs — pinned bygraph_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.5matched to the HippoRAG 2 config default, L1 convergence at1e-6, bounded atMAX_PPR_ITER = 50), reachability pruning capped attrace::MAX_VISITED = 256, and seed→chunk expansion viarelationships.knowledge_idwith the sameflagged=0/valid_to IS NULLvisibility rules as the other retrievers.- Third RRF leg:
SearchSource::Graph,Provenance.graph_rank,SearchTelemetry.graph_ms/graph_candidates, and a 3-wayrrf_fuse(the same formula, sameRRF_K = 60). The graph leg runs concurrently on its own pooled read connection inside the existingstd::thread::scope; the disabled path pays zero latency (graph_ms = 0). - Opt-in plumbing:
graph: boolonSearchFilters,QueryDoc,RecallRequest, GET/searchSearchParams, andbrain 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=truereturnsgraph_candidates=107–112,graph_ms≈4ms; exact entity-name queries seed the graph leg and surfacesource=graph/bothhits that the vector+lexical legs miss (e.g.acme_v17c_1785593852 ceo→ thedave works at acme_v17c+acme_v17c ceo is carolpair atgraph_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_withtaxonomy noise; the graph leg still retrieves but the cleanest multi-hop paths are the syntheticdave/acme/carolbench 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.05documents the upgrade path. classifyremains a deterministic keyword router, not a learned classifier./suggeststill 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 vianext_stepedges carrying an explicitstep_index(Graphiti’s NextEpisodeEdge pattern at chunk level, reusing the v0.9.8evidence_linkstable). 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 normalizedmemory_kind. The read path runs throughMemoryKind::from_strso an unknown stored kind falls back tofact(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.
generalwith confidence 0.0 when no keyword clears the threshold. No LLM, no cloud.
- the full taxonomy.
POST /decision/{id}/evaluate— load the decision rule stored as JSON on adecision-kind chunk and evaluate it against numeric variables. First matching branch wins; otherwise the rule’sdefault_branch. Returns the outcome + citation chain. Pure rule engine (no LLM).knowledge.node_kindrepurposed as the Mem0-stylememory_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
classifymatched-keywords bug (src/procedural.rs) — the winning category was correct but its keyword list came from the wrong lexicon: the lookup used the sortedscoresslot as the LEXICON index, and aftersort_bythat slot no longer matches the category. Resolved via theCATEGORIESposition (shares LEXICON ordering). Pinned byclassify_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 theponytail:comment inrun_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’sfind_near_duplicatesJOINed the legacyembeddingsJSON table, which froze at v0.9.0 — production ingests write onlyvec_knowledge, so on the live DB the scan silently covered 2 of 8538 chunks. It now readsembedding_int8from the vec0 index and dequantizes via the (previously dead)decode_embeddinghelper. Regression test ingests two near-identical chunks through the realvec_quantize_int8path (zeroembeddingsrows) 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 fullauthorize()gate, and/suggestreturns 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 explicitcontext(what they’re working on); server embeds it via the existingStaticModel, runsvec0_knnwith an over-fetch equal tok + exclude.len(), filters out the caller-suppliedexcludeids, truncates tok, and tags every hitprovenance.reason = "anticipated". Reuses the v1.6.0valid_to IS NULLdefault 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 hashedreason, optionalsession). Validates the chunk exists (404 on typo so the metric isn’t poisoned). Tenant-scoped via the JWT principal. Thesuggest_feedbacktable IS the audit surface (append-only, hash-of-reason, tenant-scoped) — no duplicateaudit_eventsrow is written.GET /suggest/metrics— the false-positive rate (dismisses / total) over the feedback ledger, with optionalsession/sincewindow filters. This IS the roadmap exit criterion, made queryable. Tenant-scoped.BRAIN_SUGGEST_ENABLEDkill switch (defaulttrue). Whenfalse, all three routes return501 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_feedbacktable +schema_version = 1.9.0(was1.4.0; v1.5–v1.8 were light cuts with no schema change). - OpenAPI → 1.9.0: three routes +
SuggestionHit/SuggestTelemetry/SuggestMetricsschemas.test_openapi_covers_routesextended.
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.”/suggestis 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;
/recallranking is unchanged.
Verification
cargo test --features bench,migrate: 428 passed, 1 ignored (was 414 at v1.8.0; +14 = 12 pure-function tests insuggest.rs+ 2 integration tests inmain.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):/suggestreturns anticipated chunks (excluded ids correctly dropped, telemetry accurate);/suggest/feedbackrecords accept+dismiss;/suggest/metrics?session=returnsfalse_positive_rate: 0.5(1/2);BRAIN_SUGGEST_ENABLED=false→ all three routes return501while/versionstays200(kill switch proven live).
Honest ceilings (carried into v2.0)
- No semantic anticipation.
/suggestis 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/dismissis binary. Mem0’sVERY_NEGATIVEis collapsed; a future “report-as-harmful” path is v2.x.- Metrics are per-process. The query scans
suggest_feedbacklive; 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: clearsvalid_toback to NULL + removes thesupersedesevidence_link, atomically in one tx. Audited viaAuditKind::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 whoseuriis 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 viaDELETE /sources/{id}. Surfaced in/consolidate/proposeresponse +brain check-consistencyreport. - 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 existingvec_knowledgeKNN 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/undoroute +stale_sources+near_duplicatesfields onConsolidateProposal.test_openapi_covers_routesextended. - 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 viabrain 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, notsummary.” Synthesizing a new chunk is a “fabricated summary” — forbidden. The medoid IS already a chunk. - M4 cross-cluster linking (proposed
related/co_occursedges). 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_duplicatesloads 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_embeddingassumes 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, useDELETE /memory/{id}on the link row directly (or a future v1.9+ generic link-delete API). - No background worker. Operators must run
brain check-consistencyon 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 carriesrelation_typeper hop; the response includes a newpathsarray with structured hop chains[{from:{id,name}, relation, to:{id,name}}, ...]. Consuming agents can render the reasoning chain verbatim. The flattraversalarray stays for back-compat. ?kind=<relation_type>edge filter. Restricts the walk to edges whoserelation_typematches. 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+explainparams,pathsarray,edge_path+from_entityfields ontraversalrows. - 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
causesfacts. Thestate='inferred'schema reservation stays unused until an evidence-gated upgrade. - M1’s
/graph/reasonnew endpoint. Not needed —/graph/traversewithexplain=trueIS 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
pathsare 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/applyrecords asupersedeslink, the prior chunk’svalid_tois set to now in the same transaction as the link insert. The existing/recallfilter(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 viaAuditKind::Reconcile(hash only, no PII). Graphiti’s pattern, applied at chunk level. /consolidate/applyrouting on kind.supersedeslinks now callresolve_supersession(link + expire + audit); other kinds keep the plainlink_evidencepath (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-consistencyCLI +unresolved_contradictionsfield on/consolidate/propose. Surfacescontradictslinks that have no pairedsupersedesresolution — 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/applyre: 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_historytable (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-consistencyor/consolidate/proposeto find them. This is the roadmap’s “manual-first” rule, not a gap. resolve_supersessionexpires 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_contradictionsis the only consistency check. Orphan entities +derived_fromcycles deferred (lower value, would balloon the diff).- No propagation to the entities/relationships KG.
resolve_supersessionoperates 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_confidencedecision 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).RecallResponsegains adecisionfield (ok|low_confidence). When the existingHeuristicEstimator(v1.4.0) classifies the query asClarifyQuery(low overlap + low lexical density + weak gap),/recallreturns{decision: "low_confidence", hits: []}instead of shipping top-1 garbage. The consuming agent (OpenClaw) can escalate or fall back to web search. Not a magicscore < 0.3cutoff — abstention is driven by the calibrated multi-signalRecommendation, which is what the evidence-gated roadmap requires. Zero new compute:confidence+recommendationwere already computed byperform_search_with_prf. POST /verifydeterministic 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 asunsupported_claim. Bounded: claim capped atMAX_QUERY(2000 chars), output ranges capped at 100.- OpenAPI contract updated:
/verifyroute +VerifyResponseschema +decisionfield on/recall.test_openapi_covers_routesextended. - 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.rssilenced (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 evalfrom 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 +
/feedbackendpoint. Roadmap-forbidden without measured benefit. Would add asource.trustcolumn, 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
ClarifyQuerythreshold 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. /verifyis lexical only. No semantic match (paraphrase, synonym). A claim that’s semantically equivalent but lexically different will reportunsupported_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
--replacenow 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 --replaceflag 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_ofedges (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.
--replaceflag (brain ingest-dir --replace). Sweeps existing chunks before re-inserting, regenerating the knowledge graph from scratch. Server-sidereplacefield onMarkdownPayload, handler deletesvec_knowledge+knowledgerows before callingwrite_markdown_ingest. CLI flag-r/--replace. No schema change.- Orphan relationship sweep.
--replacenow deletes relationships withknowledge_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. PreventsLast Testedfrom becoming an entity or contributing “tested” to verb discovery. - Excluded-range threading into between-text analysis. Both
find_relationshipsanddiscover_verb_patternsnow 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-atesuffix) 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 indiscover_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
tested105→0 anddate76→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_ofrelationships.extract_heading_relationships()walks the markdown heading tree and createspart_ofKG 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.entitiesmade pub.brain ingest-dir --replaceflag (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:
| Layer | Technique | Research |
|---|---|---|
| Stage 1: Retrieval | Hybrid dense + lexical | vec0 KNN (sqlite-vec) + FTS5 BM25 |
| Stage 1: Fusion | Reciprocal Rank Fusion (RRF, k=60) | RRF (Cornell, 2009) — still the standard model-free fusion algorithm per 2026 production patterns |
| Stage 2: Rerank | Cross-encoder (optional) | BGE-RerankerV2M3 via fastembed — most-deployed production reranker |
| KG: Edges | Bi-temporal (valid_at/invalid_at) | Graphiti / Zep — bi-temporal KG model, SOTA for temporal facts, 82.2 benchmark |
| KG: Traversal | Typed-edge prefix vocabulary | TRACE: State-Aware Query Processing over Temporal Evidence Graphs (July 2026) |
| Packing | Budgeted submodular maximization | What 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_atcolumns. 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:
/ingestrelations now accept optional explicitvalid_at/invalid_at; when absent, the extractor populates them from the ingested content (best-effort). - Query path:
/recalland/graph/traverseaccept?at=<ISO8601>. The SQL filter isvalid_at <= ? AND (invalid_at IS NULL OR invalid_at > ?)(Graphiti-validity semantics). Distinct fromas_of(transaction-time / revision recall). - Normalization:
atis normalized inperform_search_tracedalongsidesinceso 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 thresholdDEDUP_SIMILARITY=0.85). Lazy greedy under a token knapsack (max_context_tokens, default 160 per the paper). /recall:max_context_tokensfield triggers packing;gold_answerdrives theanswer_in_contextdiagnostic (did the gold survive?). Both reported in telemetry.SearchTelemetry: gainedpacked_tokens,packing_candidates,answer_in_context.
M3 — TRACE state-aware traversal
- Typed-edge prefixes:
update:,supersedes:,contradicts:,causes:onrelation_type. The validator (RELTYPE_RE) now accepts an optionalprefix:baseform. - 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-temporalatfilter skips expired edges; the walk is hard-capped on depth + visited nodes.- Schema reservation:
knowledge.node_kind(default'event') +parent_idcolumns 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::evallib module: pure metric functions (precision@k, recall@k, MRR, NDCG,answer_in_context_rate). Hand-computed value checks pin each metric. bench evalmode: loads a judgments file (BRAIN_EVAL_JUDGMENTS), runs each query through/recall, reports the metrics. Optional ship gate viaBENCH_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
vec0tables. 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). Themultivecfeature 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_idcolumns 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.rsJSON-RPC notification id handling (wasunwrap()onOption<Value>when the request had no id — a notification),vault.rsfirst-line unwrap (wasunwrap()onOption<&str>before the guard that proves it’sSome),github_app.rsmutex poison (wasexpect()— now usesunwrap_or_else(|e| e.into_inner())for poison recovery). unsafeaudit (M2): extractedregister_sqlite_vec()— a single documented safe wrapper that replaces 10 duplicate unsafe transmute blocks acrossmain.rs,domain_registry.rs,handlers/domains.rs,audit.rs,brain_migrate_rehearse.rs. Every remainingunsafeblock 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
/healthhardening (M7): exposeshardening: { 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_lexare 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:
AuthzPolicytrait,InMemoryPolicy,AuthzError,SharedPolicy,default_policy(YAGNI until v2.1 OPA/Cedar swap — theis_authorizedfunction does the actual work). - Removed unused items:
TokenType::as_str,DEFAULT_ALG,AuthError::Revoked,op_tenant,Durationconst. authorize()now usesprincipal.tenantas 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.
jsonwebtokenpromoted from optional to required (withuse_pem+rust_cryptofeatures);rsa+rand+base64added as direct deps.openapi.yaml→ 1.2.0 with/auth/*,/.well-known/*, and theTokenPair/RefreshRequest/RevokeRequest/OidcConfig/JwkSet/Jwk/Principal/Scopeschemas.
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/prunerequires aninstall-service.shrestart 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=Strictcookie 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 priorct_eq(a manualfoldofacc | (x ^ y)) had noblack_boxbarrier, 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.subtle2.6.1 was already a transitive dep (viasha2/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 existingtest_ct_eq.
Considered and left as-is (documented best-practice judgment calls)
verify_chain’swant == gothash 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 editprev_hashto match. Wrapping it inct_eqwould be gold-plating without a real threat model — the auth path was the actual surface.record_tenant’s raw-SQLSAVEPOINTleft as-is. rusqlite 0.40.1 exposes a canonicalsavepoint_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_chainfalse-negative on migrated DBs (src/audit.rs). The v1.1.0 walk assumed at most one NULLprev_hashrow at the start of the table. After the additive migration, every pre-v1.1 row has NULLprev_hash— so on a real migrated DB the second NULL row hit the_ => return falsefallthrough and/audit/verify(plusbrain_audit_chain_okvia/metrics) reported tampering on a clean DB. The walk now treats NULLprev_hashas “no backref to verify” (advances the running link but never fails) and only fails when a v1.1 row’s storedprev_hashdisagrees with the recomputed link. Pinned byhash_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_migrationbuilds a DB with the pre-v1.1audit_eventsschema, inserts rows, runs the actualrun_migration, and verifies the chain holds across the NULL → Some boundary with realrecord()calls afterward. record_tenantnow wraps its read+INSERT in aSAVEPOINT. ABEGINwould error when called inside a caller’s existing transaction (e.g.delete_quarantine);SAVEPOINTnests cleanly. Rolling back the savepoint on audit-INSERT failure touches only the audit row, not the caller’s work. Pinned byrecord_tenant_is_safe_inside_caller_transaction./metricsno longer triggers a full chain scan on every scrape.brain_audit_chain_okis now backed by a TTL-memoized result (AUDIT_CHAIN_CACHE_TTL_SECS=60)./audit/verifyremains 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-256prev_hashover the prior row’s(ts, kind, actor, target_hash, prev_hash)tuple.GET /audit/verifywalks the chain and returns{ "ok": bool }. Tampering with any field breaks the read-side check; pinned byhash_chain_detects_tampering+hash_chain_rejects_tampered_kind.idis deliberately excluded so a renumbered restore keeps the chain intact. - Per-tenant audit scoping. New
tenant_idcolumn (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_tenantis the variant that takes a tenant; existing call sites default toglobal. - File-watch token rotation (
src/auth.rs).AUTH_TOKEN_FILEis 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 anauth_token_rotatedaudit row (target = file path; no PII). Pinned byreload_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 withVACUUM INTO <db>.snapshot-<ts>.bak, runsPRAGMA integrity_checkon the snapshot, and keeps the last 4 copies (default 6h cadence, runs once on boot)./healthnow reportsbackup: { last_backup, integrity_ok }. - Graceful shutdown drain cap + WAL checkpoint. SIGTERM/SIGINT now drains
in-flight requests under a hard
SHUTDOWN_DRAIN_SECS=30cap, then runsPRAGMA 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_mibacross two samples logserror!. Opt-in exit for supervisor restart viaBRAIN_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 (noprometheuscrate dep — the plan itself flagged the dep as risky). Exportsbrain_rss_mib,brain_pool_connections{state},brain_capacity_status,brain_audit_chain_ok. Auth-gated like other operator surfaces. GET /audit/verifyas a separate route fromGET /auditbecause the chain check is a full-table scan and shouldn’t run on every list call.
Migration
- Additive:
audit_eventsgainedtenant_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_versionstamped1.1.0.
Updated
- Cargo.toml 1.0.1 → 1.1.0.
openapi.yaml→ 1.1.0 with/audit/verify,/metrics, thetenantquery param on/audit, and thetenant_idfield on theAuditRowschema.
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_idcolumn 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 (SAVEPOINTwrap).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 breakprev_hashNULL on pre-v1.1 rows.verify_chain.**/audit/verify+/metricsfull-table scan per call./audit/verifystill scans fully (that is its job — you cannot verify a chain without walking every link); v1.1.1 added a TTL cache on the/metricspath 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
| Release | Name | Capability |
|---|---|---|
| v1.2.0 | AuthN | JWT/JWS + AuthZ layer (full plan in v1.2.0_AuthN.md) |
| v1.3.0 | Bedrock | Memory-safety: panic elimination, unsafe audit, cargo-fuzz, miri, LSAN, loom, proptests |
| v1.4.0 | Calibrate | Bi-temporal KGs + submodular packing + TRACE-style state-aware query + multi-vector |
| v1.5.0 | Epistemic | Confidence calibration + “I don’t know” + counterfactual influence + source trust + hallucination resistance |
| v1.6.0 | Reconcile | Contradiction detection + supersession + conflict policy + knowledge editing + consistency checker |
| v1.7.0 | Reason | Multi-hop reasoning + causal subgraph + counterfactual simulation + transitive inference |
| v1.8.0 | Consolidate | Sleep-time worker + near-duplicate detection + extractive summarization + cross-cluster linking |
| v1.9.0 | Anticipate | Session 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.RateLimitertrait withInMemory(default) andRedisRateLimiter(GCRA atomic Lua script,--features ratelimit-redis) impls. Per-tenant cost tracking (tokens/egress) feeding v4.0 marketplace billing. StandardX-RateLimit-*+Retry-Afterheaders.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/metricsbehind--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).
- JSON Web Token Cheat Sheet (
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 /ingestnow auto-creates entities referenced by relations but not declared in the inputentitiesarray. The canonical plan example (vitamin d3 helps inflammationwith onlyvitamin d3declared) works.entities_added/relations_addednow 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-Domainheader 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 viaDomainRegistry.domainquery param onGET /searchandGET /statsfor 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’sdetailsfield now carriesknown_domainsso an unknown-domain400is actionable.
Added — federated search (M3)
- Cross-domain RRF merge. The previous
/recallcross-domain sort used rawscore(wrong: scores aren’t comparable across domains because IDF tables and post-quantization norms differ). Replaced with rank-based RRF using the sameRRF_K = 60constant as the in-domain hybrid fusion. ?cross_domain=trueon/graph/traversewalks edges across every known domain pool, labelling each hop with its source domain.- The
/recallhandler already supported centroid routing for domain-aware recall (v0.9.1domain_router). Verified end-to-end for the v1.0 cutover: multi-domain federation with labelleddomains_searchedon the response.
Added — structured ingest (M4)
POST /ingestaccepts{ 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_ingestupdated to callPOST /ingestwith structured fields when the caller suppliesentities/relations/domain(the agent does extraction client-side, per the plan). Legacy memory-style ingest with justcontentstill routes to/ingest/memoryfor back-compat. - Fixed the validator regression. The hand-rolled
is_matchchecker ignored itspatternargument and silently rejected spaces in entity names — breaking the canonicalvitamin d3example. 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.globalis 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.dbfile viaVACUUM INTO(safe under concurrent writes).POST /domains/{name}/import— restore a snapshot into a NEW domain (target must not exist;globalprotected; 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=trueis set at startup and the legacybrain.dbhas data, the server performs a one-shotVACUUM INTOintoglobal.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 d3example), and export round-trip.
Changed
- Cargo.toml version 0.9.9 → 1.0.1.
openapi.yamlinfo version → 1.0.0; the new domain lifecycle routes are documented (thetest_openapi_covers_routestest asserts coverage).- Handlers that previously used
state.pooldirectly now resolve viahandlers::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/quantare 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>.dbfiles on disk. This is simpler and avoids a separateregistry.dbto manage, but means there’s no per-domaindim/quant/versionmetadata store. - The
globaldomain continues to read the legacybrain.dbeven in multi-db mode. The boot-time snapshot createsglobal.dbas a backup + rehearsal target, but the runtime path stays onbrain.dbforglobalso the 430-doc live DB never silently shifts under the operator. - Cross-domain
ATTACHwas not used. Per-domain pool queries + RRF merge is simpler and avoids sqlite-vec attach complications; benchmark on ARM eMMC remains an operator step (seeBENCHMARKS.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)
StorageLayoutabstraction (src/storage_layout.rs). Every on-disk path brain-server touches (legacybrain.db, futureglobal.db, per-domainbrain-<name>.db, backups, registry, connector configs) derived from one root.config::brain_db_path()delegates to it; the back-compat invariant (existingBRAIN_DB_PATHcallers see the same path) is locked by a test. NewBRAIN_DATA_ROOTenv var is the v1.0 relocation knob.- Schema-version reader (
storage_layout::schema_version+SCHEMA_VERSION_V0_9_9).run_migrationrecordsschema_versioninschema_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) + theauthoritycolumn + the recorded schema version. is_valid_domainlifted tostorage_layoutso the security-critical filename check lives in exactly one place;DomainRegistrydelegates.
Added — M2 (migration rehearsal)
brain-migrate-rehearsebinary (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). Therehearseall-in-one exits 0 only when every parity check passes.run_migrationextracted tosrc/migration.rs(lib module). Mechanical move frommain.rs; the one signature change isrun_migration(db, mmap_mib: i64)so the lib has no dep on the server-privateconfigmodule. 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) andCapacityTarget::Jetson(10k docs / 512 MiB DB / 320 MB RSS). Resolved fromBRAIN_CAPACITY_TARGET(default: jetson). Tightenable viaCAPACITY_MAX_*env vars. /healthcapacity field. Reports{target, docs, max_docs, db_mib, max_db_mib, rss_mib, max_rss_mib, status}wherestatusisok|warning|exceeded.- HTTP 507 on writes when over-capacity. Every ingest path (
/add,/ingest,/ingest/memory,/ingest/markdown) callsguard_capacity. Read routes (/search,/recall,/get) are NEVER blocked — an over-capacity brain still answers. bench --envelopeassertion mode.BENCH_ENVELOPE=desktop|jetsonturns the benchmark report into a ship gate: exits non-zero on RSS or p95 ceiling breach.
Documentation
openapi.yaml→ 0.9.9:/healthcapacity 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.toml0.9.8 → 0.9.9. Newmigratefeature +brain-migrate-rehearse[[bin]]entry.
Honest ceilings (carried into v1.0.0)
- No
BRAIN_MULTI_DB=truecutover 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; thebench --envelopemode 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 inrate_limit_middleware, or add an inter-request delay inbench. SeeBENCHMARKS.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).
knowledgegainsobserved_at,valid_from,valid_to,authority, populated bysources::stamp_evidenceon every ingest (vault = 0.8, manual = 1.0).QueryDocgainsas_of(point-in-time recall — returns the revision active at a timestamp) andevidence(include structuredEvidenceon every hit). Both retrievers apply the historicalas_ofpredicate againstsource_revisions.fetched_at. - Structured
Evidence(M2).Evidencenow carriesvalid_from,valid_to,observed_at,authority,lifecycle, and typedlinks(supports/supersedes/contradicts/references/derived_from).enrich_evidenceloads links a chunk participates in (both directions). - Consolidation (M2.3). New
src/consolidate.rsdetection (find_exact_duplicates,find_subject_conflicts) +evidence_linkstable.POST /consolidate/propose(read-only detection) andPOST /consolidate/apply(operator records typed links; never automatic). - Freshness + conflict flags (M2.4/M3.1). Recall honors
observed_atas a stable freshness tie-break.RecallHit.conflictistruewhen a hit has acontradicts/supersedeslink to a current chunk. - Evidence metrics (M3.2).
tests/metrics.rsaddsstale_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_linkstable, not theentities/relationshipsKG. Graph use improves conflict detection (entity-keyed subject), not link storage. - No automatic mutation: consolidation is review-only via
brain consolidateapply. No autonomous deletion, no LLM judgment.
as_ofpoint-in-time recall is derived fromsource_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_ofedges. - 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:
/recalland/graph/traverseaccept anattimestamp and return only facts valid at that moment. - Budgeted context packing on
/recallmaximizes 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 newbench evalmode 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.
/healthreports 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 —
noneand 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/verifywalks 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.0unlessBIND_PUBLIC=1is set; an invalidBIND_HOSTnow 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 GitHubX-Hub-Signature-256HMAC, enqueues onto a bounded FIFO (WEBHOOK_QUEUE_MAX), and is idempotent viaUNIQUE(delivery_hash)+ awebhook_seenreplay window (WEBHOOK_REPLAY_SECS). Stale/futureDateheaders 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_eventstable 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.rsInjectionPolicy):contains_suspicious_patternhardened 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:”). Underquarantine(default) suspicious content is stored butflagged = 1and excluded from retrieval;GET /quarantine,POST /quarantine/{id}/release,POST /quarantine/{id}/deletelet an operator review/approve/purge.flag_if_quarantined+suppress_flagged_evidence(retrieval-side evidence stripping unlessinclude_flagged). - Untrusted-evidence boundary (OWASP LLM01:2025): every
SearchResult,RecallHit, andEvidencenow serializesuntrusted: true, so the consuming agent treats recalled content as data, never as instructions. vec0/FTS search gains aninclude_flaggedfilter (default excludes flagged rows). - Multi-token auth + live rotation (
src/config.rsauth_tokens()):AUTH_TOKEN/AUTH_TOKEN_FILEaccept 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 +.sha256checksum, secret-file bytes excluded (path+hash recorded only), and a.baksafety snapshot taken before any overwrite. openapi.yaml: documents/webhooks/{kind},/audit,/quarantine,/quarantine/{id}/release,/quarantine/{id}/delete, and theuntrustedfield onSearchResult/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). Treatwebhook_seenas the primary protection. contains_suspicious_patternis 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 theuntrustedflag, 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/revokeHTTP 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-tokenpattern.
[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 (thebrain-connector-ghbinary + GitHub REST client + issue→Markdown translation + backfill with rate-limit-aware pagination + durable cursors), M2.3 (periodic reconcile via the existing/sources/reconcileroute), and M3 (thebrain connect github,brain sync, andbrain connector-statusCLI 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.rsline 4 invariant preserved). The connector binary is free to depend onreqwest+jsonwebtoken+rsa— all feature-gated onconnector-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::Commandwithnext_backoffrestart (exponential capped at 60s, no jitter — single local supervisor, no herd risk). - Auth is a trait, not a struct.
AuthProvideris the unified surface;StaticTokenProvider(stub + tests),GitHubAppProvider(M2.1), and the futureOAuthProvider(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—AuthProvidertrait +AccessToken(with redactedDisplay) +StaticTokenProvider.src/connector/auth/store.rs—CredentialStore<T>: per-connector JSON config at~/.config/brain-server/connectors/{kind}-{instance}.json(0600). Atomic save viastd::fs::rename. No at-rest encryption beyond filesystem permissions + FileVault/LUKS — matches the existingauth-tokenpattern.src/connector/auth/github_app.rs—GitHubAppProvider: full JWT (RS256) → installation-token flow. Token-level repo scoping via the optionalrepositoriesbody field (the DoD-1 mechanism). In-memory single-slot cache refreshed withinREFRESH_SKEW=60sof 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_checkpointstable).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 onlypub mod connector. Server modules stay private tosrc/main.rs.- Migration: additive
connectors+connector_checkpointstables. Idempotent (CREATE TABLE IF NOT EXISTS). No data migration. GET /connectorsroute +ConnectorRowOpenAPI schema.brain connect githubCLI: writes connector config (0600, atomic) from--app-id,--install-id,--key-file,--repoargv.brain sync [github]CLI: spawnsbrain-connector-ghwith the right argv; surfaces its JSON-lines event stream to the operator.brain connector-statusCLI: lists every registered connector.
Changed
Cargo.toml:version0.9.5 → 0.9.6. New optional depsjsonwebtoken(rust_crypto+use_pemfeatures) +reqwest(rustls+json+blocking), both feature-gated onconnector-github. New[[bin]]brain-connector-stub(always built) +brain-connector-gh(requiresconnector-github). New dev-depsrsa+rand+base64(for JWT-shape tests).openapi.yaml: bumped to 0.9.6; added/connectorsroute +ConnectorRowschema.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_requestfield); 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.
AuthProvideris 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-Resetwindow). 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 theauth-tokenpattern; 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-statuscover 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-10is current; installation tokens support therepositoriesbody 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(requiresuse_pemfeature) 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-rsBGE cross-encoder), deleted in3fcac72. It pegged the M1 CPU and blew the 8s recall timeout, and was too heavy for the Jetson edge GPU. The hybridvec0KNN + FTS5 BM25 + RRF + PRF retrieval is the right ceiling for this edge-only deployment./statsnow reportsrerank_status: "off". Thererank_score/rerank_truncated/rerank_msAPI fields are retained (alwaysnull/false/0) for contract stability. ThererankCargo feature flag andsrc/search/rerank.rswere deleted entirely, not stubbed — to re-add the tier, revert3fcac72on a CUDA-GPU deployment.
Added (v0.9.5 M1 — “Inspect”)
- Structured query document (
QueryDoc). Both/searchand/recalllower their params into one versionedQueryDoc(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-lexpassthrough (which returned opaque SQLite errors on bad input). Caller input can no longer inject FTS5 operators./recallacceptslexas either a bare string ({"lex":"foo"}) or a fullLexSpecobject;/search(GET) takes a comma-separatedlexstring mapped to one term. - Multi-source OR scoping.
SearchFilters.sources: Vec<String>appliessource IN (?,?…)in bothvec0_knnandfts_search; the legacy singlesource=is still honored whensourcesis empty./searchtakes comma-separatedsources=a,b. intentis provenance-only. Recorded into telemetry/provenance; never injected as a search term and never relaxessince/source/domainfilters (verified by code trace).
Changed
/searchand/recallresponses now reflect the compiled lexical query and OR source scope in theirexplain/query_planblocks.
Known ceilings (not bugs)
profilefield is accepted but passthrough (no rerank/weighting yet).LexSpeccovers terms/phrases/exclusions/exact-code only — noNEAR, prefix*, or column filters./searchGET takes a flatlexstring, not a nestedLexSpec; the full structured form is on/recallPOST and will back the M3brain queryCLI.
Added (v0.9.5 M2 — “Evidence quality”)
- Structured
Evidenceon every hit.SearchResult/RecallHitnow carryevidence={ text, line_start, line_end, heading_path, source_uri, revision_id, highlights }.textis a verbatim substring of the chunk;highlightsare byte-offset ranges within that window (the server never injects HTML).source_uri/revision_idlink 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}andPOST /multi-getnow returnsource_uri+revision_id;multi-getbound raised to 1000 (was hardcoded 100).explainredaction + reproducibility./search?explain=trueredacts fullcontentfrom results (only the boundedevidence.text/snippetserialize) and addsk/source/domain/since/profiletoquery_plan. AMAX_EXPLAIN_BYTES(64 KiB) hard cap falls back to the summary if exceeded. Snippet window bounded byMAX_SNIPPET_CHARS(240)SNIPPET_CONTEXT_CHARS(60), centralized inconfig.rs.
config.rs: addedMAX_SNIPPET_CHARS,SNIPPET_CONTEXT_CHARS,MAX_EXPLAIN_BYTES,MAX_MULTI_GET.
Added (v0.9.5 M3 — “Product interface”)
brain queryon the structured contract.brain query "<q>"now POSTsPOST /recallwith a v0.9.5QueryDoc: repeatable--phrase/--exclude/--code(lowered intoLexSpec), multi---sourceOR scope,--intent,--profile,--since,--k,--explain. Back-compat bare-string queries still work.brain get <id>implemented against the existingGET /get/{id}route (M2.3 ceiling closed). Prints title/source/heading/line span/source_uri/revision_id+ content; 404 → “no chunk with id”.brain explainunified on/recall’sprovenance/telemetryenvelope (closes the M2.2 split where/searchusedquery_planand/recallusedtelemetry).GET /openapi.yamlserves the canonical OpenAPI 3.0 contract (embedded viainclude_str!, so it ships with the binary).openapi.yamlupdated to v0.9.5: all 23 routes +QueryDoc/LexSpec/Evidence/Chunk/QueryPlan/SearchTelemetryschemas.examples/client_example.rs— a typed client over the shared dependency- free HTTP client, demonstrating a structuredQueryDocroundtrip.- MCP tool schema (
mcpserver):brain_search/brain_recall/brain_ingestupdated to the v0.9.5QueryDoc; both search tools now POSTPOST /recallvia one shared body-lowerer. - API versioning + deprecation. Every response carries
X-Api-Version: <semver>; deprecatedPOST /addandGET /searchreturn an RFC 8594Deprecation: version="0.9.5"header. Policy + migration mapping documented inAPI_CONTRACT.md§Versioning & deprecation. test_openapi_covers_routes: asserts every route registered inbuild_appappears inopenapi.yaml.
Known ceilings (carried into v0.9.6)
highlightsover the full chunk still requireGET /get/{id};brain getreturns full content so a client can compute its own.profileaccepted 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 asvault/manual) andsource_revisions(immutable snapshots; supersession chain). Two new columns onknowledge(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 bytest_migration_schema_contract. /ingest/markdown+/ingest/memorynow write source linkage inside their existing transactions. Vault ingests use the canonical file path as the URI; manual memories usemanual://{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 ofkindwhose 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. BoundedMAX_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 +.brainignoresemantics + canonicalized-absolute-path URI form thatbrain ingest-diruses, so URIs match what’s stored. POSTs the live set to/sources/reconcile. Recommended after everybrain ingest-dir <vault>to detect deletes / renames.brain source-delete <id>— companion CLI for the DELETE route.scripts/install-service.shnow installs the operator CLIs (brain,mcp,bench) alongsidebrain-server, with--features benchso thebenchbinary compiles. Previously only the server binary was installed, sobrain doctor/brain statuswere not on$PATH.- macOS
com.apple.provenancexattr cleanup ininstall-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 assources.uriandknowledge.source_path; content is preserved inknowledge.content; per-chunkcontent_hashis 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 misleadingMAX_CHUNK_CHARStoMAX_CHUNK_BYTES(it was always bytes). Verified bytest_special_characters_survive_ingest_pipeline. brain --helplost its 2-space indentation. Theprint_usagestring 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./statsreported a staleembeddingscount (e.g.2on a 430-doc corpus). The handler counted the legacyembeddingstable, which has been frozen read-only since v0.9.0 — all post-v0.9.0 vectors live in thevec_knowledgevec0 table./statsnow countsvec_knowledge, so the number reflects the live index (backfilled legacy + new ingests).brain,mcp, andbenchCLIs returned401on every authenticated route (/search,/stats,/recall,/ingest/*,/sources/*). The shared HTTP client insrc/bin_common/http.rshad no auth support;get()/post()did not accept headers, so noAuthorization: Bearerwas ever sent. The client now takes an optionalbearer: Option<&str>, and each binary resolves the token viaBRAIN_TOKEN_FILE→BRAIN_TOKEN→~/.config/brain-server/auth-token(mirroring the server’sAUTH_TOKEN_FILE→AUTH_TOKENladder). Zero-config for the common install — same file the launchd plist already sources.brain-server --versionsilently started the server.main.rsdid no argv inspection, so any flag was ignored and execution fell through tobind(). 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/-Vprints and exits 0;--help/-hprints brief usage and exits 0; unknown--prefixed flags exit 2 instead of launching the server.brain --versionwas rejected as an unknown subcommand (error: unknown subcommand '--version', exit 2). Added a-V/--versionarm to the existing command matcher; bothbrainandbrain-servernow reportenv!("CARGO_PKG_VERSION")and exit 0.
Changed
write_markdown_ingesttakes a newraw_content: &strparameter (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 warningsandcargo test --all-targets --features bench. Thebenchbinary is feature-gated and was previously untested upstream. - Chunker rewritten on top of
pulldown-cmark0.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 walkspulldown-cmark’s event stream withinto_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; thehtml/getoptsdefault 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 chunktext. The only lines consumed (not buffered verbatim) are ATX and setext headings; their text becomes the chunk’sheading_pathbreadcrumb instead. The misleadingMAX_CHUNK_CHARSconstant was renamedMAX_CHUNK_BYTES(it was always bytes —str::len). Verified bytest_special_characters_survive_ingest_pipelineplus 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 viamod 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 removedparse_headingtest. - New
test_migration_schema_contractasserts the full table/column contract afterrun_migrationand 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-cmarkitself does not handle Obsidian-specific wikilink syntax ([[target]]) at the structural level — it emits them as Text events, which our chunker passes through verbatim. Thevault::parse_wikilinkspost-pass extracts them asreferencesKG 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.3RRF-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 byprf_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 bycandidate_window_equals_k_when_disabledand the rerank contract tests. - Benchmark is reproducible.
BENCHMARKS.mdfixes the workload, hardware, metrics, and commands; thebenchfeature andtests/metrics.rsimplement 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 withsource_pathprovenance on every ingested chunk. Walks are bounded (MAX_INGEST_FILES=50k,MAX_INGEST_BYTES=500MiB);.brainignoreand 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). Newsrc/vault.rsmodule — pure, no YAML dependency. [[wikilink]]→ knowledge graph:[[Target]],[[Target|Alias]],[[Target#Heading]]become traversablereferencesedges. Non-existent targets are created as placeholder entities so the graph completes as their files are ingested.- Frontmatter → entity metadata:
tags:→tagentities withtagged_withedges;aliases:→alias_ofedges (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 withsource_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 TEXTcolumn (additive migration, NULL for existing / interactive rows) +idx_knowledge_source_pathindex.
Fixed
/graph/entityand/graph/traverserejected entity names containing spaces, but note titles are stored with spaces (perNAME_RE). Both now allow spaces, so the wikilink graph is traversable from note titles likebignay fruit.
Changed
- The
/ingest/markdownDB-write was extracted intowrite_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
.brainignorehonoring.
Out of scope (paid tier / later releases)
- Live file-watcher sync (
notifycrate), multi-vault, scheduled re-index — paid “Brain Vault” tier behindlive-sync. - Obsidian plugin UI — paid tier.
- Per-domain isolation — v1.0.0 upgrades an ingested vault from flat
globalcontent 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.3threshold (top RRF ≈ 2/60 ≈ 0.033), so expansion never ran. PRF now uses a deterministic, calibrated gate (prf_should_expandinsrc/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
kbefore reranking, so a relevant candidate just belowkcould never be promoted. It now over-fetches a candidate window (RERANK_CANDIDATES = 30, fixed constant) and reranks it before truncating tok. - Silent
sincefilter replaced. The temporal filter is now validated as ISO-8601 (RFC3339 orYYYY-MM-DD HH:MM:SS) vianormalize_sinceand rejected if malformed, instead of relying on a lexical string comparison. /recallnow surfaces per-result provenance. The handler previously computed per-retriever ranks and fused scores internally but dropped them at the handler boundary.RecallHitnow carries an optionalProvenance(populated whenprovenance=trueon the request), closing the gap between/search(which already surfaced it) and the/recall+ MCPbrain_recallpath.- Quantization DoD met: no raw f32 JSON in the DB. All five ingest paths
(
add_chunk,ingest_memory,ingest_markdown,reindex, and the/ingestplugin handler) no longer write the legacy JSONembeddings.vectorcolumn.vec0(int8 + binary) is the sole write target. Theembeddingstable is retained read-only for one-time backfill of pre-v0.9.0 DBs. - Version source-of-truth. The
mcpbinary now derivesSERVER_VERSIONfromenv!("CARGO_PKG_VERSION")(was hardcoded"0.9.1", which would drift on the next bump).
Added
- Hybrid retrieval with Reciprocal Rank Fusion. Vector (
vec0KNN) 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 itsProvenance. - 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 theknowledge_fts_vocabtable (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 (
flaggedcolumn onknowledge). 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(defaulttrue),PRF_DEPTH(default10, clamped 1–100),PRF_TERMS(default5, clamped 1–50),PRF_MAX_RANK(default5, 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). UsesBGERerankerV2M3viafastembed::TextRerank::rerank(scores query–doc pairs), memory-bounded byRERANK_CANDIDATES(30) andRERANK_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), anddomainfilters are pushed into thevec0KNN and FTS5WHEREclauses (parameterized — no SQL injection).sourceandcreated_atare declared asvec0metadata columns. - Per-stage latency telemetry (embed / vector / fts / fusion / prf /
rerank) recorded in
SearchTelemetryand emitted at debug level./search?explain=1returns per-stage telemetry and the query plan. - Structured query (
lex/vec/hyde/intent) on/searchand/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 (
benchCargo 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/searchlatency. No new dependencies (reuses the shared HTTP client). - Recall eval harness (
#[ignore]d testeval_recall_harness): loads the model, builds a temp DB, and measures recall@5 / recall@10 across pure-vector / hybrid / hybrid+PRF configs. Runnable viacargo test --release -- --ignored --nocapture eval_recall_harness. - Migration safety. Pre-migration
VACUUM INTObackup (one-shot, marker-guarded, skipped for fresh DBs) runs beforerun_migrationso the rollback path is always possible. Addedmigrate_down_0_9_0()reversibility path (drops vec0 + FTS5 + vocab + schema markers; preservesknowledge/embeddings). Post-backfill parity check warns whenCOUNT(vec_knowledge) < COUNT(embeddings). - Developer surface: a
brainCLI (src/bin/brain.rs: query, explain,ingest-dirwith.brainignore+ content-hash idempotency +--dry-run, bench, status, doctor), a minimal stdio MCP server (src/bin/mcp.rs), andopenapi.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_tocolumns onknowledge, withdomainscoping in the retrievers (single-DB tagged model). - Structure-aware Markdown chunking (
src/chunker.rs):/ingest/markdownnow splits documents at heading boundaries (keeping code fences intact), stores one chunk perknowledgerow withdocument_id,chunk_index,heading_path, and 1-indexed line span, and embeds each chunk. AddedGET /get/{id}andPOST /multi-getfor stable chunk retrieval. - Implemented
POST /ingest(wasunimplemented!()/panic): the structured store now embeds, dedups viacontent_hash, routes to the resolved domain, and inserts knowledge + vec0 + entities + relations in one transaction. - Delete + tombstones:
DELETE /memory/{id}now also cleans thevec_knowledgerow (no FK cascade) and records atombstonesaudit row; deleted content is gone from retrieval immediately. POST /reindexrebuilds allvec_knowledgefromknowledge.GET /domainsnow lists real per-domain counts.- Per-domain DB registry (P2 foundation):
src/domain_registry.rsadds aDomainRegistrywith 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)./ingestand/recallroute through it;globalkeeps using the existingbrain.db(no data migration required). - Centroid routing + federation (P2):
src/domain_router.rscomputes a mean embedding centroid per domain (stored indomain_centroids, refreshed on ingest/reindex) and a pureroute()with a confidence threshold. In multi-db mode/recallauto-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 andstrict=false.
Changed
- The optional rerank tier remains feature-gated and off by default: it
compiles only with
--features rerankand activates only whenRERANK_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 inrun_migration, letting SQLite memory-map the DB without loading it all into RSS.- CORS loopback guard. When
CORS_ORIGINSis unset, the fallback now strips non-loopback origins, preventing an accidental open CORS policy in production.CORS_MAX_AGE_SECSis wired into theCorsLayer(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 (
benchfeature) 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.mdcells remainPENDINGuntil 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 wherevec0is empty. It is no longer the primary path —vec0KNN 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.