mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-21 23:01:37 +02:00
fix: harden checkpoint provenance, scope and atomic binding
This commit is contained in:
1 parent
015e16457e
commit
2c311a6ab1
10 files changed
+256
-13
No files matched your search
@@ -83,6 +83,39 @@ func ContextCheckpointVisible(f *ProjectFact, conversationID string, includeHist
|
||||
(strings.TrimSpace(conversationID) != "" && f.SourceConversationID == strings.TrimSpace(conversationID)))
|
||||
}
|
||||
|
||||
// SaveContextCheckpoint tests project binding in the same SQLite write statement,
|
||||
// so a concurrent conversation rebind cannot slip between a read and an upsert.
|
||||
func (db *DB) SaveContextCheckpoint(f *ProjectFact) error {
|
||||
if f == nil || !IsContextCheckpointKey(f.FactKey) || strings.TrimSpace(f.SourceConversationID) == "" {
|
||||
return fmt.Errorf("invalid context checkpoint")
|
||||
}
|
||||
if err := ValidateFactKey(f.FactKey); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
result, err := db.Exec(`INSERT INTO project_facts
|
||||
(id, project_id, fact_key, category, summary, body, confidence, source_conversation_id, pinned, created_at, updated_at)
|
||||
SELECT ?, ?, ?, 'note', ?, ?, 'tentative', ?, 0, ?, ?
|
||||
WHERE EXISTS (SELECT 1 FROM conversations WHERE id = ? AND project_id = ?)
|
||||
ON CONFLICT(project_id, fact_key) DO UPDATE SET
|
||||
category = 'note', summary = excluded.summary, body = excluded.body,
|
||||
confidence = 'tentative', source_conversation_id = excluded.source_conversation_id,
|
||||
pinned = 0, updated_at = excluded.updated_at`,
|
||||
uuid.New().String(), f.ProjectID, f.FactKey, f.Summary, f.Body, f.SourceConversationID,
|
||||
now, now, f.SourceConversationID, f.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changed, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if changed == 0 {
|
||||
return fmt.Errorf("checkpoint project does not match current conversation binding")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func contextCheckpointScopeSQL(conversationID string) (string, []interface{}) {
|
||||
return " AND (substr(fact_key, 1, ?) != ? OR (source_conversation_id = ? AND ? != ''))",
|
||||
[]interface{}{len(ContextCheckpointKeyPrefix), ContextCheckpointKeyPrefix, strings.TrimSpace(conversationID), strings.TrimSpace(conversationID)}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestCheckpointWriteRechecksBindingAtomically(t *testing.T) {
|
||||
db, err := NewDB(filepath.Join(t.TempDir(), "binding.db"), zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
a, err := db.CreateProject(&Project{Name: "a"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := db.CreateProject(&Project{Name: "b"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conv, err := db.CreateConversation("run", ConversationCreateMeta{ProjectID: a.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := &ProjectFact{ProjectID: a.ID, FactKey: "note/context-test", SourceConversationID: conv.ID, Summary: "first", Body: "first body"}
|
||||
if err := db.SaveContextCheckpoint(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := db.GetProjectFactByKey(a.ID, f.FactKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A running agent has already read a.ID; binding changes before its write.
|
||||
if err := db.SetConversationProjectID(conv.ID, b.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Body = "wrong-project update"
|
||||
if err := db.SaveContextCheckpoint(f); err == nil {
|
||||
t.Fatal("stale run updated old-project memory")
|
||||
}
|
||||
f.FactKey = "note/context-new"
|
||||
if err := db.SaveContextCheckpoint(f); err == nil {
|
||||
t.Fatal("stale run inserted old-project memory")
|
||||
}
|
||||
existing, err := db.GetProjectFactByKey(a.ID, first.FactKey)
|
||||
if err != nil || existing.Body != "first body" {
|
||||
t.Fatalf("failed write changed evidence: %#v %v", existing, err)
|
||||
}
|
||||
if _, err := db.GetProjectFactByKey(a.ID, f.FactKey); err == nil {
|
||||
t.Fatal("failed write left a row")
|
||||
}
|
||||
f.ProjectID = b.ID
|
||||
if err := db.SaveContextCheckpoint(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.SetConversationProjectID(conv.ID, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.SaveContextCheckpoint(f); err == nil {
|
||||
t.Fatal("unbound conversation still persisted project memory")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user