brainiac-cli 0.20.2__tar.gz → 0.20.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {brainiac_cli-0.20.2/src/brainiac_cli.egg-info → brainiac_cli-0.20.4}/PKG-INFO +1 -1
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/pyproject.toml +1 -1
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/AGENTS.md +39 -4
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/tools/cos_reconcile_metrics.py +24 -7
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_version.py +1 -1
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/audit.py +77 -6
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/config.py +31 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/core.py +13 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/doctor.py +24 -6
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/index.py +16 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4/src/brainiac_cli.egg-info}/PKG-INFO +1 -1
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/LICENSE +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/MANIFEST.in +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/README.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/setup.cfg +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/__init__.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/__main__.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/assets/graph-explorer-template.html +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/brand/brand-guide.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/cos/auto-archive.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/cos/drafts.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/cos/ingest.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/cos/priorities.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/keywords/glossary.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/people/roster.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/voice/voice-profile.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/routines/manifest.json +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/brain-brief-mac.plist +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/brain-brief.sh +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/brain-synthesis-mac.plist +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/brain-synthesis.sh +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/brainiac-alerts.sh +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/install-brief-mac.sh +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/install-brief-windows.ps1 +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/register_tasks.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/vm-boundary-probe.sh +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/vm-selftest.sh +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/templates/company.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/templates/concept.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/templates/daily.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/templates/decision.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/templates/meeting.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/templates/person.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/templates/project.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/templates/state-moc.md +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/tools/cos_browser_scan.mjs +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/tools/cos_contract.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/tools/cos_retro.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/anchor.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/autolink.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/backup.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/brief.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/capture.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/chunk.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/classification.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/cli.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/connect.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/context.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/conventions.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/cos.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/cos_chips.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/cos_corpus.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/cos_deploy.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/cos_runverify.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/dbretry.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/egress.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/embed.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/encryption.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/frontmatter.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/golden_probe.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/graph.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/graphify.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/graphreport.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/healthreport.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/inbox.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/__init__.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/__init__.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/base.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/docx.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/email.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/html.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/image.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/pdf.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/pptx.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/tables.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/text.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/xlsx.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/handlers/zip.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/pipeline.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/ingest/transcript.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/init.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/lock.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/maintenance.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/mcp_adapter.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/multihop.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/notes.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/overlay.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/progress.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/projection.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/provenance.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/querylog.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/rerank.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/retro.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/snapshot.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/spine.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/update.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/vectors.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/versionlink.py +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brainiac_cli.egg-info/SOURCES.txt +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brainiac_cli.egg-info/dependency_links.txt +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brainiac_cli.egg-info/entry_points.txt +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brainiac_cli.egg-info/requires.txt +0 -0
- {brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brainiac_cli.egg-info/top_level.txt +0 -0
|
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
|
|
|
8
8
|
# check. The IMPORT package stays `brain` and the console script stays
|
|
9
9
|
# `brain` — only the distribution/project name changes.
|
|
10
10
|
name = "brainiac-cli"
|
|
11
|
-
version = "0.20.
|
|
11
|
+
version = "0.20.4"
|
|
12
12
|
description = "Brainiac — local any-LLM second-brain core engine + brain CLI (Markdown truth, derived SQLite index)."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
license = "Apache-2.0"
|
|
@@ -547,6 +547,33 @@ round-trip). Both temporal flags stay **VM_ALLOWED** — they are read-only
|
|
|
547
547
|
filters over already-gated rows, no different in trust from any other
|
|
548
548
|
`bases-query`.
|
|
549
549
|
|
|
550
|
+
**Breadth-intent routing (RTE-01).** A heuristic adapted from NapMem's
|
|
551
|
+
observed navigator behavior (arXiv 2607.05794) — the paper never classifies
|
|
552
|
+
query breadth itself, but its top-down-vs-bottom-up entry choice transfers as
|
|
553
|
+
a rule of thumb for frontier-model navigators over this vault. This governs
|
|
554
|
+
*entry point* only, never a mandated step sequence: probing lexically first
|
|
555
|
+
and escalating only when needed (§5's agentic tool surface) still applies
|
|
556
|
+
once you're in. When a question is BROAD — "state of play", "overview", "how
|
|
557
|
+
do we usually…" asked about VAULT knowledge — enter TOP-DOWN instead of
|
|
558
|
+
grepping cold:
|
|
559
|
+
|
|
560
|
+
```
|
|
561
|
+
brain get index --json # the map note (id: index) — start here
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
then drill down via wikilinks with `get`/`graph-expand`, confirming each
|
|
565
|
+
candidate on its cited note (`graph-expand` stays DISCOVERY-ONLY — never
|
|
566
|
+
treat its derived graph as authoritative). If no state-MOC or `index.md`
|
|
567
|
+
entry fits, fall back to `search`. **Owner persona/voice/preference
|
|
568
|
+
questions are NOT this route:** voice/brand/keywords/people live in
|
|
569
|
+
`vault/overlay/`, which is excluded from retrieval indexing entirely, so the
|
|
570
|
+
vault map can never contain those answers — keep the existing overlay-loading
|
|
571
|
+
path for those. When a question is NARROW factual recall — a specific
|
|
572
|
+
entity, exact term, date — keep the existing lexical-first entry
|
|
573
|
+
(`grep`/`search`). This rule is additive only: decision-state questions still
|
|
574
|
+
route to `brain dossier` and temporal questions still route to TMP-03 above,
|
|
575
|
+
regardless of breadth.
|
|
576
|
+
|
|
550
577
|
**`brain supersede <old-id> <new-id> [--reason R]`** retires `old-id` in favour
|
|
551
578
|
of `new-id` — both sides of the version chain, written through the audited
|
|
552
579
|
`write_note` path in one call. **HOST-broker only** (refused on `role=vm`
|
|
@@ -724,10 +751,18 @@ Obsidian "five-step retrieval cascade" rule for any harness reading this file.
|
|
|
724
751
|
(`--check-content` adds the per-note list), `brain doctor` carries the same
|
|
725
752
|
count as a gating row, and any UNEXPLAINED drift makes the health verdict
|
|
726
753
|
DEGRADED. A vault carrying historical drift (notes edited outside the audited
|
|
727
|
-
write path before this was visible) triages it once into
|
|
728
|
-
|
|
729
|
-
to the bytes it was ruled on**, so the same note changing again
|
|
730
|
-
unexplained. Never re-sign or delete drifted notes to clear the
|
|
754
|
+
write path before this was visible) triages it once into a **host-private**
|
|
755
|
+
disposition file (`brain doctor --json` prints its path); each disposition is
|
|
756
|
+
**pinned to the bytes it was ruled on**, so the same note changing again
|
|
757
|
+
returns as unexplained. Never re-sign or delete drifted notes to clear the
|
|
758
|
+
count. That file moved OFF `<vault>/.brain/` on 2026-08-07: it decides
|
|
759
|
+
whether tampering counts as EXPLAINED, and a match needs only path + issue +
|
|
760
|
+
observed hash — every one of which is known to whoever edited the note — so
|
|
761
|
+
on the shared mount the untrusted VM leg could forge one and drive
|
|
762
|
+
`unexplained` to 0 while `verify-audit` still reported `ok`. Same treatment
|
|
763
|
+
and same reason as the approved queue (INT-01), the attachment anchors
|
|
764
|
+
(INT-04) and the writer lock (INT-05). An existing file is carried forward
|
|
765
|
+
once, stamped `migrated_from_mount`.
|
|
731
766
|
|
|
732
767
|
---
|
|
733
768
|
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/tools/cos_reconcile_metrics.py
RENAMED
|
@@ -54,6 +54,7 @@ import json
|
|
|
54
54
|
import os
|
|
55
55
|
import re
|
|
56
56
|
import sys
|
|
57
|
+
import tempfile
|
|
57
58
|
from collections import defaultdict
|
|
58
59
|
from pathlib import Path
|
|
59
60
|
|
|
@@ -455,13 +456,29 @@ def append_metric(ops_dir: Path, row: dict) -> str:
|
|
|
455
456
|
f"conflicting metrics row for {(row['date'], row['run'])!r}")
|
|
456
457
|
|
|
457
458
|
prior = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
os.replace
|
|
459
|
+
payload = (prior.rstrip("\n") + ("\n" if prior else "")
|
|
460
|
+
+ json.dumps(row, separators=(",", ":"), ensure_ascii=False) + "\n")
|
|
461
|
+
# The temp file used to be the fixed name `_cos_metrics.jsonl.tmp`, written
|
|
462
|
+
# with `Path.write_text` (Codex cloud review, 2026-08-07). A symlink planted
|
|
463
|
+
# at that predictable path is FOLLOWED by the open, so the write truncates
|
|
464
|
+
# and overwrites whatever it points at -- outside the vault included -- and
|
|
465
|
+
# the later `os.replace` only swaps the link, long after the damage. A COS
|
|
466
|
+
# run writes here as a documented step, so nothing unusual has to happen.
|
|
467
|
+
#
|
|
468
|
+
# `mkstemp` is the stdlib answer to exactly this: a random name created
|
|
469
|
+
# with O_CREAT|O_EXCL|O_NOFOLLOW-equivalent semantics (it refuses to open an
|
|
470
|
+
# existing path at all), 0600, in the same directory so `os.replace` stays
|
|
471
|
+
# atomic on one filesystem.
|
|
472
|
+
fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix="._cos_metrics-",
|
|
473
|
+
suffix=".jsonl.tmp")
|
|
474
|
+
tmp = Path(tmp_name)
|
|
475
|
+
try:
|
|
476
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
477
|
+
fh.write(payload)
|
|
478
|
+
os.replace(tmp, path)
|
|
479
|
+
except BaseException:
|
|
480
|
+
tmp.unlink(missing_ok=True)
|
|
481
|
+
raise
|
|
465
482
|
return "appended"
|
|
466
483
|
|
|
467
484
|
|
|
@@ -507,21 +507,92 @@ class AuditChain:
|
|
|
507
507
|
# drifting AGAIN produces a different `actual_sha256`, no longer matches, and
|
|
508
508
|
# comes back as unexplained. Absorbing a whole path forever is exactly the
|
|
509
509
|
# failure mode this instrument exists to prevent.
|
|
510
|
+
#
|
|
511
|
+
# WHERE IT LIVES (changed 2026-08-07): the host-private app-data dir, NOT
|
|
512
|
+
# `<vault>/.brain/`. This file decides whether tampering counts as explained,
|
|
513
|
+
# and a match needs only path + issue + observed hash -- all of which whoever
|
|
514
|
+
# edited the note already knows. On the old path, the Cowork VM could write it,
|
|
515
|
+
# so the untrusted leg could silence the host's own tamper alarm. Pinning to
|
|
516
|
+
# bytes is what stops a disposition absorbing a path forever; being off the
|
|
517
|
+
# mount is what stops it being forged. Both are needed.
|
|
510
518
|
DRIFT_DISPOSITIONS_FILENAME = "audit-drift-dispositions.json"
|
|
511
519
|
|
|
512
520
|
|
|
513
521
|
def drift_dispositions_path(vault: Path) -> Path:
|
|
514
|
-
"""Host-
|
|
515
|
-
|
|
522
|
+
"""Host-private triage file, OFF the VM-visible mount (2026-08-07).
|
|
523
|
+
|
|
524
|
+
Raises ``config.HostPathUnsafe`` when it cannot resolve somewhere the
|
|
525
|
+
Cowork VM is unable to reach — see ``config.audit_drift_dispositions_path``
|
|
526
|
+
for why this file in particular must be out of reach."""
|
|
527
|
+
from . import config
|
|
528
|
+
|
|
529
|
+
return config.audit_drift_dispositions_path(vault)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def legacy_drift_dispositions_path(vault: Path) -> Path:
|
|
533
|
+
"""Where this file lived until 2026-08-07: on the shared mount. Read ONLY
|
|
534
|
+
by the one-time carry-forward below; nothing else may consult it again."""
|
|
516
535
|
return Path(vault) / ".brain" / DRIFT_DISPOSITIONS_FILENAME
|
|
517
536
|
|
|
518
537
|
|
|
538
|
+
def migrate_drift_dispositions(vault: Path) -> str | None:
|
|
539
|
+
"""Carry a pre-2026-08-07 triage file forward to the host-private location.
|
|
540
|
+
|
|
541
|
+
Copy, never move: the destination is the only thing read from now on, and
|
|
542
|
+
deleting the operator's historical record on their behalf is not this
|
|
543
|
+
function's call. Returns a one-line note when it acted, else ``None``.
|
|
544
|
+
|
|
545
|
+
The carried-forward records came from a VM-writable path, so the copy is
|
|
546
|
+
STAMPED (``migrated_from_mount``) rather than laundered into looking
|
|
547
|
+
host-authored. Without this, a vault with triaged historical drift would
|
|
548
|
+
report every previously-explained record as unexplained on first run after
|
|
549
|
+
the upgrade — a false tamper alarm, which is the fastest way to teach an
|
|
550
|
+
operator to ignore a real one."""
|
|
551
|
+
legacy = legacy_drift_dispositions_path(vault)
|
|
552
|
+
if not legacy.is_file():
|
|
553
|
+
return None
|
|
554
|
+
try:
|
|
555
|
+
dest = drift_dispositions_path(vault)
|
|
556
|
+
except Exception: # noqa: BLE001 — unsafe destination: stay fail-closed
|
|
557
|
+
return None
|
|
558
|
+
if dest.exists():
|
|
559
|
+
return None
|
|
560
|
+
try:
|
|
561
|
+
raw = json.loads(legacy.read_text(encoding="utf-8"))
|
|
562
|
+
except (OSError, ValueError):
|
|
563
|
+
return None
|
|
564
|
+
records = raw.get("dispositions") if isinstance(raw, dict) else raw
|
|
565
|
+
if not isinstance(records, list):
|
|
566
|
+
return None
|
|
567
|
+
try:
|
|
568
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
569
|
+
try:
|
|
570
|
+
dest.parent.chmod(0o700)
|
|
571
|
+
except OSError:
|
|
572
|
+
pass
|
|
573
|
+
dest.write_text(json.dumps(
|
|
574
|
+
{"dispositions": records,
|
|
575
|
+
"migrated_from_mount": legacy.as_posix()},
|
|
576
|
+
indent=2), encoding="utf-8")
|
|
577
|
+
except OSError:
|
|
578
|
+
return None
|
|
579
|
+
return (f"carried {len(records)} drift disposition(s) forward from the shared "
|
|
580
|
+
f"mount to {dest} — they were recorded where a Cowork VM could write, "
|
|
581
|
+
f"so re-check them if you have any reason to doubt that host")
|
|
582
|
+
|
|
583
|
+
|
|
519
584
|
def load_drift_dispositions(vault: Path) -> dict[str, dict]:
|
|
520
|
-
"""``{path: record}`` from the triage file; ``{}`` when absent
|
|
521
|
-
|
|
522
|
-
|
|
585
|
+
"""``{path: record}`` from the triage file; ``{}`` when absent, unreadable,
|
|
586
|
+
or resolvable only to a VM-visible path. Fails CLOSED into "nothing is
|
|
587
|
+
explained" — an unreadable or untrustworthy disposition file must never
|
|
588
|
+
silently clear a drift count."""
|
|
589
|
+
migrate_drift_dispositions(vault)
|
|
590
|
+
try:
|
|
591
|
+
path = drift_dispositions_path(vault)
|
|
592
|
+
except Exception: # noqa: BLE001 — HostPathUnsafe and anything else
|
|
593
|
+
return {}
|
|
523
594
|
try:
|
|
524
|
-
raw = json.loads(
|
|
595
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
525
596
|
except (OSError, ValueError):
|
|
526
597
|
return {}
|
|
527
598
|
records = raw.get("dispositions") if isinstance(raw, dict) else raw
|
|
@@ -633,6 +633,37 @@ def writer_lock_path(vault: str | os.PathLike[str] | None = None) -> Path:
|
|
|
633
633
|
return host_lock_dir(vault) / f"writer-{vault_slug8(vault)}.lock"
|
|
634
634
|
|
|
635
635
|
|
|
636
|
+
def audit_drift_dispositions_dir(vault: str | os.PathLike[str] | None = None) -> Path:
|
|
637
|
+
"""Directory holding the INT-02 drift-triage files. Resolution only — the
|
|
638
|
+
writing path creates it, same convention as ``host_lock_dir``."""
|
|
639
|
+
return proven_off_mount(host_private_base() / "audit-drift", vault,
|
|
640
|
+
what="audit drift disposition store")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def audit_drift_dispositions_path(vault: str | os.PathLike[str] | None = None) -> Path:
|
|
644
|
+
"""The file that decides whether post-signing content drift is EXPLAINED.
|
|
645
|
+
|
|
646
|
+
OFF THE MOUNT (2026-08-07, Codex cloud security review). It used to be
|
|
647
|
+
``<vault>/.brain/audit-drift-dispositions.json``, on the VirtioFS mount a
|
|
648
|
+
Cowork VM session can write. That made it a tamper-suppression authority
|
|
649
|
+
sitting in reach of the untrusted leg: a disposition matches on path, issue
|
|
650
|
+
and OBSERVED hash — every one of which is known to whoever just edited the
|
|
651
|
+
note — so adding one record drives ``drift_summary()['unexplained']`` to 0,
|
|
652
|
+
and ``verify_audit`` keeps reporting ``ok`` while a signed note's bytes have
|
|
653
|
+
changed. The signature check cannot catch it; the whole point of the content
|
|
654
|
+
pass was to catch what signatures cannot.
|
|
655
|
+
|
|
656
|
+
Same treatment, same reason, and now the same helper as the COS approved
|
|
657
|
+
queue (INT-01), the attachment acceptance anchors (INT-04) and the
|
|
658
|
+
single-writer lock (INT-05): being unreachable is the control, not a
|
|
659
|
+
permission bit on a mount that "may only partially honour POSIX bits".
|
|
660
|
+
|
|
661
|
+
Raises :class:`HostPathUnsafe` when the resolved location is VM-visible.
|
|
662
|
+
Callers must treat that as "nothing is explained" — never as a clean bill.
|
|
663
|
+
Keyed by vault identity so two vaults never share one triage file."""
|
|
664
|
+
return audit_drift_dispositions_dir(vault) / f"dispositions-{vault_slug8(vault)}.json"
|
|
665
|
+
|
|
666
|
+
|
|
636
667
|
def graph_dir(vault: str | os.PathLike[str] | None = None) -> Path:
|
|
637
668
|
"""GRF-01 discovery-graph runtime artifacts (ADR-0003 Ruling 6/(a)) —
|
|
638
669
|
gitignored, host-only, never published into the VM snapshot. Holds the
|
|
@@ -3202,6 +3202,19 @@ class BrainCore:
|
|
|
3202
3202
|
# `brain maintain`, an interactive session, and the test suite must NEVER
|
|
3203
3203
|
# trigger an unattended pip upgrade / plugin reinstall of the machine —
|
|
3204
3204
|
# so default OFF and require the explicit opt-in the scheduler provides.
|
|
3205
|
+
# KILL SWITCH, checked first (2026-08-07). Unattended auto-apply is an
|
|
3206
|
+
# ACCEPTED RISK, not an oversight -- see docs/adr/0005-update-versioning-ux.md
|
|
3207
|
+
# "Risk acceptance" for what is being accepted and why. But an acceptance
|
|
3208
|
+
# you cannot revoke is not an acceptance: `scripts/brain-brief.sh` sets
|
|
3209
|
+
# BRAIN_AUTO_UPDATE=1 INLINE, so an inherited 0 cannot switch it off, and
|
|
3210
|
+
# that runner is a shipped file the next update overwrites. This is the
|
|
3211
|
+
# one way to turn auto-apply off without editing shipped files or
|
|
3212
|
+
# enabling full BRAIN_MANAGED lockdown -- set it in the environment the
|
|
3213
|
+
# scheduled task runs in (launchd plist / cron). Default off: setting
|
|
3214
|
+
# nothing changes nothing.
|
|
3215
|
+
if _os.environ.get("BRAIN_NO_AUTO_UPDATE", "").strip().lower() in (
|
|
3216
|
+
"1", "true", "yes", "on"):
|
|
3217
|
+
return {"auto_update": "disabled", "reason": "BRAIN_NO_AUTO_UPDATE set"}
|
|
3205
3218
|
if _os.environ.get("BRAIN_AUTO_UPDATE") != "1":
|
|
3206
3219
|
return {"auto_update": "disabled", "reason": "BRAIN_AUTO_UPDATE!=1 (scheduled-task only)"}
|
|
3207
3220
|
|
|
@@ -601,10 +601,25 @@ def check_desktop_plugin_store(
|
|
|
601
601
|
# slash-command skill); in a Cowork session /skill-creator is what
|
|
602
602
|
# repackages + presents the skill for Save-and-Replace. /brainiac-update
|
|
603
603
|
# is host-only (refuses --role vm) so it is NOT the Cowork fix.
|
|
604
|
-
|
|
604
|
+
#
|
|
605
|
+
# ANY mismatch needs action, not just `installed < ssot` (Codex cloud
|
|
606
|
+
# review, 2026-08-07). `installed > ssot` was reported as "looks current
|
|
607
|
+
# — no action needed", but ADR-0004 Ruling 5 / the CLI-plugin path
|
|
608
|
+
# explicitly handle RECONCILIATION DOWNGRADES, where an old installed
|
|
609
|
+
# plugin legitimately carries a numerically higher version than the
|
|
610
|
+
# current SSOT. The surface left stale by a false green here is the
|
|
611
|
+
# LLM-facing Cowork/Desktop skill instructions, so "newer" is not a
|
|
612
|
+
# reason to leave it alone after a security-relevant release.
|
|
613
|
+
skew = _compare(str(version), ssot)
|
|
614
|
+
if skew < 0:
|
|
605
615
|
remediation = ("in a Cowork session use /skill-creator to repackage + "
|
|
606
616
|
"Save-and-Replace the stale skill(s); re-run brain doctor on "
|
|
607
617
|
"the host to confirm it took")
|
|
618
|
+
elif skew > 0:
|
|
619
|
+
remediation = (f"installed {version} is AHEAD of SSOT {ssot} — a "
|
|
620
|
+
"reconciliation downgrade, not a current install; in a "
|
|
621
|
+
"Cowork session use /skill-creator to repackage + "
|
|
622
|
+
"Save-and-Replace so the shipped skill matches SSOT")
|
|
608
623
|
else:
|
|
609
624
|
remediation = "looks current — no action needed"
|
|
610
625
|
rows.append(_row(surface, MANUAL_REQUIRED, detail, remediation=remediation,
|
|
@@ -1209,10 +1224,12 @@ def check_audit_content_drift(vault: Path) -> dict:
|
|
|
1209
1224
|
files against hashes already in the log, so this row costs one vault read
|
|
1210
1225
|
and never resolves the signing key (``verify-audit`` does that separately).
|
|
1211
1226
|
|
|
1212
|
-
Only UNEXPLAINED drift gates. Drift a human triaged into
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1227
|
+
Only UNEXPLAINED drift gates. Drift a human triaged into the disposition
|
|
1228
|
+
file is reported in the detail and subtracted from the verdict — pinned to
|
|
1229
|
+
the bytes it was ruled on, so the same file changing again comes straight
|
|
1230
|
+
back as unexplained. That file lives in the HOST-PRIVATE app-data dir since
|
|
1231
|
+
2026-08-07 (``config.audit_drift_dispositions_path``); on the old
|
|
1232
|
+
``.brain/`` path a Cowork VM could write it and zero out this very row."""
|
|
1216
1233
|
from . import audit as _audit
|
|
1217
1234
|
from . import config
|
|
1218
1235
|
|
|
@@ -1233,7 +1250,8 @@ def check_audit_content_drift(vault: Path) -> dict:
|
|
|
1233
1250
|
f"{unexplained} signed note(s) changed after signing with no recorded "
|
|
1234
1251
|
f"disposition ({explained} triaged, {total} total)",
|
|
1235
1252
|
remediation="brain verify-audit --check-content --json # then triage into "
|
|
1236
|
-
"
|
|
1253
|
+
"the host-private disposition file (brain doctor --json shows "
|
|
1254
|
+
"its path) or restore the note",
|
|
1237
1255
|
raw={"total": total, "unexplained": unexplained})
|
|
1238
1256
|
detail = ("no drift — every signed note matches its signed bytes" if not total
|
|
1239
1257
|
else f"0 unexplained ({explained} triaged historical drift record(s))")
|
|
@@ -2868,7 +2868,23 @@ class BrainIndex:
|
|
|
2868
2868
|
row = self.conn.execute(
|
|
2869
2869
|
"SELECT rowid, classification FROM notes WHERE id=?", (target_id,)
|
|
2870
2870
|
).fetchone()
|
|
2871
|
+
# EXISTENCE ORACLE (Codex cloud review, 2026-08-07). A missing id used
|
|
2872
|
+
# to answer `candidate-miss` and echo the id back, while an id that
|
|
2873
|
+
# exists above the caller's cap answered `withheld` — two distinguishable
|
|
2874
|
+
# replies, on a verb the VM is allowed to call. That let an untrusted leg
|
|
2875
|
+
# probe guessed slugs and learn WHICH Restricted/MNPI notes exist, which
|
|
2876
|
+
# is precisely what the egress gate is there to prevent. Note ids are
|
|
2877
|
+
# readable slugs, so guessing is cheap.
|
|
2878
|
+
#
|
|
2879
|
+
# Whenever a cap can hide something, both cases now return the identical
|
|
2880
|
+
# `withheld` payload, so the two are indistinguishable. On an UNCAPPED
|
|
2881
|
+
# caller (the host default is the full vault) nothing can be hidden, so
|
|
2882
|
+
# there is no oracle to close and `candidate-miss` stays the useful
|
|
2883
|
+
# answer for a typo'd id.
|
|
2884
|
+
capped = max_tier != cls_mod.TIERS[-1]
|
|
2871
2885
|
if not row:
|
|
2886
|
+
if capped:
|
|
2887
|
+
return {"target": "withheld", "verdict": "withheld"}
|
|
2872
2888
|
return {
|
|
2873
2889
|
"target": target_id,
|
|
2874
2890
|
"verdict": "candidate-miss",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/assets/graph-explorer-template.html
RENAMED
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/brand/brand-guide.md
RENAMED
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/cos/auto-archive.md
RENAMED
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/cos/drafts.md
RENAMED
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/cos/ingest.md
RENAMED
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/cos/priorities.md
RENAMED
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/keywords/glossary.md
RENAMED
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/overlay/template/people/roster.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/brain-synthesis-mac.plist
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brainiac_cli-0.20.2 → brainiac_cli-0.20.4}/src/brain/_assets/scripts/install-brief-windows.ps1
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|