134 lines
4.9 KiB
Go
134 lines
4.9 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"agent-system-backend/pkg/extension"
|
|
)
|
|
|
|
const contextManifestAction = "model.context_manifest"
|
|
|
|
// ContextManifestScope records only that the request came from a trusted model
|
|
// guard. Raw goals and model output are never persisted in this receipt.
|
|
type ContextManifestScope struct {
|
|
Bound bool
|
|
}
|
|
|
|
// AppendContextManifest writes to the trusted receipt table, never to the
|
|
// plugin-writable generic audit log. A terminal state must follow a matching
|
|
// attempted state for the same owner, target, project and input hash.
|
|
func (s *Store) AppendContextManifest(owner, targetID string, m extension.ProjectContextManifest) error {
|
|
return s.appendContextManifest(owner, targetID, m, ContextManifestScope{})
|
|
}
|
|
|
|
func (s *Store) AppendContextManifestWithScope(owner, targetID string, m extension.ProjectContextManifest, scope ContextManifestScope) error {
|
|
if !scope.Bound {
|
|
return errors.New("trusted context manifest resolve scope is not bound")
|
|
}
|
|
return s.appendContextManifest(owner, targetID, m, scope)
|
|
}
|
|
|
|
func (s *Store) appendContextManifest(owner, targetID string, m extension.ProjectContextManifest, scope ContextManifestScope) error {
|
|
if owner == "" || targetID == "" || m.ProjectID == "" || m.InputSHA256 == "" || m.AttemptID == "" {
|
|
return errors.New("context manifest requires owner, target, project, attempt and input hash")
|
|
}
|
|
if m.Delivery != "attempted" && m.Delivery != "responded" && m.Delivery != "unknown" {
|
|
return errors.New("context manifest has invalid delivery state")
|
|
}
|
|
if err := requireRecheckableSources(m); err != nil {
|
|
return err
|
|
}
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if m.Delivery != "attempted" {
|
|
var attemptedBound int
|
|
err := tx.QueryRow(s.q(`SELECT scope_bound FROM context_manifests WHERE attempt_id=? AND delivery='attempted' AND owner=? AND target_id=? AND project_id=? AND input_sha256=?`),
|
|
m.AttemptID, owner, targetID, m.ProjectID, m.InputSHA256).Scan(&attemptedBound)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return errors.New("context manifest terminal receipt lacks matching attempt")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if (attemptedBound != 0) != scope.Bound {
|
|
return errors.New("context manifest terminal resolve scope changed")
|
|
}
|
|
var terminal int
|
|
err = tx.QueryRow(s.q(`SELECT 1 FROM context_manifests WHERE attempt_id=? AND delivery IN ('responded','unknown') LIMIT 1`), m.AttemptID).Scan(&terminal)
|
|
if err == nil {
|
|
return errors.New("context manifest attempt already terminal")
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
}
|
|
bound := 0
|
|
if scope.Bound {
|
|
bound = 1
|
|
}
|
|
_, err = tx.Exec(s.q(`INSERT INTO context_manifests(id,attempt_id,owner,target_id,project_id,input_sha256,delivery,manifest_json,created_at,scope_bound) VALUES(?,?,?,?,?,?,?,?,?,?)`),
|
|
NewID("ctxm"), m.AttemptID, owner, targetID, m.ProjectID, m.InputSHA256, m.Delivery, string(raw), nowISO(), bound)
|
|
if err != nil {
|
|
return fmt.Errorf("insert context manifest: %w", err)
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// requireRecheckableSources refuses a receipt, and therefore the model request
|
|
// it gates, when an actual input could not be reauthorized on later reuse.
|
|
func requireRecheckableSources(m extension.ProjectContextManifest) error {
|
|
for _, e := range m.Included {
|
|
if e.SourceID == "" || e.Revision == "" || e.SourceHash == "" || e.InputSHA256 == "" {
|
|
return errors.New("context manifest included source lacks stable identity")
|
|
}
|
|
}
|
|
for _, ref := range m.OfferedReferences {
|
|
if ref.ID == "" || ref.SourceID == "" || ref.Revision == "" || ref.Hash == "" {
|
|
return errors.New("context manifest offered reference lacks reauthorizable identity")
|
|
}
|
|
}
|
|
for _, e := range m.ReadEvidence {
|
|
if e.ReferenceID == "" || e.SourceID == "" || e.Revision == "" || e.SourceHash == "" || e.InputSHA256 == "" {
|
|
return errors.New("context manifest read evidence lacks reauthorizable identity")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListContextManifests reads receipts only within the authenticated owner's
|
|
// scope. The target is a run ID for ordinary work, or a conversation ID for
|
|
// direct WorkCompletionHost calls.
|
|
func (s *Store) ListContextManifests(owner, targetID string) ([]extension.ProjectContextManifest, error) {
|
|
if owner == "" || targetID == "" {
|
|
return nil, errors.New("context manifest query requires owner and target")
|
|
}
|
|
rows, err := s.db.Query(s.q(`SELECT manifest_json FROM context_manifests WHERE owner=? AND target_id=? ORDER BY created_at,id`), owner, targetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []extension.ProjectContextManifest
|
|
for rows.Next() {
|
|
var raw string
|
|
if err := rows.Scan(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
var m extension.ProjectContextManifest
|
|
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|