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
+4 -1
View File
@@ -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:
+19
View File
@@ -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",
+309
View File
@@ -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
}
+71
View File
@@ -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"}
}