批准 dataset 分类入库;SKIP LOCKED 分批;销毁默认 fail closed。restore 演练拒绝共享 env DSN。 Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
5.2 KiB
Go
181 lines
5.2 KiB
Go
package dataset
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/artifactvault"
|
|
)
|
|
|
|
const (
|
|
ClassPinned = "pinned"
|
|
ClassDemo = "demo"
|
|
ClassRegression = "regression"
|
|
ClassMigrationBaseline = "migration_baseline"
|
|
ClassEphemeral = "ephemeral"
|
|
ClassQuarantine = "quarantine"
|
|
|
|
JobRunning = "running"
|
|
JobPaused = "paused"
|
|
JobCompleted = "completed"
|
|
JobFailed = "failed"
|
|
|
|
OldestSupportedSchema = artifactvault.OldestSupportedSchema
|
|
MinQuarantineWindow = 7 * 24 * time.Hour
|
|
)
|
|
|
|
type Dataset struct {
|
|
ID string `json:"dataset_id"`
|
|
Class string `json:"class"`
|
|
DisplayName string `json:"display_name"`
|
|
StorageIdentity string `json:"storage_identity"`
|
|
SourceSchema int `json:"source_schema"`
|
|
SourceCommit string `json:"source_commit"`
|
|
Status string `json:"status"`
|
|
ProjectCount int `json:"project_count"`
|
|
}
|
|
|
|
func ValidateClass(class string) error {
|
|
switch class {
|
|
case ClassPinned, ClassDemo, ClassRegression, ClassMigrationBaseline, ClassEphemeral, ClassQuarantine:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("dataset: invalid class %q", class)
|
|
}
|
|
}
|
|
|
|
type Checkpoint struct {
|
|
JobID string
|
|
Shard int
|
|
SourceKind string
|
|
LastPK int64
|
|
Scanned int
|
|
Succeeded int
|
|
Quarantined int
|
|
RetryWatermark int
|
|
}
|
|
|
|
type JobStats struct {
|
|
Scanned int
|
|
Succeeded int
|
|
Quarantined int
|
|
}
|
|
|
|
func Recalc(cps []Checkpoint) JobStats {
|
|
var s JobStats
|
|
for _, c := range cps {
|
|
s.Scanned += c.Scanned
|
|
s.Succeeded += c.Succeeded
|
|
s.Quarantined += c.Quarantined
|
|
}
|
|
return s
|
|
}
|
|
|
|
type DeleteManifest struct {
|
|
DatasetIDs []string `json:"dataset_ids"`
|
|
StorageIdentity string `json:"storage_identity"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
func CanonicalDestroyBytes(m DeleteManifest) []byte {
|
|
ids := append([]string(nil), m.DatasetIDs...)
|
|
sort.Strings(ids)
|
|
raw, _ := json.Marshal(struct {
|
|
DatasetIDs []string `json:"dataset_ids"`
|
|
StorageIdentity string `json:"storage_identity"`
|
|
Reason string `json:"reason"`
|
|
}{ids, m.StorageIdentity, m.Reason})
|
|
return raw
|
|
}
|
|
|
|
func DestroyManifestSHA(m DeleteManifest) string {
|
|
sum := sha256.Sum256(CanonicalDestroyBytes(m))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
type DestroyRequest struct {
|
|
TargetIdentity string
|
|
TargetDSN string
|
|
Manifest DeleteManifest
|
|
ExpectedManifestSHA string
|
|
RecoverableLocation string
|
|
CreatedAt time.Time
|
|
QuarantineUntil time.Time
|
|
Approval1 string
|
|
Approval2 string
|
|
Coverage artifactvault.Coverage
|
|
BackupOK bool
|
|
CorpusOK bool
|
|
VaultReady bool
|
|
Now time.Time
|
|
}
|
|
|
|
func RefuseOperationalDSN(dsn, identity string) error {
|
|
low := strings.ToLower(dsn + " " + identity)
|
|
for _, ban := range []string{"agent_system_dev", "agent_system_test", "agent_system_main"} {
|
|
if strings.Contains(low, ban) && !strings.Contains(low, "scratch") {
|
|
return fmt.Errorf("dataset: refuse operational database %s", ban)
|
|
}
|
|
}
|
|
if strings.Contains(low, ":5433/") || strings.Contains(low, ":5435/") {
|
|
if !strings.Contains(low, "scratch") {
|
|
return fmt.Errorf("dataset: refuse shared env port")
|
|
}
|
|
}
|
|
if strings.Contains(low, ":5434/") && strings.Contains(low, "agent_system_test") {
|
|
return fmt.Errorf("dataset: refuse shared test database")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EvaluateDestroy(req DestroyRequest) error {
|
|
if err := RefuseOperationalDSN(req.TargetDSN, req.TargetIdentity); err != nil {
|
|
return err
|
|
}
|
|
if !strings.Contains(strings.ToLower(req.RecoverableLocation), "scratch") {
|
|
return fmt.Errorf("dataset: recoverable location must be scratch")
|
|
}
|
|
if req.QuarantineUntil.Sub(req.CreatedAt) < MinQuarantineWindow {
|
|
return fmt.Errorf("dataset: quarantine observation window must be at least 7 days")
|
|
}
|
|
now := req.Now
|
|
if now.IsZero() {
|
|
now = time.Now().UTC()
|
|
}
|
|
if now.Before(req.QuarantineUntil) {
|
|
return fmt.Errorf("dataset: quarantine window still open")
|
|
}
|
|
if strings.TrimSpace(req.Approval1) == "" || strings.TrimSpace(req.Approval2) == "" || req.Approval1 == req.Approval2 {
|
|
return fmt.Errorf("dataset: two distinct user approvals required")
|
|
}
|
|
got := DestroyManifestSHA(req.Manifest)
|
|
if req.ExpectedManifestSHA != "" && req.ExpectedManifestSHA != got {
|
|
return fmt.Errorf("dataset: destroy manifest sha mismatch")
|
|
}
|
|
if len(req.Manifest.DatasetIDs) == 0 || strings.TrimSpace(req.Manifest.StorageIdentity) == "" {
|
|
return fmt.Errorf("dataset: destroy manifest incomplete")
|
|
}
|
|
return artifactvault.PreflightDestroy(req.Coverage, req.BackupOK, req.CorpusOK, req.VaultReady)
|
|
}
|
|
|
|
func RequireScratchRestore(dsn, storageIdentity, manifestSHA, expectedSHA string, hasAdmin bool) error {
|
|
if err := RefuseOperationalDSN(dsn, storageIdentity); err != nil {
|
|
return err
|
|
}
|
|
if !strings.Contains(strings.ToLower(dsn+" "+storageIdentity), "scratch") {
|
|
return fmt.Errorf("dataset: restore target must be scratch")
|
|
}
|
|
if !hasAdmin {
|
|
return fmt.Errorf("dataset: restore requires role-admin")
|
|
}
|
|
if manifestSHA == "" || expectedSHA == "" || manifestSHA != expectedSHA {
|
|
return fmt.Errorf("dataset: restore manifest mismatch or missing")
|
|
}
|
|
return nil
|
|
}
|