Files
weichenandCursor ffb93dc55d feat(artifactvault): add eligibility, encrypted CAS, and idempotent publisher
Keep vault roots off ./data/dev and reject unknown types or sensitive payloads before staging.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 10:51:25 +00:00

108 lines
2.9 KiB
Go

package artifactvault
import (
"errors"
"strings"
)
var (
ErrNotTerminal = errors.New("artifactvault: facts are not a terminal state")
ErrExplicitBypass = errors.New("artifactvault: explicit publish/pin cannot bypass terminal allowlist")
ErrSensitiveRejected = errors.New("artifactvault: sensitive or unclassified payload rejected")
ErrUnknownArtifactType = errors.New("artifactvault: unknown artifact type rejected")
ErrSnapshotInconsistent = errors.New("artifactvault: snapshot/event inconsistent")
)
// EvaluateEligibility is the only domain gate. Unclassified types default to reject.
func EvaluateEligibility(f Facts) error {
if err := rejectSensitive(f.Classification, f.Payload); err != nil {
return err
}
switch f.ArtifactType {
case TypeAgentCommit:
if f.CandidateStatus != "committed" || f.OutputSnapshotID <= 0 {
return ErrNotTerminal
}
if f.ChangeKind != "" && (f.ChangeKind != "agent_commit" || f.ChangeToSnapshotID != f.OutputSnapshotID) {
return ErrSnapshotInconsistent
}
return nil
case TypeSealedVersion:
if f.VersionStatus != "sealed" || f.SealedAt == nil || f.HeadSnapshotID <= 0 {
return ErrNotTerminal
}
return nil
case TypeApprovedDocument:
if f.DocState != "approved" || f.ApprovedAt == nil || f.ReviewSnapshotID <= 0 {
return ErrNotTerminal
}
return nil
case TypeFreeformPublish:
if !f.ExplicitPublish || !f.HasImmutableSnapshot {
if f.ExplicitPublish && !f.HasImmutableSnapshot {
return ErrExplicitBypass
}
return ErrNotTerminal
}
return nil
case TypeAssistantReply:
if f.ChatTurnStatus != "completed" || f.CompletedAt == nil || f.AssistantMessageID <= 0 {
if f.ExplicitPin {
return ErrExplicitBypass
}
return ErrNotTerminal
}
switch f.ExportPolicy {
case "full":
return nil
case "copy_only":
if f.ExplicitPin && f.PinAfterCompleted {
return nil
}
if f.ExplicitPin {
return ErrExplicitBypass
}
return ErrNotTerminal
default:
return ErrNotTerminal
}
default:
return ErrUnknownArtifactType
}
}
func rejectSensitive(class string, payload []byte) error {
if class == "" {
class = ClassifyPayload(payload)
}
switch class {
case ClassificationSecret, ClassificationRestricted, ClassificationFailed:
return ErrSensitiveRejected
case ClassificationOK:
return nil
default:
return ErrSensitiveRejected
}
}
// ClassifyPayload is a fail-closed server-side gate. It never logs the body.
func ClassifyPayload(payload []byte) string {
text := strings.ToLower(string(payload))
if text == "" {
return ClassificationFailed
}
needles := []string{
"-----begin ", "private_key", "api_key=", "secret=", "authorization: bearer",
"password=", "access_token=", "aws_secret",
}
for _, n := range needles {
if strings.Contains(text, n) {
return ClassificationSecret
}
}
if strings.Contains(text, "restricted:") || strings.Contains(text, "classification_failed") {
return ClassificationRestricted
}
return ClassificationOK
}