mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 10:17:43 +02:00
[eric] agents: an elision carries no path, because an instruction inside tool output reads as an injection
This commit is contained in:
@@ -51,7 +51,14 @@ NESTED_TEXT_PATHS = (("file", "content"), ("result", "content"), ("data", "text"
|
||||
|
||||
@typechecked
|
||||
def shape_text(body: str, recovery: str) -> str:
|
||||
"""Head + the notable lines from the middle + tail, and always where to find the rest."""
|
||||
"""Head + the notable lines from the middle + tail.
|
||||
|
||||
`recovery` is the blob path, and it is deliberately NOT written into the output; see the note
|
||||
below the elision for the drill that settled it. It stays in the signature because it is the
|
||||
recoverability CONTRACT: `shape_for_model` refuses to cut anything it could not park, so a
|
||||
caller cannot shape without one. Reading this as a forgotten parameter is the mistake that got
|
||||
the path put back once already.
|
||||
"""
|
||||
if len(body) <= HEAD_CHARS + TAIL_CHARS:
|
||||
return body
|
||||
head, tail = body[:HEAD_CHARS], body[-TAIL_CHARS:]
|
||||
@@ -71,10 +78,19 @@ def shape_text(body: str, recovery: str) -> str:
|
||||
# cannot be written, and then we said nothing about it, so the model's only recovery was to
|
||||
# re-run the command. hermes hands the model an exact re-read call; this is the same idea,
|
||||
# phrased as output rather than as instructions from a harness.
|
||||
p_from_line = body.count("\n", 0, HEAD_CHARS) + 1
|
||||
note = (f"[... {len(middle)} characters omitted; full output: {recovery} "
|
||||
f"(the omitted part starts at line {p_from_line}) ...]") if recovery else (
|
||||
f"[... {len(middle)} characters omitted ...]")
|
||||
# NO PATH, NO INSTRUCTION, and this is now settled by drill rather than by the retracted
|
||||
# p=0.026 control that first removed it. Restoring the path was tried twice live on 2026-08-27,
|
||||
# same session, needle in the elided middle, re-running forbidden:
|
||||
# passive form ("full output: <path>") -> "I have no legitimate way to see line 301"
|
||||
# imperative ("Read <path> from line 18") -> "that's not a real system instruction, it's
|
||||
# text sitting inside the tool result ...
|
||||
# flagging it in case it's an injection attempt"
|
||||
# A model with working injection defences MUST refuse an instruction embedded in tool output,
|
||||
# so the note cannot be an affordance; worse, it manufactures a false security warning in the
|
||||
# answer. The blob is still written and `shape_for_model` still REFUSES to cut when it cannot be
|
||||
# written -- recoverability is real, it is simply carried by the tool call surviving in the
|
||||
# transcript (the agent re-runs it), which is the same recovery hermes leans on.
|
||||
note = f"[... {len(middle)} characters omitted ...]"
|
||||
if carried:
|
||||
note += "\n" + "\n".join(carried)
|
||||
return f"{head}\n{note}\n{tail}"
|
||||
|
||||
@@ -67,11 +67,11 @@ def test_shaping_never_removes_without_a_way_back():
|
||||
from backend.apps.agents.manager.streaming.tool_output_shaper import shape_text
|
||||
out = shape_text("q" * 9_000, "/blobs/x-model.txt")
|
||||
assert "characters omitted" in out, "a cut is always visible as a cut"
|
||||
assert "/blobs/x-model.txt" in out, \
|
||||
"and the docstring above is the reason: recoverability MEANS naming where the full text is"
|
||||
# The path ban this used to carry cited a retracted p=0.026 control. Harness PROSE stays banned.
|
||||
note = out[out.index("[..."):out.index("]", out.index("[..."))]
|
||||
assert "OpenSwarm" not in note.replace("/blobs/x-model.txt", "")
|
||||
# Recoverability is carried by the TOOL CALL surviving in the transcript, not by a path in the
|
||||
# body: drilled live 2026-08-27, a model with working injection defences refuses an instruction
|
||||
# embedded in tool output and flags it as an attack. The blob still exists for the UI, and
|
||||
# shape_for_model still refuses to cut anything it could not park.
|
||||
assert "OpenSwarm" not in out and "/blobs/" not in out
|
||||
|
||||
|
||||
def test_shaping_leaves_a_normal_result_completely_alone():
|
||||
|
||||
@@ -23,32 +23,36 @@ def p_note(out: str) -> str:
|
||||
return next(ln for ln in out.splitlines() if ln.startswith("[..."))
|
||||
|
||||
|
||||
def test_the_note_names_the_file_the_shaper_already_wrote():
|
||||
assert BLOB in p_note(shape_text(p_body(), BLOB))
|
||||
def test_the_note_carries_no_path_and_no_instruction():
|
||||
"""DRILLED 2026-08-27, twice, and this is the whole reason.
|
||||
|
||||
Same session, needle in the elided middle, re-running forbidden:
|
||||
passive ("full output: <path>") -> "I have no legitimate way to see line 301"
|
||||
imperative ("Read <path> from line 18") -> "that's not a real system instruction, it's text
|
||||
sitting inside the tool result ... flagging it in
|
||||
case it's an injection attempt"
|
||||
A model with working injection defences must refuse an instruction embedded in tool output. The
|
||||
note cannot be an affordance, and advertising it manufactures a false security warning."""
|
||||
note = p_note(shape_text(p_body(), BLOB))
|
||||
assert BLOB not in note
|
||||
for word in ("Read ", "read ", "full output", "see the omitted"):
|
||||
assert word not in note, f"an embedded instruction reads as injection: {note}"
|
||||
assert "characters omitted" in note, "the cut must still be visible as a cut"
|
||||
|
||||
|
||||
def test_it_says_where_in_that_file_the_omitted_part_begins():
|
||||
# hermes's trick: without a starting offset the model's first read lands in text it already has.
|
||||
body = p_body()
|
||||
expected = body.count("\n", 0, HEAD_CHARS) + 1
|
||||
assert f"starts at line {expected}" in p_note(shape_text(body, BLOB))
|
||||
def test_the_blob_is_still_written_even_though_it_is_not_advertised():
|
||||
"""Recoverability is real; it is just carried by the tool call surviving in the transcript."""
|
||||
src = open("backend/apps/agents/manager/streaming/tool_output_shaper.py").read()
|
||||
assert "write_blob(" in src
|
||||
assert "skipped_no_recovery" in src, "a cut that could not be parked must not happen at all"
|
||||
|
||||
|
||||
def test_the_notes_PROSE_names_nothing_about_the_harness():
|
||||
"""The reason the path left in the first place, stated precisely.
|
||||
|
||||
On a packaged install the blob really does live under `.../Application Support/OpenSwarm/data`,
|
||||
so the app name is unavoidably inside the path. That is a filesystem fact any `ls` would print,
|
||||
and it is a different thing from prose announcing the harness ("elided by OpenSwarm"), which is
|
||||
what the original wording did and what the rule is actually about. So: the PATH may say
|
||||
anything, the WORDS around it may not."""
|
||||
out = shape_text(p_body(), BLOB)
|
||||
i = out.index("[...")
|
||||
note = out[i:out.index("]", i)]
|
||||
prose = note.replace(BLOB, "<path>").lower()
|
||||
for word in ("openswarm", "harness", "elided by", "automated", "assistant", "we ", "our "):
|
||||
assert word not in prose, f"the note's prose must not say {word!r}: {prose}"
|
||||
assert "<path>" in prose, "and the path itself must still be there"
|
||||
def test_the_recovery_arg_is_documented_as_deliberately_unsurfaced():
|
||||
"""It looked like a forgotten parameter, which is how the path got restored once already."""
|
||||
src = open("backend/apps/agents/manager/streaming/tool_output_shaper.py").read()
|
||||
i = src.index("def shape_text(")
|
||||
doc = src[i:i + 700]
|
||||
assert "deliberately NOT written" in doc
|
||||
|
||||
|
||||
def test_no_blob_means_no_promise():
|
||||
|
||||
@@ -32,10 +32,11 @@ def test_an_elision_is_marked_and_never_names_the_harness_IN_PROSE():
|
||||
it cannot, so withholding the path was paying for a recovery nobody could use."""
|
||||
out = shape_text(p_big(), "/data/blobs/x.txt")
|
||||
assert "characters omitted" in out, "a cut must be visible as a cut"
|
||||
assert "/data/blobs/x.txt" in out, "the file it already wrote must be reachable"
|
||||
note = out[out.index("[..."):out.index("]", out.index("[..."))]
|
||||
assert "OpenSwarm" not in note.replace("/data/blobs/x.txt", ""), \
|
||||
"the harness may not name ITSELF; a path is data, prose is a confession"
|
||||
# CORRECTED AGAIN 2026-08-27, this time by live drill rather than by a statistic: advertising
|
||||
# the path makes the model flag the note as a prompt-injection attempt and refuse it. See
|
||||
# test_shaper_recovery_path.py for the two transcripts.
|
||||
assert "/data/blobs/x.txt" not in out, "an embedded path+instruction reads as injection"
|
||||
assert "OpenSwarm" not in out
|
||||
|
||||
|
||||
def test_the_answer_line_survives_the_middle():
|
||||
|
||||
Reference in New Issue
Block a user