diff --git a/data/self_modify_reports/20260226_070455_add_docstring.md b/data/self_modify_reports/20260226_070455_add_docstring.md
new file mode 100644
index 0000000..3eee61a
--- /dev/null
+++ b/data/self_modify_reports/20260226_070455_add_docstring.md
@@ -0,0 +1,19 @@
+# Self-Modify Report: 20260226_070455
+
+**Instruction:** Add docstring
+**Target files:** src/foo.py
+**Dry run:** True
+**Backend:** ollama
+**Branch:** N/A
+**Result:** SUCCESS
+**Error:** none
+**Commit:** none
+**Attempts:** 1
+**Autonomous cycles:** 0
+
+## Attempt 1 -- dry_run
+
+### LLM Response
+```
+llm raw
+```
diff --git a/data/self_modify_reports/20260226_070455_break_it.md b/data/self_modify_reports/20260226_070455_break_it.md
new file mode 100644
index 0000000..be08013
--- /dev/null
+++ b/data/self_modify_reports/20260226_070455_break_it.md
@@ -0,0 +1,31 @@
+# Self-Modify Report: 20260226_070455
+
+**Instruction:** Break it
+**Target files:** src/foo.py
+**Dry run:** False
+**Backend:** ollama
+**Branch:** N/A
+**Result:** FAILED
+**Error:** Tests failed after 1 attempt(s).
+**Commit:** none
+**Attempts:** 1
+**Autonomous cycles:** 0
+
+## Attempt 1 -- complete
+
+### LLM Response
+```
+llm raw
+```
+
+### Edits Written
+#### src/foo.py
+```python
+x = 1
+
+```
+
+### Test Result: FAILED
+```
+1 failed
+```
diff --git a/data/self_modify_reports/20260226_070455_do_something_vague.md b/data/self_modify_reports/20260226_070455_do_something_vague.md
new file mode 100644
index 0000000..dce18a5
--- /dev/null
+++ b/data/self_modify_reports/20260226_070455_do_something_vague.md
@@ -0,0 +1,12 @@
+# Self-Modify Report: 20260226_070455
+
+**Instruction:** do something vague
+**Target files:** (auto-detected)
+**Dry run:** False
+**Backend:** ollama
+**Branch:** N/A
+**Result:** FAILED
+**Error:** No target files identified. Specify target_files or use more specific language.
+**Commit:** none
+**Attempts:** 0
+**Autonomous cycles:** 0
diff --git a/data/self_modify_reports/20260226_070455_fix_foo.md b/data/self_modify_reports/20260226_070455_fix_foo.md
new file mode 100644
index 0000000..9978258
--- /dev/null
+++ b/data/self_modify_reports/20260226_070455_fix_foo.md
@@ -0,0 +1,31 @@
+# Self-Modify Report: 20260226_070455
+
+**Instruction:** Fix foo
+**Target files:** src/foo.py
+**Dry run:** False
+**Backend:** ollama
+**Branch:** N/A
+**Result:** FAILED
+**Error:** Tests failed after 1 attempt(s).
+**Commit:** none
+**Attempts:** 1
+**Autonomous cycles:** 0
+
+## Attempt 1 -- complete
+
+### LLM Response
+```
+llm raw
+```
+
+### Edits Written
+#### src/foo.py
+```python
+x = 2
+
+```
+
+### Test Result: FAILED
+```
+FAILED
+```
diff --git a/data/self_modify_reports/20260226_070455_fix_foo_important_correction_from_previ.md b/data/self_modify_reports/20260226_070455_fix_foo_important_correction_from_previ.md
new file mode 100644
index 0000000..5cd91d3
--- /dev/null
+++ b/data/self_modify_reports/20260226_070455_fix_foo_important_correction_from_previ.md
@@ -0,0 +1,34 @@
+# Self-Modify Report: 20260226_070455
+
+**Instruction:** Fix foo
+
+IMPORTANT CORRECTION from previous failure:
+Fix: do X instead of Y
+**Target files:** src/foo.py
+**Dry run:** False
+**Backend:** ollama
+**Branch:** N/A
+**Result:** SUCCESS
+**Error:** none
+**Commit:** abc123
+**Attempts:** 1
+**Autonomous cycles:** 0
+
+## Attempt 1 -- complete
+
+### LLM Response
+```
+llm raw
+```
+
+### Edits Written
+#### src/foo.py
+```python
+x = 2
+
+```
+
+### Test Result: PASSED
+```
+PASSED
+```
diff --git a/src/dashboard/templates/mobile_test.html b/src/dashboard/templates/mobile_test.html
index e0662ae..c6d31cc 100644
--- a/src/dashboard/templates/mobile_test.html
+++ b/src/dashboard/templates/mobile_test.html
@@ -343,15 +343,41 @@
const pct = TOTAL ? Math.round((passed / TOTAL) * 100) : 0;
const color = failed > 0 ? "var(--red)" : "var(--green)";
- summaryBody.innerHTML = `
-
${passed} / ${TOTAL}
- ${pct}% pass rate
-
-
PASSED${passed}
-
FAILED${failed}
-
SKIPPED${skipped}
-
- `;
+ // Safely build summary UI using DOM API to avoid XSS from potentially untrusted variables
+ summaryBody.innerHTML = '';
+
+ const scoreDiv = document.createElement('div');
+ scoreDiv.className = 'mt-summary-score';
+ scoreDiv.style.color = color;
+ scoreDiv.textContent = passed + ' / ' + TOTAL;
+ summaryBody.appendChild(scoreDiv);
+
+ const pctDiv = document.createElement('div');
+ pctDiv.className = 'mt-summary-pct';
+ pctDiv.textContent = pct + '% pass rate';
+ summaryBody.appendChild(pctDiv);
+
+ const statsContainer = document.createElement('div');
+ statsContainer.style.marginTop = '16px';
+
+ const createRow = (label, value, colorVar) => {
+ const row = document.createElement('div');
+ row.className = 'mt-summary-row';
+ const labelSpan = document.createElement('span');
+ labelSpan.textContent = label;
+ const valSpan = document.createElement('span');
+ valSpan.style.color = 'var(--' + colorVar + ')';
+ valSpan.style.fontWeight = '700';
+ valSpan.textContent = value;
+ row.appendChild(labelSpan);
+ row.appendChild(valSpan);
+ return row;
+ };
+
+ statsContainer.appendChild(createRow('PASSED', passed, 'green'));
+ statsContainer.appendChild(createRow('FAILED', failed, 'red'));
+ statsContainer.appendChild(createRow('SKIPPED', skipped, 'amber'));
+ summaryBody.appendChild(statsContainer);
const statusMsg = document.createElement('p');
statusMsg.style.marginTop = '12px';
diff --git a/src/dashboard/templates/partials/agent_chat_msg.html b/src/dashboard/templates/partials/agent_chat_msg.html
index 9d05a12..8c6a222 100644
--- a/src/dashboard/templates/partials/agent_chat_msg.html
+++ b/src/dashboard/templates/partials/agent_chat_msg.html
@@ -1,18 +1,18 @@
YOU // {{ timestamp }}
-
{{ message }}
+
{{ message | e }}
{% if response %}
{{ agent.name | upper }} // {{ timestamp }}
-
{{ response }}
+
{{ response | e }}
{% elif error %}
SYSTEM // {{ timestamp }}
-
{{ error }}
+
{{ error | e }}
{% elif task_id %}
diff --git a/src/dashboard/templates/partials/agent_panel.html b/src/dashboard/templates/partials/agent_panel.html
index 0451d30..d37d4be 100644
--- a/src/dashboard/templates/partials/agent_panel.html
+++ b/src/dashboard/templates/partials/agent_panel.html
@@ -7,9 +7,9 @@
@@ -57,7 +57,7 @@
- {{ agent.name | upper }}
+ {{ agent.name | upper | e }}
@@ -21,7 +21,7 @@
{{ agent.status }}
{% if agent.capabilities %}
CAPS
- {{ agent.capabilities }}
+ {{ agent.capabilities | e }}
{% endif %}
SEEN
{{ agent.last_seen[:19].replace("T"," ") if agent.last_seen else "—" }}
diff --git a/tests/test_security_fixes_xss.py b/tests/test_security_fixes_xss.py
new file mode 100644
index 0000000..ba07448
--- /dev/null
+++ b/tests/test_security_fixes_xss.py
@@ -0,0 +1,69 @@
+import pytest
+from fastapi.templating import Jinja2Templates
+
+def test_agent_chat_msg_xss_prevention():
+ """Verify XSS prevention in agent_chat_msg.html."""
+ templates = Jinja2Templates(directory="src/dashboard/templates")
+ payload = ""
+ class MockAgent:
+ def __init__(self):
+ self.name = "TestAgent"
+ self.id = "test-agent"
+
+ response = templates.get_template("partials/agent_chat_msg.html").render({
+ "message": payload,
+ "response": payload,
+ "error": payload,
+ "agent": MockAgent(),
+ "timestamp": "12:00:00"
+ })
+
+ # Check that payload is escaped
+ assert "<script>alert('xss')</script>" in response
+ assert payload not in response
+
+def test_agent_panel_xss_prevention():
+ """Verify XSS prevention in agent_panel.html."""
+ templates = Jinja2Templates(directory="src/dashboard/templates")
+ payload = ""
+ class MockAgent:
+ def __init__(self):
+ self.name = payload
+ self.id = "test-agent"
+ self.status = "idle"
+ self.capabilities = payload
+
+ class MockTask:
+ def __init__(self):
+ self.id = "task-1"
+ self.status = type('obj', (object,), {'value': 'completed'})
+ self.created_at = "2026-02-26T12:00:00"
+ self.description = payload
+ self.result = payload
+
+ response = templates.get_template("partials/agent_panel.html").render({
+ "agent": MockAgent(),
+ "tasks": [MockTask()]
+ })
+
+ assert "<script>alert('xss')</script>" in response
+ assert payload not in response
+
+def test_swarm_sidebar_xss_prevention():
+ """Verify XSS prevention in swarm_agents_sidebar.html."""
+ templates = Jinja2Templates(directory="src/dashboard/templates")
+ payload = ""
+ class MockAgent:
+ def __init__(self):
+ self.name = payload
+ self.id = "test-agent"
+ self.status = "idle"
+ self.capabilities = payload
+ self.last_seen = "2026-02-26T12:00:00"
+
+ response = templates.get_template("partials/swarm_agents_sidebar.html").render({
+ "agents": [MockAgent()]
+ })
+
+ assert "<script>alert('xss')</script>" in response
+ assert payload not in response