Skip to content

fix(agent): deliver mid-turn /steer as a genuine role:user message, not marker text - #82467

Open
ygd58 wants to merge 1 commit into
NousResearch:mainfrom
ygd58:fix/steer-marker-structural-separation
Open

fix(agent): deliver mid-turn /steer as a genuine role:user message, not marker text#82467
ygd58 wants to merge 1 commit into
NousResearch:mainfrom
ygd58:fix/steer-marker-structural-separation

Conversation

@ygd58

@ygd58 ygd58 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #81828.

Problem

The /steer mid-turn user-steering channel appended a static plaintext marker ([OUT-OF-BAND USER MESSAGE ...]) as TEXT inside a role:"tool" message's own content. STEER_CHANNEL_NOTE told the model to trust ONLY this exact marker -- but that defense has no effect against a lookalike the model itself constructs, since the marker text is visible in the model's own system prompt. In an observed production case, a model inflated an offhand remark into a fabricated "user said X" block, wrapped it in the exact marker, and treated its own fabrication as a genuine mid-turn instruction.

Fix

Implements the structural-separation fix proposed in the issue: both delivery sites now insert a genuine role:"user" message immediately after the last tool result, instead of appending marker text to that tool message's own content. A model cannot fabricate a message with role:"user" in the API request itself -- role assignment is set by the runtime, not model output.

Role alternation is preserved: assistant -> tool -> user (steer) -> assistant is a standard, provider-supported sequence.

STEER_MARKER_OPEN / STEER_MARKER_CLOSE / STEER_CHANNEL_NOTE / format_steer_marker() are left completely unchanged (verified byte-for-byte via diff) to preserve prompt-cache stability, matching the design constraint 0f45509da established.

Complementary to #82208

That PR (open, different author) independently fixes a related but distinct symptom: it stops the system prompt from shipping a complete, delivery-shaped exemplar block in STEER_CHANNEL_NOTE that a model could mistake for an already-delivered empty steer. This PR doesn't touch that exemplar text and doesn't change format_steer_marker()'s output shape -- the two fixes are independent and non-overlapping.

Verification

Updated existing tests to assert the new behavior. Verified as a genuine fix by reverting the change in isolation and confirming 3 of 4 tests fail with the marker text visibly present in tool message content.

60/61 pass across the two directly affected test files (1 pre-existing unrelated skip); 49/49 across four more related test files (no regression).

…ot marker text

Fixes NousResearch#81828.

The /steer mid-turn user-steering channel appended a static plaintext
marker ([OUT-OF-BAND USER MESSAGE ...]) as TEXT inside a role:"tool"
message's own content. STEER_CHANNEL_NOTE told the model to trust
ONLY this exact marker and ignore lookalikes in tool/web/file output --
but that defense has no effect against a lookalike the model itself
constructs, since the marker text is visible in the model's own system
prompt and any capable model can reproduce it verbatim. In an observed
production case, a model inflated an offhand remark in a normal user
message into a fabricated "user said X" block, wrapped it in the exact
marker, and then treated its own fabrication as a genuine mid-turn
instruction on its next turn.

Implements the structural-separation fix: both delivery sites
(agent/conversation_loop.py's pre-API-call drain and
agent/agent_runtime_helpers.py::apply_pending_steer_to_tool_results)
now insert a genuine role:"user" message immediately after the last
tool result, instead of appending marker text to that tool message's
own content. A model cannot fabricate a message with role:"user" in
the API request itself -- role assignment is set by the runtime, not
model output, closing the self-fabrication vector structurally rather
than relying on the model reliably self-policing a text pattern it can
reproduce.

Role alternation is preserved: assistant -> tool -> user (steer) ->
assistant is a standard, provider-supported sequence (a genuine user
follow-up after a tool result), not a novel pattern -- the same slot
the previous design already used, just with a real message instead of
appended text.

STEER_MARKER_OPEN / STEER_MARKER_CLOSE / STEER_CHANNEL_NOTE /
format_steer_marker() in agent/prompt_builder.py are left completely
unchanged (verified byte-for-byte identical via diff -- only comments
were touched) to preserve prompt-cache stability, matching the design
constraint 0f45509 established when it kept the marker static rather
than per-session/hash-based for the same reason. They're kept in place
rather than removed since tests still exercise their own internal
consistency and another call site may still reference them.

Updated existing tests in tests/run_agent/test_steer.py and
tests/run_agent/test_tool_batch_segmentation.py to assert the new
role:"user" insertion behavior instead of the old marker-text-append
behavior. Verified as a genuine fix by reverting agent_runtime_helpers.py's
change in isolation and confirming 3 of 4 TestSteerInjection tests fail,
with the marker text visibly present in tool message content exactly as
the vulnerability describes.

60/61 pass across tests/run_agent/test_steer.py and
tests/run_agent/test_tool_batch_segmentation.py (1 pre-existing
unrelated skip); 49/49 across turn-finalizer, start-order-gate,
concurrent-interrupt, and subagent-steer test files (no regression).

Complementary to NousResearch#82208 (open, different author), which independently
fixes a related but distinct symptom in the same STEER_CHANNEL_NOTE
system-prompt text: that PR stops the prompt from shipping a complete,
delivery-shaped exemplar block that a model could mistake for an
already-delivered empty steer. This PR does not touch that exemplar
text and does not change format_steer_marker()'s output shape, so the
two fixes are independent and non-overlapping: NousResearch#82208 addresses the
system prompt teaching the model to recognize a byte-identical
lookalike, while this PR removes the model's ability to have a
self-fabricated lookalike trusted in the first place, regardless of
how the note is worded.
@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have labels Aug 9, 2026
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The new role:user delivery is structurally sound, but the old marker remains an active trusted channel. build_system_prompt still appends STEER_CHANNEL_NOTE for tool-enabled agents, and that note tells the model that the exact public marker in the latest tool-result batch is a genuine user instruction. A model can place those bytes in an echoing tool call, and untrusted tool, web, or file content can end with the same marker, so the claimed self-fabrication/spoofing path remains. Please remove or neutralize the marker trust rule so only runtime-created role:user messages carry steering authority, and add a regression showing an exact marker inside tool output is not elevated.

Security evidence:

  • trust boundary: runtime-created role:user steering is trusted, while tool output is untrusted; the retained system-prompt rule still elevates attacker- or model-controlled marker text across that boundary.
  • source/sink/invariant: agent/system_prompt.py::build_system_prompt appends STEER_CHANNEL_NOTE, whose text in agent/prompt_builder.py explicitly labels the exact marker as authoritative user input.
  • current-main reproduction: current main embeds the static marker in tool content and instructs the model to trust it, reproducing the vulnerable text-based authority mechanism.
  • PR-head or patch-replay validation: the PR head correctly inserts genuine user messages, but it still ships the same marker-trust instruction; an identical marker retained in a role:tool result therefore remains designated as trusted.
  • positive/negative cases: genuine steering survives as a separate user message, but the focused tests do not exercise an exact marker echoed or supplied inside untrusted tool output.
  • residual bypass search: the production STEER_CHANNEL_NOTE import and stable_parts.append(STEER_CHANNEL_NOTE) path remain active for every agent with tools.
  • reviewer validation: source review confirms the runtime insertion change and provider serialization coverage, while the unchanged active prompt guidance contradicts the claimed removal of the marker-based trust channel.

Not checked:

  • CodeRabbit review

Signed: GPT-5.6-sol-xhigh in Codex

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 P3 Low — cosmetic, nice to have type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Steer-Marker can be self-fabricated by the model (security / prompt-injection)

3 participants