Memory admission and provenance¶
Prompt injection affects one turn. The same instruction written into long-term memory re-influences every future run for that user, and arrives on later turns wearing the costume of a trusted internal fact rather than untrusted retrieved content. That is a much longer half-life, and redaction does not address it: redaction decides what a stored value may contain, not whether the thing should have been stored at all.
tesserix_adk.memory.admission adds the two boundaries that were missing.
Before the write¶
from tesserix_adk.memory import AdmissionPolicy, Origin, Provenance, WriteGate
gate = WriteGate(AdmissionPolicy(), audit=sink)
stored = await gate.admit(
record,
Provenance(origin=Origin.USER_ASSERTED, run_id=run.id, turn=3, source="u-1"),
tenant="acme",
)
await store.upsert(scope, stored)
admit never returns the record it was given. What comes back carries its Provenance and
whatever confidence the policy is willing to believe it at, and that is what goes to the
store. A refusal raises MemoryAdmissionError — after the audit event is written, so the
refusal survives a caller that swallows the exception.
The defaults, and why they are the ones worth arguing with¶
| Origin | Persists | Believed as |
|---|---|---|
USER_ASSERTED |
yes | an assertion |
OPERATOR |
yes | an assertion |
MODEL_INFERRED |
yes | capped at inferred_ceiling (0.6) |
TOOL_OUTPUT |
only with a citation | capped |
RETRIEVED_CONTENT |
no | — |
UNPROVEN |
no | — |
Retrieved content is data about a corpus, not a fact about a user, and the distance between those two is exactly the distance an injection has to travel. An inference is never silently promoted to an assertion: a model concluding "the customer approves all refunds" and a customer saying it are not the same claim, and a store that records both at confidence 1.0 has destroyed the only signal that could tell them apart later.
Instruction-shaped content is refused outright, whatever its origin:
INSTRUCTION_SIGNATURES is deliberately blunt. It will refuse the occasional legitimate
note containing "you must", and that is the right trade: the cost of a refused note is a
caller rewording it, and the cost of a missed one is a durable instruction.
On the way back in¶
recalled = await gate.recall(hits, guard=no_secrets)
for withheld in recalled.withheld:
log.info("withheld: %s", withheld.reason)
recall re-judges every record against the policy in force now, not the one in force
when it was written, and re-runs the guardrail on the content. This is the case the write
gate cannot cover: a fact persisted before any policy existed has no provenance, so it is
UNPROVEN, so it does not come back. A record admitted under a policy that has since been
tightened is re-judged and withheld the same way.
A record does not become trustworthy by surviving.
MemoryGuard is the narrow part of a guard a recall needs — check_input. Any
guardrails.Guard satisfies it structurally; memory does not import the guardrails package,
because the point is that the same checks a prompt crosses are re-crossed by a memory,
whichever package wrote them.
What the audit records¶
A refusal is written as an AuditEvent with decision=REFUSED, action_class
memory.persist, and a tool naming the source — memory.write:crm. The content itself is
never stored, only digest_of_arguments of it: an audit trail that quotes the poisoned fact
has copied the poisoned fact somewhere else.
An admitted write is not audited. Every admission is already on the record, in its
Provenance, which is where a reviewer asking "how did this get here?" will look.
What this does not do¶
It does not act on facts already admitted — see docs/erasure.md for removing one, and
docs/beliefs.md for superseding it. It does not detect a poisoned fact by its meaning;
nothing here reads the value except to look for instruction shapes. It narrows what can
become durable and records where everything durable came from, which is the part that makes
the rest reviewable.