feat: verify attack-chain nodes against real tool calls

A node claiming something ran must cite a tool_call_id the host issued in
the trajectory. A citation absent from the trajectory is invented, so the
node is dropped along with its edges; a missing citation counts as invented
only when every real ID survived budget fitting, so a trimmed payload cannot
silently empty a graph. Judging is skipped where the evidence cannot be
enumerated: the process-detail supplement dumps raw event JSON and the
messages-table fallback carries no call IDs. Verified citations land in node
metadata rather than tool_execution_id, which is foreign-keyed to
tool_executions.

Also close two smaller holes: fact text can no longer carry the host index
delimiters, which would close the block early and spill the remaining index
into the system prompt; and the console fact endpoints reject the reserved
note/context-* prefix, matching the MCP tool layer.
This commit is contained in:
Sycun committed 2026-10-13 00:10:33 +08:00
1 parent 16189e0b76
commit 3d2dfdac36
10 files changed
+341 -15

No files matched your search

+1 -1
View File
@@ -246,7 +246,7 @@ func TestBuildAndCachedContextKeepConversationScopeAndGraphEvidence(t *testing.T
return
}
requests <- payload.Messages[1].Content
_ = json.NewEncoder(w).Encode(map[string]interface{}{"choices": []interface{}{map[string]interface{}{"message": map[string]string{"content": `{"nodes":[{"id":"target","type":"target","label":"scope.example"},{"id":"scan","type":"action","label":"Actual scan","metadata":{"tool_name":"scan"}}],"edges":[{"source":"target","target":"scan","type":"leads_to"}],"context_summaries":[{"content":"MODEL_INVENTED_SUMMARY"}]}`}}}})
_ = json.NewEncoder(w).Encode(map[string]interface{}{"choices": []interface{}{map[string]interface{}{"message": map[string]string{"content": `{"nodes":[{"id":"target","type":"target","label":"scope.example"},{"id":"scan","type":"action","label":"Actual scan","tool_call_id":"call-1","metadata":{"tool_name":"scan"}}],"edges":[{"source":"target","target":"scan","type":"leads_to"}],"context_summaries":[{"content":"MODEL_INVENTED_SUMMARY"}]}`}}}})
}))
t.Cleanup(server.Close)
b := NewBuilder(db, &config.OpenAIConfig{BaseURL: server.URL, APIKey: "test-only", Model: "gpt-4", MaxTotalTokens: 32000}, zap.NewNop())