8d838d9cb4
build / build (push) Successful in 1m45s
执行时真实时钟与持久授权回读(撤销/过期即停并保留预备份)、写集合写前落盘、 租约与 attempt 身份、恢复队列与原发送身份分离、成功收尾必须释放维护后再登记终态。
144 lines
5.2 KiB
Go
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, ¤t) != 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()
|
|
}
|