Files
Agent-System/internal/artifactvault/publisher.go
T
weichenandCursor ad9b67d599 feat(artifactvault): close same-tx staging, recover-apply, and GET zero-write gaps
Approve/commit now write FAV staging in the domain transaction, recover-apply materializes scratch collections, and unpublished or GET paths cannot mutate the registry.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 12:52:28 +00:00

104 lines
3.1 KiB
Go

package artifactvault
import (
"context"
"encoding/json"
"fmt"
"time"
)
type Publisher struct {
Reg Registry
Store *FilesystemStore
}
func (p *Publisher) Drain(ctx context.Context) error {
arts, err := p.Reg.LoadRequested(ctx)
if err != nil {
return err
}
var first error
for _, a := range arts {
if err := p.PublishOne(ctx, a); err != nil && first == nil {
first = err
}
}
return first
}
func (p *Publisher) PublishOne(ctx context.Context, art Artifact) error {
if p.Store == nil {
return fmt.Errorf("artifactvault: store required")
}
if p.Reg.IsDenied(ctx, art.UUID) {
return fmt.Errorf("artifactvault: artifact denied")
}
_ = p.Reg.MarkPublishing(ctx, art.UUID)
st, err := p.Reg.LoadStaging(ctx, art.SourceEventID)
if err != nil {
_ = p.Reg.MarkFailed(ctx, art.UUID, "staging_missing")
return err
}
if SHA256Hex(st.Payload) != art.PayloadSHA256 || SHA256Hex(st.Payload) != st.PayloadSHA256 {
_ = p.Reg.MarkFailed(ctx, art.UUID, "sha_mismatch")
return fmt.Errorf("artifactvault: staging sha mismatch")
}
if ClassifyPayload(st.Payload) != ClassificationOK {
_ = p.Reg.MarkFailed(ctx, art.UUID, "sensitive")
return ErrSensitiveRejected
}
art.VaultStorageIdentity = p.Store.Identity()
keyRef, err := p.Store.PutBlob(art.Environment, art.ProjectExternalKey, art.PayloadSHA256, st.Payload)
if err != nil {
_ = p.Reg.MarkFailed(ctx, art.UUID, "blob")
return err
}
man := BuildManifest(art, art.PayloadSHA256, len(st.Payload), keyRef)
if err := ValidateManifest(man); err != nil {
_ = p.Reg.MarkFailed(ctx, art.UUID, "manifest")
return err
}
manRaw := EncodeJSON(man)
if err := p.Store.PutManifest(art.UUID, man.ManifestSHA256, manRaw); err != nil {
_ = p.Reg.MarkFailed(ctx, art.UUID, "manifest_write")
return err
}
marker := CommitMarker{
ArtifactUUID: art.UUID, ManifestSHA256: man.ManifestSHA256,
MemberSHAs: []string{art.PayloadSHA256}, VaultStorageIdentity: p.Store.Identity(),
CommittedAt: time.Now().UTC().Format(time.RFC3339), PublisherVersion: PublisherVersion,
}
if err := ValidateCommitMarker(marker); err != nil {
return err
}
if err := p.Store.PutCommit(art.UUID, man.ManifestSHA256, EncodeJSON(marker)); err != nil {
_ = p.Reg.MarkFailed(ctx, art.UUID, "commit")
return err
}
_ = p.Reg.RecordReference(ctx, art.UUID, art.ProjectExternalKey, art.PayloadSHA256, keyRef)
return p.Reg.MarkVerified(ctx, art.UUID, man.ManifestSHA256)
}
func (p *Publisher) DegradedPayload(ctx context.Context, env, logical, projectKey string) ([]byte, string, error) {
art, err := p.Reg.GetByLogical(ctx, env, logical)
if err != nil {
return nil, "", err
}
if p.Reg.IsDenied(ctx, art.UUID) || p.Store.HasTombstone(art.UUID) {
return nil, "", fmt.Errorf("artifactvault: union-deny")
}
if art.ManifestSHA256 == "" || !p.Store.HasCommit(art.UUID, art.ManifestSHA256) {
return nil, "", fmt.Errorf("artifactvault: unpublished")
}
b, err := p.Store.GetBlob(env, projectKey, art.PayloadSHA256)
if err != nil {
return nil, "", err
}
return b, art.PayloadSHA256, nil
}
func MustJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}