The client says which project it wants; the type refuses to carry that claim into the store

AideMemo

A Rust working memory for coding agents where a mutation cannot reach any backend without a server-owned authorization, and the audit row committed beside it takes its tenant and actor from the server rather than the request.

Carries 2 of 7 rubric mechanisms. Most systems here carry none or one (44%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

AideMemo is "[p]ortable working memory for coding agents and orchestrators" — MIT or Apache-2.0, Rust, version 0.1.0, 95,480 lines across 135 files in thirteen crates, shipping "[o]ne Rust binary. One embedded store. A code-first SDK, MCP tools, CLI, and native bindings."

The mechanism to take away is where the project id comes from. Almost every multi-tenant store in this corpus accepts the caller's word for which tenant or project a write belongs to, and then relies on a predicate somebody remembered to add. AideMemo makes that impossible to express:

"Command paired with server-owned authorization context."

"Returns DomainError::ProjectScopeMismatch when the untrusted envelope selects a different project."

AuthorizedCommand::authorize is the only constructor, and it compares the server's ProjectAuthorization against the envelope's project_id before producing anything. The enforcement then rides the type: MutationCommand — the single argument CommandStore::execute accepts — holds an AuthorizedCommand<()> as a field, so a backend adapter cannot be handed a mutation whose scope came from the request. There is no unscoped path to forget, because there is no unscoped value to pass.

The audit row follows the same rule, and it is transactional. The storage contract is stated on MutationCommand itself: adapters "persist its canonical fingerprint, resource mutation, receipt, change entry, and audit entry in one transaction." An AuditEntry is "[i]mmutable audit record committed with a mutation and its receipt", and the two fields a reader should check are annotated with their provenance rather than their contents — tenant_id is the "[s]erver-derived tenant", actor_id the "[s]erver-derived actor provenance". What is recorded is who the server determined acted, not who the request said did.

Two smaller lines carry the same instinct. ActorKind distinguishes Human — "[i]nteractive person" — from Agent, "[n]amed coding or reasoning agent profile", and Service, so the audit can say what kind of thing made a change and not only which identifier. And on the tenant record, display_name carries the annotation "Human-readable label; never used for authorization", which closes the rename-widens-access bug in advance.

What is absent is any judgement about the memories themselves. RecordStatus is Active, Suspended, Archived, and it governs accounts rather than claims: a suspended record "does not grant access or accept mutations", an archived project is "read-only and retained for export or audit". Nothing in the model says a stored fact is doubtful, superseded or wrong, and the only time fields are created_at_ms, updated_at_ms and a revision — a version chain over write time with no second axis.

2. Mental Model

A command is authorized before it is a command.

A scope is what the server decided, never what the envelope asked for.

An audit row commits with the mutation or the mutation does not commit.

A deletion is a tombstone replicas receive, not a row that vanishes.

A status describes the account, not the claim.

Diagram — a client envelope is bound to a server-owned authorization before it can become a mutation, the type carrying that binding is the only thing a backend accepts, and the audit row with its server-derived tenant and actor commits in the same transaction as the change
Diagram source
%% caption: a client envelope is bound to a server-owned authorization before it can become a mutation, the type carrying that binding is the only thing a backend accepts, and the audit row with its server-derived tenant and actor commits in the same transaction as the change
flowchart TB
    CLIENT["a client envelope: project_id, command_id,<br/>precondition, payload — UNTRUSTED"] --> AUTHZ{"AuthorizedCommand::authorize(<br/>server ProjectAuthorization, envelope)"}
    SERVER["the server's own ProjectAuthorization"] --> AUTHZ
    AUTHZ -->|"authorization.project_id() != envelope.project_id"| REJECT["DomainError::ProjectScopeMismatch<br/>naming requested AND authorized"]
    AUTHZ -->|"they agree"| AC["AuthorizedCommand — the only way<br/>a scope becomes trusted"]
    AC --> MC["MutationCommand carries AuthorizedCommand<br/>as a FIELD, plus fingerprint, resource,<br/>change operation and canonical body"]
    MC -.->|"CommandStore::execute accepts only this type,<br/>so no adapter can be handed a mutation<br/>whose project came from the caller"| TYPED["the scope rule is a type, not a convention"]
    MC --> TX{"one transaction, per the storage contract"}
    TX --> R1[("canonical resource: ProjectScope · revision ·<br/>Present body or Deleted tombstone")]
    TX --> R2[("receipt: command_id + fingerprint")]
    TX --> R3[("change entry — replicas follow by cursor")]
    TX --> R4[("AuditEntry — IMMUTABLE")]
    R4 --> F1["tenant_id: SERVER-DERIVED tenant"]
    R4 --> F2["actor_id: SERVER-DERIVED actor provenance"]
    R4 --> F3["project_seq · command_id · operation ·<br/>resource · committed_at_ms"]
    F1 & F2 -.->|"who changed a memory is not a string<br/>the caller supplied"| HONEST["the audit cannot be authored by its subject"]
    R2 -.->|"fingerprint rejects command-ID reuse with a<br/>different body, so a replay cannot overwrite<br/>what was audited"| IDEM["idempotency with a tamper check"]
    ACTOR["ActorKind: Human, Agent, Service"] -.->|"provenance records WHAT KIND of thing acted,<br/>not only which id"| F2
    NAME["display_name: 'never used for authorization'"] -.->|"a rename can never widen access"| SERVER
    STATUS["RecordStatus: Active, Suspended, Archived"] -.->|"governs the tenant, project or actor —<br/>never whether a stored claim is true"| NOTS["no trust-state mark"]
    CONF["conformance.rs — one backend-neutral fixture<br/>every adapter must pass"] -.->|"the refusals are checked against each store<br/>rather than argued once in the abstraction"| TX

3. Architecture

Area Role
crates/aidememo-domain/command.rs Authorization binding, the mutation type, the audit record
crates/aidememo-domain/conformance.rs The fixture every storage adapter must satisfy
crates/aidememo-domain/record.rs Tenants, projects, actors, and what their statuses mean
crates/aidememo-store-local, -store-postgres Two adapters behind one trait
crates/aidememo-client The replica cache and its change cursor
docs/MEASUREMENTS.md, benchmarks/ The public measurement ledger and its evidence

4. Essential Implementation Paths

crates/aidememo-domain/src/command.rs:190-213 — the only way a scope becomes trusted, and the error when it does not.

:244-250 — the field that makes the rule unavoidable for every backend.

:239-243 — the one-transaction storage contract, stated on the type it governs.

:306-325 — an audit record whose tenant and actor are server-derived.

crates/aidememo-domain/src/record.rs:18-28, :36 — actor kinds, and a display name barred from authorization.

5. Memory Data Model

A canonical resource keyed by tenant, project and resource reference, with a revision and a state that is either recursively key-sorted JSON or a durable deletion tombstone. Around it: commands with idempotency keys and canonical fingerprints, receipts, change entries with a project sequence, and immutable audit rows.

6. Retrieval Mechanics

Facts, graph traversal and history over canonical state, with replicas following a change feed by cursor and a local cache materialising it. Cursor epoch and range mismatches are refused with named errors rather than silently reset.

7. Write Mechanics

Every mutation carries an idempotency key and a fingerprint over "the real project, precondition, operation, and payload". A retry with the same id and the same body replays the stored receipt; a retry with the same id and a different body is rejected. The mutation, its receipt, its change entry and its audit row commit together.

8. Agent Integration

One binary serving a CLI and an MCP surface over both stdio and HTTP, a Python agent SDK, and native bindings for Python, Node, Elixir and C — with a COMPARE.md that sets itself against mem0, Graphiti and Letta by name.

9. Reliability, Safety, and Trust

The strong parts are the type-enforced scope, the transactional and server-derived audit, the idempotency fingerprint, and a conformance fixture that holds every backend to the same observable outcomes. The absent part is any statement about the content: nothing marks a memory as stale, disputed or corrected, and there is no second time axis to ask what was true rather than what was written.

10. Tests, Evals, and Benchmarks

A backend-neutral conformance fixture that "describes outcomes, not I/O" and is run by each adapter, an identity conformance suite beside it, a benchmarks tree, and a public measurement ledger that states where durable numbers live and deliberately ignores raw scenario output "because it contains temporary paths and run-specific identifiers".

11. For Your Own Build

Make the authorized scope a different type from the requested one. A predicate can be forgotten on one read path; a constructor that refuses a mismatched envelope cannot, and a store that only accepts the authorized type has no unscoped path left to audit.

Derive the audit's actor on the server. An audit row whose actor field is filled in by the actor is a record of a claim, not of an act.

Put the transactional requirement on the type the adapters implement. "Persist … in one transaction" written on the command struct is a contract every backend author reads; the same sentence in a README is one they may not.

Annotate the fields that must never be used for authorization. display_name: "never used for authorization" costs nothing and forecloses a whole class of bug.

12. Open Questions

Whether a memory will ever carry a status of its own. Every guarantee here is about who may write and what was written; none is about whether what was written is still true, and a working memory for long-running agents eventually needs that vocabulary.

Whether the conformance fixture will grow retrieval assertions. It is the right instrument, already run against every backend, and it currently checks that bad commands are refused rather than that withdrawn material stops coming back.

Appendix: File Index

Path What to read it for
crates/aidememo-domain/src/command.rs:190-213 A scope the caller cannot assert
:244-250 Why no backend can be handed an unscoped mutation
:306-325 An audit row whose actor is derived, not declared
crates/aidememo-domain/src/record.rs:36 One annotation that forecloses a class of bug
crates/aidememo-domain/src/conformance.rs:1-6 One fixture, every storage backend

History

2026-09-1658b803cc… — first reading, at a commit dated 16 September 2026. Screened before opening, from a shallow clone: two auto-run surfaces, five build-time execution points, five unpinned dependency surfaces and twenty-seven dependency files inside the seven-day cooldown, with Cargo.lock present. AGENTS.md and CLAUDE.md are addressed to a reading agent and were recorded as data. Nothing was installed, built or run, and no benchmark was reproduced — the measurement ledger is cited as the project's own record rather than as a verified result.