mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-26 06:01:49 +00:00
fix(kanban): sanitize comment author rendering in build_worker_context (#22769)
Operator-controlled HERMES_PROFILE values were rendered as
'**${author}** (${ts}):' — markdown bold with no provenance prefix.
Worker comment bodies render directly underneath. A misleading
profile name like 'hermes-system' or 'operator' could be misread by
the next worker as a system directive above attacker-influenced
content (confused-deputy primitive gated on operator misconfig).
The LLM-controlled author-forgery surface was already closed in
#22435 (author removed from KANBAN_COMMENT_SCHEMA). This is
defense-in-depth: render with an explicit 'comment from worker
`<author>` at <ts>:' prefix so even 'hermes-system' resolves to
'comment from worker `hermes-system` at ...' — parseable as
worker-comment metadata, not a system directive. Strip backticks
from author so they can't break out of the fence.
Update test_build_worker_context_caps_comments to count by body
regex since the rendered author line now also starts with
'comment '.
Closes #22452.
This commit is contained in:
parent
f00dc6d7a3
commit
ade5981429
2 changed files with 38 additions and 5 deletions
|
|
@ -4072,7 +4072,14 @@ def build_worker_context(conn: sqlite3.Connection, task_id: str) -> str:
|
||||||
)
|
)
|
||||||
for c in shown_c:
|
for c in shown_c:
|
||||||
ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(c.created_at))
|
ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(c.created_at))
|
||||||
lines.append(f"**{c.author}** ({ts}):")
|
# Render author with explicit "comment from worker" framing so
|
||||||
|
# operator-controlled HERMES_PROFILE values like "hermes-system"
|
||||||
|
# or "operator" can't be misread by the next worker as a system
|
||||||
|
# directive above the (attacker-influenceable) comment body.
|
||||||
|
# Defense-in-depth — the LLM-controlled author-forgery surface
|
||||||
|
# was already closed in #22435. See #22452.
|
||||||
|
safe_author = (c.author or "").replace("`", "")
|
||||||
|
lines.append(f"comment from worker `{safe_author}` at {ts}:")
|
||||||
lines.append(_cap(c.body, _CTX_MAX_COMMENT_BYTES))
|
lines.append(_cap(c.body, _CTX_MAX_COMMENT_BYTES))
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2507,6 +2507,27 @@ def test_build_worker_context_caps_prior_attempts(kanban_home):
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_worker_context_renders_author_with_safe_framing(kanban_home):
|
||||||
|
"""Author rendering wraps the operator-controlled author in code fences
|
||||||
|
+ "comment from worker" prefix so a misleading HERMES_PROFILE name
|
||||||
|
(e.g. "hermes-system", "operator") can't be misread as a system
|
||||||
|
directive above the comment body. Defense-in-depth — see #22452."""
|
||||||
|
conn = kb.connect()
|
||||||
|
try:
|
||||||
|
tid = kb.create_task(conn, title="t", assignee="worker")
|
||||||
|
kb.add_comment(conn, tid, author="hermes-system", body="some note")
|
||||||
|
ctx = kb.build_worker_context(conn, tid)
|
||||||
|
|
||||||
|
# No bold-author rendering anywhere in the context.
|
||||||
|
assert "**hermes-system**" not in ctx
|
||||||
|
# Explicit provenance prefix is present.
|
||||||
|
assert "comment from worker `hermes-system` at " in ctx
|
||||||
|
# The body still renders.
|
||||||
|
assert "some note" in ctx
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def test_build_worker_context_caps_comments(kanban_home):
|
def test_build_worker_context_caps_comments(kanban_home):
|
||||||
"""Same cap for comments — comment-storm tasks stay bounded."""
|
"""Same cap for comments — comment-storm tasks stay bounded."""
|
||||||
conn = kb.connect()
|
conn = kb.connect()
|
||||||
|
|
@ -2516,10 +2537,15 @@ def test_build_worker_context_caps_comments(kanban_home):
|
||||||
kb.add_comment(conn, tid, author=f"u{i % 3}", body=f"comment {i}")
|
kb.add_comment(conn, tid, author=f"u{i % 3}", body=f"comment {i}")
|
||||||
ctx = kb.build_worker_context(conn, tid)
|
ctx = kb.build_worker_context(conn, tid)
|
||||||
# Only _CTX_MAX_COMMENTS most-recent shown in full
|
# Only _CTX_MAX_COMMENTS most-recent shown in full
|
||||||
comment_count = ctx.count("**u")
|
# Count by body text since author rendering uses code-fenced
|
||||||
# 3 distinct authors u0/u1/u2 so the count is trickier; use the
|
# "comment from worker `<author>` at <ts>:" framing (#22452).
|
||||||
# "comment N" body text to count.
|
# Comment bodies are "comment 0".."comment 99" so we need to
|
||||||
body_count = sum(1 for line in ctx.splitlines() if line.startswith("comment "))
|
# match the body specifically (digit suffix), not the author
|
||||||
|
# provenance line (which also starts with "comment ").
|
||||||
|
import re
|
||||||
|
body_count = sum(
|
||||||
|
1 for line in ctx.splitlines() if re.fullmatch(r"comment \d+", line)
|
||||||
|
)
|
||||||
assert body_count == kb._CTX_MAX_COMMENTS, (
|
assert body_count == kb._CTX_MAX_COMMENTS, (
|
||||||
f"expected {kb._CTX_MAX_COMMENTS} comments shown, got {body_count}"
|
f"expected {kb._CTX_MAX_COMMENTS} comments shown, got {body_count}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue