From b60224b662b8a62f7917e20cb212a09d4d906a4f Mon Sep 17 00:00:00 2001 From: MuchoRio Date: Sat, 10 Oct 2026 19:48:34 +0700 Subject: [PATCH 1/2] fix(agents): prevent lost subagent findings and warn on unfiled reports In multi-agent pentests (e.g. Issue #1439 against DVWA), subagents frequently discover exploitable vulnerabilities and supply them as narrative strings in agent_finish(findings=[...]) without first calling create_vulnerability_report. Because parent agents strictly rely on filed_report_ids from authoritative state and avoid refiling from narrative text, these findings are silently dropped, causing scans of vulnerable targets to complete with 0 findings. 1. In child_initial_input, explicitly instruct subagents to file confirmed vulnerabilities via create_vulnerability_report before calling agent_finish, clarifying that passing findings to agent_finish does not persist reports. 2. In agent_finish, when findings is non-empty but filed_report_ids is empty, attach an explicit warning to the tool response payload and the rendered completion report delivered to the parent inbox. 3. Add regression tests in test_agent_graph_coordination.py and test_inputs.py. --- strix/core/inputs.py | 6 ++-- strix/tools/agents_graph/tools.py | 44 +++++++++++++++++++------- tests/test_agent_graph_coordination.py | 30 ++++++++++++++++++ tests/test_inputs.py | 2 ++ 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/strix/core/inputs.py b/strix/core/inputs.py index c36f71b6..45707b15 100644 --- a/strix/core/inputs.py +++ b/strix/core/inputs.py @@ -364,8 +364,10 @@ def child_initial_input( ) parts.append( f"You are agent {name} ({child_id}); your parent is {parent_id}. " - "Maintain your own identity. Call agent_finish when your task " - "is complete.", + "Maintain your own identity. File any confirmed vulnerabilities using " + "create_vulnerability_report (or create_dependency_report) BEFORE calling " + "agent_finish — passing findings to agent_finish does not file reports. " + "Call agent_finish when your task is complete.", ) parts.append(task) return [{"role": "user", "content": "\n\n".join(parts)}] diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index a26c1dc4..0c4ddbd9 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -101,6 +101,12 @@ def _render_completion_report( lines.extend(_render_filed_report(r) for r in filed_reports) else: lines.append("- (none)") + if findings: + lines.append( + " [WARNING: This agent reported narrative findings above but filed 0 " + "authoritative vulnerability reports. Unreported findings are not included " + "in scan results or SARIF unless filed via create_vulnerability_report.]" + ) lines.append("") lines.append("Open items (unresolved, need follow-up):") if open_items: @@ -693,6 +699,7 @@ async def agent_finish( filed_reports = _filed_reports_by(me) filed_report_ids = [str(r.get("id")) for r in filed_reports] + unfiled_findings = bool((findings or []) and not filed_report_ids) parent_notified = False if report_to_parent and await coordinator.claim_parent_notice(me): @@ -718,6 +725,7 @@ async def agent_finish( "type": "completion", "priority": "high", "filed_report_ids": filed_report_ids, + "unfiled_findings": unfiled_findings, }, ) parent_notified = True @@ -735,19 +743,33 @@ async def agent_finish( len(filed_report_ids), parent_notified, ) + if unfiled_findings: + logger.warning( + "agent_finish: %s reported %d narrative finding(s) but filed 0 vulnerability reports", + me, + len(findings or []), + ) + + response_payload: dict[str, Any] = { + "success": True, + "agent_completed": True, + "parent_notified": parent_notified, + "agent_id": me, + "summary": result_summary, + "filed_report_ids": filed_report_ids, + "findings_count": len(findings or []), + "open_items_count": len(open_items or []), + "has_recommendations": bool(final_recommendations), + } + if unfiled_findings: + response_payload["warning"] = ( + f"Reported {len(findings or [])} finding(s) but filed 0 vulnerability reports. " + "Narrative findings in agent_finish do not register vulnerabilities in the scan " + "report. Verified findings must be filed via create_vulnerability_report." + ) return json.dumps( - { - "success": True, - "agent_completed": True, - "parent_notified": parent_notified, - "agent_id": me, - "summary": result_summary, - "filed_report_ids": filed_report_ids, - "findings_count": len(findings or []), - "open_items_count": len(open_items or []), - "has_recommendations": bool(final_recommendations), - }, + response_payload, ensure_ascii=False, default=str, ) diff --git a/tests/test_agent_graph_coordination.py b/tests/test_agent_graph_coordination.py index 9694656a..495c5dc4 100644 --- a/tests/test_agent_graph_coordination.py +++ b/tests/test_agent_graph_coordination.py @@ -256,3 +256,33 @@ async def test_agent_finish_without_report_state_still_completes() -> None: assert result["success"] is True assert result["filed_report_ids"] == [] + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("report_state") +async def test_agent_finish_warns_when_findings_reported_without_filing() -> None: + coordinator = await _graph(interactive=False) + + result = await _call( + agent_finish, + coordinator, + "child", + { + "result_summary": "discovered injection", + "findings": ["SQL injection on /login.php"], + }, + parent_id="root", + ) + + assert result["success"] is True + assert result["filed_report_ids"] == [] + assert "warning" in result + assert "Reported 1 finding(s) but filed 0 vulnerability reports" in result["warning"] + + delivered = coordinator.runtimes["root"].mailbox + assert len(delivered) == 1 + assert delivered[0]["unfiled_findings"] is True + body = delivered[0]["content"] + assert "authoritative vulnerability reports" in body + assert "create_vulnerability_report" in body + diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 2bdde73a..e58a5cfc 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -35,6 +35,7 @@ def test_child_initial_input_single_message_without_history() -> None: content = result[0]["content"] assert "agent scout (agent-2)" in content assert "Audit the login flow." in content + assert "create_vulnerability_report" in content assert "Inherited context" not in content @@ -49,6 +50,7 @@ def test_child_initial_input_single_message_with_history() -> None: assert "previous work" in content assert "agent scout (agent-2)" in content assert "Audit the login flow." in content + assert "create_vulnerability_report" in content @pytest.mark.parametrize( From 95587d3855d8810ae1a47bd355a8bded29858b8a Mon Sep 17 00:00:00 2001 From: MuchoRio Date: Sat, 10 Oct 2026 19:55:32 +0700 Subject: [PATCH 2/2] refactor(agents): refine subagent filing directive and observation notice Addresses code review feedback from Greptile: 1. In child_initial_input, clarify that subagents authorized to report findings should file confirmed vulnerabilities via create_vulnerability_report (or ensure their reporting specialist has filed them) before calling agent_finish, preserving specialist delegation roles. 2. In agent_finish and completion report, soften wording to a structured notice/advisory so that clean assessments with negative observations (e.g. findings=['ruled out X']) do not produce false alarms while still alerting parents to unfiled vulnerabilities. --- strix/core/inputs.py | 9 +++++---- strix/tools/agents_graph/tools.py | 17 +++++++++-------- tests/test_agent_graph_coordination.py | 6 +++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/strix/core/inputs.py b/strix/core/inputs.py index 45707b15..13aa0624 100644 --- a/strix/core/inputs.py +++ b/strix/core/inputs.py @@ -364,10 +364,11 @@ def child_initial_input( ) parts.append( f"You are agent {name} ({child_id}); your parent is {parent_id}. " - "Maintain your own identity. File any confirmed vulnerabilities using " - "create_vulnerability_report (or create_dependency_report) BEFORE calling " - "agent_finish — passing findings to agent_finish does not file reports. " - "Call agent_finish when your task is complete.", + "Maintain your own identity. If authorized to report findings, file any " + "confirmed vulnerabilities using create_vulnerability_report (or " + "create_dependency_report) BEFORE calling agent_finish (or ensure your " + "reporting specialist has filed them) — passing findings to agent_finish " + "does not file reports. Call agent_finish when your task is complete.", ) parts.append(task) return [{"role": "user", "content": "\n\n".join(parts)}] diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index 0c4ddbd9..64d71673 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -103,9 +103,9 @@ def _render_completion_report( lines.append("- (none)") if findings: lines.append( - " [WARNING: This agent reported narrative findings above but filed 0 " - "authoritative vulnerability reports. Unreported findings are not included " - "in scan results or SARIF unless filed via create_vulnerability_report.]" + " [NOTICE: This agent noted narrative findings above with 0 formal " + "reports filed. If any finding represents an unfiled vulnerability, ensure it " + "is reported via create_vulnerability_report.]" ) lines.append("") lines.append("Open items (unresolved, need follow-up):") @@ -744,8 +744,8 @@ async def agent_finish( parent_notified, ) if unfiled_findings: - logger.warning( - "agent_finish: %s reported %d narrative finding(s) but filed 0 vulnerability reports", + logger.info( + "agent_finish: %s reported %d narrative finding(s) with 0 reports filed", me, len(findings or []), ) @@ -762,10 +762,11 @@ async def agent_finish( "has_recommendations": bool(final_recommendations), } if unfiled_findings: - response_payload["warning"] = ( - f"Reported {len(findings or [])} finding(s) but filed 0 vulnerability reports. " + response_payload["notice"] = ( + f"Reported {len(findings or [])} finding(s)/observation(s) with 0 reports filed. " "Narrative findings in agent_finish do not register vulnerabilities in the scan " - "report. Verified findings must be filed via create_vulnerability_report." + "report. If any finding represents an unfiled vulnerability, ensure it is filed " + "via create_vulnerability_report." ) return json.dumps( diff --git a/tests/test_agent_graph_coordination.py b/tests/test_agent_graph_coordination.py index 495c5dc4..7ce82abd 100644 --- a/tests/test_agent_graph_coordination.py +++ b/tests/test_agent_graph_coordination.py @@ -276,13 +276,13 @@ async def test_agent_finish_warns_when_findings_reported_without_filing() -> Non assert result["success"] is True assert result["filed_report_ids"] == [] - assert "warning" in result - assert "Reported 1 finding(s) but filed 0 vulnerability reports" in result["warning"] + assert "notice" in result + assert "Reported 1 finding(s)/observation(s) with 0 reports filed" in result["notice"] delivered = coordinator.runtimes["root"].mailbox assert len(delivered) == 1 assert delivered[0]["unfiled_findings"] is True body = delivered[0]["content"] - assert "authoritative vulnerability reports" in body + assert "0 formal reports filed" in body assert "create_vulnerability_report" in body