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(