feat(scum): gate live data on schema evidence

This commit is contained in:
npc0-hue
2026-08-11 13:28:36 +08:00
parent e5d26e5420
commit a58da47c60
18 changed files with 1265 additions and 3 deletions
+295
View File
@@ -0,0 +1,295 @@
package domain
import "time"
type SCUMDataCapability string
const (
SCUMDataCapabilitySchemaProbe SCUMDataCapability = "schema-probe"
SCUMDataCapabilityPlayerRead SCUMDataCapability = "players.read"
SCUMDataCapabilityPlayerDetailRead SCUMDataCapability = "player-details.read"
SCUMDataCapabilitySquadRead SCUMDataCapability = "squads.read"
SCUMDataCapabilitySquadMemberRead SCUMDataCapability = "squad-members.read"
SCUMDataCapabilityVehicleRead SCUMDataCapability = "vehicles.read"
SCUMDataCapabilityFlagRead SCUMDataCapability = "flags.read"
SCUMDataCapabilityPositionRead SCUMDataCapability = "positions.read"
SCUMDataCapabilityProfileXMLWrite SCUMDataCapability = "profile-xml.write"
SCUMDataCapabilityEconomyCommand SCUMDataCapability = "economy-command.write"
SCUMDataCapabilityGiftCommand SCUMDataCapability = "gift-command.write"
)
type SCUMCapabilityEvidenceStatus string
const (
SCUMCapabilityEvidenceMissing SCUMCapabilityEvidenceStatus = "missing"
SCUMCapabilityEvidenceCompatible SCUMCapabilityEvidenceStatus = "compatible"
SCUMCapabilityEvidenceIncompatible SCUMCapabilityEvidenceStatus = "incompatible"
SCUMCapabilityEvidenceFailed SCUMCapabilityEvidenceStatus = "failed"
)
type SCUMCapabilityGateState string
const (
SCUMCapabilityGateEnabled SCUMCapabilityGateState = "enabled"
SCUMCapabilityGateDisabled SCUMCapabilityGateState = "disabled"
)
type SCUMSafeErrorCode string
const (
SCUMSafeErrorNone SCUMSafeErrorCode = "none"
SCUMSafeErrorProbeExecutorAbsent SCUMSafeErrorCode = "probe_executor_absent"
SCUMSafeErrorProbeMissing SCUMSafeErrorCode = "probe_missing"
SCUMSafeErrorProbeFailed SCUMSafeErrorCode = "probe_failed"
SCUMSafeErrorSchemaIncompatible SCUMSafeErrorCode = "schema_incompatible"
SCUMSafeErrorBindingMismatch SCUMSafeErrorCode = "binding_mismatch"
SCUMSafeErrorAdapterMismatch SCUMSafeErrorCode = "adapter_mismatch"
SCUMSafeErrorFingerprintMismatch SCUMSafeErrorCode = "fingerprint_mismatch"
SCUMSafeErrorDigestMismatch SCUMSafeErrorCode = "digest_mismatch"
SCUMSafeErrorEvidenceExpired SCUMSafeErrorCode = "evidence_expired"
SCUMSafeErrorInvalidProbePayload SCUMSafeErrorCode = "invalid_probe_payload"
)
type SCUMSafeError struct {
Code SCUMSafeErrorCode
Message string
Retryable bool
}
type SCUMBindingIdentity struct {
ServerInstanceID string
RunBindingID string
RunEndpointID string
PluginID string
PluginVersion string
AdapterVersion string
GameVersion string
DatabaseIdentity string
}
type SCUMSchemaProbeBounds struct {
MaxObjects int
MaxColumnsPerObject int
MaxIndexesPerObject int
MaxForeignKeys int
MaxCardinalityReads int
MaxSampleRows int
TimeoutMS int
MaxResultBytes int
}
func DefaultSCUMSchemaProbeBounds() SCUMSchemaProbeBounds {
return SCUMSchemaProbeBounds{MaxObjects: 256, MaxColumnsPerObject: 128, MaxIndexesPerObject: 64, MaxForeignKeys: 64, MaxCardinalityReads: 64, MaxSampleRows: 3, TimeoutMS: 5000, MaxResultBytes: 512 * 1024}
}
type SCUMSchemaProbeRequest struct {
RequestID string
JobID string
Binding SCUMBindingIdentity
Bounds SCUMSchemaProbeBounds
RequestedAt time.Time
}
type SCUMSchemaProbeDeclaration struct {
Capability string
TargetKey string
Bounds SCUMSchemaProbeBounds
}
type SCUMLiveDataCapabilityGateDeclaration struct {
Capability SCUMDataCapability
Gate SCUMCapabilityGateState
AdapterVersion string
RequiredSchemaFingerprint string
RequiredAssetDigests []string
EvidenceStatus SCUMCapabilityEvidenceStatus
SafeReason string
}
type SCUMLiveDataManifest struct {
SchemaVersion string
Probe SCUMSchemaProbeDeclaration
CapabilityGates []SCUMLiveDataCapabilityGateDeclaration
}
type SCUMSchemaColumnEvidence struct {
NameFingerprint string
DeclaredType string
Nullable *bool
PrimaryKey bool
Ordinal int
}
type SCUMSchemaIndexEvidence struct {
NameFingerprint string
Unique bool
ColumnHashes []string
}
type SCUMSchemaForeignKeyEvidence struct {
FromColumnHash string
ToObjectHash string
ToColumnHash string
}
type SCUMSchemaObjectEvidence struct {
ObjectHash string
Kind string
NameFingerprint string
DeclaredColumns []SCUMSchemaColumnEvidence
Indexes []SCUMSchemaIndexEvidence
ForeignKeys []SCUMSchemaForeignKeyEvidence
ApproximateRows *int64
SampleFingerprints []string
}
type SCUMSchemaProbeResult struct {
RequestID string
JobID string
Binding SCUMBindingIdentity
Status SCUMCapabilityEvidenceStatus
SchemaFingerprint string
ObservedAt time.Time
ResultDigest string
Objects []SCUMSchemaObjectEvidence
SafeError SCUMSafeError
Limits SCUMSchemaProbeBounds
}
type SCUMCapabilityRequirement struct {
Capability SCUMDataCapability
AdapterVersion string
SchemaFingerprint string
AssetDigests []string
}
type SCUMCapabilityEvidence struct {
Capability SCUMDataCapability
Status SCUMCapabilityEvidenceStatus
Binding SCUMBindingIdentity
AdapterVersion string
SchemaFingerprint string
ProbeResultDigest string
AssetDigests []string
ObservedAt time.Time
ExpiresAt time.Time
SafeError SCUMSafeError
}
type SCUMCapabilityGate struct {
Capability SCUMDataCapability
State SCUMCapabilityGateState
Enabled bool
ReasonCode SCUMSafeErrorCode
Reason string
Evidence SCUMCapabilityEvidence
}
func EvaluateSCUMCapabilityGate(requirement SCUMCapabilityRequirement, evidence SCUMCapabilityEvidence, active SCUMBindingIdentity, probeExecutorAvailable bool, now time.Time) SCUMCapabilityGate {
gate := SCUMCapabilityGate{Capability: requirement.Capability, State: SCUMCapabilityGateDisabled, ReasonCode: SCUMSafeErrorProbeMissing, Reason: "current-service evidence is required before this SCUM capability can run"}
if !probeExecutorAvailable {
gate.ReasonCode = SCUMSafeErrorProbeExecutorAbsent
gate.Reason = "bound Run does not expose the generic SQLite schema-probe executor"
return gate
}
if evidence.Status == SCUMCapabilityEvidenceMissing || evidence.Capability == "" {
return gate
}
gate.Evidence = CopySCUMCapabilityEvidence(evidence)
if evidence.Status == SCUMCapabilityEvidenceFailed {
gate.ReasonCode = SCUMSafeErrorProbeFailed
gate.Reason = safeReason(evidence.SafeError.Message, "last schema probe failed")
return gate
}
if evidence.Status == SCUMCapabilityEvidenceIncompatible {
gate.ReasonCode = SCUMSafeErrorSchemaIncompatible
gate.Reason = safeReason(evidence.SafeError.Message, "current schema is incompatible with the plugin adapter")
return gate
}
if evidence.Capability != requirement.Capability {
gate.ReasonCode = SCUMSafeErrorSchemaIncompatible
gate.Reason = "capability evidence does not match the requested SCUM capability"
return gate
}
if !sameSCUMBinding(evidence.Binding, active) {
gate.ReasonCode = SCUMSafeErrorBindingMismatch
gate.Reason = "evidence belongs to a different server, Run binding, plugin, adapter, game, or database identity"
return gate
}
if evidence.AdapterVersion != requirement.AdapterVersion {
gate.ReasonCode = SCUMSafeErrorAdapterMismatch
gate.Reason = "evidence adapter version does not match the plugin requirement"
return gate
}
if evidence.SchemaFingerprint == "" || evidence.SchemaFingerprint != requirement.SchemaFingerprint {
gate.ReasonCode = SCUMSafeErrorFingerprintMismatch
gate.Reason = "schema fingerprint does not match the plugin requirement"
return gate
}
if !containsAllStrings(evidence.AssetDigests, requirement.AssetDigests) {
gate.ReasonCode = SCUMSafeErrorDigestMismatch
gate.Reason = "packaged asset digest does not match the compatible evidence"
return gate
}
if !evidence.ExpiresAt.IsZero() && !now.IsZero() && !now.Before(evidence.ExpiresAt) {
gate.ReasonCode = SCUMSafeErrorEvidenceExpired
gate.Reason = "current-service evidence has expired and must be probed again"
return gate
}
gate.State = SCUMCapabilityGateEnabled
gate.Enabled = true
gate.ReasonCode = SCUMSafeErrorNone
gate.Reason = "current-service evidence matches the versioned plugin adapter"
return gate
}
func CopySCUMCapabilityEvidence(value SCUMCapabilityEvidence) SCUMCapabilityEvidence {
value.AssetDigests = append([]string(nil), value.AssetDigests...)
return value
}
func CopySCUMSchemaProbeResult(value SCUMSchemaProbeResult) SCUMSchemaProbeResult {
value.Objects = append([]SCUMSchemaObjectEvidence(nil), value.Objects...)
for index := range value.Objects {
value.Objects[index].DeclaredColumns = append([]SCUMSchemaColumnEvidence(nil), value.Objects[index].DeclaredColumns...)
value.Objects[index].Indexes = append([]SCUMSchemaIndexEvidence(nil), value.Objects[index].Indexes...)
value.Objects[index].ForeignKeys = append([]SCUMSchemaForeignKeyEvidence(nil), value.Objects[index].ForeignKeys...)
value.Objects[index].SampleFingerprints = append([]string(nil), value.Objects[index].SampleFingerprints...)
for idx := range value.Objects[index].Indexes {
value.Objects[index].Indexes[idx].ColumnHashes = append([]string(nil), value.Objects[index].Indexes[idx].ColumnHashes...)
}
}
return value
}
func CopySCUMLiveDataManifest(value SCUMLiveDataManifest) SCUMLiveDataManifest {
value.CapabilityGates = append([]SCUMLiveDataCapabilityGateDeclaration(nil), value.CapabilityGates...)
for index := range value.CapabilityGates {
value.CapabilityGates[index].RequiredAssetDigests = append([]string(nil), value.CapabilityGates[index].RequiredAssetDigests...)
}
return value
}
func sameSCUMBinding(a SCUMBindingIdentity, b SCUMBindingIdentity) bool {
return a.ServerInstanceID == b.ServerInstanceID && a.RunBindingID == b.RunBindingID && a.RunEndpointID == b.RunEndpointID && a.PluginID == b.PluginID && a.PluginVersion == b.PluginVersion && a.AdapterVersion == b.AdapterVersion && a.GameVersion == b.GameVersion && a.DatabaseIdentity == b.DatabaseIdentity
}
func containsAllStrings(values []string, required []string) bool {
set := map[string]struct{}{}
for _, value := range values {
set[value] = struct{}{}
}
for _, value := range required {
if _, ok := set[value]; !ok {
return false
}
}
return true
}
func safeReason(value string, fallback string) string {
if value == "" {
return fallback
}
return value
}