Files
knowledge-governance/internal/platform/postgres/library_legacy_source.go
T
姚章浩 8d838d9cb4
build / build (push) Successful in 1m45s
feat(library): 整库发布身份、写集合与收尾边界
执行时真实时钟与持久授权回读(撤销/过期即停并保留预备份)、写集合写前落盘、
租约与 attempt 身份、恢复队列与原发送身份分离、成功收尾必须释放维护后再登记终态。
2026-11-01 11:35:10 +08:00

144 lines
5.2 KiB
Go

package postgres
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"strings"
"time"
"knowledge-governance/internal/governance"
"knowledge-governance/internal/libraryagent"
"knowledge-governance/internal/librarysnapshot"
)
// BindLegacyLibrarySource adds evidence, not a new snapshot or rewritten origin.
// Both the archived manifest and the original capture response must independently
// identify the configured source. A scope match by itself can never authorize it.
func (s *Store) BindLegacyLibrarySource(ctx context.Context, snapshotID, archiveRoot string, source governance.LibrarySourceIdentity, actor string, now time.Time) error {
sourceID, err := source.ID()
if err != nil {
return err
}
if strings.TrimSpace(actor) == "" || now.IsZero() {
return errors.New("source association requires audited actor and time")
}
archive, err := librarysnapshot.Open(archiveRoot, 256*1024*1024)
if err != nil {
return err
}
defer archive.Close()
manifest, err := archive.Load(snapshotID)
if err != nil {
return err
}
if manifest.Scope != source.Scope || manifest.TenantID != source.TenantID || manifest.KnowledgeBaseID != source.KnowledgeBaseID {
return errors.New("historical manifest differs from configured source")
}
tx, err := s.db.Begin(ctx)
if err != nil {
return err
}
defer func() {
cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
_ = tx.Rollback(cleanup)
}()
rows, err := tx.Query(ctx, `SELECT id,source_job_id,evidence FROM library_versions WHERE scope=$1 AND snapshot_id=$2 AND kind='test_sealed' AND target_id='' FOR UPDATE`, source.Scope, snapshotID)
if err != nil {
return err
}
type sealed struct {
id, job string
evidence []byte
}
items := []sealed{}
for rows.Next() {
var v sealed
if err = rows.Scan(&v.id, &v.job, &v.evidence); err != nil {
rows.Close()
return err
}
items = append(items, v)
}
err = rows.Err()
rows.Close()
if err != nil {
return err
}
if len(items) != 1 || items[0].job == "" {
return errors.New("unique original captured seal required")
}
old := items[0]
commitAndVerify := func() error {
if err := tx.Commit(ctx); err != nil {
return err
}
var verifiedID string
if err := s.db.QueryRow(ctx, `SELECT evidence->>'source_id' FROM library_versions WHERE id=$1 AND scope=$2 AND snapshot_id=$3`, old.id, source.Scope, snapshotID).Scan(&verifiedID); err != nil {
return err
}
if verifiedID != sourceID {
return errors.New("committed source association readback mismatch")
}
return nil
}
var evidence map[string]json.RawMessage
if json.Unmarshal(old.evidence, &evidence) != nil || evidence == nil {
return errors.New("original capture evidence is not an object")
}
var captured struct {
SnapshotID string `json:"snapshot_id"`
Identity struct {
Project string `json:"project"`
App string `json:"source_app"`
Root string `json:"archive_root"`
Fingerprint string `json:"expectation_fingerprint"`
} `json:"agent_identity"`
}
var expected string
if json.Unmarshal(evidence["agent_result"], &captured) != nil || json.Unmarshal(evidence["expectation_fingerprint"], &expected) != nil {
return errors.New("original capture identity evidence missing")
}
identity := captured.Identity
fingerprint := libraryagent.NewExpectation(identity.Project, identity.App, identity.Root).Fingerprint
if captured.SnapshotID != snapshotID || identity.Project != source.Project || identity.App != source.App || identity.Root == "" || expected != fingerprint || identity.Fingerprint != expected {
return errors.New("original capture identity or fingerprint mismatch")
}
if raw, exists := evidence["source_id"]; exists {
var current string
var bound governance.LibrarySourceIdentity
if json.Unmarshal(raw, &current) != nil || current != sourceID || json.Unmarshal(evidence["source_identity"], &bound) != nil || bound != source {
return errors.New("sealed source association cannot be changed")
}
return commitAndVerify()
}
for _, reserved := range []string{"source_identity", "legacy_source_binding"} {
if _, exists := evidence[reserved]; exists {
return errors.New("partial source association cannot be overwritten")
}
}
sum := sha256.Sum256(old.evidence)
addition, err := json.Marshal(map[string]any{"source_id": sourceID, "source_identity": source, "legacy_source_binding": map[string]any{"snapshot_id": snapshotID, "original_event_id": old.id, "original_source_job_id": old.job, "original_evidence_sha256": hex.EncodeToString(sum[:]), "expectation_fingerprint": expected, "actor": actor, "bound_at": now.UTC()}})
if err != nil {
return err
}
tag, err := tx.Exec(ctx, `UPDATE library_versions SET evidence=evidence || $2::jsonb WHERE id=$1 AND NOT (evidence ? 'source_id')`, old.id, addition)
if err != nil {
return err
}
if tag.RowsAffected() != 1 {
return errors.New("source association changed concurrently")
}
var actual string
if err = tx.QueryRow(ctx, `SELECT evidence->>'source_id' FROM library_versions WHERE id=$1`, old.id).Scan(&actual); err != nil {
return err
}
if actual != sourceID {
return errors.New("source association readback mismatch")
}
return commitAndVerify()
}