Round one of the p2-4 review found the behaviour sound and the record wrong
in eight places. All eight are fixed here, each against a measurement:
D1 "compare_digest" was written as if it covered holder tokens. It does not:
the store keeps only a sha256 digest, so verify_token is SQL equality;
compare_digest guards the operator's plaintext (api_auth.py:54, :110).
D2 register_minimal is called at memory.py:94, not :120.
D3 the compiler cache key never had an "as_holder or ''" step — the old key
had no identity component at all, which is a stronger defect than the one
I described.
D4 browse.py's own comment said four copies, two here. There are five, three
here (:52, :66, :93), which is what test_visibility_rule asserts.
D5 the store_search / vector_index line pointers moved with the as_of work.
D6 my review prompt said 33 routes; measured 34 decorators / 31 distinct
paths at the parent, 38 / 35 at the reviewed commit.
D7 the semgrep claim cannot be re-run in this container (neither semgrep nor
opengrep is installed), so it is recorded as an observation from the p0-1
gate run, and the anti-drift job is given to the test that does not need
that tool.
D8 "a holder can only write as itself, now guaranteed by the token" is the
opposite of what runs: only the operator writes, attribution comes from
default_holder, and the real guarantee is that RememberRequest has no
holder field.
Four precision notes are recorded too: a revoked token is a hard 401 rather
than a downgrade to anonymous, an anonymous caller passing an empty ?holder=
gets 403, the capability card also carries newest_memory timestamps and
token_issued, and leg_check's commit line is the cwd's HEAD.
The p2-3 entry gains the as_of gap the key test exposed, and the final-accept
entry now carries the seven measured scenarios with the two reasons it stays
open: scenario 7 unmeasured, and recall@10 0.863 against a 0.90 bar.
A holder is now somebody who can prove it. Schema v11 stores a sha256 of a
per-holder token beside the directory row, so a copied database cannot replay
it and the plaintext is shown exactly once. The operator token mints and
revokes those tokens; a holder token reads that holder's own memories and
nothing else; naming somebody else without the operator token is refused with
both ids in the message.
The visibility rule was leaking before this: browse had none at all, and the
search rules short-circuited to the whole library whenever no holder was
named. The rule is now written out in five copies with one parameter carried
four times, and a test compares the copies so they cannot drift — semgrep
rejects an interpolated constant, so the copies are not a choice.
Eyes carry three states on purpose. NULL means the whole library, which is
what a process that owns the store is entitled to; a holder id means that
holder; the empty string means nobody, which is what a tokenless HTTP caller
is. Making NULL mean nobody instead would have forced every internal caller —
CLI, proofs, the compiler, the bench harnesses — to name eyes, contradicting
the rule that a local process reads as its own trusted principal.
The capability card answers "can this store answer my kind of question" before
anything is sent. It is derived from what the store holds, so it cannot
promise a modality the store lacks, and it carries no memory text, so it is
safe to read before trusting the store. HTTP, CLI and one MCP tool all serve
it without a token.
9f91ff7 pushed schemas.py from 498 to 534 lines and broke the 500-line rule
the repo enforces on every file. The status family (CacheLayerStatus,
MemoryStatus, RuntimeStatus, SenseStatus, SystemStatus) moves to
status_schemas.py, where it is self-contained: it needs only
TrainingAdapterKind from schemas.py, and schemas.py no longer looks back.
The same commit added three multi-line docstrings (browse.picture,
senses.sense_facts, SenseStatus). Each is now one line; the absent-vs-missing
distinction they carried survives in SenseStatus's own line, and the producer
senses.py still states why the two failures need different sentences.
Measured, not asserted: 263 tests pass; ruff clean on the eight touched
files; repo-wide mypy still 24 errors in 6 files (runtime.py's two are in
that baseline and reproduce on the HEAD copy of the file); pyright reports
the same two pre-existing runtime.py tuple findings on both trees;
multi-line docstrings repo-wide are back to 114, the parent's count.
The store had a write path, a mount path and a search path, and the console
still showed a dashboard of counters. Nothing in the UI could ask what someone
knows, page through atoms, look at a wordless picture, forget one memory, or
mount a cartridge — so the mechanisms built in P0 were invisible.
Backend:
- senses.py: one function decides what this machine can perceive. /api/status
and doctor each computed it before, which is two truths. Picture atoms are now
counted by image_ref rather than by modality, so a screenshot crop from a PDF
counts and a picture whose bytes were deleted stops claiming it can be encoded.
- browse.py: GET /api/atoms pages newest-first through v_atoms_current with
modality/holder/source filters; GET /api/atoms/{id}/image serves the stored
bytes so a picture with no OCR text can be looked at.
- Deleted 8 HTTP routes nobody calls (4 benchmark, research modules, dense index,
visual-contradiction-proof, memory-map). Their functions stay: the CLI and the
proof checks use them. The surface is 30 routes, pinned as a set by a test so a
new unused route fails the suite.
Frontend: App.tsx (517 lines) became a 131-line shell plus seven panels that own
their own fetches, and lib/format.ts. types.ts now carries the memory dimensions
the backend has been sending since schema v2.
Two invented fields were rendering lies: AdapterPanel showed gpu_available and
probe_status, which AdapterLoadReport does not have — every machine looked GPU-less
including this one. The panel now shows only what the server says.
Proof: scripts/ui_smoke.sh starts the API on a fresh temp home, serves the built
UI from the same origin, and drives five interactions in a real browser — search
as somebody in a scenario, page and view a wordless picture, forget, mount, read
adapter states with reasons. It prints the mount refusal verbatim rather than
pretending the mount succeeded.
Gates: 263 pytest passed; ruff and mypy clean on the 8 touched backend files
(repo-wide mypy unchanged at 24 errors / 6 files); pyright 0 errors there;
go build/vet/test/gofmt clean; npm build + eslint clean; ui_smoke and
mcp_write_smoke both PASS.