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
148 lines
5.2 KiB
Go
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
|
|
}
|