Add SCUM capability negotiation gate

This commit is contained in:
npc0-hue
2026-08-13 15:10:57 +08:00
parent 4f55c71de5
commit d831e4ade9
12 changed files with 350 additions and 2 deletions
+135
View File
@@ -17,6 +17,141 @@ const (
scumParsedLogBatchExecutionKind = "log.parsed-events"
)
func (svc *CoreService) NegotiateSCUMCapabilitiesForSession(sessionID, serverInstanceID string) (domain.SCUMCapabilityNegotiation, error) {
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
if err != nil {
return domain.SCUMCapabilityNegotiation{}, err
}
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
return domain.SCUMCapabilityNegotiation{}, err
}
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
if err != nil {
return domain.SCUMCapabilityNegotiation{}, err
}
adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData)
active := domain.SCUMBindingIdentity{ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, PluginID: plugin.ID, PluginVersion: plugin.Version, AdapterVersion: adapterVersion, DatabaseIdentity: scumSchemaProbeDatabaseIdentity(plugin.SCUMLiveData.Probe.TargetKey)}
binding, bindingErr := svc.runtimeBindingForServer(instance.ID)
if bindingErr == nil {
binding, bindingErr = normalizeRuntimeBinding(plugin, binding)
}
if bindingErr == nil {
active.RunBindingID = binding.ID
}
probeExecutorAvailable := scumRunCapabilityAvailable(endpoint, domain.SCUMDataCapabilitySchemaProbe)
negotiation := domain.SCUMCapabilityNegotiation{ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, RunBindingID: active.RunBindingID, PluginID: plugin.ID, PluginVersion: plugin.Version, AdapterVersion: adapterVersion, GameVersion: active.GameVersion, DatabaseIdentity: active.DatabaseIdentity, ProbeExecutorAvailable: probeExecutorAvailable, EvaluatedAt: svc.now()}
evidenceByCapability := svc.latestSCUMCapabilityEvidenceByCapability(instance.ID)
for _, declaration := range plugin.SCUMLiveData.CapabilityGates {
gate := domain.SCUMCapabilityGate{Capability: declaration.Capability, State: domain.SCUMCapabilityGateDisabled, ReasonCode: domain.SCUMSafeErrorProbeMissing, Reason: safeSCUMGateReason(declaration.SafeReason, "current-service evidence is required before this SCUM capability can run")}
if requiredRunCapability := scumRequiredRunCapabilityForDataCapability(declaration.Capability); requiredRunCapability != "" && !containsString(endpoint.Capabilities, requiredRunCapability) {
gate.ReasonCode = domain.SCUMSafeErrorProbeExecutorAbsent
gate.Reason = "bound Run does not expose the generic executor required for this SCUM capability"
negotiation.Gates = append(negotiation.Gates, gate)
continue
}
if bindingErr != nil || binding.Status != domain.RuntimeBindingStatusComplete || binding.PluginVersion != plugin.Version {
gate.ReasonCode = domain.SCUMSafeErrorBindingMismatch
gate.Reason = "active runtime binding is missing, incomplete, or stale for this plugin version"
negotiation.Gates = append(negotiation.Gates, gate)
continue
}
if declaration.Gate != domain.SCUMCapabilityGateEnabled {
gate.ReasonCode = scumReasonCodeForEvidenceStatus(declaration.EvidenceStatus)
negotiation.Gates = append(negotiation.Gates, gate)
continue
}
requirement := domain.SCUMCapabilityRequirement{Capability: declaration.Capability, AdapterVersion: declaration.AdapterVersion, SchemaFingerprint: declaration.RequiredSchemaFingerprint, AssetDigests: domain.CopyStringSlice(declaration.RequiredAssetDigests)}
gate = domain.EvaluateSCUMCapabilityGate(requirement, evidenceByCapability[declaration.Capability], active, probeExecutorAvailable, negotiation.EvaluatedAt)
negotiation.Gates = append(negotiation.Gates, gate)
}
return negotiation, nil
}
func (svc *CoreService) latestSCUMCapabilityEvidenceByCapability(serverInstanceID string) map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence {
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: serverInstanceID})
if err != nil {
return map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence{}
}
evidence := map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence{}
for _, job := range jobs {
for _, candidate := range scumCapabilityEvidenceFromJob(job) {
current, exists := evidence[candidate.Capability]
if !exists || current.ObservedAt.Before(candidate.ObservedAt) {
evidence[candidate.Capability] = candidate
}
}
}
return evidence
}
func scumCapabilityEvidenceFromJob(job domain.Job) []domain.SCUMCapabilityEvidence {
var evidence []domain.SCUMCapabilityEvidence
if result := job.ExecutionResult.SQLiteSchemaProbe; result != nil {
status := domain.SCUMCapabilityEvidenceFailed
if result.Status == domain.SCUMSchemaProbeStatusSucceeded || result.Status == domain.SCUMCapabilityEvidenceCompatible {
status = domain.SCUMCapabilityEvidenceCompatible
} else if result.Status == domain.SCUMCapabilityEvidenceIncompatible {
status = domain.SCUMCapabilityEvidenceIncompatible
}
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: domain.SCUMDataCapabilitySchemaProbe, Status: status, Binding: result.Binding, AdapterVersion: result.Binding.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
}
if result := job.ExecutionResult.SQLiteTemplate; result != nil {
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
}
if result := job.ExecutionResult.RCONTemplate; result != nil {
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
}
if result := job.ExecutionResult.GuardedMutation; result != nil {
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
}
return evidence
}
func scumTerminalEvidenceStatus(status domain.SCUMTerminalResultStatus) domain.SCUMCapabilityEvidenceStatus {
if status == domain.SCUMTerminalResultSucceeded {
return domain.SCUMCapabilityEvidenceCompatible
}
return domain.SCUMCapabilityEvidenceFailed
}
func scumRunCapabilityAvailable(endpoint domain.RunEndpoint, capability domain.SCUMDataCapability) bool {
return containsString(endpoint.Capabilities, scumRequiredRunCapabilityForDataCapability(capability))
}
func scumRequiredRunCapabilityForDataCapability(capability domain.SCUMDataCapability) string {
switch capability {
case domain.SCUMDataCapabilitySchemaProbe:
return domain.JobCapabilityRemoteRunDBSQLiteProbe
case domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead:
return domain.JobCapabilityRemoteRunDBSQLiteQuery
case domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
return domain.JobCapabilityRemoteRunProtectedRCON
case domain.SCUMDataCapabilityProfileXMLWrite:
return domain.JobCapabilityRemoteRunProtectedSQL
default:
return ""
}
}
func scumReasonCodeForEvidenceStatus(status domain.SCUMCapabilityEvidenceStatus) domain.SCUMSafeErrorCode {
switch status {
case domain.SCUMCapabilityEvidenceFailed:
return domain.SCUMSafeErrorProbeFailed
case domain.SCUMCapabilityEvidenceIncompatible:
return domain.SCUMSafeErrorSchemaIncompatible
default:
return domain.SCUMSafeErrorProbeMissing
}
}
func safeSCUMGateReason(value, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return value
}
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
idempotencyKey = strings.TrimSpace(idempotencyKey)
if idempotencyKey == "" {