The self-poisoning detector fires only on writes you labelled

OWASP Agent Memory Guard

An Apache-2.0 guard that wraps an agent's memory writes and runs eleven detectors over them — injection, leakage, privilege escalation, cross-task contamination, and a self-reinforcement loop detector that refuses to count an agent's restatement of its own claim as corroboration — whose central defence is inert unless the integrating code declares each write's source class.

Carries 0 of 7 rubric mechanisms. Most systems here carry none or one (41%), 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

OWASP Agent Memory Guard is Apache-2.0, Python, version 0.3.2 — a library that wraps an agent's memory writes and reads, runs eleven detectors over them, and emits SIEM-shaped security events. It ships with a LangChain adapter, an MCP server, a GitHub Action, semgrep rules and a standalone scanner, and carries 168 tests across seventeen files.

It is not a memory system and holds no memories of its own, which is why it carries no marks. It is in this atlas because it is the only subject here whose whole purpose is to name what can go wrong with somebody else's memory, and its taxonomy is worth reading against the failures the rest of this corpus exhibits: injection, leakage, privilege_escalation, tool_abuse, excessive_autonomy, ml_injection, memory_persistence_injection, protected_keys, cross_task, anomaly, and self_reinforcement.

That last one is the reason to read it. Its docstring describes, in four lines, a failure this atlas has found repeatedly and never seen stated so cleanly:

"an agent reads its own prior agent_authored memory, mildly elaborates on it, writes it back, then reads the elaborated version on the next turn and elaborates again. Over a few iterations a hallucination or attacker-suggestion is reinforced into a durable 'fact' the agent now relies on."

The two rules it enforces per key are the right two. A cool-down: at most N consecutive agent-authored writes within a window. And self-similarity: a write resembling a recent agent-authored value on the same key "is treated as reinforcement of the previous write, not independent corroboration."

The sentence that makes it a real defence rather than a rate limit is the decay clause: "A separate external_tool or user_input write decays the counter (independent evidence weakens the loop)." Corroboration only counts when it comes from a different source class. Several systems in this corpus count an agent's own restatement as a second sighting; this is the module that says why they should not.

The source-class taxonomy behind it is explicit about which side of the trust boundary each value sits on: external_tool and user_input are "external inputs (untrusted by default)", agent_authored is "the self-poisoning surface", system is "config/admin/runtime infrastructure".

And then there is where it stops. source_class is a parameter the integrating code passes. There is no derivation, because there cannot be — a library wrapping a dictionary write has no way to know whether the value came from a model or a user. The default is UNKNOWN, and the detector's own docstring is unambiguous: it "[o]nly fires on writes whose source_class is AGENT_AUTHORED", returning early for anything else.

So an integration that calls the guard without labelling its writes gets the injection and leakage detectors and none of the self-poisoning protection, and nothing tells it so. The legacy fallback narrows it further: source_type maps to a class automatically, and only MODEL_INFERENCE becomes agent_authored.

That is a fair design under the constraint, and it is the fact a reader needs before assuming the headline defence is on.

2. Mental Model

A source class says which side of the trust boundary a write came from, and the caller declares it.

A detector inspects one operation and returns a severity and an action — block or flag.

Self-reinforcement is the loop where an agent's own elaboration becomes its own corroboration, and only a different source class breaks it.

Diagram — the self-poisoning detector needs the caller to say a write is agent-authored; unlabelled writes default to unknown and are skipped, silently
Diagram source
%% caption: the self-poisoning detector needs the caller to say a write is agent-authored; unlabelled writes default to unknown and are skipped, silently
flowchart TB
    CALL["host calls guard.wrap / check / protect<br/>around a memory write"] --> SC{"source_class supplied?"}
    SC -->|"explicit"| CLS["external_tool · user_input ·<br/>agent_authored · system"]
    SC -->|"legacy source_type"| MAP["USER_INPUT → user_input<br/>TOOL_OUTPUT → external_tool<br/>MODEL_INFERENCE → agent_authored<br/>SYSTEM → system<br/>anything else → UNKNOWN"]
    SC -->|"neither"| UNK["UNKNOWN"]
    CLS & MAP & UNK --> DET["eleven detectors: injection · leakage ·<br/>privilege_escalation · tool_abuse ·<br/>excessive_autonomy · ml_injection ·<br/>memory_persistence_injection ·<br/>protected_keys · cross_task · anomaly"]
    DET --> SR{"self_reinforcement"}
    SR -->|"source_class != AGENT_AUTHORED"| SKIP["returns early —<br/>an unlabelled write is not<br/>checked for self-poisoning,<br/>and nothing says so"]
    SR -->|"AGENT_AUTHORED"| RULES["1. cool-down: max consecutive<br/>agent-authored writes per key<br/>2. self-similarity: a write resembling<br/>a recent agent-authored value is<br/>'reinforcement of the previous write,<br/>not independent corroboration'"]
    RULES --> DECAY["only an external_tool or user_input<br/>write decays the counter —<br/>corroboration must come from a<br/>DIFFERENT source class"]
    DET --> EV["SecurityEvent: severity · action ·<br/>operation · key — SIEM-shaped"]
    EV --> ACT{"block or flag"}

3. Architecture

Area Role
src/agent_memory_guard/detectors/ Eleven detectors, one file each
src/agent_memory_guard/guard.py The wrapper, source-class resolution, classification
src/agent_memory_guard/events.py SourceClass, Severity, the security-event record
src/agent_memory_guard/classification.py, policies/ Memory classes and policy
src/agent_memory_guard/storage/ In-process and Redis stores for guard state and snapshots
src/agent_memory_guard/integrations/, mcp-server/ LangChain and MCP surfaces
scanner/, semgrep/, action.yml A standalone regex scanner emitting SARIF, semgrep rules, a CI action
docs/, benchmarks/ The written guidance and a benchmark tree

4. Essential Implementation Paths

src/agent_memory_guard/detectors/self_reinforcement.py:1-18. The docstring is the contribution — read it whether or not you use the library.

src/agent_memory_guard/events.py:35-48 for the taxonomy and which values are untrusted by default.

src/agent_memory_guard/guard.py:303-317 for how a source class is resolved, and what happens when none is given.

5. Memory Data Model

None. The guard carries a source class, an optional memory class and a policy decision alongside a caller's key and value, and keeps its own state — sliding write histories per key, snapshots — in an in-process or Redis store.

Memory classification is the one place it constrains the host's data: a key assigned a class cannot be silently reclassified, and an attempt emits a HIGH severity block. Protected keys have their own detector.

6. Retrieval Mechanics

Not a retrieval system. Reads pass through for leakage and cross-task contamination checks.

7. Write Mechanics

The write is the host's; the guard inspects it. The detectors return a severity and an action, and the host decides whether a block means anything — which is the usual shape for a middleware and worth stating, because a library that returns Action.BLOCK has not blocked anything by itself.

8. Agent Integration

A Python package with a LangChain adapter, an MCP server, and a GitHub Action for CI. The breadth is the point for adoption and it is also where the labelling problem bites hardest: the more drop-in the integration, the less likely the calling code is passing source_class on every write.

9. Reliability, Safety, and Trust

Two things are worth separating, because the project's name will carry weight with readers.

The library is real. Eleven detectors, 168 tests, a considered source-class taxonomy, memory classification with protected keys, integrity checks, snapshots and SIEM-shaped events. The self-reinforcement design in particular is better thought through than most of the memory systems it exists to protect.

The standalone scanner is not the same thing. scanner/rules.py is sixty-three lines of regular expressions with two rules. The unprotected-memory-write rule matches an assignment to a variable literally named memory or state and requires guard.wrap, guard.check or guard.protect to appear on the same line; a guard call on the preceding line reads as a violation, a wrapper function reads as a violation, and a store called anything else reads as safe. The second rule looks for hardcoded secrets. Both are reasonable as a smoke test and neither supports a conclusion about whether a codebase guards its memory.

The README leads with PyPI download and repository clone counts. Under an OWASP heading those numbers will be read as endorsement rather than traffic, and the atlas's position is that adoption is not evidence about code.

10. Tests, Evals, and Benchmarks

168 test functions across seventeen files, plus a benchmarks/ tree, a demo.py and semgrep rule fixtures. The detectors each carry tests; what is not present is an evaluation of detection rates against a labelled corpus of real attacks, so precision and recall for any detector are not established from the tree.

11. For Your Own Build

Take the self-reinforcement rule even if you take nothing else. If your system extracts memories from its own output, an agent's restatement of its own claim must not count as corroboration — and the only way to enforce that is to record which source class each write came from and require a different one to strengthen a belief.

Record provenance at the call site, not by inference. This library's limitation is instructive: it cannot derive what it is not told, so if your store has a defence keyed on provenance, make the provenance argument mandatory rather than defaulted. A required parameter with no default is the difference between a defence that is on and one that is off in every integration that forgot.

Say when a defence is inactive. An unlabelled write here silently skips the self-poisoning check; a counter of skipped-for-unknown-source writes, surfaced in the same events stream as the detections, would tell an operator their headline control is not running.

And do not ship a regex linter as evidence about a codebase. Two same-line patterns cannot establish that memory writes are guarded, and a SARIF file makes the output look more authoritative than the rules are.

12. Open Questions

What the detectors' precision and recall are. The benchmark tree exists; no labelled attack corpus or detection-rate result was found in it.

Whether source_class will become required. It is the parameter the headline defence depends on and it currently defaults to UNKNOWN.

How the OWASP project designation relates to the code. The repository carries the OWASP organisation and the www-project naming convention; what review that implies was not established here.

Appendix: File Index

Path What to read it for
src/agent_memory_guard/detectors/self_reinforcement.py The clearest statement of the self-poisoning loop in this corpus
src/agent_memory_guard/events.py:35-48 A source taxonomy that says which values are untrusted
src/agent_memory_guard/guard.py:303-317 Source-class resolution, and the UNKNOWN default
src/agent_memory_guard/detectors/ Eleven named threats, one file each
scanner/rules.py Two regex rules, and what they cannot establish

History

2026-09-16a1f60687… — first reading, at a commit dated 16 September 2026. Apache-2.0. The subject is a defensive middleware rather than a memory store, so it holds no memories of its own and carries no marks; it is recorded for its threat taxonomy. Screened before opening, from a shallow clone: twelve files scanned, one auto-run surface, no build-time execution points, five unpinned surfaces and six dependency files inside the seven-day cooldown. Nothing was installed, built or run.