Skip to content

v1.87.0 - Consent Before the Write

Choose a tag to compare

@itechmeat itechmeat released this 17 Oct 16:46
f6d9f95

Open Second Brain v1.87.0 - Consent Before the Write

Open Second Brain wrote to host configuration on demand: o2b install --target X --apply, brain protect --apply and brain doctor --repair --apply rewrote files on the call, and brain unprotect rewrote a protected file on every invocation with no consent, no backup and no record. v1.87.0 puts the guarded host-write lanes behind an explicit consent ladder - the intended change shown as a unified diff, a timestamped backup beside an audit receipt before anything is written, and exactly the approved preview landed, with the write refused when the file moved under it - and brain unprotect joins the lanes. An opt-in PreToolUse hook extends the same care to an agent's file writes against host Claude settings, thirteen more MCP list tools walk any large result set by an offset cursor instead of stopping at one capped page, a fourth decision-model route speaks the bearer-authenticated Converse protocol (Bedrock-style) beside the existing three adapters, and the trigger queue learns to snooze while a dismissal holds until its finding is grounded in something new.

Consent Before the Write

What ships

  • The three apply lanes demand consent before touching a file. o2b install --target X --apply, o2b brain protect --apply and o2b brain doctor --repair --apply refuse a --json or non-TTY run with a non-zero exit unless --yes is passed (the install refusal exits 6 and names the flag) and prompt once in an interactive run; both protect --apply and doctor --repair print a unified diff of the intended change before anything is written, and a headless refusal emits its own envelope ({ consent: "headless-refused", written: false, error }) on the payload stream.
  • The applied change is the approved preview. brain protect --apply and brain unprotect --apply carry the before/after pair the consent ladder showed into the write: the write re-derives the change and refuses it (PREVIEW_DRIFT, nothing written, no backup taken) when the target file or one of the manifest inputs moved since the preview, and the write re-reads the target right before the rename, absent a target excepted, so the bytes that land are the bytes that were approved.
  • brain unprotect joins the consent lanes. Plain brain unprotect becomes a read-only preview - under --json a "preview": true envelope carrying the before/after pair, so a headless consumer can tell it from the apply envelope it shares keys with - --yes without --apply is refused by name, and --apply runs the same fail-closed ladder: a confirmed run takes a timestamped backup of the file's current content beside it and appends the audit receipt before anything is written, so a refused consent or a failed backup leaves the protected file untouched.
  • An opt-in PreToolUse hook backs up host Claude settings before agent file writes. With host_settings_backup on (env OPEN_SECOND_BRAIN_HOST_SETTINGS_BACKUP over the _brain.yaml key, default off), a Write/Edit/MultiEdit/apply_patch call whose path canonically names a .claude/settings.json or .claude/settings.local.json gets its current content copied into a timestamped backup at <dir>/.o2b-backups/<name>/<UTC stamp> - created private at 0600, the backup carries the same secrets the settings file does, and exclusive-create ladders same-second names so exhaustion is a typed error, never an overwrite - beside one audit receipt, before the call is allowed to proceed; a backup that cannot be taken denies the call via stdout permissionDecision: "deny", a successful backup emits nothing, and the off path is a byte-identical no-op.
  • Cursor pagination across the MCP list tools, so an agent can walk any large result set. Thirteen limit-only list tools adopt the shared offset cursor beside the three surfaces that already paged, bringing the walking surface to sixteen: each declares a cursor input, cuts one offset window over the full filtered set through the one core cursor helper, and answers next_cursor present in every response and null when the walk is exhausted; a malformed cursor is refused as INVALID_PARAMS rather than silently restarting the walk; two of the walking scanners, brain_event_trace and brain_unlinked_mentions, bound each page's scan one record past the page's reach so a cursor walk costs one page of work per call; and brain_search and brain_file_context stay deliberately limit-only, because offset-paging a ranked, index-dependent answer would present an index race as data.
  • A converse decision-model route for bearer-authenticated Converse-protocol (Bedrock-style) gateways, declared uncalibrated. A fourth adapter beside systemone, vercel-evaluate and llm-emulation, with a converse preset whose base URL, pinned model id and key variable are the operator's, riding the request path (POST <base>/model/<model_id>/converse); the reply's message text holds one JSON object keyed by question id, validated item by item by the same rules as every route, and a stopReason that is not end_turn refuses the reply rather than half-parsing it; the reply is generated text whose probabilities the model wrote itself, so an enforce setting is a config error until the operator opts in with decision_model_allow_uncalibrated: true, and even then it runs as shadow until this route's own shadow data tunes its threshold profile; the request passes the shared egress guard under the decision-model-converse registry id, and o2b decision-model check, doctor and the setup skill name the new preset and profile.
  • A snooze action on the trigger queue, with unsnooze and a terminal snoozed status. o2b brain trigger snooze <id> and the MCP brain_trigger snooze operation defer an open trigger - legal from open states only - for a bounded window (three days) or until the finding's grounding moves, whichever comes first; unsnooze wakes it by hand, and the window's end or a grounding movement wakes it on its own, both restoring the exact status the snooze interrupted; the movement half persists the same grounding fingerprint the dismissal gate does, so a bare re-scan of an unchanged finding keeps the snooze holding and only stamps the ledger; a snoozed trigger is terminal like a suppressed one - hidden from list, present in history, never in the brief, and governed by its snooze window rather than the TTL expiry clock - and re-snoozing a still-snoozed trigger is refused rather than treated as idempotent.
  • A dismissed trigger no longer returns on a cooldown clock. Dismissal used to silence a cooldown key for trigger_cooldown_days (default 7) after the dismissal, so a finding nobody had touched resurfaced on a timer and a genuinely moved finding stayed hidden until the clock ran out; a dismissed finding now stays dismissed for however long the finding's grounding holds still, and returns - as a new record through the normal creation path, never as a status flip of the resolved one - only when the grounding moves: the re-fire introduces a source artifact the dismissed record does not name, or its grounding fingerprint (a sha256 of the finding's reason and context, normalized so the clock projections those texts embed - day counts, ages, scores - cannot read as movement) differs from the one persisted at dismissal time; the comparison is set-based and order-insensitive, a bare re-scan of an unchanged finding keeps the silence whatever the calendar says, scans name the silenced re-fires with a new dismissed skip reason, and the acted status keeps its cooldown window while suppress stays clockless.

Process wins

  • One source per fact: every What ships claim traces to the 1.87.0 CHANGELOG entry or the merged PR #266 description, and the quality numbers come from the CI run at the merge commit, never estimates.
  • Reviewed before the merge: the OCR delegation review covered 55 of 55 reviewable files with 6 confirmed findings, all 6 fixed; bot review is skipped for this change set because at 131 changed files it exceeds the 100-file limit above which CodeRabbit declines to review, and the internal review covered the whole diff.
  • Quality record: 19,074 tests pass / 0 fail / 20 skip on the validate job of the CI run at the merge commit f6d9f95, the exact content this release tags, on the CI-pinned Bun 1.4.0; typecheck clean, lint at 0 errors, sync-version green across its 8 manifests, the plugin scanners green, and every check green at that head, all ten Windows shards included.
  • The housekeeping in the wave: #262 bumps hashgraph-online/ai-plugin-scanner-action from 1.2.772 to 1.2.787, and #263 bumps the dev toolchain (@types/bun 1.4.2 to 1.4.3, oxfmt 0.72.0 to 0.73.0, oxlint 1.87.0 to 1.88.0).
  • #265 moves the osb-pr-prepare playbook to 1.4.0: one shared dev quality cycle replaces its four review nodes, an owner approval gate now sits before anything is pushed, and fixes made after a review are reviewed too; the PR also fixes a date-seeded storage test that would have failed on every runner from 2026-10-17.

Notes

  • Breaking on purpose: headless callers that scripted the old write-on---apply behavior must add --yes (the install refusal's exit code 6 names it), and headless scripts that called brain unprotect unattended must move to --apply --yes; plain brain unprotect answers a read-only preview instead of rewriting.
  • The converse route's honesty has a price: until the operator opts in, an enforce setting on the preset is a config error; as a chat-shaped route its output tokens are billed beside input ones once both prices are configured, and one price alone leaves the cost unknown.
  • The PreToolUse hook documents its honest hard floor - no deny when bun or the hook script is missing - and its uncovered lanes (Bash-class writes, temp-then-rename) in hooks/README.md.
  • brain_truth keeps its documented advance-since time-window paging, and the diagnostics whose rows are aggregates a second page could not continue carry recorded report-cap reasons pinned beside the census tests.
  • A snoozed trigger is governed by its snooze window rather than the TTL expiry clock, so a long snooze cannot be cut short by expiry; unsnooze on a hand-edited record that lost its interrupted status refuses instead of inventing a lifecycle position to restore into.
  • check-release.sh now also refuses a new CHANGELOG heading when the change kind is no-release (#265).
  • The version bump to 1.87.0 shipped inside the PR (#266), per the project rule in CLAUDE.md.
  • Release image: the canonical terminal style, here as an interaction diagram (animated GIF in this body; static PNG, 2x PNG and the SVG source attached as assets).