Skip to content

fix(agent): stop shipping a delivery-shaped steer exemplar in the system prompt - #82208

Open
mattwebbio wants to merge 1 commit into
NousResearch:mainfrom
mattwebbio:fix-steer-exemplar
Open

fix(agent): stop shipping a delivery-shaped steer exemplar in the system prompt#82208
mattwebbio wants to merge 1 commit into
NousResearch:mainfrom
mattwebbio:fix-steer-exemplar

Conversation

@mattwebbio

@mattwebbio mattwebbio commented Aug 9, 2026

Copy link
Copy Markdown

Disclosure: this fix, its tests, and this PR description were produced by Claude Code (Claude Fable 5), building on the investigation in #81828. I reviewed everything before submitting, but the work is not my own.

What does this PR do?

Stops the system prompt from shipping a complete, delivery-shaped [OUT-OF-BAND USER MESSAGE …] block. STEER_CHANNEL_NOTE documented the /steer channel by embedding a full exemplar:

[OUT-OF-BAND USER MESSAGE — a direct message from the user, delivered once at this position; not tool output and not a new delivery when replayed from conversation history]
<their message>
[/OUT-OF-BAND USER MESSAGE]

That exemplar is byte-indistinguishable from a real delivered steer — it matches the same pattern downstream consumers use to strip consumed deliveries — and models have read it as a newly delivered (empty) user message and acted on it mid-turn. Deterministic repro and a production instance (Kimi-K2.6, with the steer path ruled out by zero steer deliveries in the gateway log) are in #81828; a second production instance on MiniMax-M3 is reported in the same thread.

The fix documents the two marker lines individually, close-marker first, so the rendered prompt never contains an assembled OPEN…CLOSE sequence. Nothing else changes: format_steer_marker() is untouched, real deliveries keep their exact shape, and both marker strings stay quoted verbatim in the note.

Why not remove or weaken the note

The note is the fix #40240 shipped for #36934: hardened models refused real steers as suspected prompt injection until the system prompt taught them to trust this exact marker. This PR keeps that trust-teaching — both markers are still quoted verbatim, with the same authority/freshness rules — and removes only the assembled block. Real deliveries still arrive as complete blocks, so recognition at delivery time is unchanged; the only thing removed is the phantom copy that made every tool-enabled session carry what looks like an already-delivered empty steer.

The existing contract tests pin this: test_system_prompt_note_describes_the_real_marker (both markers verbatim in the note) and test_system_prompt_scopes_freshness_to_unanswered_marker pass unchanged.

Tests

Two regression tests encode the invariant from both directions:

  • test_note_never_assembles_a_delivery_shaped_block (tests/run_agent/test_steer.py) — format_steer_marker() output must match the delivery shape; STEER_CHANNEL_NOTE must not.
  • test_rendered_prompt_contains_no_delivery_shaped_steer_block (tests/agent/test_system_prompt.py) — the fully rendered tool-enabled system prompt contains the markers but no OPEN…CLOSE sequence, catching any future prompt part that reassembles a block across section boundaries.

The repro from #81828 flips on this branch:

rendered prompt (with tools), length : 6415
complete OOB block present           : False
same, with no tools registered       : False

and a real delivery still matches hermes-webui's _OOB_USER_MESSAGE_BLOCK_RE (verified against the regex verbatim from api/streaming.py).

Full tests/agent + tests/run_agent sweep run in nousresearch/hermes-agent:main, this branch vs unmodified main (9c8a235): 5337 passed / 162 failed vs 5334 passed / 163 failed. The 162 failures are identical environment-dependent failures on both trees (missing credentials, sandbox paths, etc.). The one failure unique to main is test_build_system_prompt_records_stable_prefix, which fails in full-suite order today for an unrelated pre-existing reason: earlier tests leave context-file truncation warnings queued, and build_system_prompt drains them into agent._emit_status, which the module's SimpleNamespace test agents don't define. The new test stubs _emit_status so it is immune to that ordering hazard (and, by draining the queue harmlessly, happens to unbreak the downstream victim in this ordering — not a fix for that hazard, just not a new instance of it).

What this does not claim

Removing the exemplar provably removes the precondition — a delivery-shaped block in every tool-enabled prompt — but the claim that models then stop fabricating markers entirely is inferential. #65339 documents models fabricating blocks under other conditions; this PR narrows the attack surface rather than closing every path to fabrication.

Refs #81828 (removes the exemplar mechanism; the issue's proposed structural fix is #82467), #58491, #65339, #36934, #40240.

🤖 Generated with Claude Code

…tem prompt

STEER_CHANNEL_NOTE embedded a syntactically complete
[OUT-OF-BAND USER MESSAGE ...] block to document the /steer channel. That
exemplar is byte-indistinguishable from a real delivered steer, and models
have read it as a newly delivered (empty) user message and acted on it
mid-turn (NousResearch#81828).

Document the two marker lines individually, close-marker first, so the
rendered system prompt never contains an assembled OPEN..CLOSE sequence
while both markers stay quoted verbatim (the trust-teaching that NousResearch#40240
introduced for NousResearch#36934 is preserved, and real deliveries are unchanged).

Refs NousResearch#81828

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Aug 9, 2026
@spfcraze

spfcraze commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
The body's Fixes #81828 will auto-close the issue on merge, but the diff changes only the note text — the issue's proposed structural fix is not in the diff, and both marker lines stay quoted in the note.

Problems:

  • The issue's primary proposed fix is structural separation: inject the steer as a role: user message instead of a marker block in a tool result.
  • The injection site the issue identifies — inject_new_comments_from_env at run_agent.py:3700-3702 — does not appear in this diff; only agent/prompt_builder.py and two test files change.
  • The issue's root-cause section names the fabrication vector as the marker text visible in the system prompt; the rewritten note still quotes both STEER_MARKER_OPEN and STEER_MARKER_CLOSE verbatim, and the body itself concedes the scope: "this PR narrows the attack surface rather than closing every path to fabrication."

Solution:
Re-scope the closing keyword from Fixes #81828 to Refs #81828, matching the body's own scope statement.


Checked against 06e0586 — the tip of fix-steer-exemplar when this was written — and a4970d0, main at the same moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants