Keep vault roots off ./data/dev and reject unknown types or sensitive payloads before staging. Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
2.9 KiB
Go
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
|
|
}
|