Files
Agent-System-Backend/internal/store/project_events.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
2026-10-13 15:47:16 +00:00

148 lines
5.2 KiB
Go

package store
import (
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
)
// ProjectEventAccess is trusted read metadata; payload claims never establish it.
type ProjectEventAccess struct {
Owner string
Shared bool
Producer string
}
type eventResource struct{ owner, project, conversation, kind, id string }
type eventQuery interface{ QueryRow(string, ...any) *sql.Row }
func cardEventID(e core.StreamEvent) (string, error) {
switch e.Type {
case core.EvCardCreated, core.EvCardUpdated, core.EvCardStatus, core.EvCardResolved:
default:
return "", nil
}
var payload struct {
Card struct {
ID string `json:"id"`
} `json:"card"`
CardID string `json:"cardId"`
}
if err := json.Unmarshal(e.Payload, &payload); err != nil {
return "", err
}
switch e.Type {
case core.EvCardCreated, core.EvCardUpdated:
return payload.Card.ID, nil
case core.EvCardStatus, core.EvCardResolved:
return payload.CardID, nil
}
return "", nil
}
func eventPayloadHash(payload []byte) string {
hash := sha256.Sum256(payload)
return hex.EncodeToString(hash[:])
}
func (s *Store) projectEventResource(q eventQuery, e core.StreamEvent) (eventResource, error) {
var resource eventResource
id, err := cardEventID(e)
if err != nil {
return resource, err
}
switch e.Type {
case core.EvCardCreated, core.EvCardUpdated, core.EvCardStatus, core.EvCardResolved:
if id == "" {
return resource, ErrNotFound
}
}
if id != "" {
resource.kind, resource.id = "card", id
err = q.QueryRow(s.q(`SELECT owner,project_id,conversation_id FROM activity_cards WHERE id=?`), id).Scan(&resource.owner, &resource.project, &resource.conversation)
} else if e.Type == core.EvArtifactCreated {
var payload struct {
Artifact struct {
ID string `json:"id"`
} `json:"artifact"`
}
if err := json.Unmarshal(e.Payload, &payload); err != nil {
return resource, err
}
if payload.Artifact.ID == "" {
return resource, ErrNotFound
}
resource.kind, resource.id = "artifact", payload.Artifact.ID
err = q.QueryRow(s.q(`SELECT owner,project_id FROM artifacts WHERE id=?`), resource.id).Scan(&resource.owner, &resource.project)
} else if e.RunID != "" {
resource.kind, resource.id = "run", e.RunID
err = q.QueryRow(s.q(`SELECT owner,project_id,conversation_id FROM runs WHERE id=?`), e.RunID).Scan(&resource.owner, &resource.project, &resource.conversation)
} else {
return resource, ErrNotFound
}
if errors.Is(err, sql.ErrNoRows) {
return resource, ErrNotFound
}
if err != nil {
return resource, err
}
project, ok := core.ProjectFromChannel(e.ConversationID)
if !ok || resource.owner == "" || resource.project != project || (e.ProjectID != "" && e.ProjectID != project) {
return resource, ErrNotFound
}
return resource, nil
}
// ResolveProjectEventAccess resolves immutable resource ownership and verifies an
// exact internal publication record. Old events retain their private default.
func (s *Store) ResolveProjectEventAccess(e core.StreamEvent) (ProjectEventAccess, error) {
resource, err := s.projectEventResource(s.db, e)
if err != nil {
return ProjectEventAccess{}, err
}
access := ProjectEventAccess{Owner: resource.owner}
var owner, kind, id, producer, typ, hash string
err = s.db.QueryRow(s.q(`SELECT owner,resource_type,resource_id,producer,event_type,payload_sha256 FROM project_event_shares WHERE channel=? AND seq=?`), e.ConversationID, e.Seq).Scan(&owner, &kind, &id, &producer, &typ, &hash)
if errors.Is(err, sql.ErrNoRows) {
return access, nil
}
if err != nil {
return ProjectEventAccess{}, err
}
if owner != resource.owner || kind != resource.kind || id != resource.id || strings.TrimSpace(producer) == "" || typ != string(e.Type) || hash != eventPayloadHash(e.Payload) || resource.conversation != "" {
return ProjectEventAccess{}, ErrNotFound
}
access.Shared, access.Producer = true, producer
return access, nil
}
// AppendProjectSharedEvents is an internal publication boundary, not an API or
// model tool. Events and their exact sharing proofs commit together. Caller
// identity must own a project-level card; private conversation cards cannot be
// published through this structural-card path.
func (s *Store) AppendProjectSharedEvents(owner, producer, channel string, events []core.StreamEvent) (int64, error) {
if owner == "" || strings.TrimSpace(producer) == "" {
return 0, fmt.Errorf("store: shared event requires owner and producer")
}
if _, ok := core.ProjectFromChannel(channel); !ok {
return 0, ErrNotFound
}
return s.appendEvents(channel, events, owner, producer)
}
func (s *Store) saveProjectEventProof(tx *sql.Tx, channel string, seq int64, e core.StreamEvent, owner, producer string) error {
e.ConversationID = channel
resource, err := s.projectEventResource(tx, e)
if err != nil {
return err
}
if resource.kind != "card" || resource.owner != owner || resource.conversation != "" {
return ErrNotFound
}
_, err = tx.Exec(s.q(`INSERT INTO project_event_shares(channel,seq,event_type,payload_sha256,owner,resource_type,resource_id,producer,created_at) VALUES(?,?,?,?,?,?,?,?,?)`), channel, seq, string(e.Type), eventPayloadHash(e.Payload), resource.owner, resource.kind, resource.id, producer, nowISO())
return err
}