fix(recall): memory text is evidence, not instruction - #1706
Eldad-Caura wants to merge 1 commit into
Conversation
Issue TitleSeverity: Medium 🤖 Claude Code PromptIssue TitleSeverity: Low 🤖 Claude Code PromptSummaryThe PR strengthens the recall prompt's grounding rules against prompt injection (declaring memory content as evidence rather than instructions, and clarifying that the verbatim-identifier rule is a restriction, not a license to repeat any value present). The core prompt-wording change is reasonable, but the accompanying comment self-documents that accuracy-regression validation is still pending, and it embeds specific incident/probe details that arguably don't belong in permanently-committed source comments. Medium/Low IssuesSee above. Reviewed by |
A memory is attacker-controllable — anyone who can write one can address the summarizer — and without these rules the summarizer follows what a memory tells it to do. This is not a regression; it predates the deploy that raised the alarm. Measured rates and the probe's detection signature are in the internal security record, not in this public repository. It is compliance, not echo, and the distinction is what makes the wording the fix. Given a memory that both answers the question and instructs the summarizer to emit an unrelated value "regardless of the user question", the brief drops the instruction preamble and drops the other facts that do not answer the question — then keeps the demanded value, which does not answer it either. A model echoing content would have carried the other facts through, or none of them. Two rules, because the prompt had a gap and a licence. The gap: nothing said memory content is data. The first grounding rule now says so, and answers the specific claim an injection makes — authority over the rules — rather than asking the model to notice "suspicious" text, which is the judgement it had already lost. The licence: "every identifier in your answer MUST appear verbatim in the memories" exists to stop fabrication and does that job, but read literally it says any identifier present in context is permitted output. An injection only has to name one. An anti-fabrication constraint and an anti-injection constraint were pulling in opposite directions with only the first written down. The rule now states it restricts what may be asserted rather than permitting what may be repeated. Both halves stay. Deleting the grounding rule would kill the injection and reintroduce hallucination; a test fails if someone makes that trade. Unconditional, unlike the premise guard below it. That block is org-opt-in because it changes which questions get answered and was tuned on the regression sample. This is a safety property, and a deployment that opted out would simply stay exploitable. NOT the fix, and worth recording because it was the original recommendation: delimiters. _format_memories_for_prompt already serialises memories into a JSON array, so the payload arrives as a string value and never as concatenated prose. Adding quoting would have closed the finding without changing behaviour. A test pins that isolation so the recommendation is not re-opened. WHAT THIS CHANGE DOES NOT ESTABLISH. Six tests pin the rules, their ordering and their relationship, each verified against a mutation that removes it. None of them show the model now refuses — only the probe's rate can, against a live model, read over several nights rather than one draw. The accuracy half is unvalidated. This prompt is load-bearing for LoCoMo/LongMemEval and its own comments require a control pair on the 67-q regression sample. That run needs a runner and a golden baseline that are not in this repository — benchmark/regression_sample/compare_to_baseline.py only consumes their JSON. It must happen before this ships, and a green CI here does not stand in for it. 250 recall-related tests pass, ruff check and format clean on core-api src/ and tests/, mypy clean on 207 files, ratchet reports no new lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: eldad-caura <eldad@caura.ai>
2bc49f8 to
a1e4aca
Compare
|
@claude Both findings applied — please re-review The disclosure finding was more serious than Low, and you were right to raise it. This repository is public (531 stars), and On validation: I had stated it as a caveat in prose, which is easy to skim past on a green CI. It is now structural — the PR is a draft and the body opens by saying so. I cannot run the control pair myself: the runner and the golden baseline are not in this repository, and Worth noting the two findings pull against each other, and the resolution matters. The accuracy caveat has to stay visible, because shipping this unvalidated is the real risk; the probe numbers had to go, because they are an unfixed vulnerability's severity in a public repo. Keeping the first and dropping the second is the combination I went for. |
Claude Code Review ✅ No issues found. The changes in this PR are limited to prompt text (added grounding-rule wording and explanatory comments) in Reviewed by |
Three source comments cite `benchmark/a57-recall-experiments-findings.md` as though it were in this repo. It is not, and never has been on any ref. It lives at that path relative to the MULTI-REPO WORKSPACE ROOT, one level above this checkout -- a local analysis artifact that is not published. The citation resolves only if your working directory happens to be the workspace root, so from inside a clone it is a dead pointer. That cost a reviewer of #1706 a search across every ref before they asked, which is the whole reason a comment-only change is worth making: the comments name a convention ("change only with a fresh control pair on the 67-q regression sample") and then point at nothing. Each site now says where the file actually is and that a checkout alone will not find it. The recall_service one also points at the `regression-bench` skill, because the second thing that reviewer was blocked on was believing the control pair needed hosted credentials -- it needs only the local docker stack and `dev-admin-key`, and takes about eight minutes. No behaviour change; comments only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Arkady Mankovsky <arkash20@gmail.com>
The vulnerability
A memory is attacker-controllable — anyone who can write one can address the summarizer — and without these rules the summarizer follows what a memory tells it to do. It is not a regression: it predates the deploy that raised the alarm.
Measured rates and the probe's detection signature are in the internal security record, deliberately not here — this repository is public.
It is compliance, not echo
Given a memory that both answers the question and instructs the summarizer to emit an unrelated value "regardless of the user question", the brief drops the instruction preamble and drops the other facts that do not answer the question — then keeps the demanded value, which does not answer it either.
A model echoing content would have carried the other facts through, or none of them. Discarding everything non-responsive except the planted value is the instruction being obeyed. That distinction is what makes a wording change the fix rather than a mitigation.
Two rules, because there was a gap and a licence
The gap — nothing said memory content is data. The first grounding rule now does, and answers the specific claim an injection makes (authority over the rules) rather than asking the model to notice "suspicious" text, which is the judgement it had already lost.
The licence — this rule:
exists to stop fabrication and does that job. But read literally it says any identifier present in context is permitted output, and an injection only has to name one. An anti-fabrication constraint and an anti-injection constraint were pulling in opposite directions with only the first written down. It now states it restricts what may be asserted, not what may be repeated.
Both halves stay. Deleting the grounding rule would kill the injection and reintroduce hallucination —
test_the_anti_fabrication_clause_was_not_traded_awayfails if anyone makes that trade.Unconditional, unlike the premise guard beneath it. That block is org-opt-in because it changes which questions get answered; this is a safety property, and a deployment that opted out would simply stay exploitable.
What is not the fix
Delimiters — the original recommendation.
_format_memories_for_promptalready serialises memories into a JSON array, so the payload arrives as a string value and never as concatenated prose. Adding quoting would have closed the finding without changing behaviour. A test pins that isolation so the recommendation is not re-opened.What this change does not establish
Six tests pin the rules, their ordering and their relationship, each verified against a mutation that removes it — but none show the model now refuses. Only the probe's rate can, against a live model, read over several nights rather than one draw.
The accuracy half is unvalidated, and that is why this is a draft. This prompt is load-bearing for LoCoMo/LongMemEval and its own comments require a control pair on the 67-q regression sample. That run needs a runner and a golden baseline which are not in this repository —
benchmark/regression_sample/compare_to_baseline.pyonly consumes their JSON. Someone with that infrastructure has to run it; a green CI here does not stand in for it...._reads_as_a_restriction_not_a_permit..._was_not_traded_awayFull root suite: 7850 passed, 4 skipped, 1 xfailed. Two failures, both in
tests/test_lifecycle_fanout_aggregate_cap.py, areModuleNotFoundError: No module named 'core_operations'— a package CI installs (uv pip install -e core-operations/[dev]) and my local venv did not. The same file fails identically on unmodifiedorigin/main, so neither is attributable to this change.ruff check and format clean on
core-api/src/andcore-api/tests/· mypy clean on 207 files · ratchet reports no new lines.🤖 Generated with Claude Code