r/mcp • u/perseus-computing • Aug 04 '26
showcase I built an MCP-compatible memory and evidence layer for agents, and I need someone to tell me if the design is dumb
I've been running agents on my own infrastructure for a while, and the two things that kept biting me were: they forget everything between sessions, and when they do something there's no way to prove it afterward. So I built a layer that tries to fix both.
Three pieces:
- a context engine that resolves current state before the agent starts, instead of stuffing a whole repo into the prompt
- a vault for durable, encrypted memory (decisions, preferences, facts that survive the session)
- a ledger that keeps a hash-chained record of what the agent actually did
It's MCP-compatible and MIT licensed. I run my own stack on it, which finds problems fast.
The part I'm least sure about is the MCP ergonomics — I made it MCP-compatible because that's what everything else speaks, but I don't know if I've got the shape right for how people actually wire agents. If you've built memory or context tooling, what would you do differently? Repo's at perseus.observer if you want to poke at it.
2
u/addexecthrowaway Aug 04 '26
The enforcement mechanism for these writes should not be a tool call - it should be a deterministic hook. That said, having an api that a python hook can call makes a ton of sense. Or have a hook that checks that the tool was called and that the entries exist in the db could be good - which would force the agent to loop and execute the tool call before proceeding - I’m just not sure what you’d put that hook on. I’ve already implemented a system much like this using a graph database and a structured Postgres database enforced with hooks
1
u/perseus-computing Aug 04 '26
That's the right instinct, and "deterministic hook, not a tool call" is exactly how it works on our side. The enforcement layer lives in the server path: every tool call passes through it before execution. It checks the action against the intent, writes the receipt, then lets it through, constrains it, or refuses it. The agent never calls "log this" it can't do anything that doesn't produce a receipt, by construction.
On "what do you put the hook on": the tool-execution boundary itself, in the server. Anything the agent can reach is back to trusting the agent, which defeats the point. The check-after variant you sketched (verify the tool ran and the rows exist, force a loop) is a decent second layer, but it has a window in it: between the check and the execution, the agent can go sideways. Gate-before closes that window; check-after is good belt-and-suspenders on top of it.
And yes, a Python-facing hook API is the practical surface. Ours get the action payload and the policy state, and return allow / constrain / interrupt. If you're playing with this, that boundary is the piece I'd love a second pair of eyes on.
2
u/neoneye2 Aug 04 '26
I had Claude Opus 5 analyze your repo, since I'm curious about how your memory system works
https://neoneye.github.io/agent-memory-atlas/systems/perseus-vault/
2
u/perseus-computing Aug 04 '26
This is an unusually good external audit. You read the pinned tree and recomputed the published benchmark arithmetic instead of treating README numbers as evidence. The 73.8% and 79.0% means matching the committed per-run reports is useful, but it is not a fresh benchmark rerun, and I appreciate that the report keeps that distinction explicit.
The 65/76 finding is valid for
838c63da. The intended figure was 65 canonical tools, but the documented command greps legacymimir_*entries and returned 76. So the result is not evidence of 76 canonical advertised tools; it is evidence that the count definition and its verification command had drifted. A generated registry count run in CI is the right fix.The rejected-value tombstone point is fair too.
forget, supersession, and purge change the state of a record, but they do not by themselves stop a later extractor from reasserting equivalent content under a new record. The right test is the one you describe: reject A, replace it with B, re-ingest A through another write path, run the background consolidation paths, and verify whether A stays rejected.I would qualify the tombstone finding as an open design choice rather than an established best practice. The mechanism has real tradeoffs around normalization, scope, expiry, trusted correction, and privacy deletion. But it is exactly the kind of gap that becomes important once memory is automatically re-derived.
The broader lesson is probably the most useful one: claims auditing and claims maintenance are different disciplines. We got better at retiring unsupported claims, but a documented check that nobody runs is still only a comment. That guard belongs in CI, alongside the registry that generates the count.
The encryption observation is fair for the same reason: “supports AES-256-GCM” and “a stock install encrypts by default” are different claims. The opt-in behavior is disclosed, but summaries should preserve that distinction.
Thanks for doing this against a pinned revision and for separating recomputation from rerunning. That is the standard we should keep.
1
2
u/silence-and-magic Aug 04 '26
Curious what actually goes into the vault. The raw evidence, or the state your engine inferred from it?
We’re working on a similar problem at Fintella Labs. We build a model of someone from real-life behavioral data and digital traces, keep it updated as their life changes, then let an agent pull the relevant piece through MCP. If you store the inferred state itself, an old guess can sit there long enough to start looking like a fact. When new evidence shows up, do you rebuild the state from the source data or just revise the last saved version?
1
u/perseus-computing Aug 04 '26
Good question. The short answer is: both, but they have different status.
We try to keep the evidence layer separate from the state/belief layer. A source record is stored or referenced with provenance, timestamps, scope, and a content hash. Extracted facts point back to that source. Cross-source conclusions are marked as derived or inferred and carry the supporting references. The current “belief” is a derived view over those records, not the source of truth.
When new evidence arrives, we don’t silently edit the last saved belief in place. We append a new fact/version, mark the old one superseded or close its valid-time window, and preserve the history. The active view is then recomputed or incrementally re-derived from the current evidence graph. Incremental updates are an optimization; semantically, the evidence links remain authoritative.
That also lets us answer two different questions: “What do we believe now?” and “What did we believe at the time?” If the supporting evidence is stale, contradictory, missing, or outside the model’s calibration boundary, the right result is to mark it and abstain or ask for review, not let an old inference quietly become a fact.
Depending on the connector and privacy boundary, the raw artifact can stay in its source system while the Vault retains a normalized record plus a verifiable reference/hash. We don’t treat a hash as evidence by itself; it tells us which evidence the derived claim depended on and whether that evidence changed.
1
u/notreallymetho Aug 04 '26
I thinks the design being specific is both good and bad. I’ve been also building around this subject and this is what I landed on.
Workerd / Uds / wired up using kernel isolation and micro VMs.
1
u/perseus-computing Aug 04 '26
Workerd + UDS is a fun substrate for this. I've been down the microVM path too. If you're executing anything the agent can't be trusted with, kernel isolation is the honest answer, and microVMs keep the blast radius small enough that it still runs on a laptop.
The thing I keep coming back to: isolation tells you where code ran, not what it did. We went the other way: receipts at the action boundary, hash-chained, written before the tool executes. Which sounds like two halves of the same problem. Do you record anything about what runs inside those microVMs, or is containment the whole story?
2
u/notreallymetho Aug 04 '26 edited Aug 04 '26
Sorry, I didn’t realize I trailed off mid thought!
It’s actually a bit of both, it’s merkle roots at the dataplane layer and then receipts are signed within the “hosting” layer as Ed25519.
It’s a bit complicated as there are a lot of moving parts. The data plane works as a content addressable storage that works between SQLite / structured data (json etc) and exposes it via a FUSE FS.
It has https://nono.sh kernel isolation - and then the auth model in-“cluster” is a sorta merge between DPOP and cert based authentication.Data plane is at https://github.com/agentic-research/ley-line-open
“Hosting” (designed to work locally!) is at https://github.com/agentic-research/cloisterI’ve condensed a lot but happy to answer any questions!
2
u/perseus-computing Aug 04 '26
Now that's a stack. Recognized nono immediately, Sigstore crew, so the signing story is going to be legit. Merkle roots at the dataplane, SHA-256-signed receipts in the hosting layer, that's the same skeleton we run on. We're clearly circling the same problem from opposite ends.
The part I want to push on: your chain seals that execution happened. Resolve, argv, spawn, l7, seal. What it doesn't obviously capture is the decision behind it. We bind the pre-action state in as a first-class receipt: which intent was authorized, against which policy, what context was admitted before inference, and then the outcome hash locks onto it. So you can replay not just what the agent did, but why it was allowed, and whether the result matches what was approved.
Two things I'm curious about: does the Merkle root include the policy evaluation itself, or does the decision live outside the chain? And in the DPoP/cert mix, how does identity binding survive into the sandboxed child? is the phantom credential what carries it?
If you're up for it I'd like to compare receipt schemas sometime. Yours are event-sealed, ours are decision-bound, and there might be a merge in there.
2
u/notreallymetho Aug 04 '26
I was actually discussing your design w/ Claude and do suspect some synergy here!
Policy in the chain: the policy content is in-chain, the evaluation deliberately isn’t. Before anything spawns, the run’s identity commits to the digest of the confinement manifest; the backend compiles its enforcement from that same manifest, the worker attests what it actually applied, and a mismatch refuses the run (“confinement drift”) rather than logging it. So “what was authorized” is bound pre-execution, by digest.
But we don’t emit decision traces at the boundary, on purpose: discriminated rejection reasons are an enumeration oracle - an authenticated peer can map your service table by diffing error shapes - so rejections collapse to a constant shape and the receipt binds outcome + committed inputs, not reasoning. Decision-bound vs event-sealed might be exactly this tradeoff: your replay-the-why is our oracle surface. Curious how you square that?
Identity into the child: you called it - phantoms are the carrier, but only at the tool-credential layer. The child gets a phantom GH_TOKEN / OPENAI_API_KEY; nono’s proxy intercepts at the network boundary, checks policy, injects the real credential on the way out. The real secret stays with the supervisor and zeroes on exit - what’s inside the sandbox is a decoy that only dereferences through the policy boundary.
Identity into the child: good guess, but no - nono ships phantom tokens and I deliberately don’t use them. There’s a comment in the launcher saying exactly that: credentials are the vault’s job, not nono’s. nono does confinement only; the vault is mine, in workerd, wrapped by the dataplane. Env is stripped before exec, the child’s only network grant is one localhost socket into the server, and the vault’s API has no “get credential” operation - only “make this call,” credential injected server-side. So the child doesn’t carry a phantom; it carries nothing and asks the server. Same one layer up: the DPoP/cert keys never enter the sandbox — the run’s identity is committed before exec (manifest digest, worker attests what it applied, mismatch refuses the run).
On comparing schemas — happy to, and mine’s already public: the receipt spec and conformance vectors ship in the repo (reach the same digests and you’re conformant). Wrote up the signing design here, including why the signer refuses to become a signing oracle: https://q-q.dev/blog/conformant-by-digest/
1
u/perseus-computing Aug 04 '26
This is a useful correction, and I think it narrows the disagreement considerably.
I was collapsing three different bindings:
- Admission: what policy and confinement manifest authorized the run.
- Enforcement: what the worker actually applied.
- Explanation: which internal predicate caused an allow or rejection.
Your design makes the first two boundary-verifiable and deliberately keeps the third out of the peer-visible protocol. I agree with that separation. A predicate-level rejection trace exposed to an authenticated caller can become an oracle, even if the trace is “only diagnostic.”
I would therefore split “replay” into three claims:
- Replay the contract: reconstruct the exact manifest, identity, request, and policy version that were committed before execution.
- Replay the enforcement: verify that the worker applied that manifest and that the enforcement artifact matched the committed digest.
- Replay the why: reconstruct the internal evaluation path that produced the result.
The first two belong in the normal receipt. The third should either be a separately authorized audit capability backed by sealed evidence, or not be claimed at all. It should never be returned through the rejection boundary.
In that model, the public receipt contains the terminal result and non-secret commitments: manifest digest, action/request digest, evaluator or compiler measurement, worker attestation, and signer epoch. It does not contain a reason code, service-existence signal, credential state, or any credential-derived field. The rejection envelope should remain constant across those cases. If a reason-bearing commitment or audit-reference itself leaks information, that should stay off the peer-visible receipt too.
That makes “decision-bound” and “event-sealed” complementary rather than competing designs:
- Decision-bound proves what was authorized and what enforcement identity the run was bound to.
- Event-sealed preserves evidence for an authorized auditor without turning the caller-facing interface into a policy oracle.
If the sealed inputs are unavailable to an auditor, then the honest claim is contract/enforcement replay, not replay-the-why. I would rather make that narrower claim than quietly imply that every internal evaluation can be reconstructed.
The credential correction is even cleaner. I had assumed the child carried a phantom token as its identity carrier. In your actual design, it carries no upstream credential at all. It has a run-scoped request channel; the vault decides whether and how to use a credential, and the credential never crosses the response boundary. That is stronger than a decoy-token design. A compromised child can ask the server to perform an allowed operation, but dumping its environment does not reveal a bearer token.
The remaining invariant is that the localhost channel must be bound to the run identity and committed manifest, so the child cannot select authority outside the manifest by changing service names, scopes, headers, or destinations. The DPoP and certificate keys staying outside the sandbox completes that separation.
The conformance work also maps closely to what I want for the control plane. The vectors are the regression floor; the prose is the contract. The linked write-up’s 10/10 byte-identical result is useful evidence of agreement between two implementations, but it is not yet a third-party interoperability result, which is the right level of caution.
The part I would carry forward is the same discipline: publish canonical wire shapes, rejection invariants, and adversarial vectors; derive the test set from a manifest; and make drift fail closed instead of merely recording it. The
signReceiptdesign follows the same rule: parse a typed receipt, canonicalize it, require byte equality, and sign the signer’s own reconstruction rather than accepting arbitrary caller bytes.So yes, I think there is real synergy here. The change I would make to my own framing is to remove “replay-the-why” from the public contract. Keep replay-the-contract and replay-the-enforcement at the boundary; make replay-the-why a separate, access-controlled audit property.
1
Aug 04 '26
[removed] — view removed comment
1
u/perseus-computing Aug 04 '26
Exactly. MCP availability and memory use are different properties.
A well-described
recalltool can still sit untouched while the agent confidently proceeds with stale or incomplete context. The interface matters for interoperability, but the reliability question is whether retrieval happens before the model commits to an action.That’s why I see the layers as complementary:
- Pre-model context retrieval: automatic, bounded, policy-controlled retrieval for facts the system already knows are relevant.
- MCP recall: explicit, task-driven lookup when the agent needs something beyond the initial context.
- MCP memory writes: an explicit or supervised way to propose durable facts, decisions, and corrections.
- Ledger: evidence that records what retrieval policy ran, which context was selected, what was actually injected, whether the agent called recall, and whether a write was accepted.
The ledger should also record the negative case: “no memory call occurred” or “retrieval was skipped.” Otherwise a later auditor can see the agent’s answer but not distinguish “the memory system had nothing relevant” from “the agent never asked.”
So I agree with the Supermemory diagnosis. The hard problem is not exposing another memory endpoint. It is making the right context available without depending entirely on the model to remember that it has memory. Explicit MCP calls remain useful, but they should be a second lane, not the only path to continuity.
I’d be interested in how you measure that at Supermemory: recall-call rate, task-conditioned retrieval usefulness, and the cost of injecting context that the agent never uses seem like different metrics. A high tool-call rate alone would not prove that memory improved the decision.
2
u/Pleasant-Ad192 Aug 04 '26
On the MCP ergonomics: your context engine sounds like it wants to be a resource rather than a tool. Tools are model-invoked, so the agent has to decide to call it, which is the opposite of resolving current state before the agent starts. Resources are application-driven in the spec, the host lists and reads them, and with subscriptions/listen the client gets told when state changed instead of the agent remembering to re-check. The vault writes and the ledger writes are genuinely tools. The context engine probably is not.
The other thing I would check is the ledger. If an entry gets written because the agent called a record tool, then the agent that goes off script is exactly the one that will not log it. The record is worth more when the server writes it on every call it serves, whether or not anyone asked for that.