Let the server decide whose memory a caller may read

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.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 14:15:56 -04:00
1 parent 209f2d0a1c
commit 83dfc14daa
29 files changed
+1335 -77

No files matched your search

+44
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
@@ -15,6 +16,7 @@ import ely_eye.cli as cli_module
from ely_eye.cli import app
from ely_eye.config import Settings
from ely_eye.db import Database
from ely_eye.holder_store import HolderStore
from ely_eye.ingestion import IngestionService
from ely_eye.memory import MemoryService
from ely_eye.migrations import SCHEMA_VERSION
@@ -249,3 +251,45 @@ def test_the_cli_asks_and_reports_the_referral(tmp_path: Path, monkeypatch: pyte
assert asked.exit_code == 0 and '"status": "referred"' in asked.stdout
assert runner.invoke(app, ["holders"]).stdout.count("wife") >= 1
assert runner.invoke(app, ["holder-set", "wife", "--contact", "not-a-pair"]).exit_code != 0
def test_the_cli_mints_a_token_that_proves_who_is_asking(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
settings = Settings(home=tmp_path / "home", default_holder="husband")
monkeypatch.setattr(cli_module, "get_settings", lambda: settings)
runner = CliRunner()
Store(settings).wife_remembers(HOME_KEYS)
issued = json.loads(runner.invoke(app, ["holder-issue", "wife"]).stdout)
assert issued["holder_id"] == "wife" and issued["token"] and issued["issued_at"]
assert HolderStore(Database(settings)).verify_token(issued["token"]) == "wife"
assert issued["token"] not in runner.invoke(app, ["card"]).stdout, "the card is public and the token is not"
revoked = json.loads(runner.invoke(app, ["holder-revoke", "wife"]).stdout)
assert revoked["holder_id"] == "wife" and revoked["token_issued_at"] is None
assert HolderStore(Database(settings)).verify_token(issued["token"]) is None
def test_the_card_is_read_back_from_the_store_rather_than_written_by_hand(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
settings = Settings(home=tmp_path / "home", default_holder="husband")
monkeypatch.setattr(cli_module, "get_settings", lambda: settings)
runner = CliRunner()
store = Store(settings)
store.wife_remembers(HOME_KEYS)
store.husband_remembers(WORK_KEYS)
store.register("neighbour", name="邻居", knows_about=["weather"])
answer = runner.invoke(app, ["card"])
assert answer.exit_code == 0, answer.stdout
card = json.loads(answer.stdout)
assert card["schema_version"] == SCHEMA_VERSION
assert card["atoms"] == 2 and card["sources"] == 2
assert card["modalities"] == {"text": 2} and card["has_audio"] is False
assert card["mounts"] == {"mounted": [], "scoped": False, "atoms_in_scope": 0}
named = {entry["holder_id"]: entry for entry in card["holders"]}
assert named["wife"]["memories"] == 1 and named["wife"]["scenarios"] == ["home"]
assert named["wife"]["newest_memory"], "a caller needs to know how stale the answer would be"
assert named["neighbour"]["memories"] == 0 and named["neighbour"]["knows_about"] == ["weather"]
assert HOME_KEYS not in answer.stdout, "the card says what a holder holds, never what they hold it to be"