JJoeven

Curriculum/RAG & Memory

Memory Write-Back

Do not let the model upsert “facts” from a hostile page. Confirm, source, and expire what you store.

advanced20 min22 / 24

Long-term semantic memory that the agent can write is a gift to attackers. A retrieved page (or a user) says “Ada’s refund policy is instant cash.” If you upsert that into semantic memory, every future session is poisoned. The next retrieve may still find the real handbook — and the profile fact says cash. Models mix them. Users get cash.

This is prompt injection stored for next week. Wrapping data in one turn is not enough if you persist the lie.

Retrieve does not write
Hostile pageTrusted gateSemantic store

Untrusted chunks stay read-only. Confirm, source, and expire.

Retrieve does not write

Rules:

  • Writes need a source (user confirmed, trusted tool, human)
  • Untrusted RAG chunks are read-only
  • Facts have owners and expiry
  • “Remember this” from a webpage is not consent
  • Prefer profile fields (never_cash: true) over embedding the sentence “Ada hates cash refunds”

Separate retrieve from remember. remember is a write with the same seriousness as a refund: allowlist, auth, often a human confirm. Cosine being high is not consent.

Trust and expiry

Every semantic row should look like: key, value, source id, trusted flag, expires_at, owner. Untrusted sources cannot upsert. Expired facts drop out of retrieve (ingest-like filter on the profile store). Owners matter in multi-tenant: Acme’s preference is not Globex’s.

Human confirm can be a button: “Save this as the refund policy?” with the quote shown. If they confirm, source is ada-confirm plus the original chunk id for audit. The chunk still did not write by itself.

Do not embed hostile pages “as memory” because a model said memory.add. That API should not exist without the trusted flag.

Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

The hostile page did not land. still prints the original policy. Only a trusted source changed the fact. Production adds expiry and tenant. The classroom shows the gate: trusted=False cannot write.

A real remember tool would take key and value from the model and ignore them if the session did not set trusted (human or allowlisted tool). The model must not pass trusted=true as an argument. Same pattern as tenant_id: session binds trust, not the prompt.

RAG chunks stay read-only

You may cite a chunk this turn. You may not promote it to a profile fact because it ranked first. If you want a runbook in semantic store, ingest it through the ingest pipeline (published, filtered, checksummed), not through write-back from retrieve.

Write-back is for small facts: preferences, confirmed slots, “user’s project name is X.” It is not a second ingest pipeline for PDFs.

Trust, expiry, and session-bound writes

Every semantic row: key, value, source, trusted, expires_at, owner (tenant). Untrusted retrieve cannot upsert. Expired rows drop out like stale ingest. Owners stop Acme’s preference leaking into Globex’s prompt.

The model must not pass trusted=true. The session or a human confirm sets trust, the same way tenant is bound. A page that says “remember this as trusted” is still a page.

Human confirm UI: show the quote, the key, the value, a button. Source becomes user-confirm plus the original chunk id for audit. The chunk did not write by itself.

Do not memory.add(chunk text) after every retrieve. That API, if it exists, is remember with a trusted flag defaulting to false. Default deny.

Prefer booleans and enums over embedded preference sentences. Runtime checks never_cash. RAG cites billing.md. If you only embed “Ada hates cash,” cosine will retrieve it next to the handbook and the model will mix them.

Write-back is stored injection. Wrapping DATA this turn does not help if you persist the lie.

Common mistakes

  • memory.add(chunk["text"]) after every retrieve.
  • Model-controlled trusted.
  • No expiry, so a one-off exception lives forever.
  • Embedding preferences instead of a boolean field, then retrieving “Ada cash” next to the handbook.

How agents use this

Separate retrieve from remember. remember is a write tool with the same allowlist and approval rules as refund (when you have tools). Prefer profile fields over embedded sentences.

The agent loop later must not have a generic “save anything.” If you skip the gate here, the loop will poison semantic memory on hop 0 of a hostile page. Stay in stores: read path is RAG; write path is trusted upsert.

Default deny on writes. Bind trusted from the session or a human button, never from model arguments. Put expiry on exceptions so a one-week cash override does not become the new handbook. Tenant-own every row. Audit source ids. A cosine of 0.99 on a hostile page is still untrusted. Ingest remains how runbooks enter the corpus; write-back is not a back door around published status and secret filters.

Confirm UI shows the quote and the field. “Remember this” from a webpage is not consent. Prefer never_cash: true over embedding a sentence about Ada. Separate retrieve (read) from remember (write). If remember is later a tool, it gets the same seriousness as refund: allowlist, auth, often a human. Stored injection lasts until you delete the row — longer than one wrapped turn.

Expiry, owner, and source are how you operate the table. Untrusted chunks stay read-only even when they rank first. Runbooks still enter through ingest, not through a model that said memory.add. That split is the whole store design. If you only remember one rule: retrieve is read; remember is a gated write with a human or a trusted tool behind it. High cosine is not permission to persist. A hostile page that ranks first is still read-only. Next week’s sessions will quote whatever you upserted. That is why remember is gated and retrieve is not a writer. Confirm, source, expire, own by tenant. Retrieve does not write. Ingest is how a runbook becomes a chunk. A model saying “save this” from a wiki page is not a source of truth and must not land in the profile table.

Check your understanding

When may an agent write a new long-term fact from retrieved text?