package service import ( "errors" "strings" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) const ( scumSchemaProbeExecutionKind = "sqlite.schema-probe" scumSQLiteTemplateExecutionKind = "sqlite.template-query" scumRCONTemplateExecutionKind = "rcon.template-command" scumGuardedMutationExecutionKind = "sqlite.guarded-mutation" 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 == "" { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("idempotencyKey is required") } instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } probe := plugin.SCUMLiveData.Probe if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin") } if !scumSchemaProbeHasDataTarget(plugin.RuntimeProfiles, probe.TargetKey) { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe target is not declared as a generated Run data target") } endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run is not online for SCUM schema probe") } if !containsString(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run does not expose the generic SQLite schema-probe executor") } binding, err := svc.runtimeBindingForServer(instance.ID) if errors.Is(err, repo.ErrNotFound) { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("runtime binding is required before SCUM schema probe") } if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData) if adapterVersion == "" { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema-probe adapter version is not declared") } bounds := probe.Bounds if bounds.MaxObjects == 0 { bounds = domain.DefaultSCUMSchemaProbeBounds() } jobID := jobIDFromParts("job-remote-adapter", instance.ID, idempotencyKey) request := domain.SCUMSchemaProbeRequest{ RequestID: jobID, JobID: jobID, Binding: domain.SCUMBindingIdentity{ ServerInstanceID: instance.ID, RunBindingID: binding.ID, RunEndpointID: instance.RunEndpointID, PluginID: plugin.ID, PluginVersion: plugin.Version, AdapterVersion: adapterVersion, DatabaseIdentity: scumSchemaProbeDatabaseIdentity(probe.TargetKey), }, Bounds: bounds, RequestedAt: svc.now(), } if err := validator.ValidateSCUMSchemaProbeRequest(request); err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{ ServerInstanceID: instance.ID, DeclarationKey: probe.TargetKey, TargetKey: probe.TargetKey, Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds), MaxAttempts: 1, IdempotencyKey: idempotencyKey, PlatformScheduled: true, SQLiteSchemaProbe: &request, }) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } return request, result, nil } func scumSchemaProbeDatabaseIdentity(targetKey string) string { trimmed := strings.TrimSpace(targetKey) return strings.TrimPrefix(trimmed, "databases/") } func scumSchemaProbeHasDataTarget(profiles domain.GamePluginRuntimeProfiles, targetKey string) bool { expectedWorkspaceKey := "databases/" + scumSchemaProbeDatabaseIdentity(targetKey) for _, target := range profiles.DataTargets { if target.Key == targetKey && target.Kind == "sqlite.snapshot" && target.WorkspaceKey == expectedWorkspaceKey && target.RefreshPolicy == "on-demand-snapshot" { return true } } return false } func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string { for _, gate := range manifest.CapabilityGates { if gate.Capability == domain.SCUMDataCapabilitySchemaProbe { return strings.TrimSpace(gate.AdapterVersion) } } for _, gate := range manifest.CapabilityGates { if strings.TrimSpace(gate.AdapterVersion) != "" { return strings.TrimSpace(gate.AdapterVersion) } } return "" } func scumSchemaProbeTimeoutSeconds(bounds domain.SCUMSchemaProbeBounds) int { if bounds.TimeoutMS <= 0 { return 1 } seconds := (bounds.TimeoutMS + 999) / 1000 if seconds <= 0 { return 1 } return seconds } func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error { if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil { return err } expected := job.ExecutionInput.SQLiteSchemaProbe if expected == nil { return validationError("SQLite schema probe request is missing from leased job") } if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID { return validationError("SQLite schema probe result does not match leased job identity") } if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) { return validationError("SQLite schema probe result does not match leased binding identity") } return nil } func validateSCUMSQLiteTemplateResultForJob(job domain.Job, result domain.SCUMSQLiteTemplateResult) error { if err := validator.ValidateSCUMSQLiteTemplateResult(result); err != nil { return err } expected := job.ExecutionInput.SQLiteTemplate if expected == nil { return validationError("SQLite template request is missing from leased job") } if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID { return validationError("SQLite template result does not match leased job identity") } if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) { return validationError("SQLite template result does not match leased binding identity") } if result.Capability != expected.Capability || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.SchemaFingerprint != expected.RequiredSchemaFingerprint || result.AssetDigest != expected.AssetDigest || result.ParameterDigest != expected.ParameterDigest { return validationError("SQLite template result does not match leased template, adapter, digest, or parameter identity") } return nil } func validateSCUMTypedRCONTemplateResultForJob(job domain.Job, result domain.SCUMTypedRCONTemplateResult) error { if err := validator.ValidateSCUMTypedRCONTemplateResult(result); err != nil { return err } expected := job.ExecutionInput.RCONTemplate if expected == nil { return validationError("typed RCON template request is missing from leased job") } if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID { return validationError("typed RCON template result does not match leased job identity") } if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) { return validationError("typed RCON template result does not match leased binding identity") } if result.Capability != expected.Capability || result.TransportKey != expected.TransportKey || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.AssetDigest != expected.AssetDigest || result.PayloadDigest != expected.PayloadDigest || result.ConfirmationDigest != expected.ConfirmationDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest { return validationError("typed RCON template result does not match leased template, target, digest, or payload identity") } if expected.RequiredSchemaFingerprint != "" && result.SchemaFingerprint != expected.RequiredSchemaFingerprint { return validationError("typed RCON template result does not match leased schema fingerprint") } return nil } func validateSCUMGuardedMutationResultForJob(job domain.Job, result domain.SCUMGuardedMutationResult) error { if err := validator.ValidateSCUMGuardedMutationResult(result); err != nil { return err } expected := job.ExecutionInput.GuardedMutation if expected == nil { return validationError("guarded mutation request is missing from leased job") } if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID { return validationError("guarded mutation result does not match leased job identity") } if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) { return validationError("guarded mutation result does not match leased binding identity") } if result.Capability != expected.Capability || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.SchemaFingerprint != expected.RequiredSchemaFingerprint || result.AssetDigest != expected.AssetDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest || result.ExpectedRowDigest != expected.ExpectedRowDigest || result.ExpectedValueDigest != expected.ExpectedValueDigest || result.ExpectedXMLDigest != expected.ExpectedXMLDigest || result.PatchDigest != expected.PatchDigest || result.BackupEvidenceDigest != expected.BackupEvidenceDigest || result.OfflineEvidenceDigest != expected.OfflineEvidenceDigest || result.DangerConfirmationDigest != expected.DangerConfirmationDigest || result.ReadbackExpectationDigest != expected.ReadbackExpectationDigest { return validationError("guarded mutation result does not match leased template, target, guard, digest, or readback identity") } return nil } func validateSCUMParsedLogBatchResultForJob(job domain.Job, result domain.SCUMParsedLogBatchResult) error { if err := validator.ValidateSCUMParsedLogBatchResult(result); err != nil { return err } expected := job.ExecutionInput.LogSource if expected == nil { return validationError("parsed log batch request is missing from leased job") } if result.JobID != job.ID { return validationError("parsed log batch result does not match leased job identity") } if result.Binding.ServerInstanceID != job.ServerInstanceID || result.Binding.RunEndpointID != job.RunEndpointID { return validationError("parsed log batch result does not match leased server or Run endpoint") } if result.SourceKey != expected.Key || result.StreamKey != expected.StreamKey { return validationError("parsed log batch result does not match leased log source identity") } for key, value := range map[string]string{ "parserKey": result.ParserKey, "parserVersion": result.ParserVersion, "parserDigest": result.ParserDigest, "adapterVersion": result.AdapterVersion, } { if expectedValue := strings.TrimSpace(job.ExecutionInput.Inputs[key]); expectedValue != "" && value != expectedValue { return validationError("parsed log batch result does not match leased parser identity") } } if job.ExecutionInput.PluginID != "" && result.Binding.PluginID != job.ExecutionInput.PluginID { return validationError("parsed log batch result does not match leased plugin identity") } if job.ExecutionInput.TargetVersion != "" && result.Binding.PluginVersion != job.ExecutionInput.TargetVersion { return validationError("parsed log batch result does not match leased plugin version") } if result.FirstCursor.SourceIdentityDigest != result.LastCursor.SourceIdentityDigest || result.FirstCursor.StreamGeneration != result.LastCursor.StreamGeneration { return validationError("parsed log batch result crosses source identity or generation boundaries") } return nil } func sameSCUMSchemaProbeBinding(a, b domain.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 }