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) 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 }