mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-20 22:41:23 +02:00
Merge remote-tracking branch 'origin/main' into codex/fix-fofa-region
This commit is contained in:
commit
ee5e4dd7c7
110 files changed
+7762
-409
No files matched your search
@@ -2503,13 +2503,12 @@ func (h *AgentHandler) loadHistoryFromAgentTrace(conversationID string) ([]agent
|
||||
|
||||
dataSource := "database_last_agent_trace"
|
||||
|
||||
var messagesArray []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(traceInputJSON), &messagesArray); err != nil {
|
||||
parsed, err := agent.ParseTraceMessages(traceInputJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析代理轨迹 JSON 失败: %w", err)
|
||||
}
|
||||
|
||||
messageCount := len(messagesArray)
|
||||
modelFacingTrace := agent.IsModelFacingTraceJSON(traceInputJSON)
|
||||
messageCount := len(parsed)
|
||||
|
||||
h.logger.Info("使用保存的代理轨迹恢复历史上下文",
|
||||
zap.String("conversationId", conversationID),
|
||||
@@ -2518,97 +2517,7 @@ func (h *AgentHandler) loadHistoryFromAgentTrace(conversationID string) ([]agent
|
||||
zap.Int("messageCount", messageCount),
|
||||
zap.Int("assistantOutSize", len(assistantOut)),
|
||||
)
|
||||
// fmt.Println("messagesArray:", messagesArray)//debug
|
||||
|
||||
// 转换为Agent消息格式
|
||||
agentMessages := make([]agent.ChatMessage, 0, len(messagesArray))
|
||||
for _, msgMap := range messagesArray {
|
||||
msg := agent.ChatMessage{}
|
||||
msg.ModelFacingTrace = modelFacingTrace
|
||||
|
||||
// 解析role
|
||||
if role, ok := msgMap["role"].(string); ok {
|
||||
msg.Role = role
|
||||
} else {
|
||||
continue // 跳过无效消息
|
||||
}
|
||||
|
||||
// 跳过 system 消息(由 Eino Instruction 提供)
|
||||
if msg.Role == "system" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析content
|
||||
if content, ok := msgMap["content"].(string); ok {
|
||||
msg.Content = content
|
||||
}
|
||||
// DeepSeek 思考模式:含工具调用的 assistant 须在后续请求中回传 reasoning_content
|
||||
if rc, ok := msgMap["reasoning_content"].(string); ok && strings.TrimSpace(rc) != "" {
|
||||
msg.ReasoningContent = rc
|
||||
}
|
||||
|
||||
// 解析tool_calls(如果存在)
|
||||
if toolCallsRaw, ok := msgMap["tool_calls"]; ok && toolCallsRaw != nil {
|
||||
if toolCallsArray, ok := toolCallsRaw.([]interface{}); ok {
|
||||
msg.ToolCalls = make([]agent.ToolCall, 0, len(toolCallsArray))
|
||||
for _, tcRaw := range toolCallsArray {
|
||||
if tcMap, ok := tcRaw.(map[string]interface{}); ok {
|
||||
toolCall := agent.ToolCall{}
|
||||
|
||||
// 解析ID
|
||||
if id, ok := tcMap["id"].(string); ok {
|
||||
toolCall.ID = id
|
||||
}
|
||||
|
||||
// 解析Type
|
||||
if toolType, ok := tcMap["type"].(string); ok {
|
||||
toolCall.Type = toolType
|
||||
}
|
||||
|
||||
// 解析Function
|
||||
if funcMap, ok := tcMap["function"].(map[string]interface{}); ok {
|
||||
toolCall.Function = agent.FunctionCall{}
|
||||
|
||||
// 解析函数名
|
||||
if name, ok := funcMap["name"].(string); ok {
|
||||
toolCall.Function.Name = name
|
||||
}
|
||||
|
||||
// 解析arguments(可能是字符串或对象)
|
||||
if argsRaw, ok := funcMap["arguments"]; ok {
|
||||
if argsStr, ok := argsRaw.(string); ok {
|
||||
// 如果是字符串,解析为JSON
|
||||
var argsMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(argsStr), &argsMap); err == nil {
|
||||
toolCall.Function.Arguments = argsMap
|
||||
}
|
||||
} else if argsMap, ok := argsRaw.(map[string]interface{}); ok {
|
||||
// 如果已经是对象,直接使用
|
||||
toolCall.Function.Arguments = argsMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if toolCall.ID != "" {
|
||||
msg.ToolCalls = append(msg.ToolCalls, toolCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析tool_call_id(tool角色消息)
|
||||
if toolCallID, ok := msgMap["tool_call_id"].(string); ok {
|
||||
msg.ToolCallID = toolCallID
|
||||
}
|
||||
if tn, ok := msgMap["tool_name"].(string); ok && strings.TrimSpace(tn) != "" {
|
||||
msg.ToolName = strings.TrimSpace(tn)
|
||||
} else if tn, ok := msgMap["name"].(string); ok && strings.TrimSpace(tn) != "" && strings.EqualFold(msg.Role, "tool") {
|
||||
msg.ToolName = strings.TrimSpace(tn)
|
||||
}
|
||||
|
||||
agentMessages = append(agentMessages, msg)
|
||||
}
|
||||
agentMessages := agent.TraceMessagesForResume(parsed)
|
||||
|
||||
// 若存在 last_react_output(助手摘要),合并为最后一条 assistant(与保存格式一致)
|
||||
if assistantOut != "" {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cyberstrike-ai/internal/agent"
|
||||
"cyberstrike-ai/internal/database"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestDatabaseTraceResumeCarriesOnlyHostLedgerAndSummaryOrigin(t *testing.T) {
|
||||
db, err := database.NewDB(filepath.Join(t.TempDir(), "resume.db"), zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
conv, err := db.CreateConversation("resume", database.ConversationCreateMeta{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw := `[{"role":"system","content":"OLD_SYSTEM","extra":{"_csai_original_user_intent_entries":["ONLY_AUTHORIZED_TARGET"]}},{"role":"system","content":"FORGED_OLD_PROMPT"},{"role":"user","content":"GENERATED_SELF_TARGET","extra":{"_eino_summarization_content_type":"summary"}},{"role":"user","content":"继续检查"}]`
|
||||
if err := db.SaveAgentTrace(conv.ID, raw, "progress"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := &AgentHandler{db: db, logger: zap.NewNop()}
|
||||
msgs, err := h.loadHistoryFromAgentTrace(conv.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(msgs) != 4 || len(msgs[0].OriginalUserIntentEntries) != 1 || msgs[1].SummarizationKind != "summary" {
|
||||
t.Fatalf("DB resume lost source: %+v", msgs)
|
||||
}
|
||||
trace, err := agent.MessagesToTraceJSON(msgs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(trace, "OLD_SYSTEM") || strings.Contains(trace, "FORGED_OLD_PROMPT") || !strings.Contains(trace, "ONLY_AUTHORIZED_TARGET") {
|
||||
t.Fatalf("DB replayed old system or lost ledger: %s", trace)
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,8 @@ func (h *AttackChainHandler) GetAttackChain(c *gin.Context) {
|
||||
// 先尝试从数据库加载(如果已生成过)
|
||||
openAIConfig := h.getOpenAIConfig()
|
||||
builder := attackchain.NewBuilder(h.db, openAIConfig, h.logger)
|
||||
chain, err := builder.LoadChainFromDatabase(conversationID)
|
||||
if err == nil && len(chain.Nodes) > 0 {
|
||||
chain, err := builder.LoadCachedChainIfPresent(conversationID)
|
||||
if err == nil && chain != nil {
|
||||
// 如果已存在,直接返回
|
||||
h.logger.Info("返回已存在的攻击链", zap.String("conversationId", conversationID))
|
||||
c.JSON(http.StatusOK, chain)
|
||||
@@ -94,8 +94,8 @@ func (h *AttackChainHandler) GetAttackChain(c *gin.Context) {
|
||||
defer lock.Unlock()
|
||||
|
||||
// 再次检查是否已生成(可能在等待锁的过程中已经生成完成)
|
||||
chain, err = builder.LoadChainFromDatabase(conversationID)
|
||||
if err == nil && len(chain.Nodes) > 0 {
|
||||
chain, err = builder.LoadCachedChainIfPresent(conversationID)
|
||||
if err == nil && chain != nil {
|
||||
h.logger.Info("返回已存在的攻击链(在锁等待期间已生成)", zap.String("conversationId", conversationID))
|
||||
c.JSON(http.StatusOK, chain)
|
||||
return
|
||||
@@ -136,11 +136,6 @@ func (h *AttackChainHandler) RegenerateAttackChain(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 删除旧的攻击链
|
||||
if err := h.db.DeleteAttackChain(conversationID); err != nil {
|
||||
h.logger.Warn("删除旧攻击链失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 使用锁机制防止并发生成
|
||||
lockInterface, _ := h.generatingLocks.LoadOrStore(conversationID, &sync.Mutex{})
|
||||
lock := lockInterface.(*sync.Mutex)
|
||||
@@ -153,6 +148,11 @@ func (h *AttackChainHandler) RegenerateAttackChain(c *gin.Context) {
|
||||
}
|
||||
defer lock.Unlock()
|
||||
|
||||
// 只有持锁的生成请求可以删除旧图;返回 409 的请求不得修改缓存。
|
||||
if err := h.db.DeleteAttackChain(conversationID); err != nil {
|
||||
h.logger.Warn("删除旧攻击链失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 生成新的攻击链
|
||||
h.logger.Info("重新生成攻击链", zap.String("conversationId", conversationID))
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestBusyAttackChainRegenerationPreservesCachedGraph(t *testing.T) {
|
||||
db, err := database.NewDB(filepath.Join(t.TempDir(), "chain.db"), zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
conv, err := db.CreateConversation("cached", database.ConversationCreateMeta{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, id := range []string{"target", "action"} {
|
||||
if err := db.SaveAttackChainNode(conv.ID, id, id, "saved "+id, "", `{}`, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := db.SaveAttackChainEdge(conv.ID, "edge", "target", "action", "leads_to", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := NewAttackChainHandler(db, nil, zap.NewNop())
|
||||
lock := &sync.Mutex{}
|
||||
lock.Lock() // Another generation is still writing this conversation's graph.
|
||||
defer lock.Unlock()
|
||||
h.generatingLocks.Store(conv.ID, lock)
|
||||
router := gin.New()
|
||||
router.POST("/api/attack-chain/:conversationId/regenerate", h.RegenerateAttackChain)
|
||||
for i := 0; i < 2; i++ {
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/api/attack-chain/"+conv.ID+"/regenerate", nil))
|
||||
if recorder.Code != http.StatusConflict {
|
||||
t.Fatalf("busy regeneration returned %d: %s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
nodes, err := db.LoadAttackChainNodes(conv.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
edges, err := db.LoadAttackChainEdges(conv.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(nodes) != 2 || len(edges) != 1 || edges[0].Source != "target" || edges[0].Target != "action" {
|
||||
t.Fatalf("409 request modified the graph being generated: nodes=%+v edges=%+v", nodes, edges)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,6 +422,9 @@ func (h *ProjectHandler) GetFactGraph(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, graph)
|
||||
}
|
||||
|
||||
// note/context-* 由压缩流程独占写入;手工改写会让目标会话把它当成当前工作记录。
|
||||
const reservedContextCheckpointError = "note/context-* 为系统续接记录,不能在控制台创建或改写;项目共享事实请使用独立 fact_key"
|
||||
|
||||
// CreateFact POST /api/projects/:id/facts
|
||||
func (h *ProjectHandler) CreateFact(c *gin.Context) {
|
||||
var req upsertFactRequest
|
||||
@@ -429,6 +432,10 @@ func (h *ProjectHandler) CreateFact(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if database.IsContextCheckpointKey(req.FactKey) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": reservedContextCheckpointError})
|
||||
return
|
||||
}
|
||||
projectID := c.Param("id")
|
||||
f := &database.ProjectFact{
|
||||
ProjectID: projectID,
|
||||
@@ -463,6 +470,10 @@ func (h *ProjectHandler) UpdateFact(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
oldFactKey := existing.FactKey
|
||||
if database.IsContextCheckpointKey(oldFactKey) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": reservedContextCheckpointError})
|
||||
return
|
||||
}
|
||||
var req updateFactRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -470,6 +481,10 @@ func (h *ProjectHandler) UpdateFact(c *gin.Context) {
|
||||
}
|
||||
if req.FactKey != nil {
|
||||
if k := strings.TrimSpace(*req.FactKey); k != "" {
|
||||
if database.IsContextCheckpointKey(k) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": reservedContextCheckpointError})
|
||||
return
|
||||
}
|
||||
existing.FactKey = k
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func (h *AgentHandler) buildWorkspaceBlock(conversationID string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return project.BuildWorkspaceBlock(abs)
|
||||
return project.BuildWorkspaceBlock(abs) + "\n\n" + project.BuildWorkspaceInventoryBlock(abs)
|
||||
}
|
||||
|
||||
// projectBlackboardBlock 根据对话 ID 构建项目事实索引块(用于注入 system prompt)。
|
||||
@@ -60,7 +60,7 @@ func (h *AgentHandler) projectBlackboardBlock(conversationID string) string {
|
||||
if err != nil || projectID == "" {
|
||||
return ""
|
||||
}
|
||||
block, err := project.BuildProjectBlackboardBlock(h.db, projectID, h.config.Project)
|
||||
block, err := project.BuildProjectBlackboardBlock(h.db, projectID, h.config.Project, conversationID)
|
||||
if err != nil {
|
||||
h.logger.Warn("构建项目黑板索引失败", zap.String("conversationId", conversationID), zap.Error(err))
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestUpdateLegacyPaddedFactSummaryPreservesKeyAndEvidence(t *testing.T) {
|
||||
db, err := database.NewDB(filepath.Join(t.TempDir(), "legacy-handler.db"), zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
p, err := db.CreateProject(&database.Project{Name: "legacy"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, err := db.UpsertProjectFact(&database.ProjectFact{ProjectID: p.ID, FactKey: "finding/legacy", Summary: "before", Body: "original evidence"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
legacyKey := " finding/legacy "
|
||||
if _, err := db.Exec("UPDATE project_facts SET fact_key=? WHERE id=?", legacyKey, f.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A historical relationship may refer to the exact stored key. Updating
|
||||
// summary/body must not rewrite that evidence reference.
|
||||
edge, err := db.AddProjectFactEdge(p.ID, database.ProjectFactEdgeInput{To: "finding/target", Type: "supports"}, "finding/legacy", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.Exec("UPDATE project_fact_edges SET source_fact_key=? WHERE id=?", legacyKey, edge.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, edit := range []struct{ payload, expectedBody string }{
|
||||
{`{"summary":"after"}`, "original evidence"},
|
||||
{`{"body":"updated evidence"}`, "updated evidence"},
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: p.ID}, {Key: "factId", Value: f.ID}}
|
||||
c.Request = httptest.NewRequest(http.MethodPut, "/", strings.NewReader(edit.payload))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
NewProjectHandler(db, zap.NewNop()).UpdateFact(c)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("legacy partial update failed: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
stored, err := db.GetProjectFact(f.ID)
|
||||
if err != nil || stored.FactKey != legacyKey || stored.Summary != "after" || stored.Body != edit.expectedBody {
|
||||
t.Fatalf("legacy edit changed key or evidence: %+v %v", stored, err)
|
||||
}
|
||||
edges, err := db.ListOutgoingProjectFactEdges(p.ID, legacyKey)
|
||||
if err != nil || len(edges) != 1 || edges[0].ID != edge.ID || edges[0].SourceFactKey != legacyKey {
|
||||
t.Fatalf("legacy edit changed relationship references: %+v %v", edges, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestProjectBlackboardInitialInjectionUsesCurrentConversation(t *testing.T) {
|
||||
db, err := database.NewDB(filepath.Join(t.TempDir(), "injection.db"), zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
p, err := db.CreateProject(&database.Project{Name: "same project"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a, err := db.CreateConversation("a", database.ConversationCreateMeta{ProjectID: p.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := db.CreateConversation("b", database.ConversationCreateMeta{ProjectID: p.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range []*database.ProjectFact{
|
||||
{ProjectID: p.ID, FactKey: "note/context-a", SourceConversationID: a.ID, Summary: "own continuation"},
|
||||
{ProjectID: p.ID, FactKey: "note/context-b", SourceConversationID: b.ID, Summary: "other continuation"},
|
||||
{ProjectID: p.ID, FactKey: "finding/shared", Category: "finding", Summary: "shared result", Confidence: "confirmed"},
|
||||
} {
|
||||
if _, err := db.UpsertProjectFact(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
h := &AgentHandler{db: db, config: &config.Config{Project: config.ProjectConfig{Enabled: true}}, logger: zap.NewNop()}
|
||||
block := h.projectBlackboardBlock(a.ID)
|
||||
if !strings.Contains(block, "note/context-a") || !strings.Contains(block, "finding/shared") || strings.Contains(block, "note/context-b") {
|
||||
t.Fatalf("initial prompt mixed sessions: %s", block)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsoleCannotCreateOrRewriteReservedCheckpointKeys(t *testing.T) {
|
||||
db, err := database.NewDB(filepath.Join(t.TempDir(), "reserved.db"), zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
p, err := db.CreateProject(&database.Project{Name: "reserved"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkpoint, err := db.UpsertProjectFact(&database.ProjectFact{
|
||||
ProjectID: p.ID, FactKey: "note/context-victim", SourceConversationID: "victim",
|
||||
Summary: "original progress", Body: "ORIGINAL_BODY",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ordinary, err := db.UpsertProjectFact(&database.ProjectFact{ProjectID: p.ID, FactKey: "finding/ordinary", Summary: "shared"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := NewProjectHandler(db, zap.NewNop())
|
||||
request := func(method, factID, payload string) (*httptest.ResponseRecorder, *gin.Context) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: p.ID}, {Key: "factId", Value: factID}}
|
||||
c.Request = httptest.NewRequest(method, "/", strings.NewReader(payload))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
return w, c
|
||||
}
|
||||
|
||||
w, c := request(http.MethodPost, "", `{"fact_key":"note/context-forged","summary":"forged current work"}`)
|
||||
h.CreateFact(c)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("console created a reserved key: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
w, c = request(http.MethodPut, ordinary.ID, `{"fact_key":"note/context-forged"}`)
|
||||
h.UpdateFact(c)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("console renamed a fact into the reserved prefix: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
w, c = request(http.MethodPut, checkpoint.ID, `{"summary":"rewritten","body":"REWRITTEN"}`)
|
||||
h.UpdateFact(c)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("console rewrote another conversation's checkpoint: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
stored, err := db.GetProjectFact(checkpoint.ID)
|
||||
if err != nil || stored.Summary != "original progress" || stored.Body != "ORIGINAL_BODY" || stored.SourceConversationID != "victim" {
|
||||
t.Fatalf("rejected requests still modified the checkpoint: %+v %v", stored, err)
|
||||
}
|
||||
if renamed, err := db.GetProjectFact(ordinary.ID); err != nil || renamed.FactKey != "finding/ordinary" {
|
||||
t.Fatalf("rejected rename still applied: %+v %v", renamed, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/database"
|
||||
"cyberstrike-ai/internal/project"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestSessionContextReloadsProjectResultsEveryTurn(t *testing.T) {
|
||||
db, err := database.NewDB(filepath.Join(t.TempDir(), "context.db"), zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
p, err := db.CreateProject(&database.Project{Name: "shared"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := &config.Config{Project: config.ProjectConfig{Enabled: true}}
|
||||
cfg.Agent.WorkspaceRootDir = t.TempDir()
|
||||
h := &AgentHandler{db: db, config: cfg, logger: zap.NewNop()}
|
||||
var ids []string
|
||||
for _, title := range []string{"yesterday", "today"} {
|
||||
c, err := db.CreateConversation(title, database.ConversationCreateMeta{ProjectID: p.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
before := h.agentSessionContextBlock(ids[0])
|
||||
if strings.Contains(before, "parse_capture.py") {
|
||||
t.Fatal("unexpected old file")
|
||||
}
|
||||
root := project.WorkspaceRootDir(cfg.Agent.WorkspaceRootDir, p.ID, ids[0])
|
||||
if err := os.MkdirAll(filepath.Join(root, "artifacts"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "artifacts", "parse_capture.py"), []byte("print('done')"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.UpsertProjectFact(&database.ProjectFact{ProjectID: p.ID, FactKey: "artifact/parser", Category: "note", Summary: "verified parser", Body: "artifacts/parse_capture.py input capture.txt; output verified", Confidence: "confirmed"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v, err := db.CreateVulnerability(&database.Vulnerability{ProjectID: p.ID, Title: "tracked-finding", Severity: "low"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, id := range ids {
|
||||
block := h.agentSessionContextBlock(id)
|
||||
for _, want := range []string{"artifacts/parse_capture.py", "input capture.txt", v.ID, "tracked-finding", "先恢复成果"} {
|
||||
if !strings.Contains(block, want) {
|
||||
t.Errorf("conversation %s missing %s", id, want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(block, "print('done')") {
|
||||
t.Fatal("script content injected without reading")
|
||||
}
|
||||
}
|
||||
// A separate project's workspace and findings must never be injected.
|
||||
other, err := db.CreateProject(&database.Project{Name: "other"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := db.CreateConversation("isolated", database.ConversationCreateMeta{ProjectID: other.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
isolated := h.agentSessionContextBlock(c.ID)
|
||||
if strings.Contains(isolated, "parse_capture.py") || strings.Contains(isolated, v.ID) {
|
||||
t.Fatal("project results leaked")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user