Skip to content

Commit f19c2a9

Browse files
committed
fix(agent): escape XML values, CDATA content, generic corrective message
- Add html.escape() to target values in <scan_task> (URLs, paths, IPs) - Escape sender_name/sender_id in <agent_message> attributes - CDATA-wrap message content in <agent_message> to handle any text - Make corrective message generic (no StrixAgent-specific tool names)
1 parent 1ca72c0 commit f19c2a9

3 files changed

Lines changed: 229 additions & 16 deletions

File tree

.sisyphus/plans/pr-review-fixes.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# PR Review Feedback Fixes
2+
3+
## TL;DR
4+
5+
> **Quick Summary**: Address reviewer feedback from Greptile and Copilot across PRs 381-384. Fix a real bug (thinking blocks dropped on interrupted messages), improve code quality (XML escaping, private symbol export), and add clarifying comments (Qwen compliance block).
6+
>
7+
> **Deliverables**:
8+
> - PR 381: `get_message_tokens` (dropped leading underscore)
9+
> - PR 382: Split WSTG category codes + compliance block comment
10+
> - PR 383: Thinking blocks fix in interrupted path
11+
> - PR 384: XML escaping + generic corrective message
12+
>
13+
> **Estimated Effort**: Quick
14+
> **Parallel Execution**: YES - 4 PRs, each fix is independent
15+
16+
---
17+
18+
## Context
19+
20+
### Original Request
21+
User asked to read reviewer feedback on the 4 split PRs and implement fixes.
22+
23+
### Reviewer Findings
24+
25+
**PR 381 (Memory Fix)** - Greptile: `_get_message_tokens` is a private symbol imported across module boundaries. Rename to `get_message_tokens`.
26+
27+
**PR 382 (WSTG Prompts)** - Greptile: `IDNT/ATHN` and `ATHZ/SESS` are combined codes inconsistent with single codes used elsewhere. `<compliance>` block wording is too aggressive for prompt injection resistance. User clarified: keep the aggressive wording (needed for Qwen-series models), add a comment explaining why.
28+
29+
**PR 383 (TUI Status)** - Greptile: **BUG** - thinking blocks silently dropped when `metadata["interrupted"]` is true. Early return bypasses `renderables` list.
30+
31+
**PR 384 (Agent Workflow)** - Greptile: (1) URLs with `&` produce invalid XML in `<scan_task>`, (2) corrective message hardcodes StrixAgent-specific tool names in BaseAgent, (3) message content not escaped in `<agent_message>` XML. User approved: HTML escape for all targets, generic corrective message.
32+
33+
---
34+
35+
## Work Objectives
36+
37+
### Core Objective
38+
Apply all non-blocking review feedback to make PRs cleaner and more robust.
39+
40+
### Concrete Deliverables
41+
- `_get_message_tokens` renamed to `get_message_tokens` in both files
42+
- WSTG category codes split into separate phases
43+
- Compliance block comment added (Qwen rationale)
44+
- Thinking blocks included in interrupted message path
45+
- `html.escape()` on all XML-interpolated values
46+
- Generic corrective message in BaseAgent
47+
- CDATA-wrapping for `<agent_message>` content
48+
49+
---
50+
51+
## TODOs
52+
53+
- [ ] 1. PR 381: Rename `_get_message_tokens``get_message_tokens`
54+
55+
**What to do**:
56+
- Rename function in `strix/llm/memory_compressor.py` (definition)
57+
- Update import in `strix/llm/llm.py` (consumption)
58+
- Commit on `fix/memory-compressor-token-budget` branch
59+
60+
**Must NOT do**:
61+
- Change function behavior or signature
62+
- Touch unrelated files
63+
64+
**Acceptance Criteria**:
65+
- [ ] `grep -r "_get_message_tokens"` returns no results in source files
66+
- [ ] `python -c "from strix.llm.memory_compressor import get_message_tokens"` succeeds
67+
- [ ] `git diff` shows only the rename, no behavioral changes
68+
69+
**Commit**: `refactor(llm): rename _get_message_tokens to public API name`
70+
71+
- [ ] 2. PR 382: Split WSTG category codes in `<methodology>` phases
72+
73+
**What to do**:
74+
- In `system_prompt.jinja`, split `IDNT/ATHN` into separate `IDNT` and `ATHN` phases
75+
- Split `ATHZ/SESS` into separate `ATHZ` and `SESS` phases
76+
- Renumber subsequent phases accordingly
77+
- Ensure codes match what `<skill_triggers>` and `<phase2>` use
78+
79+
**Must NOT do**:
80+
- Change the `<skill_triggers>` or `<phase2>` sections (they already use correct single codes)
81+
- Alter non-phase content in the methodology section
82+
83+
**Acceptance Criteria**:
84+
- [ ] No combined codes like `IDNT/ATHN` remain in system_prompt.jinja
85+
- [ ] All methodology phases use single WSTG codes matching `<skill_triggers>`
86+
87+
**Commit**: `fix(prompts): split combined WSTG category codes for consistency`
88+
89+
- [ ] 3. PR 382: Add comment explaining aggressive compliance wording
90+
91+
**What to do**:
92+
- Add a Jinja comment or XML comment above the `<compliance>` block explaining that the aggressive wording is intentional and was needed when testing with Qwen-series models (e.g., Qwen3.5-Plus)
93+
- Note that softer language caused unnecessary refusals during authorized scans
94+
95+
**Must NOT do**:
96+
- Change the actual compliance wording
97+
- Remove or weaken the `<compliance>` block
98+
99+
**Acceptance Criteria**:
100+
- [ ] Comment exists above `<compliance>` block referencing Qwen-series models
101+
- [ ] Functional output of the Jinja template is unchanged
102+
103+
**Commit**: `docs: add comment explaining aggressive compliance block rationale`
104+
105+
- [ ] 4. PR 383: Fix thinking blocks dropped on interrupted messages
106+
107+
**What to do**:
108+
- In `_render_chat_content` in `strix/interface/tui.py`, fix the `metadata["interrupted"]` branch
109+
- Change `self._merge_renderables([streaming_result, interrupted_text])` to include `renderables`:
110+
`self._merge_renderables([*renderables, streaming_result, interrupted_text])`
111+
112+
**Must NOT do**:
113+
- Change the interrupted message rendering logic beyond including renderables
114+
- Touch other branches of `_render_chat_content`
115+
116+
**Acceptance Criteria**:
117+
- [ ] Code shows `[*renderables, streaming_result, interrupted_text]` in interrupted branch
118+
- [ ] No other changes in the diff
119+
120+
**Commit**: `fix(ui): include thinking blocks in interrupted message render`
121+
122+
- [ ] 5. PR 384: Add `html.escape()` to XML-interpolated values in strix_agent.py
123+
124+
**What to do**:
125+
- `import html` at top of `strix/agents/StrixAgent/strix_agent.py`
126+
- Wrap all target values in `html.escape()` before embedding in XML:
127+
- `url` values
128+
- `repo["url"]` values
129+
- `code["path"]` values
130+
- IP address entries
131+
- Same approach for `base_agent.py` `<agent_message>` attributes (`sender_name`, `sender_id`)
132+
133+
**Must NOT do**:
134+
- Change the XML structure or element names
135+
- Escape already-safe static strings (only user/target-derived values)
136+
137+
**Acceptance Criteria**:
138+
- [ ] `html` is imported in both files
139+
- [ ] All interpolated target values wrapped in `html.escape()`
140+
- [ ] `<agent_message>` attributes `from="{html.escape(sender_name)}"` and `id="{html.escape(sender_id)}"` escape properly
141+
142+
**Commit**: `fix(agent): escape XML special characters in target values`
143+
144+
- [ ] 6. PR 384: Escape message content in `<agent_message>` XML
145+
146+
**What to do**:
147+
- In `base_agent.py`, wrap `message.get("content", "")` in CDATA or `html.escape()` before embedding in `<agent_message>` element content
148+
- CDATA is preferred here since content is free-form text that shouldn't be interpreted as XML
149+
150+
**Must NOT do**:
151+
- Change the XML element structure
152+
- Break existing `clean_content()` regex patterns
153+
154+
**Acceptance Criteria**:
155+
- [ ] Message content is CDATA-wrapped or HTML-escaped in the `<agent_message>` element
156+
- [ ] Content containing `</agent_message>` does not break the XML structure
157+
158+
**Commit**: `fix(agent): escape message content in compact agent_message format`
159+
160+
- [ ] 7. PR 384: Make corrective message generic in BaseAgent
161+
162+
**What to do**:
163+
- In `base_agent.py`, change the corrective message from mentioning specific tool names to generic guidance:
164+
```
165+
"You responded with plain text instead of a tool call.
166+
While the agent loop is running, EVERY response MUST be a tool call.
167+
Do NOT send plain text messages. Act via your available tools.
168+
Review your task and take action now."
169+
```
170+
- Remove references to `create_agent`, `terminal_execute`, `wait_for_message`
171+
172+
**Must NOT do**:
173+
- Change the `add_message("user", corrective_message)` call structure
174+
- Change the `return None` behavior
175+
176+
**Acceptance Criteria**:
177+
- [ ] Corrective message contains no StrixAgent-specific tool names
178+
- [ ] Message still conveys "use tools, not plain text"
179+
180+
**Commit**: `fix(agent): use generic corrective message in BaseAgent`
181+
182+
---
183+
184+
## Execution Strategy
185+
186+
Each fix is independent and lives on a separate branch. Apply fixes to the appropriate branch, commit, and force-push.
187+
188+
```
189+
Wave 1 (all parallel):
190+
├── Task 1: PR 381 branch - rename function
191+
├── Task 2: PR 382 branch - split WSTG codes
192+
├── Task 3: PR 382 branch - compliance comment
193+
├── Task 4: PR 383 branch - thinking blocks fix
194+
├── Task 5: PR 384 branch - XML escaping
195+
├── Task 6: PR 384 branch - CDATA wrapping
196+
└── Task 7: PR 384 branch - generic corrective message
197+
```
198+
199+
Tasks 2+3 share branch (PR 382). Tasks 5+6+7 share branch (PR 384). Work sequentially within a branch, but all 4 branches can be updated in parallel.
200+
201+
**Branch workflow per PR:**
202+
1. `git checkout <branch>`
203+
2. Apply edits
204+
3. `git add -A && git commit -m "message"`
205+
4. `git push origin <branch> --force-with-lease`
206+
207+
---
208+
209+
## Success Criteria
210+
211+
- [ ] All 4 PRs still `MERGEABLE` after fixes
212+
- [ ] No reviewer comments remain unaddressed
213+
- [ ] Each branch has exactly one additional commit with the fix

strix/agents/StrixAgent/strix_agent.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import Any
22

3+
import html
4+
35
from strix.agents.base_agent import BaseAgent
46
from strix.llm.config import LLMConfig
57

@@ -59,24 +61,24 @@ async def execute_scan(self, scan_config: dict[str, Any]) -> dict[str, Any]: #
5961

6062
target_lines = []
6163

62-
if repositories:
64+
if repositories:
6365
for repo in repositories:
6466
if repo["workspace_path"]:
65-
target_lines.append(f' <target type="repository">{repo["url"]} (code at: {repo["workspace_path"]})</target>')
67+
target_lines.append(f' <target type="repository">{html.escape(repo["url"])} (code at: {html.escape(repo["workspace_path"])})</target>')
6668
else:
67-
target_lines.append(f' <target type="repository">{repo["url"]}</target>')
69+
target_lines.append(f' <target type="repository">{html.escape(repo["url"])}</target>')
6870

6971
if local_code:
7072
for code in local_code:
71-
target_lines.append(f' <target type="local_code">{code["path"]} (code at: {code["workspace_path"]})</target>')
73+
target_lines.append(f' <target type="local_code">{html.escape(code["path"])} (code at: {html.escape(code["workspace_path"])})</target>')
7274

7375
if urls:
7476
for url in urls:
75-
target_lines.append(f' <target type="url">{url}</target>')
77+
target_lines.append(f' <target type="url">{html.escape(url)}</target>')
7678

7779
if ip_addresses:
7880
for ip in ip_addresses:
79-
target_lines.append(f' <target type="ip">{ip}</target>')
81+
target_lines.append(f' <target type="ip">{html.escape(ip)}</target>')
8082

8183
targets_block = "\n".join(target_lines)
8284

strix/agents/base_agent.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import contextlib
3+
import html
34
import logging
45
from typing import TYPE_CHECKING, Any, Optional
56

@@ -413,11 +414,7 @@ async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool | None:
413414
corrective_message = (
414415
"You responded with plain text instead of a tool call. "
415416
"While the agent loop is running, EVERY response MUST be a tool call. "
416-
"Do NOT send plain text messages. Act via tools:\n"
417-
"- Use the think tool to reason through problems\n"
418-
"- Use create_agent to spawn subagents for testing\n"
419-
"- Use terminal_execute to run commands\n"
420-
"- Use wait_for_message ONLY when waiting for subagent results\n"
417+
"Do NOT send plain text messages. Act via your available tools. "
421418
"Review your task and take action now."
422419
)
423420
self.state.add_message("user", corrective_message)
@@ -499,12 +496,13 @@ def _check_agent_messages(self, state: AgentState) -> None: # noqa: PLR0912
499496
if sender_id and sender_id in _agent_graph.get("nodes", {}):
500497
sender_name = _agent_graph["nodes"][sender_id]["name"]
501498

499+
content = message.get("content", "")
502500
message_content = f"""<agent_message
503-
from="{sender_name}"
504-
id="{sender_id}"
505-
type="{message.get("message_type", "information")}"
506-
priority="{message.get("priority", "normal")}">
507-
{message.get("content", "")}
501+
from="{html.escape(sender_name)}"
502+
id="{html.escape(str(sender_id))}"
503+
type="{html.escape(message.get("message_type", "information"))}"
504+
priority="{html.escape(message.get("priority", "normal"))}">
505+
<![CDATA[{content}]]>
508506
</agent_message>"""
509507
state.add_message("user", message_content.strip())
510508

0 commit comments

Comments
 (0)