Skip to content

feat: sign memory records and verify authorship on read (#31) - #43

Merged
stormer78 merged 3 commits into
mainfrom
feat/signed-records
Nov 10, 2026
Merged

stormer78 merged 3 commits into
mainfrom
feat/signed-records

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Closes #31.

What changes

  • Every save is signed by the writer's own did:key: a Data Integrity proof (vta_sdk::trust_task_sign::sign_object_for_assertion, the primitive Data Rooms use for sealed record bodies) over a v2 record that names author, authorKind (agent | operator), contextId and key. A Store without a signer refuses to save.
  • Every read verifies before a value becomes an Entry (Store::check, the single place). A record is refused — not returned as memory, but reported by key + reason — when it:
    • was altered after signing,
    • was copied to another key or context,
    • claims an author other than its signer, or
    • is v >= 2 with its proof stripped (refused, not downgraded).
  • Legacy v0/v1 records stay readable, marked unattributed.
  • Output: recall/get/list summaries carry an author block (verified with did/kind/thisAgent, or unattributed); recall and list carry refused; the SessionStart hook notes other writers and lists refusals; doctor prints the signing DID.
  • The signer is read from the session store after connecting (enrolment rotates the key on first connect). setup/enrol fail early if the session cannot sign.
  • Verification uses a did:key-only resolver: no network I/O, so a stored record cannot make recall fetch anything.

Skill, README and CLAUDE.md updated. Plugin and crate bumped to 0.4.0.

Acceptance

  • Record schema v2 carries author plus signature; v1 still decodes.
  • Tampered or relocated records are refused on recall, with tests (a_tampered_record_is_refused_on_recall_list_and_get, a_relocated_record_is_refused, plus stripped-proof and forged-author cases).
  • Recall output names the author kind.

Known limits

  • A writer with context access can still write an unsigned v1-style record; it shows as unattributed, never verified. A "require signed" config switch would close that — not added here.
  • Config::signer() (session-store load, post-rotation) isn't covered by tests; it needs a real session. Not yet exercised against a live VTA — nothing on the wire changes except the content of the stored value.

Testing

cargo fmt, cargo clippy --all-targets clean; all suites pass (82 unit, 29 roundtrip, plus binary/hook/stdio tests).

Every save is now signed by the writer's own did:key: a Data Integrity
proof (the SDK's sign_object_for_assertion, as Data Rooms use for sealed
record bodies) over a v2 record that names its `author`, `authorKind`
and the `(contextId, key)` it was written to.

Every read checks it before a value becomes an Entry. A record that was
altered after signing, relocated to another key or context, claims an
author other than its signer, or is v2 with its proof stripped is
Refused: not returned as memory, but reported by key and reason from
recall, list, get and the SessionStart hook. Legacy v0/v1 records stay
readable, marked `unattributed`. Verification is did:key-only and does
no network I/O.

The signer is loaded from the session store after connecting, since the
first connect of an enrolled identity rotates its key; setup and enrol
fail early if the session cannot sign. `doctor` reports the signing DID.

Bumps the plugin to 0.4.0.

Closes #31

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…32)

Every save now states what kind of claim it is, inside the record's
signature: `basis` (declared | observed | verified | inferred), an
optional `confidence` (0–1), and `evidence` citing a credential, another
memory, or a data-room record. The vocabulary and the credential/record
citation shapes are the ones rooms/records/put/0.5 publishes, so a
record keeps its meaning between memory and a room.

- memory_save requires `basis`; `verified` must cite evidence;
  confidence must be in [0, 1]. Checked before the store, like the
  size limits.
- Recall/get/list show `basis` and `confidence` (get adds `evidence`);
  a `verified` record citing nothing carries a `basisNote`. Records
  from before provenance read as `unstated`.
- Ranking multiplies relevance by a basis weight (declared/evidenced
  verified 10, observed 9, unstated 8, inferred 6), and breaks empty-
  query ties on it, so stated memories survive a limit ahead of
  inferred ones. Confidence is never ranked on: the rooms schema says
  it is not comparable across authors.
- The hook/CLI render notes the basis; the skill and /remember say how
  to choose one.

basis and evidence are kept as strings/raw values in the record so a
later writer's unknown basis or citation kind does not make the record
undecodable.

Bumps the plugin to 0.5.0.

Closes #32

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
feat: epistemic provenance on memories — basis, confidence, evidence (#32)
@stormer78
stormer78 merged commit 8bf8e0e into main Nov 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sign memory records and verify authorship on recall

1 participant