Add SCUM capability negotiation gate
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user