package service import ( "errors" "strconv" "strings" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) const scumSchemaProbeExecutionKind = "sqlite.schema.probe" 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") } 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: "logical:" + 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, InputRef: "input://scum-schema-probe/" + instance.ID + "/" + idempotencyKey, Inputs: scumSchemaProbeInputs(request, probe.TargetKey), PlatformScheduled: true, }) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } return request, result, nil } 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 scumSchemaProbeInputs(request domain.SCUMSchemaProbeRequest, targetKey string) map[string]string { return map[string]string{ "requestId": request.RequestID, "jobId": request.JobID, "targetKey": targetKey, "serverInstanceId": request.Binding.ServerInstanceID, "runBindingId": request.Binding.RunBindingID, "runEndpointId": request.Binding.RunEndpointID, "pluginId": request.Binding.PluginID, "pluginVersion": request.Binding.PluginVersion, "adapterVersion": request.Binding.AdapterVersion, "gameVersion": request.Binding.GameVersion, "databaseIdentity": request.Binding.DatabaseIdentity, "maxObjects": strconv.Itoa(request.Bounds.MaxObjects), "maxColumnsPerObject": strconv.Itoa(request.Bounds.MaxColumnsPerObject), "maxIndexesPerObject": strconv.Itoa(request.Bounds.MaxIndexesPerObject), "maxForeignKeys": strconv.Itoa(request.Bounds.MaxForeignKeys), "maxCardinalityReads": strconv.Itoa(request.Bounds.MaxCardinalityReads), "maxSampleRows": strconv.Itoa(request.Bounds.MaxSampleRows), "timeoutMs": strconv.Itoa(request.Bounds.TimeoutMS), "maxResultBytes": strconv.Itoa(request.Bounds.MaxResultBytes), } } func scumSchemaProbeBindingFromInputs(inputs map[string]string) domain.SCUMBindingIdentity { return domain.SCUMBindingIdentity{ ServerInstanceID: inputs["serverInstanceId"], RunBindingID: inputs["runBindingId"], RunEndpointID: inputs["runEndpointId"], PluginID: inputs["pluginId"], PluginVersion: inputs["pluginVersion"], AdapterVersion: inputs["adapterVersion"], GameVersion: inputs["gameVersion"], DatabaseIdentity: inputs["databaseIdentity"], } } func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error { if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil { return err } if result.JobID != job.ID || result.JobID != job.ExecutionInput.Inputs["jobId"] || result.RequestID != job.ExecutionInput.Inputs["requestId"] { return validationError("SQLite schema probe result does not match leased job identity") } if !sameSCUMSchemaProbeBinding(result.Binding, scumSchemaProbeBindingFromInputs(job.ExecutionInput.Inputs)) { return validationError("SQLite schema probe result does not match leased binding identity") } 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 }