feat(scum): gate live data on schema evidence
This commit is contained in:
@@ -633,6 +633,7 @@ type GamePluginManifest struct {
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
RuntimeProfiles GamePluginRuntimeProfiles
|
||||
GameClientBridge GameClientBridgeManifest
|
||||
SCUMLiveData SCUMLiveDataManifest
|
||||
MapTrajectories *GameMapTrajectoryDeclaration
|
||||
}
|
||||
|
||||
@@ -673,6 +674,7 @@ type GamePlugin struct {
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
RuntimeProfiles GamePluginRuntimeProfiles
|
||||
GameClientBridge GameClientBridgeManifest
|
||||
SCUMLiveData SCUMLiveDataManifest
|
||||
MapTrajectories *GameMapTrajectoryDeclaration
|
||||
ValidationViolations []string
|
||||
Status GamePluginStatus
|
||||
@@ -1078,6 +1080,7 @@ const (
|
||||
JobCapabilityRemoteRunProcessStart = "remote.run.process.start"
|
||||
JobCapabilityRemoteRunProcessStop = "remote.run.process.stop"
|
||||
JobCapabilityRemoteRunDBMySQLQuery = "remote.run.db.mysql.query"
|
||||
JobCapabilityRemoteRunDBSQLiteProbe = "remote.run.db.sqlite.probe"
|
||||
JobCapabilityRemoteRunDBSQLiteQuery = "remote.run.db.sqlite.query"
|
||||
JobCapabilityRemoteRunLogsTransfer = "remote.run.logs.transfer"
|
||||
JobCapabilityRemoteRunRCONCommand = "remote.run.rcon.command"
|
||||
@@ -1694,6 +1697,7 @@ func CopyGamePlugin(plugin GamePlugin) GamePlugin {
|
||||
plugin.RemoteAccess = CopyGamePluginRemoteAccess(plugin.RemoteAccess)
|
||||
plugin.RuntimeProfiles = CopyGamePluginRuntimeProfiles(plugin.RuntimeProfiles)
|
||||
plugin.GameClientBridge = CopyGameClientBridgeManifest(plugin.GameClientBridge)
|
||||
plugin.SCUMLiveData = CopySCUMLiveDataManifest(plugin.SCUMLiveData)
|
||||
if plugin.MapTrajectories != nil {
|
||||
value := CopyGameMapTrajectoryDeclaration(*plugin.MapTrajectories)
|
||||
plugin.MapTrajectories = &value
|
||||
@@ -1764,6 +1768,7 @@ func CopyGamePluginManifest(manifest GamePluginManifest) GamePluginManifest {
|
||||
manifest.RemoteAccess = CopyGamePluginRemoteAccess(manifest.RemoteAccess)
|
||||
manifest.RuntimeProfiles = CopyGamePluginRuntimeProfiles(manifest.RuntimeProfiles)
|
||||
manifest.GameClientBridge = CopyGameClientBridgeManifest(manifest.GameClientBridge)
|
||||
manifest.SCUMLiveData = CopySCUMLiveDataManifest(manifest.SCUMLiveData)
|
||||
if manifest.MapTrajectories != nil {
|
||||
value := CopyGameMapTrajectoryDeclaration(*manifest.MapTrajectories)
|
||||
manifest.MapTrajectories = &value
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSCUMCapabilityGateDefaultsClosedWithoutProbeEvidence(t *testing.T) {
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPlayerRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:query"}}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, SCUMCapabilityEvidence{}, active, true, time.Now())
|
||||
|
||||
if gate.Enabled || gate.State != SCUMCapabilityGateDisabled || gate.ReasonCode != SCUMSafeErrorProbeMissing {
|
||||
t.Fatalf("expected closed gate without evidence, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateDefaultsClosedWhenProbeExecutorUnavailable(t *testing.T) {
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPlayerRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1"}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityPlayerRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1"}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, evidence, active, false, time.Now())
|
||||
|
||||
if gate.Enabled || gate.ReasonCode != SCUMSafeErrorProbeExecutorAbsent {
|
||||
t.Fatalf("expected executor gate failure, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateEnablesOnlyMatchingEvidence(t *testing.T) {
|
||||
now := time.Date(2026, 8, 11, 12, 0, 0, 0, time.UTC)
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityVehicleRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:vehicle-query"}}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityVehicleRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:vehicle-query", "sha256:result-schema"}, ExpiresAt: now.Add(time.Hour)}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, evidence, active, true, now)
|
||||
|
||||
if !gate.Enabled || gate.State != SCUMCapabilityGateEnabled || gate.ReasonCode != SCUMSafeErrorNone {
|
||||
t.Fatalf("expected enabled gate, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateRejectsMismatchedCurrentServiceEvidence(t *testing.T) {
|
||||
now := time.Date(2026, 8, 11, 12, 0, 0, 0, time.UTC)
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPositionRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:positions"}}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityPositionRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:positions"}, ExpiresAt: now.Add(time.Hour)}
|
||||
|
||||
changedBinding := evidence
|
||||
changedBinding.Binding.DatabaseIdentity = "db-other"
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedBinding, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorBindingMismatch {
|
||||
t.Fatalf("expected binding mismatch, got %#v", gate)
|
||||
}
|
||||
|
||||
changedFingerprint := evidence
|
||||
changedFingerprint.SchemaFingerprint = "schema-other"
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedFingerprint, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorFingerprintMismatch {
|
||||
t.Fatalf("expected fingerprint mismatch, got %#v", gate)
|
||||
}
|
||||
|
||||
changedDigest := evidence
|
||||
changedDigest.AssetDigests = []string{"sha256:different"}
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedDigest, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorDigestMismatch {
|
||||
t.Fatalf("expected digest mismatch, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultSCUMSchemaProbeBoundsAreBoundedAndDiagnosticOnly(t *testing.T) {
|
||||
bounds := DefaultSCUMSchemaProbeBounds()
|
||||
if bounds.MaxObjects <= 0 || bounds.MaxSampleRows > 3 || bounds.TimeoutMS > 5000 || bounds.MaxResultBytes > 512*1024 {
|
||||
t.Fatalf("unexpected unsafe default probe bounds: %#v", bounds)
|
||||
}
|
||||
}
|
||||
|
||||
func scumGateBinding() SCUMBindingIdentity {
|
||||
return SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "db-current"}
|
||||
}
|
||||
@@ -399,6 +399,28 @@ type GameMapTrajectoryDeclarationBody struct {
|
||||
RetentionSeconds int `json:"retentionSeconds"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDeclarationBody struct {
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
Bounds SCUMSchemaProbeBoundsDTO `json:"bounds"`
|
||||
}
|
||||
|
||||
type SCUMLiveDataCapabilityGateBody struct {
|
||||
Capability string `json:"capability"`
|
||||
Gate string `json:"gate"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
|
||||
RequiredAssetDigests []string `json:"requiredAssetDigests,omitempty"`
|
||||
EvidenceStatus string `json:"evidenceStatus"`
|
||||
SafeReason string `json:"safeReason"`
|
||||
}
|
||||
|
||||
type SCUMLiveDataManifestBody struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Probe SCUMSchemaProbeDeclarationBody `json:"probe"`
|
||||
CapabilityGates []SCUMLiveDataCapabilityGateBody `json:"capabilityGates"`
|
||||
}
|
||||
|
||||
type GamePluginManifestBody struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -419,6 +441,7 @@ type GamePluginManifestBody struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
}
|
||||
|
||||
@@ -458,6 +481,7 @@ type GamePluginCreateRequest struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
}
|
||||
@@ -486,6 +510,7 @@ type GamePluginResponse struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesResponseBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData *SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
@@ -1082,11 +1107,20 @@ func (request GamePluginManifestRegistrationRequest) ToDomain() domain.GamePlugi
|
||||
RemoteAccess: request.Manifest.RemoteAccess.ToDomain(),
|
||||
RuntimeProfiles: request.Manifest.RuntimeProfiles.ToDomain(),
|
||||
GameClientBridge: request.Manifest.GameClientBridge.ToDomain(),
|
||||
SCUMLiveData: request.Manifest.SCUMLiveData.ToDomain(),
|
||||
MapTrajectories: mapTrajectoryDeclarationToDomain(request.Manifest.MapTrajectories),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (body SCUMLiveDataManifestBody) ToDomain() domain.SCUMLiveDataManifest {
|
||||
gates := make([]domain.SCUMLiveDataCapabilityGateDeclaration, len(body.CapabilityGates))
|
||||
for index, gate := range body.CapabilityGates {
|
||||
gates[index] = domain.SCUMLiveDataCapabilityGateDeclaration{Capability: domain.SCUMDataCapability(gate.Capability), Gate: domain.SCUMCapabilityGateState(gate.Gate), AdapterVersion: gate.AdapterVersion, RequiredSchemaFingerprint: gate.RequiredSchemaFingerprint, RequiredAssetDigests: domain.CopyStringSlice(gate.RequiredAssetDigests), EvidenceStatus: domain.SCUMCapabilityEvidenceStatus(gate.EvidenceStatus), SafeReason: gate.SafeReason}
|
||||
}
|
||||
return domain.SCUMLiveDataManifest{SchemaVersion: body.SchemaVersion, Probe: domain.SCUMSchemaProbeDeclaration{Capability: body.Probe.Capability, TargetKey: body.Probe.TargetKey, Bounds: scumProbeBoundsToDomain(body.Probe.Bounds)}, CapabilityGates: gates}
|
||||
}
|
||||
|
||||
func pluginAssetFilesToDomain(files []PluginAssetFileBody) []domain.PluginAssetFile {
|
||||
if files == nil {
|
||||
return nil
|
||||
@@ -1268,6 +1302,7 @@ func (request GamePluginCreateRequest) ToDomain() domain.GamePlugin {
|
||||
RemoteAccess: request.RemoteAccess.ToDomain(),
|
||||
RuntimeProfiles: request.RuntimeProfiles.ToDomain(),
|
||||
GameClientBridge: request.GameClientBridge.ToDomain(),
|
||||
SCUMLiveData: request.SCUMLiveData.ToDomain(),
|
||||
MapTrajectories: mapTrajectoryDeclarationToDomain(request.MapTrajectories),
|
||||
ValidationViolations: domain.CopyStringSlice(request.ValidationViolations),
|
||||
}
|
||||
@@ -1521,6 +1556,7 @@ func GamePluginFromDomain(plugin domain.GamePlugin) GamePluginResponse {
|
||||
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
|
||||
RuntimeProfiles: runtimeProfilesFromDomain(plugin.RuntimeProfiles),
|
||||
GameClientBridge: gameClientBridgeManifestFromDomain(plugin.GameClientBridge),
|
||||
SCUMLiveData: scumLiveDataManifestPtrFromDomain(plugin.SCUMLiveData),
|
||||
MapTrajectories: mapTrajectoryDeclarationFromDomain(plugin.MapTrajectories),
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
@@ -1625,6 +1661,23 @@ func productionLifecycleFromDomain(lifecycle domain.GamePluginProductionLifecycl
|
||||
return GamePluginProductionLifecycleBody{Operations: lifecycle.Operations, DependencyPolicy: lifecycle.DependencyPolicy, ApprovalRequired: lifecycle.ApprovalRequired}
|
||||
}
|
||||
|
||||
func scumLiveDataManifestFromDomain(value domain.SCUMLiveDataManifest) SCUMLiveDataManifestBody {
|
||||
value = domain.CopySCUMLiveDataManifest(value)
|
||||
gates := make([]SCUMLiveDataCapabilityGateBody, len(value.CapabilityGates))
|
||||
for index, gate := range value.CapabilityGates {
|
||||
gates[index] = SCUMLiveDataCapabilityGateBody{Capability: string(gate.Capability), Gate: string(gate.Gate), AdapterVersion: gate.AdapterVersion, RequiredSchemaFingerprint: gate.RequiredSchemaFingerprint, RequiredAssetDigests: domain.CopyStringSlice(gate.RequiredAssetDigests), EvidenceStatus: string(gate.EvidenceStatus), SafeReason: gate.SafeReason}
|
||||
}
|
||||
return SCUMLiveDataManifestBody{SchemaVersion: value.SchemaVersion, Probe: SCUMSchemaProbeDeclarationBody{Capability: value.Probe.Capability, TargetKey: value.Probe.TargetKey, Bounds: scumProbeBoundsFromDomain(value.Probe.Bounds)}, CapabilityGates: gates}
|
||||
}
|
||||
|
||||
func scumLiveDataManifestPtrFromDomain(value domain.SCUMLiveDataManifest) *SCUMLiveDataManifestBody {
|
||||
if value.SchemaVersion == "" && value.Probe.Capability == "" && len(value.CapabilityGates) == 0 {
|
||||
return nil
|
||||
}
|
||||
body := scumLiveDataManifestFromDomain(value)
|
||||
return &body
|
||||
}
|
||||
|
||||
func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) GameClientBridgeManifestBody {
|
||||
value = domain.CopyGameClientBridgeManifest(value)
|
||||
commands := make([]GameClientBridgeCommandDeclarationBody, len(value.Commands))
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type SCUMBindingIdentityDTO struct {
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
RunBindingID string `json:"runBindingId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
PluginVersion string `json:"pluginVersion"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
GameVersion string `json:"gameVersion"`
|
||||
DatabaseIdentity string `json:"databaseIdentity"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeBoundsDTO struct {
|
||||
MaxObjects int `json:"maxObjects"`
|
||||
MaxColumnsPerObject int `json:"maxColumnsPerObject"`
|
||||
MaxIndexesPerObject int `json:"maxIndexesPerObject"`
|
||||
MaxForeignKeys int `json:"maxForeignKeys"`
|
||||
MaxCardinalityReads int `json:"maxCardinalityReads"`
|
||||
MaxSampleRows int `json:"maxSampleRows"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
MaxResultBytes int `json:"maxResultBytes"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Bounds SCUMSchemaProbeBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMSafeErrorDTO struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Retryable bool `json:"retryable"`
|
||||
}
|
||||
|
||||
type SCUMSchemaColumnEvidenceDTO struct {
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
DeclaredType string `json:"declaredType"`
|
||||
Nullable *bool `json:"nullable,omitempty"`
|
||||
PrimaryKey bool `json:"primaryKey"`
|
||||
Ordinal int `json:"ordinal"`
|
||||
}
|
||||
|
||||
type SCUMSchemaIndexEvidenceDTO struct {
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
Unique bool `json:"unique"`
|
||||
ColumnHashes []string `json:"columnHashes"`
|
||||
}
|
||||
|
||||
type SCUMSchemaForeignKeyEvidenceDTO struct {
|
||||
FromColumnHash string `json:"fromColumnHash"`
|
||||
ToObjectHash string `json:"toObjectHash"`
|
||||
ToColumnHash string `json:"toColumnHash"`
|
||||
}
|
||||
|
||||
type SCUMSchemaObjectEvidenceDTO struct {
|
||||
ObjectHash string `json:"objectHash"`
|
||||
Kind string `json:"kind"`
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
DeclaredColumns []SCUMSchemaColumnEvidenceDTO `json:"declaredColumns"`
|
||||
Indexes []SCUMSchemaIndexEvidenceDTO `json:"indexes"`
|
||||
ForeignKeys []SCUMSchemaForeignKeyEvidenceDTO `json:"foreignKeys"`
|
||||
ApproximateRows *int64 `json:"approximateRows,omitempty"`
|
||||
SampleFingerprints []string `json:"sampleFingerprints,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest,omitempty"`
|
||||
Objects []SCUMSchemaObjectEvidenceDTO `json:"objects,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMSchemaProbeBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMCapabilityGateDTO struct {
|
||||
Capability string `json:"capability"`
|
||||
State string `json:"state"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ReasonCode string `json:"reasonCode"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeRequestToDomain(value SCUMSchemaProbeRequestDTO) domain.SCUMSchemaProbeRequest {
|
||||
return domain.SCUMSchemaProbeRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Bounds: scumProbeBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSchemaProbeResultDTO {
|
||||
value = domain.CopySCUMSchemaProbeResult(value)
|
||||
objects := make([]SCUMSchemaObjectEvidenceDTO, len(value.Objects))
|
||||
for index, object := range value.Objects {
|
||||
columns := make([]SCUMSchemaColumnEvidenceDTO, len(object.DeclaredColumns))
|
||||
for i, column := range object.DeclaredColumns {
|
||||
columns[i] = SCUMSchemaColumnEvidenceDTO{NameFingerprint: column.NameFingerprint, DeclaredType: column.DeclaredType, Nullable: column.Nullable, PrimaryKey: column.PrimaryKey, Ordinal: column.Ordinal}
|
||||
}
|
||||
indexes := make([]SCUMSchemaIndexEvidenceDTO, len(object.Indexes))
|
||||
for i, item := range object.Indexes {
|
||||
indexes[i] = SCUMSchemaIndexEvidenceDTO{NameFingerprint: item.NameFingerprint, Unique: item.Unique, ColumnHashes: append([]string(nil), item.ColumnHashes...)}
|
||||
}
|
||||
foreignKeys := make([]SCUMSchemaForeignKeyEvidenceDTO, len(object.ForeignKeys))
|
||||
for i, item := range object.ForeignKeys {
|
||||
foreignKeys[i] = SCUMSchemaForeignKeyEvidenceDTO{FromColumnHash: item.FromColumnHash, ToObjectHash: item.ToObjectHash, ToColumnHash: item.ToColumnHash}
|
||||
}
|
||||
objects[index] = SCUMSchemaObjectEvidenceDTO{ObjectHash: object.ObjectHash, Kind: object.Kind, NameFingerprint: object.NameFingerprint, DeclaredColumns: columns, Indexes: indexes, ForeignKeys: foreignKeys, ApproximateRows: object.ApproximateRows, SampleFingerprints: append([]string(nil), object.SampleFingerprints...)}
|
||||
}
|
||||
return SCUMSchemaProbeResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), SchemaFingerprint: value.SchemaFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, Objects: objects, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumProbeBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMCapabilityGateFromDomain(value domain.SCUMCapabilityGate) SCUMCapabilityGateDTO {
|
||||
return SCUMCapabilityGateDTO{Capability: string(value.Capability), State: string(value.State), Enabled: value.Enabled, ReasonCode: string(value.ReasonCode), Reason: value.Reason}
|
||||
}
|
||||
|
||||
func scumBindingIdentityToDomain(value SCUMBindingIdentityDTO) domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: value.ServerInstanceID, RunBindingID: value.RunBindingID, RunEndpointID: value.RunEndpointID, PluginID: value.PluginID, PluginVersion: value.PluginVersion, AdapterVersion: value.AdapterVersion, GameVersion: value.GameVersion, DatabaseIdentity: value.DatabaseIdentity}
|
||||
}
|
||||
|
||||
func scumBindingIdentityFromDomain(value domain.SCUMBindingIdentity) SCUMBindingIdentityDTO {
|
||||
return SCUMBindingIdentityDTO{ServerInstanceID: value.ServerInstanceID, RunBindingID: value.RunBindingID, RunEndpointID: value.RunEndpointID, PluginID: value.PluginID, PluginVersion: value.PluginVersion, AdapterVersion: value.AdapterVersion, GameVersion: value.GameVersion, DatabaseIdentity: value.DatabaseIdentity}
|
||||
}
|
||||
|
||||
func scumProbeBoundsToDomain(value SCUMSchemaProbeBoundsDTO) domain.SCUMSchemaProbeBounds {
|
||||
return domain.SCUMSchemaProbeBounds{MaxObjects: value.MaxObjects, MaxColumnsPerObject: value.MaxColumnsPerObject, MaxIndexesPerObject: value.MaxIndexesPerObject, MaxForeignKeys: value.MaxForeignKeys, MaxCardinalityReads: value.MaxCardinalityReads, MaxSampleRows: value.MaxSampleRows, TimeoutMS: value.TimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumProbeBoundsFromDomain(value domain.SCUMSchemaProbeBounds) SCUMSchemaProbeBoundsDTO {
|
||||
return SCUMSchemaProbeBoundsDTO{MaxObjects: value.MaxObjects, MaxColumnsPerObject: value.MaxColumnsPerObject, MaxIndexesPerObject: value.MaxIndexesPerObject, MaxForeignKeys: value.MaxForeignKeys, MaxCardinalityReads: value.MaxCardinalityReads, MaxSampleRows: value.MaxSampleRows, TimeoutMS: value.TimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# SCUM Live Data Contracts
|
||||
|
||||
This contract replaces SCUM projection/Workflow-facing reads with evidence-gated local management data. It is intentionally generic at the Run boundary: Platform and plugins may name SCUM capabilities, but Run receives only packaged generic SQLite probe/template/mutation work and never hardcodes SCUM table names, command keys, host paths, sockets, credentials, or browser-supplied SQL.
|
||||
|
||||
## Capability gate
|
||||
|
||||
Every database-backed SCUM read/write capability is disabled until all of the following are true for the active server binding:
|
||||
|
||||
- the bound Run advertises `remote.run.db.sqlite.probe`;
|
||||
- Platform has a current `SCUMCapabilityEvidence` row for the exact server instance, Run binding, Run endpoint, plugin id/version, adapter version, game version, and database identity;
|
||||
- evidence status is `compatible` for the requested capability;
|
||||
- the evidence schema fingerprint equals the versioned adapter requirement;
|
||||
- every required packaged asset digest is present in the evidence;
|
||||
- evidence has not expired or been invalidated by rebinding, database identity change, plugin version change, adapter version change, or schema fingerprint change.
|
||||
|
||||
If any condition fails, APIs and plugin pages receive a safe disabled state such as `probe_missing`, `probe_executor_absent`, `binding_mismatch`, `fingerprint_mismatch`, `digest_mismatch`, `schema_incompatible`, or `evidence_expired`. Disabled states are ordinary availability results, not projection/audit/workflow work items.
|
||||
|
||||
## Probe request
|
||||
|
||||
`SCUMSchemaProbeRequest` is a Platform durable-job payload addressed through the active authenticated Run binding.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `bounds`: `maxObjects`, `maxColumnsPerObject`, `maxIndexesPerObject`, `maxForeignKeys`, `maxCardinalityReads`, `maxSampleRows`, `timeoutMs`, `maxResultBytes`;
|
||||
- `requestedAt`.
|
||||
|
||||
The payload must not include a host database path, DSN, socket, credential, raw SQL text, raw rows, or SCUM-specific table names. Target resolution happens inside the active Run package from logical bindings only.
|
||||
|
||||
## Probe result
|
||||
|
||||
`SCUMSchemaProbeResult` returns only redacted schema evidence:
|
||||
|
||||
- request/job/binding identity;
|
||||
- status: `missing`, `compatible`, `incompatible`, or `failed`;
|
||||
- schema fingerprint and result digest;
|
||||
- bounded object metadata with object/name/column/index/fk fingerprints, declared types, nullable/primary-key flags, approximate row counts, and sample fingerprints;
|
||||
- safe error code/message when failed;
|
||||
- limits actually applied.
|
||||
|
||||
Samples are hashes/fingerprints only. Raw row content, XML payloads, SQL, paths, DSNs, sockets, credentials, host names, IPs, and RCON text are never returned to Platform Web, plugin pages, AI prompts, or safe diagnostic fields.
|
||||
|
||||
## Release behavior
|
||||
|
||||
The first-party SCUM plugin declares `scumLiveData` with `remote.run.db.sqlite.probe` and per-capability gates. Until current-service evidence exists, all gates remain `disabled` with `evidenceStatus: missing`. Query assets, RCON templates, XML mutations, map transforms, and gift transports may be added only after current-service probe evidence proves their adapter requirements; unsupported or ambiguous capabilities stay disabled independently.
|
||||
@@ -826,6 +826,7 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
|
||||
RemoteAccess: manifest.RemoteAccess,
|
||||
RuntimeProfiles: manifest.RuntimeProfiles,
|
||||
GameClientBridge: manifest.GameClientBridge,
|
||||
SCUMLiveData: manifest.SCUMLiveData,
|
||||
MapTrajectories: manifest.MapTrajectories,
|
||||
Status: domain.GamePluginStatusInstalled,
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
|
||||
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(plugin.RuntimeProfiles, plugin.RequiredRunCapabilities)...)
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("runtimeProfiles.logEvents", plugin.RuntimeProfiles, plugin.DeclaredPermissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateSCUMLiveDataManifest("scumLiveData", plugin.SCUMLiveData, plugin.RequiredRunCapabilities, plugin.RemoteAccess, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("mapTrajectories", plugin.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...)
|
||||
violations = append(violations, validatePluginAssetFiles("lifecycleAssets", plugin.LifecycleAssets)...)
|
||||
@@ -229,6 +230,7 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
|
||||
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(manifest.RuntimeProfiles, manifest.Capabilities)...)
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateSCUMLiveDataManifest("manifest.scumLiveData", manifest.SCUMLiveData, manifest.Capabilities, manifest.RemoteAccess, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("manifest.mapTrajectories", manifest.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginAssetFileDeclarations("manifest.assetFiles", manifest.AssetFiles)...)
|
||||
violations = append(violations, validatePluginAssetFiles("assetFiles", registration.AssetFiles)...)
|
||||
@@ -2199,7 +2201,7 @@ func validPluginRunCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||
domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram,
|
||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||
@@ -2233,6 +2235,7 @@ func remoteCapabilityRequiresInputRef(capability string) bool {
|
||||
domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery,
|
||||
domain.JobCapabilityRemoteRunDBSQLiteProbe,
|
||||
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL,
|
||||
domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram:
|
||||
|
||||
@@ -295,6 +295,25 @@ func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermi
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationValidatesSCUMLiveDataGate(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
registration.Manifest.ID = "game.scum"
|
||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}
|
||||
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}}
|
||||
registration.Manifest.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "等待当前服务证据。"}}}
|
||||
|
||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||
t.Fatalf("expected disabled live-data gate to validate, got %v", err)
|
||||
}
|
||||
|
||||
registration.Manifest.SCUMLiveData.CapabilityGates[0].Gate = domain.SCUMCapabilityGateEnabled
|
||||
err := ValidateGamePluginManifestRegistration(registration)
|
||||
if err == nil || !strings.Contains(err.Error(), "evidenceStatus must be compatible") || !strings.Contains(err.Error(), "requiredSchemaFingerprint") {
|
||||
t.Fatalf("expected enabled gate evidence violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServerInstanceDependencies(t *testing.T) {
|
||||
instance := domain.ServerInstance{
|
||||
ID: "server-1",
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSCUMProbeObjects = 512
|
||||
maxSCUMProbeColumnsPerObject = 256
|
||||
maxSCUMProbeIndexesPerObject = 128
|
||||
maxSCUMProbeForeignKeys = 128
|
||||
maxSCUMProbeSamples = 3
|
||||
maxSCUMProbeTimeoutMS = 10000
|
||||
maxSCUMProbeResultBytes = 1024 * 1024
|
||||
)
|
||||
|
||||
var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`)
|
||||
|
||||
func ValidateSCUMSchemaProbeRequest(request domain.SCUMSchemaProbeRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds("bounds", request.Bounds)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMCapabilityEvidenceStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if result.ResultDigest != "" && !validSCUMFingerprint(result.ResultDigest) {
|
||||
violations = append(violations, "resultDigest must be a digest/fingerprint")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds("limits", result.Limits)...)
|
||||
if len(result.Objects) > result.Limits.MaxObjects && result.Limits.MaxObjects > 0 {
|
||||
violations = append(violations, "objects exceeds declared limit")
|
||||
}
|
||||
for i, object := range result.Objects {
|
||||
field := fmt.Sprintf("objects[%d]", i)
|
||||
violations = append(violations, validateSCUMSchemaObjectEvidence(field, object)...)
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error {
|
||||
var violations []string
|
||||
if !validSCUMDataCapability(evidence.Capability) {
|
||||
violations = append(violations, "capability is invalid")
|
||||
}
|
||||
if !validSCUMCapabilityEvidenceStatus(evidence.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", evidence.Binding)...)
|
||||
violations = appendRequired(violations, "adapterVersion", evidence.AdapterVersion)
|
||||
if evidence.SchemaFingerprint != "" && !validSCUMFingerprint(evidence.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if evidence.ProbeResultDigest != "" && !validSCUMFingerprint(evidence.ProbeResultDigest) {
|
||||
violations = append(violations, "probeResultDigest must be a digest/fingerprint")
|
||||
}
|
||||
for i, digest := range evidence.AssetDigests {
|
||||
if !validSCUMDigest(digest) {
|
||||
violations = append(violations, fmt.Sprintf("assetDigests[%d] must be sha256 digest", i))
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", evidence.SafeError)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validateSCUMLiveDataManifest(prefix string, value domain.SCUMLiveDataManifest, capabilities []string, remoteAccess domain.GamePluginRemoteAccess, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
|
||||
if value.SchemaVersion == "" && value.Probe.Capability == "" && len(value.CapabilityGates) == 0 {
|
||||
return nil
|
||||
}
|
||||
var violations []string
|
||||
if value.SchemaVersion != "1" {
|
||||
violations = append(violations, prefix+".schemaVersion must be 1")
|
||||
}
|
||||
if value.Probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe {
|
||||
violations = append(violations, prefix+".probe.capability must be "+domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
}
|
||||
if !containsString(capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) || !containsString(remoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
violations = append(violations, prefix+".probe requires declared remote.run.db.sqlite.probe capability")
|
||||
}
|
||||
probeTransportFound := false
|
||||
for _, transport := range runtimeProfiles.TransportProfiles {
|
||||
if transport.Key != value.Probe.TargetKey && transport.TargetKey != value.Probe.TargetKey {
|
||||
continue
|
||||
}
|
||||
probeTransportFound = true
|
||||
if transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference sqlite transport with remote.run.db.sqlite.probe")
|
||||
}
|
||||
}
|
||||
if !probeTransportFound {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference a declared transport")
|
||||
}
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds(prefix+".probe.bounds", value.Probe.Bounds)...)
|
||||
seen := map[domain.SCUMDataCapability]struct{}{}
|
||||
for i, gate := range value.CapabilityGates {
|
||||
field := fmt.Sprintf("%s.capabilityGates[%d]", prefix, i)
|
||||
if !validSCUMDataCapability(gate.Capability) {
|
||||
violations = append(violations, field+".capability is invalid")
|
||||
}
|
||||
if _, exists := seen[gate.Capability]; exists {
|
||||
violations = append(violations, field+".capability is duplicated")
|
||||
}
|
||||
seen[gate.Capability] = struct{}{}
|
||||
if gate.Gate != domain.SCUMCapabilityGateDisabled && gate.Gate != domain.SCUMCapabilityGateEnabled {
|
||||
violations = append(violations, field+".gate is invalid")
|
||||
}
|
||||
violations = appendRequired(violations, field+".adapterVersion", gate.AdapterVersion)
|
||||
if !validSCUMCapabilityEvidenceStatus(gate.EvidenceStatus) {
|
||||
violations = append(violations, field+".evidenceStatus is invalid")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(gate.SafeReason) || len(gate.SafeReason) > 240 || strings.TrimSpace(gate.SafeReason) == "" {
|
||||
violations = append(violations, field+".safeReason is unsafe")
|
||||
}
|
||||
if gate.Gate == domain.SCUMCapabilityGateEnabled {
|
||||
if gate.EvidenceStatus != domain.SCUMCapabilityEvidenceCompatible {
|
||||
violations = append(violations, field+".evidenceStatus must be compatible when enabled")
|
||||
}
|
||||
if !validSCUMFingerprint(gate.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, field+".requiredSchemaFingerprint is required when enabled")
|
||||
}
|
||||
if len(gate.RequiredAssetDigests) == 0 {
|
||||
violations = append(violations, field+".requiredAssetDigests is required when enabled")
|
||||
}
|
||||
}
|
||||
if gate.Gate == domain.SCUMCapabilityGateDisabled && gate.EvidenceStatus == domain.SCUMCapabilityEvidenceCompatible {
|
||||
violations = append(violations, field+".evidenceStatus must not claim compatibility while disabled")
|
||||
}
|
||||
for digestIndex, digest := range gate.RequiredAssetDigests {
|
||||
if !validSCUMDigest(digest) {
|
||||
violations = append(violations, fmt.Sprintf("%s.requiredAssetDigests[%d] must be sha256 digest", field, digestIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMBindingIdentity(prefix string, value domain.SCUMBindingIdentity) []string {
|
||||
var violations []string
|
||||
for _, item := range []struct{ name, value string }{{"serverInstanceId", value.ServerInstanceID}, {"runBindingId", value.RunBindingID}, {"runEndpointId", value.RunEndpointID}, {"pluginId", value.PluginID}, {"pluginVersion", value.PluginVersion}, {"adapterVersion", value.AdapterVersion}, {"databaseIdentity", value.DatabaseIdentity}} {
|
||||
violations = appendRequired(violations, prefix+"."+item.name, item.value)
|
||||
if containsSCUMProtectedMaterial(item.value) {
|
||||
violations = append(violations, prefix+"."+item.name+" contains protected connection material")
|
||||
}
|
||||
}
|
||||
if value.GameVersion != "" && containsSCUMProtectedMaterial(value.GameVersion) {
|
||||
violations = append(violations, prefix+".gameVersion contains protected connection material")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSchemaProbeBounds(prefix string, value domain.SCUMSchemaProbeBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxObjects < 1 || value.MaxObjects > maxSCUMProbeObjects {
|
||||
violations = append(violations, prefix+".maxObjects is out of bounds")
|
||||
}
|
||||
if value.MaxColumnsPerObject < 1 || value.MaxColumnsPerObject > maxSCUMProbeColumnsPerObject {
|
||||
violations = append(violations, prefix+".maxColumnsPerObject is out of bounds")
|
||||
}
|
||||
if value.MaxIndexesPerObject < 0 || value.MaxIndexesPerObject > maxSCUMProbeIndexesPerObject {
|
||||
violations = append(violations, prefix+".maxIndexesPerObject is out of bounds")
|
||||
}
|
||||
if value.MaxForeignKeys < 0 || value.MaxForeignKeys > maxSCUMProbeForeignKeys {
|
||||
violations = append(violations, prefix+".maxForeignKeys is out of bounds")
|
||||
}
|
||||
if value.MaxCardinalityReads < 0 || value.MaxCardinalityReads > maxSCUMProbeObjects {
|
||||
violations = append(violations, prefix+".maxCardinalityReads is out of bounds")
|
||||
}
|
||||
if value.MaxSampleRows < 0 || value.MaxSampleRows > maxSCUMProbeSamples {
|
||||
violations = append(violations, prefix+".maxSampleRows is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMProbeResultBytes {
|
||||
violations = append(violations, prefix+".maxResultBytes is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSchemaObjectEvidence(prefix string, value domain.SCUMSchemaObjectEvidence) []string {
|
||||
var violations []string
|
||||
if !validSCUMFingerprint(value.ObjectHash) {
|
||||
violations = append(violations, prefix+".objectHash must be a digest/fingerprint")
|
||||
}
|
||||
if value.Kind != "table" && value.Kind != "view" && value.Kind != "index" && value.Kind != "trigger" {
|
||||
violations = append(violations, prefix+".kind is invalid")
|
||||
}
|
||||
if !validSCUMFingerprint(value.NameFingerprint) || containsSCUMProtectedMaterial(value.NameFingerprint) {
|
||||
violations = append(violations, prefix+".nameFingerprint must be redacted")
|
||||
}
|
||||
for i, column := range value.DeclaredColumns {
|
||||
field := fmt.Sprintf("%s.declaredColumns[%d]", prefix, i)
|
||||
if !validSCUMFingerprint(column.NameFingerprint) || containsSCUMProtectedMaterial(column.NameFingerprint) {
|
||||
violations = append(violations, field+".nameFingerprint must be redacted")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(column.DeclaredType) {
|
||||
violations = append(violations, field+".declaredType contains protected material")
|
||||
}
|
||||
}
|
||||
for i, index := range value.Indexes {
|
||||
field := fmt.Sprintf("%s.indexes[%d]", prefix, i)
|
||||
if !validSCUMFingerprint(index.NameFingerprint) || containsSCUMProtectedMaterial(index.NameFingerprint) {
|
||||
violations = append(violations, field+".nameFingerprint must be redacted")
|
||||
}
|
||||
for columnIndex, hash := range index.ColumnHashes {
|
||||
if !validSCUMFingerprint(hash) {
|
||||
violations = append(violations, fmt.Sprintf("%s.columnHashes[%d] must be a digest/fingerprint", field, columnIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, fk := range value.ForeignKeys {
|
||||
field := fmt.Sprintf("%s.foreignKeys[%d]", prefix, i)
|
||||
for _, item := range []struct{ name, value string }{{"fromColumnHash", fk.FromColumnHash}, {"toObjectHash", fk.ToObjectHash}, {"toColumnHash", fk.ToColumnHash}} {
|
||||
if !validSCUMFingerprint(item.value) {
|
||||
violations = append(violations, field+"."+item.name+" must be a digest/fingerprint")
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, sample := range value.SampleFingerprints {
|
||||
if !validSCUMFingerprint(sample) || containsSCUMProtectedMaterial(sample) {
|
||||
violations = append(violations, fmt.Sprintf("%s.sampleFingerprints[%d] must be a redacted fingerprint", prefix, i))
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSafeError(prefix string, value domain.SCUMSafeError) []string {
|
||||
var violations []string
|
||||
if !validSCUMSafeErrorCode(value.Code) {
|
||||
violations = append(violations, prefix+".code is invalid")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(value.Message) {
|
||||
violations = append(violations, prefix+".message contains protected material")
|
||||
}
|
||||
if len(value.Message) > 320 {
|
||||
violations = append(violations, prefix+".message is too long")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validSCUMDataCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilitySchemaProbe, domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead, domain.SCUMDataCapabilityProfileXMLWrite, domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMSafeErrorCode(value domain.SCUMSafeErrorCode) bool {
|
||||
switch value {
|
||||
case "", domain.SCUMSafeErrorNone, domain.SCUMSafeErrorProbeExecutorAbsent, domain.SCUMSafeErrorProbeMissing, domain.SCUMSafeErrorProbeFailed, domain.SCUMSafeErrorSchemaIncompatible, domain.SCUMSafeErrorBindingMismatch, domain.SCUMSafeErrorAdapterMismatch, domain.SCUMSafeErrorFingerprintMismatch, domain.SCUMSafeErrorDigestMismatch, domain.SCUMSafeErrorEvidenceExpired, domain.SCUMSafeErrorInvalidProbePayload:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMFingerprint(value string) bool { return scumHashPattern.MatchString(value) }
|
||||
func validSCUMDigest(value string) bool {
|
||||
return regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$`).MatchString(value)
|
||||
}
|
||||
|
||||
func containsSCUMProtectedMaterial(value string) bool {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
lowered := strings.ToLower(trimmed)
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lowered, "select ") || strings.Contains(lowered, "insert into") || strings.Contains(lowered, "update ") || strings.Contains(lowered, "delete from") || strings.Contains(lowered, "pragma ") || strings.Contains(lowered, "attach database") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lowered, "dsn") || strings.Contains(lowered, "password") || strings.Contains(lowered, "credential") || strings.Contains(lowered, "token") || strings.Contains(lowered, "socket") || strings.Contains(lowered, "rcon") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(lowered, "sqlite://") || strings.HasPrefix(lowered, "mysql://") || strings.HasPrefix(lowered, "file://") || strings.HasPrefix(lowered, "tcp://") || strings.HasPrefix(lowered, "unix://") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\\\") || regexp.MustCompile(`^[A-Za-z]:[\\/]`).MatchString(trimmed) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
const scumProbeHash = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
func TestValidateSCUMSchemaProbeRequestAllowsBoundedGenericProbe(t *testing.T) {
|
||||
request := domain.SCUMSchemaProbeRequest{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Bounds: domain.DefaultSCUMSchemaProbeBounds(), RequestedAt: time.Now()}
|
||||
|
||||
if err := ValidateSCUMSchemaProbeRequest(request); err != nil {
|
||||
t.Fatalf("expected valid probe request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeRequestRejectsHostPathsAndLooseBounds(t *testing.T) {
|
||||
request := domain.SCUMSchemaProbeRequest{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Bounds: domain.DefaultSCUMSchemaProbeBounds()}
|
||||
request.Binding.DatabaseIdentity = `C:\SCUM\Saved\SaveFiles\SCUM.db`
|
||||
request.Bounds.MaxSampleRows = 25
|
||||
|
||||
err := ValidateSCUMSchemaProbeRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "protected connection material") || !strings.Contains(err.Error(), "maxSampleRows") {
|
||||
t.Fatalf("expected protected material and bound violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeResultRejectsRawSQLAndRows(t *testing.T) {
|
||||
result := domain.SCUMSchemaProbeResult{
|
||||
RequestID: "probe-1",
|
||||
JobID: "job-1",
|
||||
Binding: validatorSCUMBinding(),
|
||||
Status: domain.SCUMCapabilityEvidenceCompatible,
|
||||
SchemaFingerprint: scumProbeHash,
|
||||
ObservedAt: time.Now(),
|
||||
ResultDigest: scumProbeHash,
|
||||
Limits: domain.DefaultSCUMSchemaProbeBounds(),
|
||||
Objects: []domain.SCUMSchemaObjectEvidence{{
|
||||
ObjectHash: scumProbeHash,
|
||||
Kind: "table",
|
||||
NameFingerprint: "select * from players",
|
||||
DeclaredColumns: []domain.SCUMSchemaColumnEvidence{{NameFingerprint: scumProbeHash, DeclaredType: "TEXT", Ordinal: 1}},
|
||||
SampleFingerprints: []string{"{\"raw\":\"row\"}"},
|
||||
}},
|
||||
}
|
||||
|
||||
err := ValidateSCUMSchemaProbeResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "nameFingerprint must be redacted") || !strings.Contains(err.Error(), "sampleFingerprints") {
|
||||
t.Fatalf("expected redaction violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMCapabilityEvidenceRequiresSafeCurrentServiceIdentity(t *testing.T) {
|
||||
evidence := domain.SCUMCapabilityEvidence{Capability: domain.SCUMDataCapabilityPlayerRead, Status: domain.SCUMCapabilityEvidenceCompatible, Binding: validatorSCUMBinding(), AdapterVersion: "adapter-1", SchemaFingerprint: scumProbeHash, ProbeResultDigest: scumProbeHash, AssetDigests: []string{scumProbeHash}, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
if err := ValidateSCUMCapabilityEvidence(evidence); err != nil {
|
||||
t.Fatalf("expected valid evidence, got %v", err)
|
||||
}
|
||||
|
||||
evidence.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorProbeFailed, Message: "sqlite:///private/tmp/SCUM.db locked"}
|
||||
if err := ValidateSCUMCapabilityEvidence(evidence); err == nil || !strings.Contains(err.Error(), "protected material") {
|
||||
t.Fatalf("expected safe error redaction violation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validatorSCUMBinding() domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "db-fingerprint-1"}
|
||||
}
|
||||
Reference in New Issue
Block a user