Wire SCUM schema probe durable run path
This commit is contained in:
@@ -11,19 +11,20 @@
|
|||||||
|
|
||||||
- The separately rooted Run task implemented and pushed generic schema-probe support at `git@git.npc0.com:admin343/run.git` commit `6cb6ba3` (`add bounded sqlite schema probe`); its focused protocol/runtime tests and `go test ./...` passed.
|
- The separately rooted Run task implemented and pushed generic schema-probe support at `git@git.npc0.com:admin343/run.git` commit `6cb6ba3` (`add bounded sqlite schema probe`); its focused protocol/runtime tests and `go test ./...` passed.
|
||||||
- The executor advertises `remote.run.db.sqlite.probe`, accepts only package-scoped logical `databases/...` SQLite targets, applies query-only fixed introspection plus binding/job/fence and output bounds, and returns SHA-256-fingerprinted redacted envelopes without SCUM-specific branches or raw database content.
|
- The executor advertises `remote.run.db.sqlite.probe`, accepts only package-scoped logical `databases/...` SQLite targets, applies query-only fixed introspection plus binding/job/fence and output bounds, and returns SHA-256-fingerprinted redacted envelopes without SCUM-specific branches or raw database content.
|
||||||
- The active binding `server-run-server-scum-1785923898033` has not yet been rebuilt and deployed from that commit, and its current capability report does not include `remote.run.db.sqlite.probe`. Keep every probe/read/write gate disabled and do not submit a current-service probe until deployment compatibility is verified.
|
- The active binding `server-run-server-scum-1785923898033` on `枣庄服务器` has been updated and reports `remote.run.db.sqlite.probe`; the endpoint was observed online through the Platform API at `2026-08-12T08:21:27Z` with 28 capabilities including the schema-probe capability.
|
||||||
|
|
||||||
### Server-management diagnostic evidence (2026-08-12)
|
### Server-management diagnostic evidence (2026-08-12)
|
||||||
|
|
||||||
- Per operator direction, the Run install target is `枣庄服务器` (`FyBDIohqPhRx7Cia`); personal server-management MCP inventory and `test_connection` both succeeded for that device.
|
- Per operator direction, the Run install target is `枣庄服务器` (`FyBDIohqPhRx7Cia`); personal server-management MCP inventory and `test_connection` both succeeded for that device.
|
||||||
- Bounded SSH diagnostics checked only process/service/capability metadata and emitted no raw SQL, database content, credentials, SCUM rows, or database reads. The server has a Windows Run process for `server-run-server-scum-1785923898033` and the current SCUM server process is running.
|
- Bounded SSH diagnostics checked only process/service/capability metadata and emitted no raw SQL, database content, credentials, SCUM rows, or database reads. The server has a Windows Run process for `server-run-server-scum-1785923898033`, the current SCUM server process is running, and `https://scum.npc0.com/healthz` returned `200` from the server side; per operator clarification, `scum.npc0.com` is the NAT entry back to the local Platform.
|
||||||
- The installed Windows Run binary on `枣庄服务器` points at `https://scum.npc0.com` and advertises `remote.run.db.sqlite.query`, but its smoke capability report does not include `remote.run.db.sqlite.probe`; because Run credentials are embedded by the platform distribution build, this browser-repository task did not manually extract secrets or rebuild the binary outside the platform-owned distribution path.
|
- Platform durable probe job `job-remote-adapter-server-scum-1785923898033-3442596095552254276` was queued through `POST /api/v1/server-instances/server-scum-1785923898033/scum/schema-probe` with idempotency key `zao-zhuang-schema-probe-20260812-1632`, claimed by the authenticated active Run binding, acknowledged, and executed with target `databases/scum-database`, `MaxAttempts=1`, and a nonzero fencing token.
|
||||||
- Because the active Windows Run deployment still cannot be proven to advertise `remote.run.db.sqlite.probe`, no Platform durable schema probe was submitted and current-service evidence tasks remain blocked.
|
- The probe terminal result was accepted by Platform as a typed `sqlite.schema-probe` result with safe status `failed`, safe error code `target_unavailable`, result digest `sha256:41624741855866ce10b3143edba66c3a6b771029256b9489a30f395885526b61`, and observed time `2026-08-12T08:31:45Z`. This proves the Platform durable job path and active Run probe executor are wired, but it does not prove current SCUM schema compatibility.
|
||||||
|
- The generated Run workspace currently contains lifecycle package assets but no `databases/scum-database` logical database target, so current-service schema capture remains blocked at the package/database-target mapping layer. Keep every database-backed SCUM read/write gate disabled until a subsequent packaged target/snapshot fix produces successful schema metadata.
|
||||||
|
|
||||||
- [x] 2.1 Add a release gate that keeps every database-backed SCUM read and write capability disabled until capability-specific current-service evidence matches a versioned plugin adapter; do not add production SQL or mutation assets before this group is complete.
|
- [x] 2.1 Add a release gate that keeps every database-backed SCUM read and write capability disabled until capability-specific current-service evidence matches a versioned plugin adapter; do not add production SQL or mutation assets before this group is complete.
|
||||||
- [x] 2.2 Define the minimal generic schema-probe request/result contract, safe error model, binding identity, bounds, and redacted evidence DTO needed by Platform and the plugin without embedding SCUM table names or host paths in Run-facing generic code.
|
- [x] 2.2 Define the minimal generic schema-probe request/result contract, safe error model, binding identity, bounds, and redacted evidence DTO needed by Platform and the plugin without embedding SCUM table names or host paths in Run-facing generic code.
|
||||||
- [ ] 2.3 If the active binding lacks the minimal bounded query-only probe executor, create/hand off a separately authorized task rooted in the independent Run repository, wait for its tests/commit/deployment evidence, and record that evidence here; do not edit or vendor Run source from this change.
|
- [x] 2.3 If the active binding lacks the minimal bounded query-only probe executor, create/hand off a separately authorized task rooted in the independent Run repository, wait for its tests/commit/deployment evidence, and record that evidence here; do not edit or vendor Run source from this change.
|
||||||
- [ ] 2.4 Use the personal server-management MCP (`list_devices`, `test_connection`, then `ssh_exec` only when needed) for device inventory, connectivity checks, and bounded diagnostics. Execute the actual schema probe only as a Platform durable job through the active authenticated Run binding; never run SCUM SQL directly over SSH or bypass the Run channel.
|
- [x] 2.4 Use the personal server-management MCP (`list_devices`, `test_connection`, then `ssh_exec` only when needed) for device inventory, connectivity checks, and bounded diagnostics. Execute the actual schema probe only as a Platform durable job through the active authenticated Run binding; never run SCUM SQL directly over SSH or bypass the Run channel.
|
||||||
- [ ] 2.5 Capture `sqlite_master`, applicable read-only PRAGMA metadata, indexes, foreign keys, declared types, cardinalities, and small redacted samples for candidate player, profile/entity, squad/member, vehicle, flag/base, economy, coordinate, and character-profile payload sources.
|
- [ ] 2.5 Capture `sqlite_master`, applicable read-only PRAGMA metadata, indexes, foreign keys, declared types, cardinalities, and small redacted samples for candidate player, profile/entity, squad/member, vehicle, flag/base, economy, coordinate, and character-profile payload sources.
|
||||||
- [ ] 2.6 Verify actual joins and meanings for external player identity, profiles/entities, squad ranks/leaders, flag ownership, vehicle identity, currency units/types, nullable fields, and the real table/column containing character XML; do not assume that `user_profile.template_xml` or any reference-project field exists.
|
- [ ] 2.6 Verify actual joins and meanings for external player identity, profiles/entities, squad ranks/leaders, flag ownership, vehicle identity, currency units/types, nullable fields, and the real table/column containing character XML; do not assume that `user_profile.template_xml` or any reference-project field exists.
|
||||||
- [ ] 2.7 Measure coordinate ranges and update cadence, query latency, lock/busy behavior, snapshot consistency, safe timeout/row limits, and whether a verified companion position source is needed for the advertised realtime-map cadence.
|
- [ ] 2.7 Measure coordinate ranges and update cadence, query latency, lock/busy behavior, snapshot consistency, safe timeout/row limits, and whether a verified companion position source is needed for the advertised realtime-map cadence.
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts/{catalogId}/revisions", h.serverGameGiftCatalogRevisions)
|
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts/{catalogId}/revisions", h.serverGameGiftCatalogRevisions)
|
||||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-gift-grants", h.serverGameGiftGrants)
|
mux.HandleFunc("/api/v1/server-instances/{id}/game-gift-grants", h.serverGameGiftGrants)
|
||||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-gift-grants/{grantId}/approve", h.serverGameGiftGrantApprove)
|
mux.HandleFunc("/api/v1/server-instances/{id}/game-gift-grants/{grantId}/approve", h.serverGameGiftGrantApprove)
|
||||||
|
mux.HandleFunc("/api/v1/server-instances/{id}/scum/schema-probe", h.serverSCUMSchemaProbe)
|
||||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/players", h.serverSCUMPlayers)
|
mux.HandleFunc("/api/v1/server-instances/{id}/scum/players", h.serverSCUMPlayers)
|
||||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/squads", h.serverSCUMSquads)
|
mux.HandleFunc("/api/v1/server-instances/{id}/scum/squads", h.serverSCUMSquads)
|
||||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/squad-members", h.serverSCUMSquadMembers)
|
mux.HandleFunc("/api/v1/server-instances/{id}/scum/squad-members", h.serverSCUMSquadMembers)
|
||||||
|
|||||||
@@ -2,8 +2,28 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"browser.local/platform/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (h *coreHandlers) serverSCUMSchemaProbe(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
writeMethodNotAllowed(w, http.MethodPost)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request, err := decodeJSON[dto.SCUMSchemaProbeDispatchRequest](r)
|
||||||
|
if err != nil {
|
||||||
|
writeDecodeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
probeRequest, queued, err := h.core.RequestSCUMSchemaProbeForSession(bearerToken(r), r.PathValue("id"), request.IdempotencyKey)
|
||||||
|
if err != nil {
|
||||||
|
writeServiceError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusAccepted, dto.SCUMSchemaProbeDispatchFromDomain(probeRequest, queued))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *coreHandlers) serverSCUMPlayers(w http.ResponseWriter, r *http.Request) {
|
func (h *coreHandlers) serverSCUMPlayers(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
writeMethodNotAllowed(w, http.MethodGet)
|
writeMethodNotAllowed(w, http.MethodGet)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"browser.local/platform/domain"
|
"browser.local/platform/domain"
|
||||||
@@ -9,6 +10,72 @@ import (
|
|||||||
"browser.local/platform/service"
|
"browser.local/platform/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestSCUMSchemaProbeEndpointQueuesPlatformScheduledDurableJob(t *testing.T) {
|
||||||
|
store := repo.NewMemoryStore()
|
||||||
|
core := service.NewCoreService(store)
|
||||||
|
plugin, err := core.CreateGamePlugin(domain.GamePlugin{
|
||||||
|
ID: "server.scum",
|
||||||
|
Name: "SCUM",
|
||||||
|
Version: "1.0.0",
|
||||||
|
ServerType: "scum",
|
||||||
|
ManifestRef: "artifact://manifests/server.scum/1.0.0",
|
||||||
|
CreateFormSchemaRef: "artifact://schemas/server.scum/create-form/1.0.0",
|
||||||
|
RequiredRunCapabilities: []string{"process.start", domain.JobCapabilityRemoteRunDBSQLiteProbe},
|
||||||
|
DeclaredPermissions: []string{"server.remote.access"},
|
||||||
|
Permissions: domain.PluginPermissions{Jobs: true, RemoteAccess: true},
|
||||||
|
LifecycleActions: domain.PluginLifecycleActions{Start: "actions/start.json"},
|
||||||
|
RemoteAccess: domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||||
|
RuntimeProfiles: domain.GamePluginRuntimeProfiles{
|
||||||
|
LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}},
|
||||||
|
TransportProfiles: []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}},
|
||||||
|
},
|
||||||
|
SCUMLiveData: domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create plugin: %v", err)
|
||||||
|
}
|
||||||
|
endpoint, err := core.CreateRunEndpoint(domain.RunEndpoint{ID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Capabilities: []string{"process.start", domain.JobCapabilityRemoteRunDBSQLiteProbe}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create run endpoint: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := core.CreateUser(domain.User{ID: "scum-probe-owner", DisplayName: "SCUM Probe Owner", Email: "scum-probe-owner@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||||
|
t.Fatalf("create owner: %v", err)
|
||||||
|
}
|
||||||
|
auth, err := core.LoginUser(domain.UserLogin{Account: "scum-probe-owner@example.test", Password: "secret-password"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("login: %v", err)
|
||||||
|
}
|
||||||
|
instance, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-probe-api", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Probe API", OwnerUserID: "scum-probe-owner", State: domain.ServerInstanceStateRunning, ConfigVersion: 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create server: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := core.UpdateServerRuntimeBindingForSession(auth.SessionID, instance.ID, domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{}}); err != nil {
|
||||||
|
t.Fatalf("create runtime binding: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := NewAuthorizedRouterWithCore(core)
|
||||||
|
unauthorized := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+instance.ID+"/scum/schema-probe", map[string]string{"idempotencyKey": "probe-api-denied"}, "")
|
||||||
|
assertStatus(t, unauthorized, http.StatusUnauthorized)
|
||||||
|
recorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+instance.ID+"/scum/schema-probe", map[string]string{"idempotencyKey": "probe-api-current"}, auth.SessionID)
|
||||||
|
assertStatus(t, recorder, http.StatusAccepted)
|
||||||
|
body := recorder.Body.String()
|
||||||
|
for _, forbidden := range []string{"SCUM.db", "sqlite_master", "SELECT", "C:\\", "secret://", "password"} {
|
||||||
|
if strings.Contains(body, forbidden) {
|
||||||
|
t.Fatalf("schema probe dispatch response leaked forbidden material %q: %s", forbidden, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs, err := core.ListJobsForSession(auth.SessionID, domain.JobFilter{ServerInstanceID: instance.ID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list jobs: %v", err)
|
||||||
|
}
|
||||||
|
if len(jobs) != 1 || jobs[0].Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || jobs[0].TargetKey != "databases/scum-database" || jobs[0].InputRef != "" || len(jobs[0].ExecutionInput.Inputs) != 0 || jobs[0].ExecutionInput.RemoteAdapterKey != "" || jobs[0].ExecutionInput.RemoteAdapterKind != "" {
|
||||||
|
t.Fatalf("schema probe endpoint did not queue fenced durable job: %+v", jobs)
|
||||||
|
}
|
||||||
|
if jobs[0].ExecutionInput.SQLiteSchemaProbe == nil || jobs[0].ExecutionInput.SQLiteSchemaProbe.RequestID != jobs[0].ID || jobs[0].ExecutionInput.SQLiteSchemaProbe.Binding.DatabaseIdentity != "scum-database" || jobs[0].ExecutionInput.SQLiteSchemaProbe.Bounds.MaxResultBytes != 524288 {
|
||||||
|
t.Fatalf("schema probe endpoint did not attach typed probe request: %+v", jobs[0].ExecutionInput.SQLiteSchemaProbe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLegacySCUMEndpointsReturnNotFoundWithoutDispatchingJobs(t *testing.T) {
|
func TestLegacySCUMEndpointsReturnNotFoundWithoutDispatchingJobs(t *testing.T) {
|
||||||
store := repo.NewMemoryStore()
|
store := repo.NewMemoryStore()
|
||||||
core := service.NewCoreService(store)
|
core := service.NewCoreService(store)
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
|
|||||||
assignment.ExecutionInput.LogSources = CopyRuntimeLogSources(assignment.ExecutionInput.LogSources)
|
assignment.ExecutionInput.LogSources = CopyRuntimeLogSources(assignment.ExecutionInput.LogSources)
|
||||||
assignment.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), assignment.ExecutionInput.DLLExtensions...)
|
assignment.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), assignment.ExecutionInput.DLLExtensions...)
|
||||||
assignment.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(assignment.ExecutionInput.SourceRCON)
|
assignment.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(assignment.ExecutionInput.SourceRCON)
|
||||||
|
assignment.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(assignment.ExecutionInput.SQLiteSchemaProbe)
|
||||||
return assignment
|
return assignment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ type RemoteAdapterRequest struct {
|
|||||||
InputRef string
|
InputRef string
|
||||||
Inputs map[string]string
|
Inputs map[string]string
|
||||||
PlatformScheduled bool
|
PlatformScheduled bool
|
||||||
|
SQLiteSchemaProbe *SCUMSchemaProbeRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoteAdapterResult struct {
|
type RemoteAdapterResult struct {
|
||||||
@@ -212,6 +213,7 @@ func CopyRemoteAdapterDeclarations(declarations []RemoteAdapterDeclaration) []Re
|
|||||||
|
|
||||||
func CopyRemoteAdapterRequest(request RemoteAdapterRequest) RemoteAdapterRequest {
|
func CopyRemoteAdapterRequest(request RemoteAdapterRequest) RemoteAdapterRequest {
|
||||||
request.Inputs = CopyStringMap(request.Inputs)
|
request.Inputs = CopyStringMap(request.Inputs)
|
||||||
|
request.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(request.SQLiteSchemaProbe)
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
func CopyRemoteAdapterResult(result RemoteAdapterResult) RemoteAdapterResult { return result }
|
func CopyRemoteAdapterResult(result RemoteAdapterResult) RemoteAdapterResult { return result }
|
||||||
|
|||||||
@@ -1143,6 +1143,7 @@ type JobExecutionInput struct {
|
|||||||
SourceRCON *RuntimeSourceRCONPlan
|
SourceRCON *RuntimeSourceRCONPlan
|
||||||
Deployment *ServerDeploymentDefinition
|
Deployment *ServerDeploymentDefinition
|
||||||
ServerDeploymentPlan *ServerDeploymentPlan
|
ServerDeploymentPlan *ServerDeploymentPlan
|
||||||
|
SQLiteSchemaProbe *SCUMSchemaProbeRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerDeploymentPlan struct {
|
type ServerDeploymentPlan struct {
|
||||||
@@ -2026,6 +2027,7 @@ func CopyJob(job Job) Job {
|
|||||||
job.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...)
|
job.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...)
|
||||||
job.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON)
|
job.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON)
|
||||||
job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)
|
job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)
|
||||||
|
job.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe)
|
||||||
job.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
|
job.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
|
||||||
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
|
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
|
||||||
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
|
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const (
|
|||||||
type SCUMCapabilityEvidenceStatus string
|
type SCUMCapabilityEvidenceStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
SCUMSchemaProbeStatusSucceeded SCUMCapabilityEvidenceStatus = "succeeded"
|
||||||
SCUMCapabilityEvidenceMissing SCUMCapabilityEvidenceStatus = "missing"
|
SCUMCapabilityEvidenceMissing SCUMCapabilityEvidenceStatus = "missing"
|
||||||
SCUMCapabilityEvidenceCompatible SCUMCapabilityEvidenceStatus = "compatible"
|
SCUMCapabilityEvidenceCompatible SCUMCapabilityEvidenceStatus = "compatible"
|
||||||
SCUMCapabilityEvidenceIncompatible SCUMCapabilityEvidenceStatus = "incompatible"
|
SCUMCapabilityEvidenceIncompatible SCUMCapabilityEvidenceStatus = "incompatible"
|
||||||
@@ -48,6 +49,16 @@ const (
|
|||||||
SCUMSafeErrorDigestMismatch SCUMSafeErrorCode = "digest_mismatch"
|
SCUMSafeErrorDigestMismatch SCUMSafeErrorCode = "digest_mismatch"
|
||||||
SCUMSafeErrorEvidenceExpired SCUMSafeErrorCode = "evidence_expired"
|
SCUMSafeErrorEvidenceExpired SCUMSafeErrorCode = "evidence_expired"
|
||||||
SCUMSafeErrorInvalidProbePayload SCUMSafeErrorCode = "invalid_probe_payload"
|
SCUMSafeErrorInvalidProbePayload SCUMSafeErrorCode = "invalid_probe_payload"
|
||||||
|
SCUMSafeErrorInvalidRequest SCUMSafeErrorCode = "invalid_request"
|
||||||
|
SCUMSafeErrorTargetUnavailable SCUMSafeErrorCode = "target_unavailable"
|
||||||
|
SCUMSafeErrorSourceUnavailable SCUMSafeErrorCode = "source_unavailable"
|
||||||
|
SCUMSafeErrorSQLiteOpenFailed SCUMSafeErrorCode = "sqlite_open_failed"
|
||||||
|
SCUMSafeErrorSQLiteReadFailed SCUMSafeErrorCode = "sqlite_read_failed"
|
||||||
|
SCUMSafeErrorDatabaseBusy SCUMSafeErrorCode = "database_busy"
|
||||||
|
SCUMSafeErrorTimeout SCUMSafeErrorCode = "timeout"
|
||||||
|
SCUMSafeErrorCancelled SCUMSafeErrorCode = "cancelled"
|
||||||
|
SCUMSafeErrorSourceChanged SCUMSafeErrorCode = "source_changed"
|
||||||
|
SCUMSafeErrorResultLimitExceeded SCUMSafeErrorCode = "result_limit_exceeded"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SCUMSafeError struct {
|
type SCUMSafeError struct {
|
||||||
@@ -262,6 +273,14 @@ func CopySCUMSchemaProbeResult(value SCUMSchemaProbeResult) SCUMSchemaProbeResul
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CopySCUMSchemaProbeRequestPtr(value *SCUMSchemaProbeRequest) *SCUMSchemaProbeRequest {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copy := *value
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
|
||||||
func CopySCUMSchemaProbeResultPtr(value *SCUMSchemaProbeResult) *SCUMSchemaProbeResult {
|
func CopySCUMSchemaProbeResultPtr(value *SCUMSchemaProbeResult) *SCUMSchemaProbeResult {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+26
-19
@@ -93,24 +93,31 @@ type RunJobResultRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RunJobExecutionInputBody struct {
|
type RunJobExecutionInputBody struct {
|
||||||
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
||||||
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
||||||
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
||||||
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
|
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
|
||||||
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
|
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
|
||||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||||
PluginID string `json:"pluginId,omitempty"`
|
PluginID string `json:"pluginId,omitempty"`
|
||||||
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
|
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
|
||||||
TargetVersion string `json:"targetVersion,omitempty"`
|
TargetVersion string `json:"targetVersion,omitempty"`
|
||||||
Inputs map[string]string `json:"inputs,omitempty"`
|
Inputs map[string]string `json:"inputs,omitempty"`
|
||||||
LogSource *RuntimeLogSourceBody `json:"logSource,omitempty"`
|
LogSource *RuntimeLogSourceBody `json:"logSource,omitempty"`
|
||||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||||
DLLExtensions []RuntimeDLLExtensionPlanBody `json:"dllExtensions,omitempty"`
|
DLLExtensions []RuntimeDLLExtensionPlanBody `json:"dllExtensions,omitempty"`
|
||||||
SourceRCON *RuntimeSourceRCONPlanBody `json:"sourceRcon,omitempty"`
|
SourceRCON *RuntimeSourceRCONPlanBody `json:"sourceRcon,omitempty"`
|
||||||
Deployment *ServerDeploymentExecutionBody `json:"deployment,omitempty"`
|
Deployment *ServerDeploymentExecutionBody `json:"deployment,omitempty"`
|
||||||
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
|
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
|
||||||
|
SQLiteSchemaProbe *RunSQLiteSchemaProbeRequestBody `json:"sqliteSchemaProbe,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunSQLiteSchemaProbeRequestBody struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||||
|
Limits SCUMSchemaProbeBoundsDTO `json:"limits"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerDeploymentPlanBody struct {
|
type ServerDeploymentPlanBody struct {
|
||||||
@@ -664,7 +671,7 @@ func RunJobAssignmentFromDomain(assignment domain.RunJobAssignment) RunJobAssign
|
|||||||
State: assignment.State,
|
State: assignment.State,
|
||||||
Progress: progressReportFromDomain(assignment.Progress),
|
Progress: progressReportFromDomain(assignment.Progress),
|
||||||
ResultRef: assignment.ResultRef,
|
ResultRef: assignment.ResultRef,
|
||||||
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan)},
|
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: runSQLiteSchemaProbeRequestPtrFromDomain(assignment.ExecutionInput.SQLiteSchemaProbe)},
|
||||||
LeaseToken: assignment.LeaseToken,
|
LeaseToken: assignment.LeaseToken,
|
||||||
Attempt: assignment.Attempt,
|
Attempt: assignment.Attempt,
|
||||||
FencingToken: assignment.FencingToken,
|
FencingToken: assignment.FencingToken,
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ func TestRunJobResultRequestParsesSQLiteSchemaProbeEnvelope(t *testing.T) {
|
|||||||
"state":"succeeded",
|
"state":"succeeded",
|
||||||
"progress":{"percent":100,"message":"done"},
|
"progress":{"percent":100,"message":"done"},
|
||||||
"executionResult":{
|
"executionResult":{
|
||||||
"kind":"sqlite.schema.probe",
|
"kind":"sqlite.schema-probe",
|
||||||
"sqliteSchemaProbe":{
|
"sqliteSchemaProbe":{
|
||||||
"requestId":"job-probe",
|
"requestId":"job-probe",
|
||||||
"jobId":"job-probe",
|
"jobId":"job-probe",
|
||||||
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"logical:scum-database"},
|
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
||||||
"status":"compatible",
|
"status":"compatible",
|
||||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||||
"observedAt":"2026-08-12T00:00:00Z",
|
"observedAt":"2026-08-12T00:00:00Z",
|
||||||
@@ -60,7 +60,7 @@ func TestRunJobResultRequestParsesSQLiteSchemaProbeEnvelope(t *testing.T) {
|
|||||||
}
|
}
|
||||||
domainRequest := request.ToDomain()
|
domainRequest := request.ToDomain()
|
||||||
probe := domainRequest.ExecutionResult.SQLiteSchemaProbe
|
probe := domainRequest.ExecutionResult.SQLiteSchemaProbe
|
||||||
if probe == nil || probe.JobID != "job-probe" || probe.Binding.DatabaseIdentity != "logical:scum-database" || probe.ResultDigest != "sha256:"+strings.Repeat("b", 64) {
|
if probe == nil || probe.JobID != "job-probe" || probe.Binding.DatabaseIdentity != "scum-database" || probe.ResultDigest != "sha256:"+strings.Repeat("b", 64) {
|
||||||
t.Fatalf("sqliteSchemaProbe envelope did not parse: %+v", probe)
|
t.Fatalf("sqliteSchemaProbe envelope did not parse: %+v", probe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ type SCUMSchemaProbeRequestDTO struct {
|
|||||||
RequestedAt time.Time `json:"requestedAt"`
|
RequestedAt time.Time `json:"requestedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SCUMSchemaProbeDispatchRequest struct {
|
||||||
|
IdempotencyKey string `json:"idempotencyKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SCUMSchemaProbeDispatchResponse struct {
|
||||||
|
ProbeRequest SCUMSchemaProbeRequestDTO `json:"probeRequest"`
|
||||||
|
QueuedJob RemoteAdapterResponse `json:"queuedJob"`
|
||||||
|
}
|
||||||
|
|
||||||
type SCUMSafeErrorDTO struct {
|
type SCUMSafeErrorDTO struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
@@ -98,6 +107,29 @@ func SCUMSchemaProbeRequestToDomain(value SCUMSchemaProbeRequestDTO) domain.SCUM
|
|||||||
return domain.SCUMSchemaProbeRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Bounds: scumProbeBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
return domain.SCUMSchemaProbeRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Bounds: scumProbeBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SCUMSchemaProbeRequestFromDomain(value domain.SCUMSchemaProbeRequest) SCUMSchemaProbeRequestDTO {
|
||||||
|
return SCUMSchemaProbeRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Bounds: scumProbeBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SCUMSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *SCUMSchemaProbeRequestDTO {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := SCUMSchemaProbeRequestFromDomain(*value)
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSQLiteSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *RunSQLiteSchemaProbeRequestBody {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &RunSQLiteSchemaProbeRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Limits: scumProbeBoundsFromDomain(value.Bounds)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SCUMSchemaProbeDispatchFromDomain(request domain.SCUMSchemaProbeRequest, queued domain.RemoteAdapterResult) SCUMSchemaProbeDispatchResponse {
|
||||||
|
return SCUMSchemaProbeDispatchResponse{ProbeRequest: SCUMSchemaProbeRequestFromDomain(request), QueuedJob: RemoteAdapterFromDomain(queued)}
|
||||||
|
}
|
||||||
|
|
||||||
func SCUMSchemaProbeResultToDomain(value SCUMSchemaProbeResultDTO) domain.SCUMSchemaProbeResult {
|
func SCUMSchemaProbeResultToDomain(value SCUMSchemaProbeResultDTO) domain.SCUMSchemaProbeResult {
|
||||||
objects := make([]domain.SCUMSchemaObjectEvidence, len(value.Objects))
|
objects := make([]domain.SCUMSchemaObjectEvidence, len(value.Objects))
|
||||||
for index, object := range value.Objects {
|
for index, object := range value.Objects {
|
||||||
|
|||||||
+16
-13
@@ -309,18 +309,21 @@ type JobExecutionInput struct {
|
|||||||
Deployment *domain.ServerDeploymentDefinition `json:"deployment,omitempty" db:"deployment"`
|
Deployment *domain.ServerDeploymentDefinition `json:"deployment,omitempty" db:"deployment"`
|
||||||
// ServerDeploymentPlan is the legacy generic deployment-plan payload.
|
// ServerDeploymentPlan is the legacy generic deployment-plan payload.
|
||||||
ServerDeploymentPlan *domain.ServerDeploymentPlan `json:"serverDeploymentPlan,omitempty" db:"server_deployment_plan"`
|
ServerDeploymentPlan *domain.ServerDeploymentPlan `json:"serverDeploymentPlan,omitempty" db:"server_deployment_plan"`
|
||||||
|
// SQLiteSchemaProbe is the typed, bounded diagnostic request delivered only to fenced Run assignments.
|
||||||
|
SQLiteSchemaProbe *domain.SCUMSchemaProbeRequest `json:"sqliteSchemaProbe,omitempty" db:"sqlite_schema_probe"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobExecutionResult struct {
|
type JobExecutionResult struct {
|
||||||
Kind string `json:"kind,omitempty" db:"kind"`
|
Kind string `json:"kind,omitempty" db:"kind"`
|
||||||
ProcessState string `json:"processState,omitempty" db:"process_state"`
|
ProcessState string `json:"processState,omitempty" db:"process_state"`
|
||||||
ExitClassification string `json:"exitClassification,omitempty" db:"exit_classification"`
|
ExitClassification string `json:"exitClassification,omitempty" db:"exit_classification"`
|
||||||
ExitCode int `json:"exitCode,omitempty" db:"exit_code"`
|
ExitCode int `json:"exitCode,omitempty" db:"exit_code"`
|
||||||
Version int `json:"version,omitempty" db:"version"`
|
Version int `json:"version,omitempty" db:"version"`
|
||||||
Checksum string `json:"checksum,omitempty" db:"checksum"`
|
Checksum string `json:"checksum,omitempty" db:"checksum"`
|
||||||
SizeBytes int64 `json:"sizeBytes,omitempty" db:"size_bytes"`
|
SizeBytes int64 `json:"sizeBytes,omitempty" db:"size_bytes"`
|
||||||
AuditSummary string `json:"auditSummary,omitempty" db:"audit_summary"`
|
AuditSummary string `json:"auditSummary,omitempty" db:"audit_summary"`
|
||||||
Content string `json:"content,omitempty" db:"content"`
|
Content string `json:"content,omitempty" db:"content"`
|
||||||
|
SQLiteSchemaProbe *domain.SCUMSchemaProbeResult `json:"sqliteSchemaProbe,omitempty" db:"sqlite_schema_probe"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Job struct {
|
type Job struct {
|
||||||
@@ -913,7 +916,7 @@ func executionInputFromDomain(input domain.JobExecutionInput) JobExecutionInput
|
|||||||
copy := domain.CopyServerDeploymentDefinition(*input.Deployment)
|
copy := domain.CopyServerDeploymentDefinition(*input.Deployment)
|
||||||
deployment = ©
|
deployment = ©
|
||||||
}
|
}
|
||||||
return JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan)}
|
return JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(input.SQLiteSchemaProbe)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
|
func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
|
||||||
@@ -922,15 +925,15 @@ func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
|
|||||||
copy := domain.CopyServerDeploymentDefinition(*input.Deployment)
|
copy := domain.CopyServerDeploymentDefinition(*input.Deployment)
|
||||||
deployment = ©
|
deployment = ©
|
||||||
}
|
}
|
||||||
return domain.JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan)}
|
return domain.JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(input.SQLiteSchemaProbe)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func executionResultFromDomain(result domain.JobExecutionResult) JobExecutionResult {
|
func executionResultFromDomain(result domain.JobExecutionResult) JobExecutionResult {
|
||||||
return JobExecutionResult{Kind: result.Kind, ProcessState: result.ProcessState, ExitClassification: result.ExitClassification, ExitCode: result.ExitCode, Version: result.Version, Checksum: result.Checksum, SizeBytes: result.SizeBytes, AuditSummary: result.AuditSummary, Content: result.Content}
|
return JobExecutionResult{Kind: result.Kind, ProcessState: result.ProcessState, ExitClassification: result.ExitClassification, ExitCode: result.ExitCode, Version: result.Version, Checksum: result.Checksum, SizeBytes: result.SizeBytes, AuditSummary: result.AuditSummary, Content: result.Content, SQLiteSchemaProbe: domain.CopySCUMSchemaProbeResultPtr(result.SQLiteSchemaProbe)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (result JobExecutionResult) ToDomain() domain.JobExecutionResult {
|
func (result JobExecutionResult) ToDomain() domain.JobExecutionResult {
|
||||||
return domain.JobExecutionResult{Kind: result.Kind, ProcessState: result.ProcessState, ExitClassification: result.ExitClassification, ExitCode: result.ExitCode, Version: result.Version, Checksum: result.Checksum, SizeBytes: result.SizeBytes, AuditSummary: result.AuditSummary, Content: result.Content}
|
return domain.JobExecutionResult{Kind: result.Kind, ProcessState: result.ProcessState, ExitClassification: result.ExitClassification, ExitCode: result.ExitCode, Version: result.Version, Checksum: result.Checksum, SizeBytes: result.SizeBytes, AuditSummary: result.AuditSummary, Content: result.Content, SQLiteSchemaProbe: domain.CopySCUMSchemaProbeResultPtr(result.SQLiteSchemaProbe)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (policy JobRetryPolicy) ToDomain() domain.JobRetryPolicy {
|
func (policy JobRetryPolicy) ToDomain() domain.JobRetryPolicy {
|
||||||
|
|||||||
@@ -119,6 +119,12 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
|||||||
PluginID: "game.scum",
|
PluginID: "game.scum",
|
||||||
Prerequisites: []domain.RuntimeServerPrerequisite{{Key: "steamcmd", Kind: "tool"}},
|
Prerequisites: []domain.RuntimeServerPrerequisite{{Key: "steamcmd", Kind: "tool"}},
|
||||||
},
|
},
|
||||||
|
SQLiteSchemaProbe: &domain.SCUMSchemaProbeRequest{
|
||||||
|
RequestID: "probe-1",
|
||||||
|
JobID: "job-1",
|
||||||
|
Binding: domain.SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "runtime-binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", DatabaseIdentity: "scum-database"},
|
||||||
|
Bounds: domain.DefaultSCUMSchemaProbeBounds(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
row := executionInputFromDomain(source)
|
row := executionInputFromDomain(source)
|
||||||
@@ -130,8 +136,13 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
|||||||
if row.Deployment == nil || row.Deployment.CreateInputs["maxPlayers"] != "128" {
|
if row.Deployment == nil || row.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||||
t.Fatalf("expected model deployment inputs to be isolated from source mutation, row=%+v", row.Deployment)
|
t.Fatalf("expected model deployment inputs to be isolated from source mutation, row=%+v", row.Deployment)
|
||||||
}
|
}
|
||||||
|
source.SQLiteSchemaProbe.Bounds.MaxObjects = 1
|
||||||
|
if row.SQLiteSchemaProbe == nil || row.SQLiteSchemaProbe.Bounds.MaxObjects != domain.DefaultSCUMSchemaProbeBounds().MaxObjects {
|
||||||
|
t.Fatalf("expected model schema probe to be isolated from source mutation, row=%+v", row.SQLiteSchemaProbe)
|
||||||
|
}
|
||||||
source.Inputs["playerId"] = "steam-123"
|
source.Inputs["playerId"] = "steam-123"
|
||||||
source.Deployment.CreateInputs["maxPlayers"] = "128"
|
source.Deployment.CreateInputs["maxPlayers"] = "128"
|
||||||
|
source.SQLiteSchemaProbe.Bounds.MaxObjects = domain.DefaultSCUMSchemaProbeBounds().MaxObjects
|
||||||
row.Inputs["playerId"] = "row-mutated"
|
row.Inputs["playerId"] = "row-mutated"
|
||||||
if source.Inputs["playerId"] != "steam-123" {
|
if source.Inputs["playerId"] != "steam-123" {
|
||||||
t.Fatalf("expected source execution inputs to be isolated from model mutation, source=%+v", source.Inputs)
|
t.Fatalf("expected source execution inputs to be isolated from model mutation, source=%+v", source.Inputs)
|
||||||
@@ -140,8 +151,13 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
|||||||
if source.Deployment.CreateInputs["maxPlayers"] != "128" {
|
if source.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||||
t.Fatalf("expected source deployment inputs to be isolated from model mutation, source=%+v", source.Deployment)
|
t.Fatalf("expected source deployment inputs to be isolated from model mutation, source=%+v", source.Deployment)
|
||||||
}
|
}
|
||||||
|
row.SQLiteSchemaProbe.Bounds.MaxObjects = 2
|
||||||
|
if source.SQLiteSchemaProbe.Bounds.MaxObjects != domain.DefaultSCUMSchemaProbeBounds().MaxObjects {
|
||||||
|
t.Fatalf("expected source schema probe to be isolated from model mutation, source=%+v", source.SQLiteSchemaProbe)
|
||||||
|
}
|
||||||
row.Inputs["playerId"] = "steam-123"
|
row.Inputs["playerId"] = "steam-123"
|
||||||
row.Deployment.CreateInputs["maxPlayers"] = "128"
|
row.Deployment.CreateInputs["maxPlayers"] = "128"
|
||||||
|
row.SQLiteSchemaProbe.Bounds.MaxObjects = domain.DefaultSCUMSchemaProbeBounds().MaxObjects
|
||||||
|
|
||||||
roundTrip := row.ToDomain()
|
roundTrip := row.ToDomain()
|
||||||
if !reflect.DeepEqual(roundTrip, source) {
|
if !reflect.DeepEqual(roundTrip, source) {
|
||||||
@@ -149,7 +165,8 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
|||||||
}
|
}
|
||||||
roundTrip.Inputs["playerId"] = "mutated"
|
roundTrip.Inputs["playerId"] = "mutated"
|
||||||
roundTrip.Deployment.CreateInputs["maxPlayers"] = "16"
|
roundTrip.Deployment.CreateInputs["maxPlayers"] = "16"
|
||||||
if source.Inputs["playerId"] != "steam-123" || row.Inputs["playerId"] != "steam-123" || source.Deployment.CreateInputs["maxPlayers"] != "128" || row.Deployment.CreateInputs["maxPlayers"] != "128" {
|
roundTrip.SQLiteSchemaProbe.Bounds.MaxObjects = 3
|
||||||
|
if source.Inputs["playerId"] != "steam-123" || row.Inputs["playerId"] != "steam-123" || source.Deployment.CreateInputs["maxPlayers"] != "128" || row.Deployment.CreateInputs["maxPlayers"] != "128" || source.SQLiteSchemaProbe.Bounds.MaxObjects != domain.DefaultSCUMSchemaProbeBounds().MaxObjects || row.SQLiteSchemaProbe.Bounds.MaxObjects != domain.DefaultSCUMSchemaProbeBounds().MaxObjects {
|
||||||
t.Fatalf("expected execution inputs to round-trip without aliasing, source=%+v row=%+v", source, row)
|
t.Fatalf("expected execution inputs to round-trip without aliasing, source=%+v row=%+v", source, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -637,7 +637,7 @@ func firstEligibleSupportedJob(jobs []domain.Job, capabilities []string, stamp t
|
|||||||
|
|
||||||
func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignment {
|
func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignment {
|
||||||
fencingToken := uint64(0)
|
fencingToken := uint64(0)
|
||||||
if isProtectedRequestCapability(job.Capability) {
|
if isProtectedRequestCapability(job.Capability) || job.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe {
|
||||||
fencingToken = uint64(job.Attempt)
|
fencingToken = uint64(job.Attempt)
|
||||||
}
|
}
|
||||||
return domain.RunJobAssignment{
|
return domain.RunJobAssignment{
|
||||||
@@ -651,7 +651,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
|
|||||||
State: job.State,
|
State: job.State,
|
||||||
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
|
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
|
||||||
ResultRef: job.ResultRef,
|
ResultRef: job.ResultRef,
|
||||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)},
|
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe)},
|
||||||
LeaseToken: leaseToken,
|
LeaseToken: leaseToken,
|
||||||
Attempt: job.Attempt,
|
Attempt: job.Attempt,
|
||||||
FencingToken: fencingToken,
|
FencingToken: fencingToken,
|
||||||
|
|||||||
@@ -86,20 +86,30 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
|
|||||||
return domain.RemoteAdapterResult{}, validationError("remote adapter timeout or retry exceeds declaration")
|
return domain.RemoteAdapterResult{}, validationError("remote adapter timeout or retry exceeds declaration")
|
||||||
}
|
}
|
||||||
inputRef := request.InputRef
|
inputRef := request.InputRef
|
||||||
if inputRef == "" {
|
isSchemaProbe := request.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe && request.PlatformScheduled && request.SQLiteSchemaProbe != nil
|
||||||
|
if inputRef == "" && !isSchemaProbe {
|
||||||
inputRef = fmt.Sprintf("input://remote-adapters/%s/%s", instance.ID, request.DeclarationKey)
|
inputRef = fmt.Sprintf("input://remote-adapters/%s/%s", instance.ID, request.DeclarationKey)
|
||||||
}
|
}
|
||||||
|
targetKey := request.TargetKey
|
||||||
|
executionInput := domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: selected.Key, RemoteAdapterKind: string(selected.Kind), TimeoutSeconds: timeout, Inputs: domain.CopyStringMap(request.Inputs), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(request.SQLiteSchemaProbe)}
|
||||||
|
if isSchemaProbe {
|
||||||
|
targetKey = sqliteSchemaProbeRunTargetKey(request.TargetKey)
|
||||||
|
inputRef = ""
|
||||||
|
executionInput.RemoteAdapterKey = ""
|
||||||
|
executionInput.RemoteAdapterKind = ""
|
||||||
|
executionInput.Inputs = nil
|
||||||
|
}
|
||||||
job := domain.Job{
|
job := domain.Job{
|
||||||
ID: jobIDFromParts("job-remote-adapter", instance.ID, request.IdempotencyKey),
|
ID: jobIDFromParts("job-remote-adapter", instance.ID, request.IdempotencyKey),
|
||||||
ServerInstanceID: instance.ID,
|
ServerInstanceID: instance.ID,
|
||||||
RunEndpointID: instance.RunEndpointID,
|
RunEndpointID: instance.RunEndpointID,
|
||||||
Capability: request.Capability,
|
Capability: request.Capability,
|
||||||
TargetKey: request.TargetKey,
|
TargetKey: targetKey,
|
||||||
InputRef: inputRef,
|
InputRef: inputRef,
|
||||||
IdempotencyKey: request.IdempotencyKey,
|
IdempotencyKey: request.IdempotencyKey,
|
||||||
Progress: domain.JobProgress{Percent: 0, Message: "scoped remote adapter queued"},
|
Progress: domain.JobProgress{Percent: 0, Message: "scoped remote adapter queued"},
|
||||||
RetryPolicy: domain.JobRetryPolicy{MaxAttempts: attempts, InitialBackoffSeconds: 2, MaxBackoffSeconds: 30},
|
RetryPolicy: domain.JobRetryPolicy{MaxAttempts: attempts, InitialBackoffSeconds: 2, MaxBackoffSeconds: 30},
|
||||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: selected.Key, RemoteAdapterKind: string(selected.Kind), TimeoutSeconds: timeout, Inputs: domain.CopyStringMap(request.Inputs)},
|
ExecutionInput: executionInput,
|
||||||
}
|
}
|
||||||
created, err := svc.CreateJob(job)
|
created, err := svc.CreateJob(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,6 +126,14 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
|
|||||||
return domain.RemoteAdapterResult{RequestID: created.ID, ServerInstanceID: instance.ID, DeclarationKey: selected.Key, TargetKey: request.TargetKey, Kind: selected.Kind, Status: string(created.State), Retryable: attempts > 1, Message: "scoped remote adapter queued", ResultRef: "job://" + created.ID, AuditEventID: auditID}, nil
|
return domain.RemoteAdapterResult{RequestID: created.ID, ServerInstanceID: instance.ID, DeclarationKey: selected.Key, TargetKey: request.TargetKey, Kind: selected.Kind, Status: string(created.State), Retryable: attempts > 1, Message: "scoped remote adapter queued", ResultRef: "job://" + created.ID, AuditEventID: auditID}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sqliteSchemaProbeRunTargetKey(targetKey string) string {
|
||||||
|
trimmed := strings.TrimSpace(targetKey)
|
||||||
|
if strings.HasPrefix(trimmed, "databases/") {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
return "databases/" + trimmed
|
||||||
|
}
|
||||||
|
|
||||||
func intersectRemoteCapabilities(profile []string, declared []string, endpoint []string) []string {
|
func intersectRemoteCapabilities(profile []string, declared []string, endpoint []string) []string {
|
||||||
result := make([]string, 0, len(profile))
|
result := make([]string, 0, len(profile))
|
||||||
for _, capability := range profile {
|
for _, capability := range profile {
|
||||||
|
|||||||
@@ -140,11 +140,11 @@ func TestSCUMSchemaProbeDispatchIsPlatformScheduledAndFenced(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get probe job: %v", err)
|
t.Fatalf("get probe job: %v", err)
|
||||||
}
|
}
|
||||||
if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || job.TargetKey != "scum-database" || job.ExecutionInput.RemoteAdapterKind != string(domain.RemoteAdapterDatabase) || job.ExecutionInput.Inputs["databaseIdentity"] != "logical:scum-database" {
|
if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || job.TargetKey != "databases/scum-database" || job.InputRef != "" || job.ExecutionInput.RemoteAdapterKey != "" || job.ExecutionInput.RemoteAdapterKind != "" || len(job.ExecutionInput.Inputs) != 0 {
|
||||||
t.Fatalf("unexpected probe job envelope: %+v", job)
|
t.Fatalf("unexpected probe job envelope: %+v", job)
|
||||||
}
|
}
|
||||||
if job.ExecutionInput.Inputs["jobId"] != job.ID || job.ExecutionInput.Inputs["requestId"] != probeRequest.RequestID || job.ExecutionInput.Inputs["maxResultBytes"] != "524288" {
|
if job.ExecutionInput.SQLiteSchemaProbe == nil || job.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || job.ExecutionInput.SQLiteSchemaProbe.Binding.DatabaseIdentity != "scum-database" || job.ExecutionInput.SQLiteSchemaProbe.Bounds.MaxResultBytes != 524288 {
|
||||||
t.Fatalf("probe inputs are not fenced and bounded: %+v", job.ExecutionInput.Inputs)
|
t.Fatalf("probe job did not include typed SQLite schema probe request: %+v", job.ExecutionInput.SQLiteSchemaProbe)
|
||||||
}
|
}
|
||||||
|
|
||||||
helloRequest := validRunControlHello()
|
helloRequest := validRunControlHello()
|
||||||
@@ -158,8 +158,32 @@ func TestSCUMSchemaProbeDispatchIsPlatformScheduledAndFenced(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("claim probe job: %v", err)
|
t.Fatalf("claim probe job: %v", err)
|
||||||
}
|
}
|
||||||
if !claim.HasJob || claim.Job == nil || claim.Job.ExecutionInput.Inputs["adapterVersion"] != "scum-live-data-v0" || claim.Job.ExecutionInput.Inputs["targetKey"] != "scum-database" {
|
if !claim.HasJob || claim.Job == nil || claim.Job.TargetKey != "databases/scum-database" || claim.Job.InputRef != "" || claim.Job.FencingToken == 0 || claim.Job.MaxAttempts != 1 || claim.Job.ExecutionInput.RemoteAdapterKey != "" || len(claim.Job.ExecutionInput.Inputs) != 0 {
|
||||||
t.Fatalf("claimed probe job lost typed inputs: %+v", claim.Job)
|
t.Fatalf("claimed probe job lost fenced typed envelope: %+v", claim.Job)
|
||||||
|
}
|
||||||
|
if claim.Job.ExecutionInput.SQLiteSchemaProbe == nil || claim.Job.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || claim.Job.ExecutionInput.SQLiteSchemaProbe.Binding.RunBindingID != probeRequest.Binding.RunBindingID {
|
||||||
|
t.Fatalf("claimed probe job lost typed schema probe request: %+v", claim.Job.ExecutionInput.SQLiteSchemaProbe)
|
||||||
|
}
|
||||||
|
assignmentBody := dto.RunJobAssignmentFromDomain(*claim.Job)
|
||||||
|
payload, err := json.Marshal(assignmentBody)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal probe Run assignment: %v", err)
|
||||||
|
}
|
||||||
|
var runWire struct {
|
||||||
|
ExecutionInput struct {
|
||||||
|
SQLiteSchemaProbe struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
JobID string `json:"jobId"`
|
||||||
|
Bounds *dto.SCUMSchemaProbeBoundsDTO `json:"bounds"`
|
||||||
|
Limits dto.SCUMSchemaProbeBoundsDTO `json:"limits"`
|
||||||
|
} `json:"sqliteSchemaProbe"`
|
||||||
|
} `json:"executionInput"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(payload, &runWire); err != nil {
|
||||||
|
t.Fatalf("unmarshal probe Run assignment: %v", err)
|
||||||
|
}
|
||||||
|
if runWire.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || runWire.ExecutionInput.SQLiteSchemaProbe.JobID != "" || runWire.ExecutionInput.SQLiteSchemaProbe.Bounds != nil || runWire.ExecutionInput.SQLiteSchemaProbe.Limits.MaxResultBytes != probeRequest.Bounds.MaxResultBytes {
|
||||||
|
t.Fatalf("probe Run assignment JSON does not match Run contract: %s", payload)
|
||||||
}
|
}
|
||||||
badBinding := probeRequest.Binding
|
badBinding := probeRequest.Binding
|
||||||
badBinding.RunBindingID = "runtime-binding-other"
|
badBinding.RunBindingID = "runtime-binding-other"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"browser.local/platform/domain"
|
"browser.local/platform/domain"
|
||||||
@@ -10,7 +9,7 @@ import (
|
|||||||
"browser.local/platform/validator"
|
"browser.local/platform/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
const scumSchemaProbeExecutionKind = "sqlite.schema.probe"
|
const scumSchemaProbeExecutionKind = "sqlite.schema-probe"
|
||||||
|
|
||||||
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
|
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
|
||||||
idempotencyKey = strings.TrimSpace(idempotencyKey)
|
idempotencyKey = strings.TrimSpace(idempotencyKey)
|
||||||
@@ -65,7 +64,7 @@ func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstan
|
|||||||
PluginID: plugin.ID,
|
PluginID: plugin.ID,
|
||||||
PluginVersion: plugin.Version,
|
PluginVersion: plugin.Version,
|
||||||
AdapterVersion: adapterVersion,
|
AdapterVersion: adapterVersion,
|
||||||
DatabaseIdentity: "logical:" + probe.TargetKey,
|
DatabaseIdentity: scumSchemaProbeDatabaseIdentity(probe.TargetKey),
|
||||||
},
|
},
|
||||||
Bounds: bounds,
|
Bounds: bounds,
|
||||||
RequestedAt: svc.now(),
|
RequestedAt: svc.now(),
|
||||||
@@ -81,9 +80,8 @@ func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstan
|
|||||||
TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds),
|
TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds),
|
||||||
MaxAttempts: 1,
|
MaxAttempts: 1,
|
||||||
IdempotencyKey: idempotencyKey,
|
IdempotencyKey: idempotencyKey,
|
||||||
InputRef: "input://scum-schema-probe/" + instance.ID + "/" + idempotencyKey,
|
|
||||||
Inputs: scumSchemaProbeInputs(request, probe.TargetKey),
|
|
||||||
PlatformScheduled: true,
|
PlatformScheduled: true,
|
||||||
|
SQLiteSchemaProbe: &request,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||||
@@ -91,6 +89,11 @@ func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstan
|
|||||||
return request, result, nil
|
return request, result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scumSchemaProbeDatabaseIdentity(targetKey string) string {
|
||||||
|
trimmed := strings.TrimSpace(targetKey)
|
||||||
|
return strings.TrimPrefix(trimmed, "databases/")
|
||||||
|
}
|
||||||
|
|
||||||
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
|
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
|
||||||
for _, gate := range manifest.CapabilityGates {
|
for _, gate := range manifest.CapabilityGates {
|
||||||
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
|
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
|
||||||
@@ -116,51 +119,18 @@ func scumSchemaProbeTimeoutSeconds(bounds domain.SCUMSchemaProbeBounds) int {
|
|||||||
return seconds
|
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 {
|
func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error {
|
||||||
if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil {
|
if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if result.JobID != job.ID || result.JobID != job.ExecutionInput.Inputs["jobId"] || result.RequestID != job.ExecutionInput.Inputs["requestId"] {
|
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")
|
return validationError("SQLite schema probe result does not match leased job identity")
|
||||||
}
|
}
|
||||||
if !sameSCUMSchemaProbeBinding(result.Binding, scumSchemaProbeBindingFromInputs(job.ExecutionInput.Inputs)) {
|
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||||
return validationError("SQLite schema probe result does not match leased binding identity")
|
return validationError("SQLite schema probe result does not match leased binding identity")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2240,7 +2240,6 @@ func remoteCapabilityRequiresInputRef(capability string) bool {
|
|||||||
domain.JobCapabilityRemoteRsyncWrite,
|
domain.JobCapabilityRemoteRsyncWrite,
|
||||||
domain.JobCapabilityRemoteRunFilesWrite,
|
domain.JobCapabilityRemoteRunFilesWrite,
|
||||||
domain.JobCapabilityRemoteRunDBMySQLQuery,
|
domain.JobCapabilityRemoteRunDBMySQLQuery,
|
||||||
domain.JobCapabilityRemoteRunDBSQLiteProbe,
|
|
||||||
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||||
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL,
|
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL,
|
||||||
domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram:
|
domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
|
|||||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||||
violations = appendRequired(violations, "jobId", result.JobID)
|
violations = appendRequired(violations, "jobId", result.JobID)
|
||||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||||
if !validSCUMCapabilityEvidenceStatus(result.Status) {
|
if !validSCUMSchemaProbeResultStatus(result.Status) {
|
||||||
violations = append(violations, "status is invalid")
|
violations = append(violations, "status is invalid")
|
||||||
}
|
}
|
||||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||||
@@ -273,9 +273,18 @@ func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validSCUMSchemaProbeResultStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
|
||||||
|
switch value {
|
||||||
|
case domain.SCUMSchemaProbeStatusSucceeded, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceFailed:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validSCUMSafeErrorCode(value domain.SCUMSafeErrorCode) bool {
|
func validSCUMSafeErrorCode(value domain.SCUMSafeErrorCode) bool {
|
||||||
switch value {
|
switch value {
|
||||||
case "", domain.SCUMSafeErrorNone, domain.SCUMSafeErrorProbeExecutorAbsent, domain.SCUMSafeErrorProbeMissing, domain.SCUMSafeErrorProbeFailed, domain.SCUMSafeErrorSchemaIncompatible, domain.SCUMSafeErrorBindingMismatch, domain.SCUMSafeErrorAdapterMismatch, domain.SCUMSafeErrorFingerprintMismatch, domain.SCUMSafeErrorDigestMismatch, domain.SCUMSafeErrorEvidenceExpired, domain.SCUMSafeErrorInvalidProbePayload:
|
case "", domain.SCUMSafeErrorNone, domain.SCUMSafeErrorProbeExecutorAbsent, domain.SCUMSafeErrorProbeMissing, domain.SCUMSafeErrorProbeFailed, domain.SCUMSafeErrorSchemaIncompatible, domain.SCUMSafeErrorBindingMismatch, domain.SCUMSafeErrorAdapterMismatch, domain.SCUMSafeErrorFingerprintMismatch, domain.SCUMSafeErrorDigestMismatch, domain.SCUMSafeErrorEvidenceExpired, domain.SCUMSafeErrorInvalidProbePayload, domain.SCUMSafeErrorInvalidRequest, domain.SCUMSafeErrorTargetUnavailable, domain.SCUMSafeErrorSourceUnavailable, domain.SCUMSafeErrorSQLiteOpenFailed, domain.SCUMSafeErrorSQLiteReadFailed, domain.SCUMSafeErrorDatabaseBusy, domain.SCUMSafeErrorTimeout, domain.SCUMSafeErrorCancelled, domain.SCUMSafeErrorSourceChanged, domain.SCUMSafeErrorResultLimitExceeded:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -54,6 +54,21 @@ func TestValidateSCUMSchemaProbeResultRejectsRawSQLAndRows(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateSCUMSchemaProbeResultAcceptsRunTerminalStatuses(t *testing.T) {
|
||||||
|
succeeded := domain.SCUMSchemaProbeResult{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Status: domain.SCUMSchemaProbeStatusSucceeded, SchemaFingerprint: scumProbeHash, ObservedAt: time.Now(), ResultDigest: scumProbeHash, Limits: domain.DefaultSCUMSchemaProbeBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||||
|
if err := ValidateSCUMSchemaProbeResult(succeeded); err != nil {
|
||||||
|
t.Fatalf("expected succeeded Run probe result to validate, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
failed := succeeded
|
||||||
|
failed.Status = domain.SCUMCapabilityEvidenceFailed
|
||||||
|
failed.SchemaFingerprint = ""
|
||||||
|
failed.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorTargetUnavailable, Retryable: false}
|
||||||
|
if err := ValidateSCUMSchemaProbeResult(failed); err != nil {
|
||||||
|
t.Fatalf("expected safe failed Run probe result to validate, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateSCUMCapabilityEvidenceRequiresSafeCurrentServiceIdentity(t *testing.T) {
|
func TestValidateSCUMCapabilityEvidenceRequiresSafeCurrentServiceIdentity(t *testing.T) {
|
||||||
evidence := domain.SCUMCapabilityEvidence{Capability: domain.SCUMDataCapabilityPlayerRead, Status: domain.SCUMCapabilityEvidenceCompatible, Binding: validatorSCUMBinding(), AdapterVersion: "adapter-1", SchemaFingerprint: scumProbeHash, ProbeResultDigest: scumProbeHash, AssetDigests: []string{scumProbeHash}, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
evidence := domain.SCUMCapabilityEvidence{Capability: domain.SCUMDataCapabilityPlayerRead, Status: domain.SCUMCapabilityEvidenceCompatible, Binding: validatorSCUMBinding(), AdapterVersion: "adapter-1", SchemaFingerprint: scumProbeHash, ProbeResultDigest: scumProbeHash, AssetDigests: []string{scumProbeHash}, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||||
if err := ValidateSCUMCapabilityEvidence(evidence); err != nil {
|
if err := ValidateSCUMCapabilityEvidence(evidence); err != nil {
|
||||||
|
|||||||
@@ -780,7 +780,9 @@ const manifest = {
|
|||||||
productionLifecycle: source.productionLifecycle,
|
productionLifecycle: source.productionLifecycle,
|
||||||
remoteAccess: source.remoteAccess,
|
remoteAccess: source.remoteAccess,
|
||||||
runtimeProfiles: localRuntimeProfiles,
|
runtimeProfiles: localRuntimeProfiles,
|
||||||
gameClientBridge: localGameClientBridge
|
gameClientBridge: localGameClientBridge,
|
||||||
|
scumLiveData: source.scumLiveData,
|
||||||
|
mapTrajectories: source.mapTrajectories
|
||||||
};
|
};
|
||||||
fs.writeFileSync(outputPath, JSON.stringify({
|
fs.writeFileSync(outputPath, JSON.stringify({
|
||||||
manifestRef: `artifact://manifests/game.scum/${source.version}`,
|
manifestRef: `artifact://manifests/game.scum/${source.version}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user