Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vta-agent-memory",
"description": "Durable agent memory stored in your own Verifiable Trust Agent, not in the tool. Save and recall facts across sessions, scoped to a VTA trust context you control and can revoke.",
"version": "0.6.1",
"version": "0.7.0",
"keywords": [
"memory",
"vta",
Expand Down
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ src/
enrol.rs two-phase enrolment (init/connect) — the primary path
pnm.rs resolving which VTA, and where its pnm session actually lives
lazy.rs connect on first use, so the server exists when the VTA doesn't
main.rs CLI: serve | setup | recall | list | forget | doctor
main.rs CLI: serve | setup | recall | list | forget | export | import | doctor
setup.rs the online provisioning flow
config.rs ~/.config/vta-agent-memory/config.json (0600)
store.rs typed ops over vta/memory/* — the only VTA-facing code
record.rs the record shape, key encoding, and recall ranking
authorship.rs signing records on save, verifying author + location on read
provenance.rs basis / confidence / evidence: vocabulary, checks, rank weight
lifecycle.rs status, validUntil/reviewAfter, history keys, standing
bundle.rs the signed export/import bundle (docs/memory-bundle.md)
server.rs the nine MCP tools
tests/
common/ the in-process fake VTA + signers, shared
memory_roundtrip.rs Store end-to-end against an in-process fake VTA
bundle.rs export -> import, and tampered bundles refused
eval.rs, eval/scenarios/ memory quality evaluation — scored scenarios
mcp_starts_without_a_vta.rs the real binary, over stdio, with no VTA
hook_contract.rs the SessionStart hook's contract
Expand Down Expand Up @@ -183,6 +185,16 @@ has to mean forget. A status change is a new version signed by whoever made it;
only `retract` is allowed on a refused record, because anything else would
re-sign content nobody vouches for.

**An import keeps the original author, verifiably.** A record's signature names
the context it was written in, so an imported record is signed by the importer
for where it now is and carries the original verbatim in `importedFrom`;
`authorship::verify` checks both proofs, the key, and that the content is the
same, then attributes the memory to the original author. Do not "simplify" this
by relaxing the location check for imports, or by re-signing as the importer —
the first lets any record be relocated, the second loses who wrote it. Any
later save drops `importedFrom` (`Store::save_versioned`); only `Store::import`
writes it. Opening a bundle is all or nothing.

## The retrieval constraint

`vta/memory/list/0.2` pages a context in ascending key order and can narrow by
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vta-agent-memory"
version = "0.6.1"
version = "0.7.0"
edition = "2024"
rust-version = "1.95.0"
description = "Agentic memory for Claude Code, stored in a Verifiable Trust Agent"
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ included in its encrypted backups. Revoking this machine's access is one

A Claude Code plugin with:

- **Six MCP tools** — `memory_recall`, `memory_get`, `memory_save`,
`memory_forget`, `memory_list`, `memory_context`.
- **Nine MCP tools** — `memory_recall`, `memory_get`, `memory_save`,
`memory_retract`, `memory_dispute`, `memory_deprecate`, `memory_forget`,
`memory_list`, `memory_context`.
- **A skill** (`agent-memory`) that says *when* to save and recall, what is
worth keeping, and what must never go in. This is most of the value; the tools
are the easy part.
Expand Down Expand Up @@ -266,6 +267,15 @@ reason) or `retracted` (a tombstone that never recalls) — plus `validUntil`
the "this was wrong" path; `forget` is still the hard delete, and takes the
history with it.

**Memories can leave the VTA without losing who wrote them.** `export` writes a
signed bundle of a context's memories — optionally narrowed by `--type`,
`--basis` or `--status` — with each record's own author signature intact and the
exporter's signature over the whole. `import` checks both and refuses a bundle
that fails either, whole. An imported memory is signed by the importer for where
it now is and carries the original, so it is still attributed to whoever first
wrote it. The format is documented in [`docs/memory-bundle.md`](docs/memory-bundle.md)
for readers that are not this crate.

**Memory is not application state.** "Forget everything" has to stay a safe thing
for a user to ask, which it stops being the moment account state lives here. The
VTA's `vta/app-state/*/1.0` family — versioned, namespaced, with a change feed —
Expand All @@ -282,6 +292,8 @@ vta-agent-memory serve # MCP over stdio (the default)
vta-agent-memory recall [--query Q] [--type T] [--limit N] [--full] [--format text|json]
vta-agent-memory list [--type T]
vta-agent-memory forget <key>
vta-agent-memory export [--out FILE] [--type T] [--basis B] [--status S]
vta-agent-memory import FILE|- [--replace] [--dry-run] [--type T] [--basis B] [--status S]
vta-agent-memory doctor
```

Expand Down
162 changes: 162 additions & 0 deletions docs/memory-bundle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Memory bundle format

A **memory bundle** is a signed JSON file holding some or all of the memories in
one trust context. `vta-agent-memory export` writes one, and
`vta-agent-memory import` reads one. This page describes the format so that
software other than this crate can read and check a bundle. You don't need a VTA
or this crate to do that.

## Example

```json
{
"type": "VtaAgentMemoryBundle",
"bundleVersion": 1,
"exportedBy": "did:key:z6Mk…",
"exportedAt": "2026-11-11T09:30:00+00:00",
"sourceContext": "home",
"filter": { "basis": ["declared", "verified"] },
"records": [
{
"key": "feedback/tabs",
"version": 7,
"record": {
"v": 2,
"name": "Tabs",
"type": "feedback",
"description": "indent with tabs",
"body": "…",
"links": [],
"updatedAt": "2026-10-02T08:00:00+00:00",
"basis": "declared",
"confidence": 0.9,
"reviewAfter": "2030-01-01T00:00:00+00:00",
"author": "did:key:z6Mk…",
"authorKind": "operator",
"contextId": "home",
"key": "feedback/tabs",
"proof": { "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", "…": "…" }
}
}
],
"proof": { "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", "…": "…" }
}
```

## Top-level fields

| field | meaning |
|---|---|
| `type` | Always `VtaAgentMemoryBundle`. |
| `bundleVersion` | `1`. A reader must refuse a version it doesn't know. |
| `exportedBy` | The DID that signed the bundle. Must be the signer of `proof`. |
| `exportedAt` | When the bundle was made (RFC 3339). |
| `sourceContext` | The trust context the memories came from. Every record's own `contextId` must equal it. |
| `filter` | What the export was narrowed to: `types`, `basis` and/or `status` lists. `{}` means everything that could be exported. A non-empty filter tells the reader the bundle is a subset. |
| `records` | One entry per memory: `key` (`<type>/<slug>`), `version` (the source VTA's version number, for information only) and `record`. |
| `proof` | The exporter's Data Integrity proof over everything above it. |

## The record

`record` is the memory **exactly as it was stored**, including its author's own
proof. Exporting never re-signs a record. Its fields are:

- **Content**: `name`, `type` (`user`, `feedback`, `project` or `reference`),
`description` (one line), `body`, `links` and `updatedAt`.
- **Provenance**: `basis` (`declared`, `observed`, `verified` or `inferred`),
`confidence` (0 to 1, the author's own estimate; never compare it across
authors) and `evidence` (citations, each with a `kind` of `credential`,
`memory` or `record`). This is the same vocabulary as Data Rooms records.
- **Lifecycle**: `status` (absent means `active`; otherwise `deprecated`,
`disputed` or `retracted`), `statusReason`, `validUntil` and `reviewAfter`.
The last two are RFC 3339 dates.
- **Authorship**: `v` (`2` for signed records), `author`, `authorKind`
(`agent` or `operator`), `contextId`, `key` and `proof`.
- **`supersedes`**: where the version this one replaced is kept in the source
context's history. History itself is not exported.
- **`importedFrom`**: present only if the record was itself imported into the
source context. See below.

A reader should ignore fields it doesn't know about.

## Checking a bundle

A bundle is accepted only if all of the following hold. If any check fails,
reject the whole bundle. `import` writes nothing from a bundle that fails a
check.

1. `type` and `bundleVersion` are as above.
2. The bundle's `proof` verifies over the bundle with `proofPurpose`
`assertionMethod`, and its signer is `exportedBy`. This check catches a
record that was added, removed, edited or moved between bundles.
3. For each record:
- Its `proof` verifies over the record as it appears in the bundle, and
`author` is the signer.
- `contextId` equals `sourceContext`, and `key` equals the entry's `key`.
- The key is `<type>/<slug>` and matches the record's own `type` and
slugified `name`.
- Unsigned records are not accepted.
4. No key appears twice.

Proofs are W3C Data Integrity proofs using `eddsa-jcs-2022` (or
`ecdsa-jcs-2019`), canonicalised with JCS (RFC 8785). That means a bundle can be
re-indented or have its keys reordered without breaking any signature. This
crate resolves only `did:key` verification methods, so checking a bundle needs
no network access.

Steps 2 and 3 do different jobs. The record proofs say who wrote each memory;
the bundle proof says which memories belong together. Each check on its own
leaves a gap: a forger can re-sign a bundle around edited records, or present
genuine records in a bundle with some of them taken out.

## Imported records

A record's signature names the context it was written in, so an imported record
cannot keep that signature as its own: in the new context it would read as
having been moved there. `import` therefore writes a new record signed by the
importer for its new location, and keeps the original, verbatim, in
`importedFrom`. To accept a record that carries `importedFrom`, a reader checks
that:

- The outer record's proof holds for where it was found. The outer record is
the importer's.
- `importedFrom` is a signed record whose `author` is its signer, whose `key`
matches, and which does not itself contain an `importedFrom`.
- The content of the two records is the same. Content here is everything except
`v`, `author`, `authorKind`, `contextId`, `key`, `proof`, `supersedes` and
`importedFrom`.

If all of that holds, the memory is attributed to the **original** author, and
the importer and the original context are shown alongside. An importer can't
change what a memory says without breaking that attribution.

Importing a memory that was itself imported carries the first original rather
than nesting another layer. The original author stays the author however many
times a memory moves. Saving or changing the status of an imported memory drops
`importedFrom`: from then on the person who saved it is the author, and the
imported version is kept in history.

## What is left out of a bundle

- **Refused records**, whose signature doesn't hold, and **unattributed
records**, written before records were signed. Neither has an author a
receiver could check. `export` lists them. Saving an unattributed memory again
signs it.
- **History.** A bundle holds the current version of each memory.

## Commands

```
vta-agent-memory export [--out FILE] [--type T]… [--basis B]… [--status S]…
vta-agent-memory import FILE|- [--replace] [--dry-run] [--type T]… [--basis B]… [--status S]…
```

Each filter option narrows the set. You can repeat an option or give it a
comma-separated list. `unstated` selects records with no `basis`. `--out`
writes the file readable by its owner only. A bundle holds your memories, so
treat it like any other private file.

`import` checks the whole bundle before it connects to a VTA. Without
`--replace`, a memory that already exists under the same key is left alone.
With `--replace`, the import becomes a new version of that memory and the
replaced version goes to history.
6 changes: 5 additions & 1 deletion skills/agent-memory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ Every memory `memory_recall`, `memory_get` and `memory_list` return carries an

- `"status": "verified"` — signed by `did`, and the signature holds for this
memory where it was found. `kind` is `agent` or `operator`; `thisAgent: true`
means this machine's own identity wrote it.
means this machine's own identity wrote it. With `importedFrom` and
`importedBy`, the memory was carried in from another context by `import`:
`did` is still whoever originally wrote it there, checked against their own
signature. It is a snapshot from that context, so it may be older than it
looks.
- `"status": "unattributed"` — written before memories were signed. Readable,
but nothing vouches for who wrote it; give it less weight than a verified one
when they disagree.
Expand Down
Loading
Loading