Revert SCUM real data management change
This commit is contained in:
@@ -103,8 +103,6 @@ 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-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}/scum/capabilities", h.serverSCUMCapabilities)
|
||||
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/squads", h.serverSCUMSquads)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/squad-members", h.serverSCUMSquadMembers)
|
||||
|
||||
@@ -17,7 +17,7 @@ All routes use JSON request and response bodies. Collection routes support `GET`
|
||||
| Server runtime distribution | n/a | `GET /api/v1/server-instances/{id}/runtime/actions`, `POST /api/v1/server-instances/{id}/run/generate`, `POST /api/v1/server-instances/{id}/run/download`, `POST /api/v1/server-instances/{id}/run/key/reset`, `POST /api/v1/server-instances/{id}/run/update`, `GET /api/v1/server-instances/{id}/run/update`, `POST /api/v1/server-instances/{id}/client-managers/generate`, `POST /api/v1/server-instances/{id}/client-managers/download`, `POST /api/v1/server-instances/{id}/client-managers/key/reset`, `GET /api/v1/server-instances/{id}/dependencies`, `POST /api/v1/server-instances/{id}/dependencies/check`, `POST /api/v1/server-instances/{id}/dependencies/install` | `ServerRuntimeActionsResponse`, `RunDistributionGenerateRequest`, `RunDistributionResponse`, `RunUpdateRequest`, `RunUpdateJobResponse`/`RunUpdateJobListResponse`, `ClientManagerBuildRequest`, `ClientManagerDistributionResponse`, `ClientManagerDownloadRequest`, `ComponentKeyResetRequest`, `ComponentKeyResponse`, `DependencyCatalogResponse`, `DependencyJobRequest` |
|
||||
| Metrics | `GET /api/v1/metrics/platform`, `GET /api/v1/metrics/server-instances` | n/a | `PlatformResourceUsageResponse`, `ServerMetricsResponse`, `ServerMetricsListResponse` |
|
||||
| File operations | `POST /api/v1/file-operations/dispatch` | n/a | `FileOperationDispatchRequest`, `FileOperationDispatchResponse` |
|
||||
| SCUM local resources | n/a | `GET /api/v1/server-instances/{id}/scum/players`, `GET .../scum/squads`, `GET .../scum/squad-members`, `GET .../scum/vehicles`, `GET .../scum/flags`, `GET .../scum/positions`; removed legacy SCUM execution routes return `404` and dispatch no job | `SCUM*Response`, `ErrorResponse` |
|
||||
| SCUM projections and workflows | n/a | `GET /api/v1/server-instances/{id}/scum/players`, `GET .../scum/squads`, `GET .../scum/squad-members`, `GET .../scum/vehicles`, `GET .../scum/flags`, `GET .../scum/positions`, `GET/POST .../scum/operations`, `POST .../scum/operations/{operationId}/approve`, `GET/POST .../scum/workflows`, `GET .../scum/workflow-steps` | `SCUM*Response`, `SCUMOperationRequestBody`, `SCUMWorkflowCreateRequest`, safe operation/workflow summaries |
|
||||
| Server administrators | `GET /api/v1/server-instances/{id}/administrators/candidates`, `POST /api/v1/server-instances/{id}/administrators` | `DELETE /api/v1/server-instances/{id}/administrators/{userId}` | `ServerMemberRequest`, `ServerMemberResponse`, `ServerMemberListResponse`, `ServerInstanceResponse` |
|
||||
| Run endpoints | `GET /api/v1/run/endpoints`, `POST /api/v1/run/endpoints` | `GET /api/v1/run/endpoints/{id}` | `RunEndpointCreateRequest`, `RunEndpointResponse`, `RunEndpointListResponse` |
|
||||
| Jobs | `GET /api/v1/jobs`, `POST /api/v1/jobs` | `GET /api/v1/jobs/{id}` | `JobCreateRequest`, `JobResponse`, `JobListResponse` |
|
||||
@@ -161,7 +161,7 @@ Server-scoped terminal log streaming (`GET /api/v1/server-instances/{id}/logs/ev
|
||||
|
||||
Runtime distribution and client-manager APIs require the current bearer session, server visibility, plugin-declared permissions, complete runtime bindings only for actions that truly depend on external logical bindings, and platform-builder readiness. Run-side lifecycle commands separately require run endpoint capability support and use plugin-declared lifecycle actions without making manual runtime-profile binding a user prerequisite. Responses and audit summaries expose artifact IDs, job IDs, checksums, key generations, fingerprints, status, and redacted `secret://runtime-keys/.../current` refs only. They do not expose raw run keys, client-manager keys, FTP passwords, database DSNs, RCON passwords, host paths, direct sockets, run endpoint private addresses, build workspace paths, or large inline logs.
|
||||
|
||||
SCUM product APIs expose only safe local resource rows, capability availability, collected timestamps, and redacted status reasons. `GET /api/v1/server-instances/{id}/scum/capabilities` returns the Platform-negotiated gate state for the active Run/plugin/adapter binding without dispatching a job. Removed legacy SCUM execution routes return `404` and dispatch no job. SCUM APIs never expose SCUM.db SQL text, DB paths, DSNs, RCON command text, raw protected request payloads, run sockets, host paths, or credentials.
|
||||
SCUM product APIs expose only safe local projections, typed operation/workflow requests, approval status, confirmation status, blocker reasons, and audit-safe summaries. They never expose SCUM.db SQL text, DB paths, DSNs, RCON command text, raw protected request payloads, run sockets, host paths, or credentials.
|
||||
|
||||
`POST /api/v1/server-instances/workflows/create` requires only the plugin type and server name. A runtime binding may still be maintained internally for advanced logical transports, but browser lifecycle controls must not force operators to choose a runtime profile before start/stop or run-package generation when the plugin deployment/lifecycle declaration is sufficient. Platform builds distributions itself and never needs a registered Run endpoint with `distribution.build` to do so.
|
||||
|
||||
|
||||
+122
-45
@@ -2,47 +2,23 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/dto"
|
||||
)
|
||||
|
||||
func (h *coreHandlers) serverSCUMCapabilities(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
negotiation, err := h.core.NegotiateSCUMCapabilitiesForSession(bearerToken(r), r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMCapabilityNegotiationFromDomain(negotiation))
|
||||
}
|
||||
|
||||
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) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMPlayerLiveStatesForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMPlayerLiveStatesFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMSquads(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -50,7 +26,12 @@ func (h *coreHandlers) serverSCUMSquads(w http.ResponseWriter, r *http.Request)
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMSquadsForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMSquadsFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMSquadMembers(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -58,7 +39,12 @@ func (h *coreHandlers) serverSCUMSquadMembers(w http.ResponseWriter, r *http.Req
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMSquadMembersForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMSquadMembersFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMVehicles(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -66,7 +52,12 @@ func (h *coreHandlers) serverSCUMVehicles(w http.ResponseWriter, r *http.Request
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMVehiclesForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMVehiclesFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMFlags(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -74,7 +65,12 @@ func (h *coreHandlers) serverSCUMFlags(w http.ResponseWriter, r *http.Request) {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMFlagsForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMFlagsFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMPositions(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -82,13 +78,36 @@ func (h *coreHandlers) serverSCUMPositions(w http.ResponseWriter, r *http.Reques
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMCurrentPositionsForSession(bearerToken(r), scumProjectionFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMCurrentPositionsFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMOperations(w http.ResponseWriter, r *http.Request) {
|
||||
serverID := r.PathValue("id")
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
case http.MethodGet:
|
||||
items, err := h.core.ListSCUMOperationsForSession(bearerToken(r), scumOperationFilterFromRequest(r, serverID))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMOperationsFromDomain(items))
|
||||
case http.MethodPost:
|
||||
request, err := decodeJSON[dto.SCUMOperationRequestBody](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
operation, err := h.core.RequestSCUMOperationForSession(bearerToken(r), serverID, dto.SCUMOperationRequestBodyToDomain(request))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, dto.SCUMOperationFromDomain(operation))
|
||||
default:
|
||||
writeMethodNotAllowed(w, "GET, POST")
|
||||
}
|
||||
@@ -99,13 +118,36 @@ func (h *coreHandlers) serverSCUMOperationApprove(w http.ResponseWriter, r *http
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
operation, err := h.core.ApproveSCUMOperationForSession(bearerToken(r), r.PathValue("operationId"))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMOperationFromDomain(operation))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||
serverID := r.PathValue("id")
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
case http.MethodGet:
|
||||
items, err := h.core.ListSCUMWorkflowsForSession(bearerToken(r), scumWorkflowFilterFromRequest(r, serverID))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMWorkflowsFromDomain(items))
|
||||
case http.MethodPost:
|
||||
request, err := decodeJSON[dto.SCUMWorkflowCreateRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
workflow, err := h.core.CreateSCUMWorkflowForSession(bearerToken(r), serverID, dto.SCUMWorkflowCreateRequestToDomain(request))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, dto.SCUMWorkflowFromDomain(workflow))
|
||||
default:
|
||||
writeMethodNotAllowed(w, "GET, POST")
|
||||
}
|
||||
@@ -116,9 +158,44 @@ func (h *coreHandlers) serverSCUMWorkflowSteps(w http.ResponseWriter, r *http.Re
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
writeRemovedSCUMEndpoint(w)
|
||||
items, err := h.core.ListSCUMWorkflowStepsForSession(bearerToken(r), scumWorkflowStepFilterFromRequest(r, r.PathValue("id")))
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMWorkflowStepsFromDomain(items))
|
||||
}
|
||||
|
||||
func writeRemovedSCUMEndpoint(w http.ResponseWriter) {
|
||||
writeAPIError(w, http.StatusNotFound, errorCodeNotFound, "legacy SCUM endpoint removed; use the local SCUM management APIs", nil)
|
||||
func scumProjectionFilterFromRequest(r *http.Request, serverID string) domain.SCUMProjectionFilter {
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMProjectionFilter{ServerInstanceID: serverID, GamePlayerID: query.Get("gamePlayerId"), GamePlayerRecordID: query.Get("gamePlayerRecordId"), UserProfileID: query.Get("userProfileId"), SteamID: query.Get("steamId"), SquadID: query.Get("squadId"), VehicleID: query.Get("vehicleId"), FlagID: query.Get("flagId"), SubjectType: domain.SCUMProjectionSubject(query.Get("subjectType")), QueryKey: query.Get("queryKey"), Freshness: domain.SCUMProjectionFreshness(query.Get("freshness")), Search: query.Get("search"), Limit: boundedQueryLimit(query.Get("limit"), 200)}
|
||||
}
|
||||
|
||||
func scumOperationFilterFromRequest(r *http.Request, serverID string) domain.SCUMOperationRequestFilter {
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMOperationRequestFilter{ServerInstanceID: serverID, TemplateKey: query.Get("templateKey"), PlayerID: query.Get("playerId"), RequesterID: query.Get("requesterId"), Status: domain.SCUMWorkflowStepStatus(query.Get("status")), IdempotencyKey: query.Get("idempotencyKey"), Limit: boundedQueryLimit(query.Get("limit"), 100)}
|
||||
}
|
||||
|
||||
func scumWorkflowFilterFromRequest(r *http.Request, serverID string) domain.SCUMWorkflowInstanceFilter {
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMWorkflowInstanceFilter{ServerInstanceID: serverID, TemplateKey: query.Get("templateKey"), RequestedBy: query.Get("requestedBy"), Status: domain.SCUMWorkflowStatus(query.Get("status")), IdempotencyKey: query.Get("idempotencyKey"), Limit: boundedQueryLimit(query.Get("limit"), 100)}
|
||||
}
|
||||
|
||||
func scumWorkflowStepFilterFromRequest(r *http.Request, serverID string) domain.SCUMWorkflowStepFilter {
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMWorkflowStepFilter{ServerInstanceID: serverID, WorkflowID: query.Get("workflowId"), StepKey: query.Get("stepKey"), Status: domain.SCUMWorkflowStepStatus(query.Get("status")), Limit: boundedQueryLimit(query.Get("limit"), 200)}
|
||||
}
|
||||
|
||||
func boundedQueryLimit(raw string, fallback int) int {
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil || parsed < 1 {
|
||||
return fallback
|
||||
}
|
||||
if parsed > 500 {
|
||||
return 500
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
@@ -1,124 +1,148 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/dto"
|
||||
"browser.local/platform/repo"
|
||||
"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.JobCapabilityRemoteRunFilesRead, 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: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||
{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
},
|
||||
DataTargets: []domain.RuntimeDataTarget{{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}}},
|
||||
},
|
||||
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.JobCapabilityRemoteRunFilesRead, 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)
|
||||
capabilities := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/"+instance.ID+"/scum/capabilities", ``, auth.SessionID)
|
||||
assertStatus(t, capabilities, http.StatusOK)
|
||||
capabilityBody := capabilities.Body.String()
|
||||
if !strings.Contains(capabilityBody, "schema-probe") || !strings.Contains(capabilityBody, "probeExecutorAvailable") {
|
||||
t.Fatalf("capability negotiation response missing SCUM gates: %s", capabilityBody)
|
||||
}
|
||||
for _, forbidden := range []string{"SCUM.db", "sqlite_master", "SELECT", "C:\\", "secret://", "password"} {
|
||||
if strings.Contains(capabilityBody, forbidden) {
|
||||
t.Fatalf("capability negotiation response leaked forbidden material %q: %s", forbidden, capabilityBody)
|
||||
}
|
||||
}
|
||||
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 TestSCUMProjectionOperationAndWorkflowAPIsExposeSafeTypedSurfaces(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-api-owner", DisplayName: "SCUM API Owner", Email: "scum-api-owner@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
if _, err := core.CreateGamePlugin(validGamePluginRequest().ToDomain()); err != nil {
|
||||
plugin := validGamePluginRequest().ToDomain()
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.command", "server.game-client.read")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedRCON)
|
||||
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-management", Kind: "rcon", TargetKey: "scum-management", Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedRCON}}}
|
||||
plugin.GameClientBridge.OperationTemplates = []domain.GameClientBridgeOperationTemplateDeclaration{{Key: "player.fame.set", Title: "Set fame", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "scum-management", TargetKey: "scum-management", PayloadSchemaRef: "schemas/bridge/player-fame-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}}}
|
||||
plugin.GameClientBridge.Retention = domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}
|
||||
if _, err := core.CreateGamePlugin(plugin); err != nil {
|
||||
t.Fatalf("create plugin: %v", err)
|
||||
}
|
||||
if _, err := core.CreateRunEndpoint(validRunEndpointRequest().ToDomain()); err != nil {
|
||||
endpoint := validRunEndpointRequest().ToDomain()
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedRCON)
|
||||
endpoint.LastHeartbeatAt = time.Now().UTC()
|
||||
if _, err := core.CreateRunEndpoint(endpoint); err != nil {
|
||||
t.Fatalf("create endpoint: %v", err)
|
||||
}
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-api", PluginID: "server.scum", RunEndpointID: "run-local", Name: "SCUM API", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateRunning, ConfigVersion: 1}); err != nil {
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-api", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM API", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateRunning, ConfigVersion: 1}); err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
if _, err := core.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-scum-api", PluginID: plugin.ID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:api-profile", ObservedAt: time.Now().UTC(), Rows: []map[string]any{{"gamePlayerId": "steam-api", "displayName": "API Player", "normalBalance": 25, "x": 1, "y": 2, "z": 3}}}); err != nil {
|
||||
t.Fatalf("seed projection: %v", err)
|
||||
}
|
||||
auth, err := core.LoginUser(domain.UserLogin{Account: "scum-api-owner@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatalf("login: %v", err)
|
||||
}
|
||||
router := NewAuthorizedRouterWithCore(core)
|
||||
for _, legacy := range []struct{ method, path string }{
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/players"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/squads"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/squad-members"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/vehicles"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/flags"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/positions"},
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/operations"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/operations"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/operations/op-1/approve"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/workflows"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/workflows"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/workflow-steps?workflowId=workflow-1"},
|
||||
} {
|
||||
assertStatus(t, requestWithAuth(t, router, legacy.method, legacy.path, `{}`, auth.SessionID), http.StatusNotFound)
|
||||
players := getJSONWithAuth[dto.SCUMPlayerLiveStateListResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/players", auth.SessionID)
|
||||
if players.Count != 1 || players.Items[0].GamePlayerID != "steam-api" || players.Items[0].Position.X != 1 {
|
||||
t.Fatalf("unexpected SCUM players response: %+v", players)
|
||||
}
|
||||
jobs, err := core.ListJobsForSession(auth.SessionID, domain.JobFilter{ServerInstanceID: "server-scum-api"})
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("removed SCUM endpoints must not dispatch jobs, got jobs=%+v err=%v", jobs, err)
|
||||
operation := postJSONWithAuth[dto.SCUMOperationResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/operations", dto.SCUMOperationRequestBody{TemplateKey: "player.fame.set", PlayerID: "steam-api", Payload: map[string]any{"fame": 12}, Reason: "api typed op", IdempotencyKey: "api-fame-1"}, auth.SessionID)
|
||||
if operation.Status != string(domain.SCUMWorkflowStepWaiting) || operation.TemplateKey != "player.fame.set" {
|
||||
t.Fatalf("unexpected SCUM operation response: %+v", operation)
|
||||
}
|
||||
operations := getJSONWithAuth[dto.SCUMOperationListResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/operations", auth.SessionID)
|
||||
if operations.Count != 1 || operations.Items[0].ID != operation.ID {
|
||||
t.Fatalf("unexpected SCUM operation list: %+v", operations)
|
||||
}
|
||||
workflow := postJSONWithAuth[dto.SCUMWorkflowResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/workflows", dto.SCUMWorkflowCreateRequest{TemplateKey: "scum.world-refresh", IdempotencyKey: "api-world-1"}, auth.SessionID)
|
||||
if workflow.Status != string(domain.SCUMWorkflowQueued) || workflow.TemplateKey != "scum.world-refresh" {
|
||||
t.Fatalf("unexpected SCUM workflow response: %+v", workflow)
|
||||
}
|
||||
steps := getJSONWithAuth[dto.SCUMWorkflowStepListResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/workflow-steps?workflowId="+workflow.ID, auth.SessionID)
|
||||
if steps.Count == 0 {
|
||||
t.Fatalf("expected workflow steps: %+v", steps)
|
||||
}
|
||||
body, err := json.Marshal([]any{players, operation, operations, workflow, steps})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal responses: %v", err)
|
||||
}
|
||||
for _, forbidden := range []string{"#SetFamePoints", "requestText", "SELECT ", "UPDATE ", "SCUM.db", "password", "run token", "hostPath"} {
|
||||
if strings.Contains(strings.ToUpper(string(body)), strings.ToUpper(forbidden)) {
|
||||
t.Fatalf("SCUM safe API leaked %q: %s", forbidden, body)
|
||||
}
|
||||
}
|
||||
for _, legacy := range []struct{ method, path string }{
|
||||
{http.MethodPost, "/api/v1/server-instances/server-scum-api/rcon/commands"},
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/logs/live"},
|
||||
{http.MethodPost, "/api/v1/server-instances/server-scum-api/logs/backfill"},
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/files/read-snapshot?key=scum-server-log"},
|
||||
{http.MethodGet, "/api/v1/server-instances/server-scum-api/config"},
|
||||
{http.MethodPost, "/api/v1/server-instances/server-scum-api/config/diff"},
|
||||
{http.MethodPost, "/api/v1/server-instances/server-scum-api/config/approve"},
|
||||
} {
|
||||
recorder := requestWithAuth(t, router, legacy.method, legacy.path, `{}`, auth.SessionID)
|
||||
assertStatus(t, recorder, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMAPIsEnforceServerAuthorization(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-api-owner", DisplayName: "SCUM API Owner", Email: "scum-api-owner-authz@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-api-other", DisplayName: "SCUM API Other", Email: "scum-api-other-authz@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateGamePlugin(validGamePluginRequest().ToDomain()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateRunEndpoint(validRunEndpointRequest().ToDomain()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-authz", PluginID: "server.scum", RunEndpointID: "run-local", Name: "SCUM Authz", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateRunning}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
auth, err := core.LoginUser(domain.UserLogin{Account: "scum-api-other-authz@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
router := NewAuthorizedRouterWithCore(core)
|
||||
assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/server-scum-authz/scum/players", "", auth.SessionID), http.StatusForbidden, errorCodeForbidden)
|
||||
}
|
||||
|
||||
func TestSCUMAPIsRequirePlatformAdminForDBMutationApproval(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
if _, err := core.CreateUser(domain.User{ID: "scum-api-owner", DisplayName: "SCUM API Owner", Email: "scum-api-owner-mutation@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin := validGamePluginRequest().ToDomain()
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.read", "server.game-client.maintenance")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedSQL}}}
|
||||
plugin.GameClientBridge.QueryTemplates = []domain.GameClientBridgeQueryTemplateDeclaration{{Key: "scum.player.profile", Title: "Read player profile", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "scum-database", TargetKey: "scum-database", ParameterSchemaRef: "schemas/bridge/queries/scum-player-profile.parameters.schema.json", ResultSchemaRef: "schemas/bridge/queries/scum-player-profile.result.schema.json", MaxRows: 10, TimeoutSeconds: 15}}
|
||||
plugin.GameClientBridge.OperationTemplates = []domain.GameClientBridgeOperationTemplateDeclaration{{Key: "player.attribute.855.set", Title: "Set attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindSQLiteMutation, TransportKey: "scum-database", TargetKey: "scum-database", PayloadSchemaRef: "schemas/bridge/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1, Mutation: domain.GameClientBridgeOperationMutationDeclaration{FieldKey: "855", TableKey: "prisoner", IdentityKey: "user_profile_id", ValueKey: "value", ConfirmationQueryKey: "scum.player.profile", AllowedValueType: "integer", MinValue: 0, MaxValue: 100000}, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresMaintenanceWindow: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true}}}
|
||||
plugin.GameClientBridge.Retention = domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}
|
||||
if _, err := core.CreateGamePlugin(plugin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
endpoint := validRunEndpointRequest().ToDomain()
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
if _, err := core.CreateRunEndpoint(endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-mutation-authz", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Mutation Authz", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateStopped}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
auth, err := core.LoginUser(domain.UserLogin{Account: "scum-api-owner-mutation@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
router := NewAuthorizedRouterWithCore(core)
|
||||
operation := postJSONWithAuth[dto.SCUMOperationResponse](t, router, "/api/v1/server-instances/server-scum-mutation-authz/scum/operations", dto.SCUMOperationRequestBody{TemplateKey: "player.attribute.855.set", PlayerID: "steam-api", Payload: map[string]any{"fieldKey": "855", "before": 10, "after": 12, "safetyWindow": "maintenance-2026-08-10", "backupRef": "backup://scum/1"}, Reason: "api typed db op", IdempotencyKey: "api-855-1"}, auth.SessionID)
|
||||
assertErrorResponse(t, requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/server-scum-mutation-authz/scum/operations/"+operation.ID+"/approve", map[string]string{}, auth.SessionID), http.StatusForbidden, errorCodeForbidden)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ type RunAutonomousLifecyclePlan struct {
|
||||
InstallPlans []RunAutonomousInstallPlan `json:"installPlans,omitempty"`
|
||||
LogSources []RunAutonomousLogSource `json:"logSources,omitempty"`
|
||||
DLLExtensions []RunAutonomousDLLExtension `json:"dllExtensions,omitempty"`
|
||||
DataTargets []RunAutonomousDataTarget `json:"dataTargets,omitempty"`
|
||||
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
|
||||
Deployment *RunAutonomousDeployment `json:"deployment,omitempty"`
|
||||
}
|
||||
@@ -209,18 +208,6 @@ type RunAutonomousDLLExtension struct {
|
||||
RCONPort int `json:"rconPort,omitempty"`
|
||||
}
|
||||
|
||||
type RunAutonomousDataTarget struct {
|
||||
Key string `json:"key"`
|
||||
Kind string `json:"kind"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
SourceRootKey string `json:"sourceRootKey"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
WorkspaceKey string `json:"workspaceKey"`
|
||||
RefreshPolicy string `json:"refreshPolicy"`
|
||||
MaxBytes int64 `json:"maxBytes,omitempty"`
|
||||
Platforms []string `json:"platforms,omitempty"`
|
||||
}
|
||||
|
||||
type RunAutonomousDeployment struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Mode ServerDeploymentMode `json:"mode"`
|
||||
@@ -435,10 +422,6 @@ func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
|
||||
assignment.ExecutionInput.LogSources = CopyRuntimeLogSources(assignment.ExecutionInput.LogSources)
|
||||
assignment.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), assignment.ExecutionInput.DLLExtensions...)
|
||||
assignment.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(assignment.ExecutionInput.SourceRCON)
|
||||
assignment.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(assignment.ExecutionInput.SQLiteSchemaProbe)
|
||||
assignment.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(assignment.ExecutionInput.SQLiteTemplate)
|
||||
assignment.ExecutionInput.RCONTemplate = CopySCUMTypedRCONTemplateRequestPtr(assignment.ExecutionInput.RCONTemplate)
|
||||
assignment.ExecutionInput.GuardedMutation = CopySCUMGuardedMutationRequestPtr(assignment.ExecutionInput.GuardedMutation)
|
||||
return assignment
|
||||
}
|
||||
|
||||
@@ -513,10 +496,6 @@ func CopyRunAutonomousLifecyclePlanPtr(plan *RunAutonomousLifecyclePlan) *RunAut
|
||||
}
|
||||
copy.LogSources = append([]RunAutonomousLogSource(nil), plan.LogSources...)
|
||||
copy.DLLExtensions = append([]RunAutonomousDLLExtension(nil), plan.DLLExtensions...)
|
||||
copy.DataTargets = append([]RunAutonomousDataTarget(nil), plan.DataTargets...)
|
||||
for i := range copy.DataTargets {
|
||||
copy.DataTargets[i].Platforms = CopyStringSlice(plan.DataTargets[i].Platforms)
|
||||
}
|
||||
copy.RuntimeBindings = CopyStringMap(plan.RuntimeBindings)
|
||||
if plan.Deployment != nil {
|
||||
deployment := *plan.Deployment
|
||||
|
||||
@@ -87,17 +87,15 @@ type RemoteAdapterDeclaration struct {
|
||||
}
|
||||
|
||||
type RemoteAdapterRequest struct {
|
||||
ServerInstanceID string
|
||||
DeclarationKey string
|
||||
TargetKey string
|
||||
Capability string
|
||||
TimeoutSeconds int
|
||||
MaxAttempts int
|
||||
IdempotencyKey string
|
||||
InputRef string
|
||||
Inputs map[string]string
|
||||
PlatformScheduled bool
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeRequest
|
||||
ServerInstanceID string
|
||||
DeclarationKey string
|
||||
TargetKey string
|
||||
Capability string
|
||||
TimeoutSeconds int
|
||||
MaxAttempts int
|
||||
IdempotencyKey string
|
||||
InputRef string
|
||||
Inputs map[string]string
|
||||
}
|
||||
|
||||
type RemoteAdapterResult struct {
|
||||
@@ -213,7 +211,6 @@ func CopyRemoteAdapterDeclarations(declarations []RemoteAdapterDeclaration) []Re
|
||||
|
||||
func CopyRemoteAdapterRequest(request RemoteAdapterRequest) RemoteAdapterRequest {
|
||||
request.Inputs = CopyStringMap(request.Inputs)
|
||||
request.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(request.SQLiteSchemaProbe)
|
||||
return request
|
||||
}
|
||||
func CopyRemoteAdapterResult(result RemoteAdapterResult) RemoteAdapterResult { return result }
|
||||
|
||||
@@ -479,18 +479,6 @@ type RuntimeTransportProfile struct {
|
||||
Capabilities []string
|
||||
}
|
||||
|
||||
type RuntimeDataTarget struct {
|
||||
Key string
|
||||
Kind string
|
||||
TransportKey string
|
||||
SourceRootKey string
|
||||
SourcePath string
|
||||
WorkspaceKey string
|
||||
RefreshPolicy string
|
||||
MaxBytes int64
|
||||
Platforms []string
|
||||
}
|
||||
|
||||
type RuntimeClientManagerProfile struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
@@ -621,7 +609,6 @@ type GamePluginRuntimeProfiles struct {
|
||||
LogSources []RuntimeLogSource
|
||||
LogEvents []RuntimeLogEvent
|
||||
TransportProfiles []RuntimeTransportProfile
|
||||
DataTargets []RuntimeDataTarget
|
||||
ClientManagers []RuntimeClientManagerProfile
|
||||
DLLExtensions []RuntimeDLLExtensionProfile
|
||||
}
|
||||
@@ -646,7 +633,6 @@ type GamePluginManifest struct {
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
RuntimeProfiles GamePluginRuntimeProfiles
|
||||
GameClientBridge GameClientBridgeManifest
|
||||
SCUMLiveData SCUMLiveDataManifest
|
||||
MapTrajectories *GameMapTrajectoryDeclaration
|
||||
}
|
||||
|
||||
@@ -687,7 +673,6 @@ type GamePlugin struct {
|
||||
RemoteAccess GamePluginRemoteAccess
|
||||
RuntimeProfiles GamePluginRuntimeProfiles
|
||||
GameClientBridge GameClientBridgeManifest
|
||||
SCUMLiveData SCUMLiveDataManifest
|
||||
MapTrajectories *GameMapTrajectoryDeclaration
|
||||
ValidationViolations []string
|
||||
Status GamePluginStatus
|
||||
@@ -1093,7 +1078,6 @@ const (
|
||||
JobCapabilityRemoteRunProcessStart = "remote.run.process.start"
|
||||
JobCapabilityRemoteRunProcessStop = "remote.run.process.stop"
|
||||
JobCapabilityRemoteRunDBMySQLQuery = "remote.run.db.mysql.query"
|
||||
JobCapabilityRemoteRunDBSQLiteProbe = "remote.run.db.sqlite.probe"
|
||||
JobCapabilityRemoteRunDBSQLiteQuery = "remote.run.db.sqlite.query"
|
||||
JobCapabilityRemoteRunLogsTransfer = "remote.run.logs.transfer"
|
||||
JobCapabilityRemoteRunRCONCommand = "remote.run.rcon.command"
|
||||
@@ -1156,10 +1140,6 @@ type JobExecutionInput struct {
|
||||
SourceRCON *RuntimeSourceRCONPlan
|
||||
Deployment *ServerDeploymentDefinition
|
||||
ServerDeploymentPlan *ServerDeploymentPlan
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeRequest
|
||||
SQLiteTemplate *SCUMSQLiteTemplateRequest
|
||||
RCONTemplate *SCUMTypedRCONTemplateRequest
|
||||
GuardedMutation *SCUMGuardedMutationRequest
|
||||
}
|
||||
|
||||
type ServerDeploymentPlan struct {
|
||||
@@ -1202,11 +1182,6 @@ type JobExecutionResult struct {
|
||||
SizeBytes int64
|
||||
AuditSummary string
|
||||
Content string
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResult
|
||||
SQLiteTemplate *SCUMSQLiteTemplateResult
|
||||
RCONTemplate *SCUMTypedRCONTemplateResult
|
||||
GuardedMutation *SCUMGuardedMutationResult
|
||||
ParsedLogBatch *SCUMParsedLogBatchResult
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidence
|
||||
DeploymentReceipt *ServerDeploymentExecutionReceipt
|
||||
}
|
||||
@@ -1719,7 +1694,6 @@ func CopyGamePlugin(plugin GamePlugin) GamePlugin {
|
||||
plugin.RemoteAccess = CopyGamePluginRemoteAccess(plugin.RemoteAccess)
|
||||
plugin.RuntimeProfiles = CopyGamePluginRuntimeProfiles(plugin.RuntimeProfiles)
|
||||
plugin.GameClientBridge = CopyGameClientBridgeManifest(plugin.GameClientBridge)
|
||||
plugin.SCUMLiveData = CopySCUMLiveDataManifest(plugin.SCUMLiveData)
|
||||
if plugin.MapTrajectories != nil {
|
||||
value := CopyGameMapTrajectoryDeclaration(*plugin.MapTrajectories)
|
||||
plugin.MapTrajectories = &value
|
||||
@@ -1790,7 +1764,6 @@ func CopyGamePluginManifest(manifest GamePluginManifest) GamePluginManifest {
|
||||
manifest.RemoteAccess = CopyGamePluginRemoteAccess(manifest.RemoteAccess)
|
||||
manifest.RuntimeProfiles = CopyGamePluginRuntimeProfiles(manifest.RuntimeProfiles)
|
||||
manifest.GameClientBridge = CopyGameClientBridgeManifest(manifest.GameClientBridge)
|
||||
manifest.SCUMLiveData = CopySCUMLiveDataManifest(manifest.SCUMLiveData)
|
||||
if manifest.MapTrajectories != nil {
|
||||
value := CopyGameMapTrajectoryDeclaration(*manifest.MapTrajectories)
|
||||
manifest.MapTrajectories = &value
|
||||
@@ -1853,10 +1826,6 @@ func CopyGamePluginRuntimeProfiles(profiles GamePluginRuntimeProfiles) GamePlugi
|
||||
for i := range profiles.TransportProfiles {
|
||||
profiles.TransportProfiles[i].Capabilities = CopyStringSlice(profiles.TransportProfiles[i].Capabilities)
|
||||
}
|
||||
profiles.DataTargets = append([]RuntimeDataTarget(nil), profiles.DataTargets...)
|
||||
for i := range profiles.DataTargets {
|
||||
profiles.DataTargets[i].Platforms = CopyStringSlice(profiles.DataTargets[i].Platforms)
|
||||
}
|
||||
profiles.ClientManagers = append([]RuntimeClientManagerProfile(nil), profiles.ClientManagers...)
|
||||
for i := range profiles.ClientManagers {
|
||||
profiles.ClientManagers[i].SupportedTargets = append([]RuntimeTarget(nil), profiles.ClientManagers[i].SupportedTargets...)
|
||||
@@ -2051,15 +2020,6 @@ func CopyJob(job Job) Job {
|
||||
job.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...)
|
||||
job.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON)
|
||||
job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)
|
||||
job.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe)
|
||||
job.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate)
|
||||
job.ExecutionInput.RCONTemplate = CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate)
|
||||
job.ExecutionInput.GuardedMutation = CopySCUMGuardedMutationRequestPtr(job.ExecutionInput.GuardedMutation)
|
||||
job.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
|
||||
job.ExecutionResult.SQLiteTemplate = CopySCUMSQLiteTemplateResultPtr(job.ExecutionResult.SQLiteTemplate)
|
||||
job.ExecutionResult.RCONTemplate = CopySCUMTypedRCONTemplateResultPtr(job.ExecutionResult.RCONTemplate)
|
||||
job.ExecutionResult.GuardedMutation = CopySCUMGuardedMutationResultPtr(job.ExecutionResult.GuardedMutation)
|
||||
job.ExecutionResult.ParsedLogBatch = CopySCUMParsedLogBatchResultPtr(job.ExecutionResult.ParsedLogBatch)
|
||||
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
|
||||
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
|
||||
if job.ExecutionInput.Deployment != nil {
|
||||
|
||||
@@ -1,712 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type SCUMDataCapability string
|
||||
|
||||
const (
|
||||
SCUMDataCapabilitySchemaProbe SCUMDataCapability = "schema-probe"
|
||||
SCUMDataCapabilityPlayerRead SCUMDataCapability = "players.read"
|
||||
SCUMDataCapabilityPlayerDetailRead SCUMDataCapability = "player-details.read"
|
||||
SCUMDataCapabilitySquadRead SCUMDataCapability = "squads.read"
|
||||
SCUMDataCapabilitySquadMemberRead SCUMDataCapability = "squad-members.read"
|
||||
SCUMDataCapabilityVehicleRead SCUMDataCapability = "vehicles.read"
|
||||
SCUMDataCapabilityFlagRead SCUMDataCapability = "flags.read"
|
||||
SCUMDataCapabilityPositionRead SCUMDataCapability = "positions.read"
|
||||
SCUMDataCapabilityProfileXMLWrite SCUMDataCapability = "profile-xml.write"
|
||||
SCUMDataCapabilityEconomyCommand SCUMDataCapability = "economy-command.write"
|
||||
SCUMDataCapabilityGiftCommand SCUMDataCapability = "gift-command.write"
|
||||
)
|
||||
|
||||
type SCUMCapabilityEvidenceStatus string
|
||||
|
||||
const (
|
||||
SCUMSchemaProbeStatusSucceeded SCUMCapabilityEvidenceStatus = "succeeded"
|
||||
SCUMCapabilityEvidenceMissing SCUMCapabilityEvidenceStatus = "missing"
|
||||
SCUMCapabilityEvidenceCompatible SCUMCapabilityEvidenceStatus = "compatible"
|
||||
SCUMCapabilityEvidenceIncompatible SCUMCapabilityEvidenceStatus = "incompatible"
|
||||
SCUMCapabilityEvidenceFailed SCUMCapabilityEvidenceStatus = "failed"
|
||||
)
|
||||
|
||||
type SCUMCapabilityGateState string
|
||||
|
||||
const (
|
||||
SCUMCapabilityGateEnabled SCUMCapabilityGateState = "enabled"
|
||||
SCUMCapabilityGateDisabled SCUMCapabilityGateState = "disabled"
|
||||
)
|
||||
|
||||
type SCUMSafeErrorCode string
|
||||
|
||||
const (
|
||||
SCUMSafeErrorNone SCUMSafeErrorCode = "none"
|
||||
SCUMSafeErrorProbeExecutorAbsent SCUMSafeErrorCode = "probe_executor_absent"
|
||||
SCUMSafeErrorProbeMissing SCUMSafeErrorCode = "probe_missing"
|
||||
SCUMSafeErrorProbeFailed SCUMSafeErrorCode = "probe_failed"
|
||||
SCUMSafeErrorSchemaIncompatible SCUMSafeErrorCode = "schema_incompatible"
|
||||
SCUMSafeErrorBindingMismatch SCUMSafeErrorCode = "binding_mismatch"
|
||||
SCUMSafeErrorAdapterMismatch SCUMSafeErrorCode = "adapter_mismatch"
|
||||
SCUMSafeErrorFingerprintMismatch SCUMSafeErrorCode = "fingerprint_mismatch"
|
||||
SCUMSafeErrorDigestMismatch SCUMSafeErrorCode = "digest_mismatch"
|
||||
SCUMSafeErrorEvidenceExpired SCUMSafeErrorCode = "evidence_expired"
|
||||
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"
|
||||
SCUMSafeErrorTemplateMissing SCUMSafeErrorCode = "template_missing"
|
||||
SCUMSafeErrorTemplateMismatch SCUMSafeErrorCode = "template_digest_mismatch"
|
||||
SCUMSafeErrorParameterInvalid SCUMSafeErrorCode = "parameter_schema_invalid"
|
||||
SCUMSafeErrorRowLimitExceeded SCUMSafeErrorCode = "row_limit_exceeded"
|
||||
SCUMSafeErrorResultSchemaInvalid SCUMSafeErrorCode = "result_schema_invalid"
|
||||
SCUMSafeErrorMutationGuardMismatch SCUMSafeErrorCode = "mutation_guard_mismatch"
|
||||
SCUMSafeErrorMutationBackupUnavailable SCUMSafeErrorCode = "mutation_backup_unavailable"
|
||||
SCUMSafeErrorMutationOfflineRequired SCUMSafeErrorCode = "mutation_offline_required"
|
||||
SCUMSafeErrorMutationConfirmationMissing SCUMSafeErrorCode = "mutation_confirmation_missing"
|
||||
SCUMSafeErrorMutationPatchInvalid SCUMSafeErrorCode = "mutation_patch_invalid"
|
||||
SCUMSafeErrorAffectedRowsMismatch SCUMSafeErrorCode = "affected_rows_mismatch"
|
||||
SCUMSafeErrorReadbackMismatch SCUMSafeErrorCode = "readback_mismatch"
|
||||
SCUMSafeErrorRollbackFailed SCUMSafeErrorCode = "rollback_failed"
|
||||
)
|
||||
|
||||
type SCUMTerminalResultStatus string
|
||||
|
||||
const (
|
||||
SCUMTerminalResultSucceeded SCUMTerminalResultStatus = "succeeded"
|
||||
SCUMTerminalResultFailed SCUMTerminalResultStatus = "failed"
|
||||
SCUMTerminalResultCancelled SCUMTerminalResultStatus = "cancelled"
|
||||
)
|
||||
|
||||
type SCUMRCONConfirmationStatus string
|
||||
|
||||
const (
|
||||
SCUMRCONConfirmationConfirmed SCUMRCONConfirmationStatus = "confirmed"
|
||||
SCUMRCONConfirmationFailed SCUMRCONConfirmationStatus = "failed"
|
||||
SCUMRCONConfirmationUnknown SCUMRCONConfirmationStatus = "unknown"
|
||||
)
|
||||
|
||||
type SCUMMutationReadbackStatus string
|
||||
|
||||
const (
|
||||
SCUMMutationReadbackConfirmed SCUMMutationReadbackStatus = "confirmed"
|
||||
SCUMMutationReadbackFailed SCUMMutationReadbackStatus = "failed"
|
||||
SCUMMutationReadbackConflict SCUMMutationReadbackStatus = "conflict"
|
||||
SCUMMutationReadbackUnknown SCUMMutationReadbackStatus = "unknown"
|
||||
)
|
||||
|
||||
type SCUMSafeError struct {
|
||||
Code SCUMSafeErrorCode
|
||||
Message string
|
||||
Retryable bool
|
||||
}
|
||||
|
||||
type SCUMBindingIdentity struct {
|
||||
ServerInstanceID string
|
||||
RunBindingID string
|
||||
RunEndpointID string
|
||||
PluginID string
|
||||
PluginVersion string
|
||||
AdapterVersion string
|
||||
GameVersion string
|
||||
DatabaseIdentity string
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeBounds struct {
|
||||
MaxObjects int
|
||||
MaxColumnsPerObject int
|
||||
MaxIndexesPerObject int
|
||||
MaxForeignKeys int
|
||||
MaxCardinalityReads int
|
||||
MaxSampleRows int
|
||||
TimeoutMS int
|
||||
MaxResultBytes int
|
||||
}
|
||||
|
||||
func DefaultSCUMSchemaProbeBounds() SCUMSchemaProbeBounds {
|
||||
return SCUMSchemaProbeBounds{MaxObjects: 256, MaxColumnsPerObject: 128, MaxIndexesPerObject: 64, MaxForeignKeys: 64, MaxCardinalityReads: 64, MaxSampleRows: 3, TimeoutMS: 5000, MaxResultBytes: 512 * 1024}
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateBounds struct {
|
||||
MaxParameters int
|
||||
MaxRows int
|
||||
TimeoutMS int
|
||||
BusyTimeoutMS int
|
||||
MaxResultBytes int
|
||||
}
|
||||
|
||||
func DefaultSCUMSQLiteTemplateBounds() SCUMSQLiteTemplateBounds {
|
||||
return SCUMSQLiteTemplateBounds{MaxParameters: 64, MaxRows: 500, TimeoutMS: 5000, BusyTimeoutMS: 250, MaxResultBytes: 1024 * 1024}
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateBounds struct {
|
||||
MaxPayloadBytes int
|
||||
TimeoutMS int
|
||||
MaxResponseBytes int
|
||||
MaxConfirmRecords int
|
||||
}
|
||||
|
||||
func DefaultSCUMTypedRCONTemplateBounds() SCUMTypedRCONTemplateBounds {
|
||||
return SCUMTypedRCONTemplateBounds{MaxPayloadBytes: 2048, TimeoutMS: 5000, MaxResponseBytes: 16 * 1024, MaxConfirmRecords: 16}
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationBounds struct {
|
||||
MaxPayloadBytes int
|
||||
TimeoutMS int
|
||||
BusyTimeoutMS int
|
||||
MaxReadbackBytes int
|
||||
MaxAffectedRows int
|
||||
}
|
||||
|
||||
func DefaultSCUMGuardedMutationBounds() SCUMGuardedMutationBounds {
|
||||
return SCUMGuardedMutationBounds{MaxPayloadBytes: 4096, TimeoutMS: 5000, BusyTimeoutMS: 250, MaxReadbackBytes: 16 * 1024, MaxAffectedRows: 1}
|
||||
}
|
||||
|
||||
type SCUMParsedLogBatchBounds struct {
|
||||
MaxEvents int
|
||||
MaxPayloadBytes int
|
||||
MaxLineBytes int
|
||||
MaxResultBytes int
|
||||
}
|
||||
|
||||
func DefaultSCUMParsedLogBatchBounds() SCUMParsedLogBatchBounds {
|
||||
return SCUMParsedLogBatchBounds{MaxEvents: 256, MaxPayloadBytes: 16 * 1024, MaxLineBytes: 4096, MaxResultBytes: 256 * 1024}
|
||||
}
|
||||
|
||||
type SCUMLogTailState string
|
||||
|
||||
const (
|
||||
SCUMLogTailAdvanced SCUMLogTailState = "advanced"
|
||||
SCUMLogTailRotated SCUMLogTailState = "rotated"
|
||||
SCUMLogTailTruncated SCUMLogTailState = "truncated"
|
||||
SCUMLogTailRestarted SCUMLogTailState = "restarted"
|
||||
SCUMLogTailPartial SCUMLogTailState = "partial-buffered"
|
||||
SCUMLogTailReplayed SCUMLogTailState = "replayed"
|
||||
)
|
||||
|
||||
type SCUMParsedLogCursor struct {
|
||||
SourceIdentityDigest string
|
||||
StreamGeneration string
|
||||
Sequence uint64
|
||||
}
|
||||
|
||||
type SCUMParsedLogEvent struct {
|
||||
EventType string
|
||||
OccurredAt time.Time
|
||||
Cursor SCUMParsedLogCursor
|
||||
LogicalEventDigest string
|
||||
EventDigest string
|
||||
PayloadDigest string
|
||||
Payload map[string]any
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Bounds SCUMSchemaProbeBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
AssetDigest string
|
||||
ParameterDigest string
|
||||
Parameters map[string]any
|
||||
Bounds SCUMSQLiteTemplateBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Capability SCUMDataCapability
|
||||
TransportKey string
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
AssetDigest string
|
||||
PayloadDigest string
|
||||
ConfirmationDigest string
|
||||
TargetIdentityDigest string
|
||||
IdempotencyKey string
|
||||
Payload map[string]any
|
||||
ReviewReason string
|
||||
Bounds SCUMTypedRCONTemplateBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
AssetDigest string
|
||||
TargetIdentityDigest string
|
||||
ExpectedRowDigest string
|
||||
ExpectedValueDigest string
|
||||
ExpectedXMLDigest string
|
||||
PatchDigest string
|
||||
BackupEvidenceDigest string
|
||||
OfflineEvidenceDigest string
|
||||
DangerConfirmationDigest string
|
||||
ReadbackExpectationDigest string
|
||||
IdempotencyKey string
|
||||
Payload map[string]any
|
||||
ReviewReason string
|
||||
Bounds SCUMGuardedMutationBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDeclaration struct {
|
||||
Capability string
|
||||
TargetKey string
|
||||
Bounds SCUMSchemaProbeBounds
|
||||
}
|
||||
|
||||
type SCUMLiveDataCapabilityGateDeclaration struct {
|
||||
Capability SCUMDataCapability
|
||||
Gate SCUMCapabilityGateState
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
RequiredAssetDigests []string
|
||||
EvidenceStatus SCUMCapabilityEvidenceStatus
|
||||
SafeReason string
|
||||
}
|
||||
|
||||
type SCUMLiveDataManifest struct {
|
||||
SchemaVersion string
|
||||
Probe SCUMSchemaProbeDeclaration
|
||||
CapabilityGates []SCUMLiveDataCapabilityGateDeclaration
|
||||
}
|
||||
|
||||
type SCUMSchemaColumnEvidence struct {
|
||||
NameFingerprint string
|
||||
DeclaredType string
|
||||
Nullable *bool
|
||||
PrimaryKey bool
|
||||
Ordinal int
|
||||
}
|
||||
|
||||
type SCUMSchemaIndexEvidence struct {
|
||||
NameFingerprint string
|
||||
Unique bool
|
||||
ColumnHashes []string
|
||||
}
|
||||
|
||||
type SCUMSchemaForeignKeyEvidence struct {
|
||||
FromColumnHash string
|
||||
ToObjectHash string
|
||||
ToColumnHash string
|
||||
}
|
||||
|
||||
type SCUMSchemaObjectEvidence struct {
|
||||
ObjectHash string
|
||||
Kind string
|
||||
NameFingerprint string
|
||||
DeclaredColumns []SCUMSchemaColumnEvidence
|
||||
Indexes []SCUMSchemaIndexEvidence
|
||||
ForeignKeys []SCUMSchemaForeignKeyEvidence
|
||||
ApproximateRows *int64
|
||||
SampleFingerprints []string
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMCapabilityEvidenceStatus
|
||||
SourceFingerprint string
|
||||
SchemaFingerprint string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
Objects []SCUMSchemaObjectEvidence
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMSchemaProbeBounds
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigest string
|
||||
ParameterDigest string
|
||||
SourceFingerprint string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
RowCount int
|
||||
Rows []map[string]any
|
||||
Truncated bool
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMSQLiteTemplateBounds
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
Capability SCUMDataCapability
|
||||
TransportKey string
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigest string
|
||||
PayloadDigest string
|
||||
ConfirmationDigest string
|
||||
TargetIdentityDigest string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
ResponseDigest string
|
||||
ConfirmationStatus SCUMRCONConfirmationStatus
|
||||
ConfirmationDigestID string
|
||||
SafeSummary string
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMTypedRCONTemplateBounds
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigest string
|
||||
SourceFingerprint string
|
||||
TargetIdentityDigest string
|
||||
ExpectedRowDigest string
|
||||
ExpectedValueDigest string
|
||||
ExpectedXMLDigest string
|
||||
PatchDigest string
|
||||
BackupEvidenceDigest string
|
||||
OfflineEvidenceDigest string
|
||||
DangerConfirmationDigest string
|
||||
ReadbackExpectationDigest string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
BeforeDigest string
|
||||
AfterDigest string
|
||||
ReadbackDigest string
|
||||
AffectedRows int
|
||||
ReadbackStatus SCUMMutationReadbackStatus
|
||||
SafeSummary string
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMGuardedMutationBounds
|
||||
}
|
||||
|
||||
type SCUMParsedLogBatchResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
SourceKey string
|
||||
StreamKey string
|
||||
ParserKey string
|
||||
ParserVersion string
|
||||
AdapterVersion string
|
||||
AssetDigest string
|
||||
ParserDigest string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
FirstCursor SCUMParsedLogCursor
|
||||
LastCursor SCUMParsedLogCursor
|
||||
TailState SCUMLogTailState
|
||||
PartialLineBuffered bool
|
||||
Replay bool
|
||||
EventCount int
|
||||
Events []SCUMParsedLogEvent
|
||||
SafeSummary string
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMParsedLogBatchBounds
|
||||
}
|
||||
|
||||
type SCUMCapabilityRequirement struct {
|
||||
Capability SCUMDataCapability
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigests []string
|
||||
}
|
||||
|
||||
type SCUMCapabilityEvidence struct {
|
||||
Capability SCUMDataCapability
|
||||
Status SCUMCapabilityEvidenceStatus
|
||||
Binding SCUMBindingIdentity
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
ProbeResultDigest string
|
||||
AssetDigests []string
|
||||
ObservedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
SafeError SCUMSafeError
|
||||
}
|
||||
|
||||
type SCUMCapabilityGate struct {
|
||||
Capability SCUMDataCapability
|
||||
State SCUMCapabilityGateState
|
||||
Enabled bool
|
||||
ReasonCode SCUMSafeErrorCode
|
||||
Reason string
|
||||
Evidence SCUMCapabilityEvidence
|
||||
}
|
||||
|
||||
type SCUMCapabilityNegotiation struct {
|
||||
ServerInstanceID string
|
||||
RunEndpointID string
|
||||
RunBindingID string
|
||||
PluginID string
|
||||
PluginVersion string
|
||||
AdapterVersion string
|
||||
GameVersion string
|
||||
DatabaseIdentity string
|
||||
ProbeExecutorAvailable bool
|
||||
EvaluatedAt time.Time
|
||||
Gates []SCUMCapabilityGate
|
||||
}
|
||||
|
||||
func EvaluateSCUMCapabilityGate(requirement SCUMCapabilityRequirement, evidence SCUMCapabilityEvidence, active SCUMBindingIdentity, probeExecutorAvailable bool, now time.Time) SCUMCapabilityGate {
|
||||
gate := SCUMCapabilityGate{Capability: requirement.Capability, State: SCUMCapabilityGateDisabled, ReasonCode: SCUMSafeErrorProbeMissing, Reason: "current-service evidence is required before this SCUM capability can run"}
|
||||
if !probeExecutorAvailable {
|
||||
gate.ReasonCode = SCUMSafeErrorProbeExecutorAbsent
|
||||
gate.Reason = "bound Run does not expose the generic SQLite schema-probe executor"
|
||||
return gate
|
||||
}
|
||||
if evidence.Status == SCUMCapabilityEvidenceMissing || evidence.Capability == "" {
|
||||
return gate
|
||||
}
|
||||
gate.Evidence = CopySCUMCapabilityEvidence(evidence)
|
||||
if evidence.Status == SCUMCapabilityEvidenceFailed {
|
||||
gate.ReasonCode = SCUMSafeErrorProbeFailed
|
||||
gate.Reason = safeReason(evidence.SafeError.Message, "last schema probe failed")
|
||||
return gate
|
||||
}
|
||||
if evidence.Status == SCUMCapabilityEvidenceIncompatible {
|
||||
gate.ReasonCode = SCUMSafeErrorSchemaIncompatible
|
||||
gate.Reason = safeReason(evidence.SafeError.Message, "current schema is incompatible with the plugin adapter")
|
||||
return gate
|
||||
}
|
||||
if evidence.Capability != requirement.Capability {
|
||||
gate.ReasonCode = SCUMSafeErrorSchemaIncompatible
|
||||
gate.Reason = "capability evidence does not match the requested SCUM capability"
|
||||
return gate
|
||||
}
|
||||
if !sameSCUMBinding(evidence.Binding, active) {
|
||||
gate.ReasonCode = SCUMSafeErrorBindingMismatch
|
||||
gate.Reason = "evidence belongs to a different server, Run binding, plugin, adapter, game, or database identity"
|
||||
return gate
|
||||
}
|
||||
if evidence.AdapterVersion != requirement.AdapterVersion {
|
||||
gate.ReasonCode = SCUMSafeErrorAdapterMismatch
|
||||
gate.Reason = "evidence adapter version does not match the plugin requirement"
|
||||
return gate
|
||||
}
|
||||
if evidence.SchemaFingerprint == "" || evidence.SchemaFingerprint != requirement.SchemaFingerprint {
|
||||
gate.ReasonCode = SCUMSafeErrorFingerprintMismatch
|
||||
gate.Reason = "schema fingerprint does not match the plugin requirement"
|
||||
return gate
|
||||
}
|
||||
if !containsAllStrings(evidence.AssetDigests, requirement.AssetDigests) {
|
||||
gate.ReasonCode = SCUMSafeErrorDigestMismatch
|
||||
gate.Reason = "packaged asset digest does not match the compatible evidence"
|
||||
return gate
|
||||
}
|
||||
if !evidence.ExpiresAt.IsZero() && !now.IsZero() && !now.Before(evidence.ExpiresAt) {
|
||||
gate.ReasonCode = SCUMSafeErrorEvidenceExpired
|
||||
gate.Reason = "current-service evidence has expired and must be probed again"
|
||||
return gate
|
||||
}
|
||||
gate.State = SCUMCapabilityGateEnabled
|
||||
gate.Enabled = true
|
||||
gate.ReasonCode = SCUMSafeErrorNone
|
||||
gate.Reason = "current-service evidence matches the versioned plugin adapter"
|
||||
return gate
|
||||
}
|
||||
|
||||
func CopySCUMCapabilityEvidence(value SCUMCapabilityEvidence) SCUMCapabilityEvidence {
|
||||
value.AssetDigests = append([]string(nil), value.AssetDigests...)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMSQLiteTemplateRequestPtr(value *SCUMSQLiteTemplateRequest) *SCUMSQLiteTemplateRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Parameters = CopySCUMValueMap(value.Parameters)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMTypedRCONTemplateRequestPtr(value *SCUMTypedRCONTemplateRequest) *SCUMTypedRCONTemplateRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Payload = CopySCUMValueMap(value.Payload)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMGuardedMutationRequestPtr(value *SCUMGuardedMutationRequest) *SCUMGuardedMutationRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Payload = CopySCUMValueMap(value.Payload)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Rows = CopySCUMRows(value.Rows)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMTypedRCONTemplateResultPtr(value *SCUMTypedRCONTemplateResult) *SCUMTypedRCONTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMGuardedMutationResultPtr(value *SCUMGuardedMutationResult) *SCUMGuardedMutationResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMParsedLogBatchResultPtr(value *SCUMParsedLogBatchResult) *SCUMParsedLogBatchResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Events = CopySCUMParsedLogEvents(value.Events)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMParsedLogEvents(events []SCUMParsedLogEvent) []SCUMParsedLogEvent {
|
||||
if events == nil {
|
||||
return nil
|
||||
}
|
||||
copy := make([]SCUMParsedLogEvent, len(events))
|
||||
for index, event := range events {
|
||||
copy[index] = event
|
||||
copy[index].Payload = CopySCUMValueMap(event.Payload)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func CopySCUMValueMap(value map[string]any) map[string]any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := make(map[string]any, len(value))
|
||||
for key, item := range value {
|
||||
copy[key] = item
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func CopySCUMRows(rows []map[string]any) []map[string]any {
|
||||
if rows == nil {
|
||||
return nil
|
||||
}
|
||||
copy := make([]map[string]any, len(rows))
|
||||
for index, row := range rows {
|
||||
copy[index] = CopySCUMValueMap(row)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func CopySCUMSchemaProbeResult(value SCUMSchemaProbeResult) SCUMSchemaProbeResult {
|
||||
value.Objects = append([]SCUMSchemaObjectEvidence(nil), value.Objects...)
|
||||
for index := range value.Objects {
|
||||
value.Objects[index].DeclaredColumns = append([]SCUMSchemaColumnEvidence(nil), value.Objects[index].DeclaredColumns...)
|
||||
value.Objects[index].Indexes = append([]SCUMSchemaIndexEvidence(nil), value.Objects[index].Indexes...)
|
||||
value.Objects[index].ForeignKeys = append([]SCUMSchemaForeignKeyEvidence(nil), value.Objects[index].ForeignKeys...)
|
||||
value.Objects[index].SampleFingerprints = append([]string(nil), value.Objects[index].SampleFingerprints...)
|
||||
for idx := range value.Objects[index].Indexes {
|
||||
value.Objects[index].Indexes[idx].ColumnHashes = append([]string(nil), value.Objects[index].Indexes[idx].ColumnHashes...)
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMSchemaProbeRequestPtr(value *SCUMSchemaProbeRequest) *SCUMSchemaProbeRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMSchemaProbeResultPtr(value *SCUMSchemaProbeResult) *SCUMSchemaProbeResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := CopySCUMSchemaProbeResult(*value)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMLiveDataManifest(value SCUMLiveDataManifest) SCUMLiveDataManifest {
|
||||
value.CapabilityGates = append([]SCUMLiveDataCapabilityGateDeclaration(nil), value.CapabilityGates...)
|
||||
for index := range value.CapabilityGates {
|
||||
value.CapabilityGates[index].RequiredAssetDigests = append([]string(nil), value.CapabilityGates[index].RequiredAssetDigests...)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func sameSCUMBinding(a SCUMBindingIdentity, b 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
|
||||
}
|
||||
|
||||
func containsAllStrings(values []string, required []string) bool {
|
||||
set := map[string]struct{}{}
|
||||
for _, value := range values {
|
||||
set[value] = struct{}{}
|
||||
}
|
||||
for _, value := range required {
|
||||
if _, ok := set[value]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func safeReason(value string, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSCUMCapabilityGateDefaultsClosedWithoutProbeEvidence(t *testing.T) {
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPlayerRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:query"}}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, SCUMCapabilityEvidence{}, active, true, time.Now())
|
||||
|
||||
if gate.Enabled || gate.State != SCUMCapabilityGateDisabled || gate.ReasonCode != SCUMSafeErrorProbeMissing {
|
||||
t.Fatalf("expected closed gate without evidence, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateDefaultsClosedWhenProbeExecutorUnavailable(t *testing.T) {
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPlayerRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1"}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityPlayerRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1"}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, evidence, active, false, time.Now())
|
||||
|
||||
if gate.Enabled || gate.ReasonCode != SCUMSafeErrorProbeExecutorAbsent {
|
||||
t.Fatalf("expected executor gate failure, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateEnablesOnlyMatchingEvidence(t *testing.T) {
|
||||
now := time.Date(2026, 8, 11, 12, 0, 0, 0, time.UTC)
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityVehicleRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:vehicle-query"}}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityVehicleRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:vehicle-query", "sha256:result-schema"}, ExpiresAt: now.Add(time.Hour)}
|
||||
|
||||
gate := EvaluateSCUMCapabilityGate(requirement, evidence, active, true, now)
|
||||
|
||||
if !gate.Enabled || gate.State != SCUMCapabilityGateEnabled || gate.ReasonCode != SCUMSafeErrorNone {
|
||||
t.Fatalf("expected enabled gate, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityGateRejectsMismatchedCurrentServiceEvidence(t *testing.T) {
|
||||
now := time.Date(2026, 8, 11, 12, 0, 0, 0, time.UTC)
|
||||
active := scumGateBinding()
|
||||
requirement := SCUMCapabilityRequirement{Capability: SCUMDataCapabilityPositionRead, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:positions"}}
|
||||
evidence := SCUMCapabilityEvidence{Capability: SCUMDataCapabilityPositionRead, Status: SCUMCapabilityEvidenceCompatible, Binding: active, AdapterVersion: "adapter-1", SchemaFingerprint: "schema-1", AssetDigests: []string{"sha256:positions"}, ExpiresAt: now.Add(time.Hour)}
|
||||
|
||||
changedBinding := evidence
|
||||
changedBinding.Binding.DatabaseIdentity = "db-other"
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedBinding, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorBindingMismatch {
|
||||
t.Fatalf("expected binding mismatch, got %#v", gate)
|
||||
}
|
||||
|
||||
changedFingerprint := evidence
|
||||
changedFingerprint.SchemaFingerprint = "schema-other"
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedFingerprint, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorFingerprintMismatch {
|
||||
t.Fatalf("expected fingerprint mismatch, got %#v", gate)
|
||||
}
|
||||
|
||||
changedDigest := evidence
|
||||
changedDigest.AssetDigests = []string{"sha256:different"}
|
||||
if gate := EvaluateSCUMCapabilityGate(requirement, changedDigest, active, true, now); gate.Enabled || gate.ReasonCode != SCUMSafeErrorDigestMismatch {
|
||||
t.Fatalf("expected digest mismatch, got %#v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultSCUMSchemaProbeBoundsAreBoundedAndDiagnosticOnly(t *testing.T) {
|
||||
bounds := DefaultSCUMSchemaProbeBounds()
|
||||
if bounds.MaxObjects <= 0 || bounds.MaxSampleRows > 3 || bounds.TimeoutMS > 5000 || bounds.MaxResultBytes > 512*1024 {
|
||||
t.Fatalf("unexpected unsafe default probe bounds: %#v", bounds)
|
||||
}
|
||||
}
|
||||
|
||||
func scumGateBinding() SCUMBindingIdentity {
|
||||
return SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "db-current"}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type SCUMProjectionSubject string
|
||||
|
||||
const (
|
||||
SCUMProjectionSubjectPlayer SCUMProjectionSubject = "player"
|
||||
SCUMProjectionSubjectLiveState SCUMProjectionSubject = "player-live-state"
|
||||
SCUMProjectionSubjectSquad SCUMProjectionSubject = "squad"
|
||||
SCUMProjectionSubjectMember SCUMProjectionSubject = "squad-member"
|
||||
SCUMProjectionSubjectVehicle SCUMProjectionSubject = "vehicle"
|
||||
SCUMProjectionSubjectFlag SCUMProjectionSubject = "flag"
|
||||
SCUMProjectionSubjectPosition SCUMProjectionSubject = "position"
|
||||
)
|
||||
|
||||
type SCUMProjectionFilter struct {
|
||||
ServerInstanceID string
|
||||
GamePlayerID string
|
||||
GamePlayerRecordID string
|
||||
UserProfileID string
|
||||
SteamID string
|
||||
SquadID string
|
||||
VehicleID string
|
||||
FlagID string
|
||||
SubjectType SCUMProjectionSubject
|
||||
QueryKey string
|
||||
Freshness SCUMProjectionFreshness
|
||||
Search string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMPlayerLiveState struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
GamePlayerRecordID string
|
||||
GamePlayerID string
|
||||
UserProfileID string
|
||||
SteamID string
|
||||
DisplayName string
|
||||
SquadID string
|
||||
SquadName string
|
||||
Online bool
|
||||
FamePoints float64
|
||||
NormalBalance float64
|
||||
GoldBalance float64
|
||||
LastLoginAt time.Time
|
||||
LastLogoutAt time.Time
|
||||
LastSaveTime time.Time
|
||||
Position SCUMCurrentPosition
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSquad struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SquadID string
|
||||
Name string
|
||||
LeaderProfileID string
|
||||
LeaderPlayerID string
|
||||
MemberCount int
|
||||
Score float64
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSquadMember struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SquadID string
|
||||
UserProfileID string
|
||||
GamePlayerRecordID string
|
||||
GamePlayerID string
|
||||
SteamID string
|
||||
DisplayName string
|
||||
Rank string
|
||||
IsLeader bool
|
||||
JoinedAt time.Time
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMVehicle struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
VehicleID string
|
||||
EntityID string
|
||||
ClassName string
|
||||
Label string
|
||||
OwnerProfileID string
|
||||
OwnerPlayerID string
|
||||
SquadID string
|
||||
Position SCUMCurrentPosition
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMFlag struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
FlagID string
|
||||
EntityID string
|
||||
OwnerProfileID string
|
||||
OwnerPlayerID string
|
||||
OwnerSquadID string
|
||||
OwnerSquadName string
|
||||
OwnershipConfidence string
|
||||
Position SCUMCurrentPosition
|
||||
UnknownFields map[string]any
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMCurrentPosition struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SubjectType SCUMProjectionSubject
|
||||
SubjectID string
|
||||
GamePlayerRecordID string
|
||||
GamePlayerID string
|
||||
VehicleID string
|
||||
EntityID string
|
||||
MapID string
|
||||
MapVersion string
|
||||
X float64
|
||||
Y float64
|
||||
Z float64
|
||||
HasCoordinates bool
|
||||
LastSaveTime time.Time
|
||||
Freshness SCUMProjectionFreshnessState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func SCUMProjectionStateUnknown() SCUMProjectionFreshnessState {
|
||||
return SCUMProjectionFreshnessState{Status: SCUMProjectionUnknown}
|
||||
}
|
||||
|
||||
func CopySCUMPlayerLiveState(value SCUMPlayerLiveState) SCUMPlayerLiveState {
|
||||
value.Position = CopySCUMCurrentPosition(value.Position)
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMSquad(value SCUMSquad) SCUMSquad {
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMSquadMember(value SCUMSquadMember) SCUMSquadMember {
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMVehicle(value SCUMVehicle) SCUMVehicle {
|
||||
value.Position = CopySCUMCurrentPosition(value.Position)
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMFlag(value SCUMFlag) SCUMFlag {
|
||||
value.Position = CopySCUMCurrentPosition(value.Position)
|
||||
value.UnknownFields = CopyGameClientBridgePayload(value.UnknownFields)
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMCurrentPosition(value SCUMCurrentPosition) SCUMCurrentPosition {
|
||||
value.Freshness = CopySCUMProjectionFreshnessState(value.Freshness)
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type SCUMObservationStatus string
|
||||
|
||||
const (
|
||||
SCUMObservationAccepted SCUMObservationStatus = "accepted"
|
||||
SCUMObservationStale SCUMObservationStatus = "stale"
|
||||
SCUMObservationFailed SCUMObservationStatus = "failed"
|
||||
)
|
||||
|
||||
type SCUMProjectionFreshness string
|
||||
|
||||
const (
|
||||
SCUMProjectionFresh SCUMProjectionFreshness = "fresh"
|
||||
SCUMProjectionStale SCUMProjectionFreshness = "stale"
|
||||
SCUMProjectionUnknown SCUMProjectionFreshness = "unknown"
|
||||
)
|
||||
|
||||
type SCUMWorkflowStatus string
|
||||
|
||||
const (
|
||||
SCUMWorkflowDraft SCUMWorkflowStatus = "draft"
|
||||
SCUMWorkflowQueued SCUMWorkflowStatus = "queued"
|
||||
SCUMWorkflowRunning SCUMWorkflowStatus = "running"
|
||||
SCUMWorkflowWaiting SCUMWorkflowStatus = "waiting"
|
||||
SCUMWorkflowBlocked SCUMWorkflowStatus = "blocked"
|
||||
SCUMWorkflowConfirming SCUMWorkflowStatus = "confirming"
|
||||
SCUMWorkflowConfirmed SCUMWorkflowStatus = "confirmed"
|
||||
SCUMWorkflowFailed SCUMWorkflowStatus = "failed"
|
||||
SCUMWorkflowUnknown SCUMWorkflowStatus = "unknown"
|
||||
SCUMWorkflowCancelled SCUMWorkflowStatus = "cancelled"
|
||||
)
|
||||
|
||||
type SCUMWorkflowStepStatus string
|
||||
|
||||
const (
|
||||
SCUMWorkflowStepQueued SCUMWorkflowStepStatus = "queued"
|
||||
SCUMWorkflowStepRunning SCUMWorkflowStepStatus = "running"
|
||||
SCUMWorkflowStepWaiting SCUMWorkflowStepStatus = "waiting"
|
||||
SCUMWorkflowStepBlocked SCUMWorkflowStepStatus = "blocked"
|
||||
SCUMWorkflowStepConfirming SCUMWorkflowStepStatus = "confirming"
|
||||
SCUMWorkflowStepConfirmed SCUMWorkflowStepStatus = "confirmed"
|
||||
SCUMWorkflowStepFailed SCUMWorkflowStepStatus = "failed"
|
||||
SCUMWorkflowStepUnknown SCUMWorkflowStepStatus = "unknown"
|
||||
SCUMWorkflowStepCancelled SCUMWorkflowStepStatus = "cancelled"
|
||||
)
|
||||
|
||||
type SCUMSafeSummary struct {
|
||||
Title string
|
||||
Message string
|
||||
Details map[string]string
|
||||
}
|
||||
|
||||
type SCUMDataObservation struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
Source string
|
||||
QueryKey string
|
||||
SubjectType string
|
||||
SubjectID string
|
||||
Sequence uint64
|
||||
Checksum string
|
||||
Status SCUMObservationStatus
|
||||
ErrorCode string
|
||||
SafeSummary SCUMSafeSummary
|
||||
ObservedAt time.Time
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMObservationResult struct {
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
Source string
|
||||
QueryKey string
|
||||
Sequence uint64
|
||||
Checksum string
|
||||
Status SCUMObservationStatus
|
||||
ErrorCode string
|
||||
SafeSummary SCUMSafeSummary
|
||||
ObservedAt time.Time
|
||||
ReceivedAt time.Time
|
||||
Rows []map[string]any
|
||||
}
|
||||
|
||||
type SCUMProjectionFreshnessState struct {
|
||||
Status SCUMProjectionFreshness
|
||||
ObservationID string
|
||||
Source string
|
||||
QueryKey string
|
||||
Sequence uint64
|
||||
Checksum string
|
||||
StaleReason string
|
||||
ObservedAt time.Time
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMMutationGuard struct {
|
||||
FieldKey string
|
||||
Before any
|
||||
After any
|
||||
MaxRowsAffected int
|
||||
SafetyWindow string
|
||||
BackupRef string
|
||||
RequiresOfflinePlayer bool
|
||||
RequiresMaintenance bool
|
||||
RequiresBackup bool
|
||||
}
|
||||
|
||||
type SCUMOperationConfirmation struct {
|
||||
Status string
|
||||
ObservationID string
|
||||
ConfirmedFields map[string]any
|
||||
AffectedRows int
|
||||
MutationChecksum string
|
||||
Checksum string
|
||||
ObservedAt time.Time
|
||||
SafeSummary SCUMSafeSummary
|
||||
}
|
||||
|
||||
type SCUMOperationRequest struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
TemplateKey string
|
||||
PlayerID string
|
||||
RequesterID string
|
||||
ApproverID string
|
||||
ApprovalLevel GameClientBridgeApprovalLevel
|
||||
Payload map[string]any
|
||||
Guard SCUMMutationGuard
|
||||
Confirmation SCUMOperationConfirmation
|
||||
Status SCUMWorkflowStepStatus
|
||||
Reason string
|
||||
IdempotencyKey string
|
||||
RunJobID string
|
||||
SafeSummary SCUMSafeSummary
|
||||
AuditReferences []string
|
||||
CreatedAt time.Time
|
||||
ApprovedAt time.Time
|
||||
CompletedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMOperationRequestFilter struct {
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
TemplateKey string
|
||||
PlayerID string
|
||||
RequesterID string
|
||||
Status SCUMWorkflowStepStatus
|
||||
IdempotencyKey string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMWorkflowInstanceFilter struct {
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
TemplateKey string
|
||||
RequestedBy string
|
||||
Status SCUMWorkflowStatus
|
||||
IdempotencyKey string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMWorkflowStepFilter struct {
|
||||
WorkflowID string
|
||||
ServerInstanceID string
|
||||
StepKey string
|
||||
Status SCUMWorkflowStepStatus
|
||||
MutatesState *bool
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMWorkflowInstance struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
TemplateKey string
|
||||
RequestedBy string
|
||||
IdempotencyKey string
|
||||
Status SCUMWorkflowStatus
|
||||
CurrentStepKey string
|
||||
Input map[string]any
|
||||
SafeSummary SCUMSafeSummary
|
||||
BlockerReason string
|
||||
AuditReferences []string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CompletedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMWorkflowStep struct {
|
||||
ID string
|
||||
WorkflowID string
|
||||
ServerInstanceID string
|
||||
StepKey string
|
||||
DependsOn []string
|
||||
Status SCUMWorkflowStepStatus
|
||||
OperationKey string
|
||||
QueryTemplateKey string
|
||||
Capability string
|
||||
TargetKey string
|
||||
JobID string
|
||||
Attempt int
|
||||
MaxAttempts int
|
||||
MutatesState bool
|
||||
Confirmation SCUMOperationConfirmation
|
||||
SafeSummary SCUMSafeSummary
|
||||
BlockerReason string
|
||||
AuditReferences []string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CompletedAt time.Time
|
||||
}
|
||||
|
||||
func CopySCUMSafeSummary(value SCUMSafeSummary) SCUMSafeSummary {
|
||||
value.Details = CopyStringMap(value.Details)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMDataObservation(value SCUMDataObservation) SCUMDataObservation {
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMObservationResult(value SCUMObservationResult) SCUMObservationResult {
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
value.Rows = CopyGameClientBridgeRows(value.Rows)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyGameClientBridgeRows(values []map[string]any) []map[string]any {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]map[string]any, len(values))
|
||||
for index, row := range values {
|
||||
out[index] = CopyGameClientBridgePayload(row)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CopySCUMProjectionFreshnessState(value SCUMProjectionFreshnessState) SCUMProjectionFreshnessState {
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMMutationGuard(value SCUMMutationGuard) SCUMMutationGuard {
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMOperationConfirmation(value SCUMOperationConfirmation) SCUMOperationConfirmation {
|
||||
value.ConfirmedFields = CopyGameClientBridgePayload(value.ConfirmedFields)
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMOperationRequest(value SCUMOperationRequest) SCUMOperationRequest {
|
||||
value.Payload = CopyGameClientBridgePayload(value.Payload)
|
||||
value.Guard = CopySCUMMutationGuard(value.Guard)
|
||||
value.Confirmation = CopySCUMOperationConfirmation(value.Confirmation)
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
value.AuditReferences = CopyStringSlice(value.AuditReferences)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMWorkflowInstance(value SCUMWorkflowInstance) SCUMWorkflowInstance {
|
||||
value.Input = CopyGameClientBridgePayload(value.Input)
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
value.AuditReferences = CopyStringSlice(value.AuditReferences)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMWorkflowStep(value SCUMWorkflowStep) SCUMWorkflowStep {
|
||||
value.DependsOn = CopyStringSlice(value.DependsOn)
|
||||
value.Confirmation = CopySCUMOperationConfirmation(value.Confirmation)
|
||||
value.SafeSummary = CopySCUMSafeSummary(value.SafeSummary)
|
||||
value.AuditReferences = CopyStringSlice(value.AuditReferences)
|
||||
return value
|
||||
}
|
||||
+20
-92
@@ -93,91 +93,24 @@ type RunJobResultRequest struct {
|
||||
}
|
||||
|
||||
type RunJobExecutionInputBody struct {
|
||||
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
||||
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
||||
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
||||
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
|
||||
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
|
||||
TargetVersion string `json:"targetVersion,omitempty"`
|
||||
Inputs map[string]string `json:"inputs,omitempty"`
|
||||
LogSource *RuntimeLogSourceBody `json:"logSource,omitempty"`
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionPlanBody `json:"dllExtensions,omitempty"`
|
||||
SourceRCON *RuntimeSourceRCONPlanBody `json:"sourceRcon,omitempty"`
|
||||
Deployment *ServerDeploymentExecutionBody `json:"deployment,omitempty"`
|
||||
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
|
||||
SQLiteSchemaProbe *RunSQLiteSchemaProbeRequestBody `json:"sqliteSchemaProbe,omitempty"`
|
||||
SQLiteTemplate *RunSQLiteTemplateRequestBody `json:"sqliteTemplate,omitempty"`
|
||||
RCONTemplate *RunTypedRCONTemplateRequestBody `json:"rconTemplate,omitempty"`
|
||||
GuardedMutation *RunGuardedMutationRequestBody `json:"guardedMutation,omitempty"`
|
||||
}
|
||||
|
||||
type RunSQLiteSchemaProbeRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Limits SCUMSchemaProbeBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type RunSQLiteTemplateRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
ParameterDigest string `json:"parameterDigest"`
|
||||
Parameters map[string]any `json:"parameters,omitempty"`
|
||||
Limits SCUMSQLiteTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type RunTypedRCONTemplateRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
PayloadDigest string `json:"payloadDigest"`
|
||||
ConfirmationDigest string `json:"confirmationDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Limits SCUMTypedRCONTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type RunGuardedMutationRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Limits SCUMGuardedMutationBoundsDTO `json:"limits"`
|
||||
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
||||
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
||||
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
||||
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
|
||||
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
|
||||
TargetVersion string `json:"targetVersion,omitempty"`
|
||||
Inputs map[string]string `json:"inputs,omitempty"`
|
||||
LogSource *RuntimeLogSourceBody `json:"logSource,omitempty"`
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionPlanBody `json:"dllExtensions,omitempty"`
|
||||
SourceRCON *RuntimeSourceRCONPlanBody `json:"sourceRcon,omitempty"`
|
||||
Deployment *ServerDeploymentExecutionBody `json:"deployment,omitempty"`
|
||||
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
|
||||
}
|
||||
|
||||
type ServerDeploymentPlanBody struct {
|
||||
@@ -256,11 +189,6 @@ type RunJobExecutionResultBody struct {
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
AuditSummary string `json:"auditSummary,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
|
||||
SQLiteTemplate *SCUMSQLiteTemplateResultDTO `json:"sqliteTemplate,omitempty"`
|
||||
RCONTemplate *SCUMTypedRCONTemplateResultDTO `json:"rconTemplate,omitempty"`
|
||||
GuardedMutation *SCUMGuardedMutationResultDTO `json:"guardedMutation,omitempty"`
|
||||
ParsedLogBatch *SCUMParsedLogBatchResultDTO `json:"parsedLogBatch,omitempty"`
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
|
||||
DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"`
|
||||
}
|
||||
@@ -520,7 +448,7 @@ func (request RunJobResultRequest) ToDomain() domain.RunJobResult {
|
||||
Message: request.Message,
|
||||
ErrorCode: request.ErrorCode,
|
||||
Retryable: request.Retryable,
|
||||
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrToDomain(request.ExecutionResult.SQLiteSchemaProbe), SQLiteTemplate: SCUMSQLiteTemplateResultPtrToDomain(request.ExecutionResult.SQLiteTemplate), RCONTemplate: SCUMTypedRCONTemplateResultPtrToDomain(request.ExecutionResult.RCONTemplate), GuardedMutation: SCUMGuardedMutationResultPtrToDomain(request.ExecutionResult.GuardedMutation), ParsedLogBatch: SCUMParsedLogBatchResultPtrToDomain(request.ExecutionResult.ParsedLogBatch), ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
|
||||
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,7 +663,7 @@ func RunJobAssignmentFromDomain(assignment domain.RunJobAssignment) RunJobAssign
|
||||
State: assignment.State,
|
||||
Progress: progressReportFromDomain(assignment.Progress),
|
||||
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), SQLiteSchemaProbe: runSQLiteSchemaProbeRequestPtrFromDomain(assignment.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: runSQLiteTemplateRequestPtrFromDomain(assignment.ExecutionInput.SQLiteTemplate), RCONTemplate: runTypedRCONTemplateRequestPtrFromDomain(assignment.ExecutionInput.RCONTemplate), GuardedMutation: runGuardedMutationRequestPtrFromDomain(assignment.ExecutionInput.GuardedMutation)},
|
||||
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)},
|
||||
LeaseToken: assignment.LeaseToken,
|
||||
Attempt: assignment.Attempt,
|
||||
FencingToken: assignment.FencingToken,
|
||||
|
||||
@@ -399,28 +399,6 @@ type GameMapTrajectoryDeclarationBody struct {
|
||||
RetentionSeconds int `json:"retentionSeconds"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDeclarationBody struct {
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
Bounds SCUMSchemaProbeBoundsDTO `json:"bounds"`
|
||||
}
|
||||
|
||||
type SCUMLiveDataCapabilityGateBody struct {
|
||||
Capability string `json:"capability"`
|
||||
Gate string `json:"gate"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
|
||||
RequiredAssetDigests []string `json:"requiredAssetDigests,omitempty"`
|
||||
EvidenceStatus string `json:"evidenceStatus"`
|
||||
SafeReason string `json:"safeReason"`
|
||||
}
|
||||
|
||||
type SCUMLiveDataManifestBody struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Probe SCUMSchemaProbeDeclarationBody `json:"probe"`
|
||||
CapabilityGates []SCUMLiveDataCapabilityGateBody `json:"capabilityGates"`
|
||||
}
|
||||
|
||||
type GamePluginManifestBody struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -441,7 +419,6 @@ type GamePluginManifestBody struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
}
|
||||
|
||||
@@ -481,7 +458,6 @@ type GamePluginCreateRequest struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
}
|
||||
@@ -510,7 +486,6 @@ type GamePluginResponse struct {
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesResponseBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
SCUMLiveData *SCUMLiveDataManifestBody `json:"scumLiveData,omitempty"`
|
||||
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
@@ -884,7 +859,6 @@ type JobExecutionResultResponse struct {
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
AuditSummary string `json:"auditSummary,omitempty"`
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1108,20 +1082,11 @@ func (request GamePluginManifestRegistrationRequest) ToDomain() domain.GamePlugi
|
||||
RemoteAccess: request.Manifest.RemoteAccess.ToDomain(),
|
||||
RuntimeProfiles: request.Manifest.RuntimeProfiles.ToDomain(),
|
||||
GameClientBridge: request.Manifest.GameClientBridge.ToDomain(),
|
||||
SCUMLiveData: request.Manifest.SCUMLiveData.ToDomain(),
|
||||
MapTrajectories: mapTrajectoryDeclarationToDomain(request.Manifest.MapTrajectories),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (body SCUMLiveDataManifestBody) ToDomain() domain.SCUMLiveDataManifest {
|
||||
gates := make([]domain.SCUMLiveDataCapabilityGateDeclaration, len(body.CapabilityGates))
|
||||
for index, gate := range body.CapabilityGates {
|
||||
gates[index] = domain.SCUMLiveDataCapabilityGateDeclaration{Capability: domain.SCUMDataCapability(gate.Capability), Gate: domain.SCUMCapabilityGateState(gate.Gate), AdapterVersion: gate.AdapterVersion, RequiredSchemaFingerprint: gate.RequiredSchemaFingerprint, RequiredAssetDigests: domain.CopyStringSlice(gate.RequiredAssetDigests), EvidenceStatus: domain.SCUMCapabilityEvidenceStatus(gate.EvidenceStatus), SafeReason: gate.SafeReason}
|
||||
}
|
||||
return domain.SCUMLiveDataManifest{SchemaVersion: body.SchemaVersion, Probe: domain.SCUMSchemaProbeDeclaration{Capability: body.Probe.Capability, TargetKey: body.Probe.TargetKey, Bounds: scumProbeBoundsToDomain(body.Probe.Bounds)}, CapabilityGates: gates}
|
||||
}
|
||||
|
||||
func pluginAssetFilesToDomain(files []PluginAssetFileBody) []domain.PluginAssetFile {
|
||||
if files == nil {
|
||||
return nil
|
||||
@@ -1303,7 +1268,6 @@ func (request GamePluginCreateRequest) ToDomain() domain.GamePlugin {
|
||||
RemoteAccess: request.RemoteAccess.ToDomain(),
|
||||
RuntimeProfiles: request.RuntimeProfiles.ToDomain(),
|
||||
GameClientBridge: request.GameClientBridge.ToDomain(),
|
||||
SCUMLiveData: request.SCUMLiveData.ToDomain(),
|
||||
MapTrajectories: mapTrajectoryDeclarationToDomain(request.MapTrajectories),
|
||||
ValidationViolations: domain.CopyStringSlice(request.ValidationViolations),
|
||||
}
|
||||
@@ -1557,7 +1521,6 @@ func GamePluginFromDomain(plugin domain.GamePlugin) GamePluginResponse {
|
||||
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
|
||||
RuntimeProfiles: runtimeProfilesFromDomain(plugin.RuntimeProfiles),
|
||||
GameClientBridge: gameClientBridgeManifestFromDomain(plugin.GameClientBridge),
|
||||
SCUMLiveData: scumLiveDataManifestPtrFromDomain(plugin.SCUMLiveData),
|
||||
MapTrajectories: mapTrajectoryDeclarationFromDomain(plugin.MapTrajectories),
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
@@ -1662,23 +1625,6 @@ func productionLifecycleFromDomain(lifecycle domain.GamePluginProductionLifecycl
|
||||
return GamePluginProductionLifecycleBody{Operations: lifecycle.Operations, DependencyPolicy: lifecycle.DependencyPolicy, ApprovalRequired: lifecycle.ApprovalRequired}
|
||||
}
|
||||
|
||||
func scumLiveDataManifestFromDomain(value domain.SCUMLiveDataManifest) SCUMLiveDataManifestBody {
|
||||
value = domain.CopySCUMLiveDataManifest(value)
|
||||
gates := make([]SCUMLiveDataCapabilityGateBody, len(value.CapabilityGates))
|
||||
for index, gate := range value.CapabilityGates {
|
||||
gates[index] = SCUMLiveDataCapabilityGateBody{Capability: string(gate.Capability), Gate: string(gate.Gate), AdapterVersion: gate.AdapterVersion, RequiredSchemaFingerprint: gate.RequiredSchemaFingerprint, RequiredAssetDigests: domain.CopyStringSlice(gate.RequiredAssetDigests), EvidenceStatus: string(gate.EvidenceStatus), SafeReason: gate.SafeReason}
|
||||
}
|
||||
return SCUMLiveDataManifestBody{SchemaVersion: value.SchemaVersion, Probe: SCUMSchemaProbeDeclarationBody{Capability: value.Probe.Capability, TargetKey: value.Probe.TargetKey, Bounds: scumProbeBoundsFromDomain(value.Probe.Bounds)}, CapabilityGates: gates}
|
||||
}
|
||||
|
||||
func scumLiveDataManifestPtrFromDomain(value domain.SCUMLiveDataManifest) *SCUMLiveDataManifestBody {
|
||||
if value.SchemaVersion == "" && value.Probe.Capability == "" && len(value.CapabilityGates) == 0 {
|
||||
return nil
|
||||
}
|
||||
body := scumLiveDataManifestFromDomain(value)
|
||||
return &body
|
||||
}
|
||||
|
||||
func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) GameClientBridgeManifestBody {
|
||||
value = domain.CopyGameClientBridgeManifest(value)
|
||||
commands := make([]GameClientBridgeCommandDeclarationBody, len(value.Commands))
|
||||
@@ -1931,7 +1877,7 @@ func JobFromDomain(job domain.Job) JobResponse {
|
||||
State: job.State,
|
||||
Progress: progressFromDomain(job.Progress),
|
||||
ResultRef: job.ResultRef,
|
||||
ExecutionResult: JobExecutionResultResponse{Kind: job.ExecutionResult.Kind, ProcessState: job.ExecutionResult.ProcessState, ExitClassification: job.ExecutionResult.ExitClassification, ExitCode: job.ExecutionResult.ExitCode, Version: job.ExecutionResult.Version, Checksum: job.ExecutionResult.Checksum, SizeBytes: job.ExecutionResult.SizeBytes, AuditSummary: job.ExecutionResult.AuditSummary, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrFromDomain(job.ExecutionResult.SQLiteSchemaProbe), ServerDeploymentEvidence: serverDeploymentEvidenceFromDomain(job.ExecutionResult.ServerDeploymentEvidence)},
|
||||
ExecutionResult: JobExecutionResultResponse{Kind: job.ExecutionResult.Kind, ProcessState: job.ExecutionResult.ProcessState, ExitClassification: job.ExecutionResult.ExitClassification, ExitCode: job.ExecutionResult.ExitCode, Version: job.ExecutionResult.Version, Checksum: job.ExecutionResult.Checksum, SizeBytes: job.ExecutionResult.SizeBytes, AuditSummary: job.ExecutionResult.AuditSummary, ServerDeploymentEvidence: serverDeploymentEvidenceFromDomain(job.ExecutionResult.ServerDeploymentEvidence)},
|
||||
RetryPolicy: JobRetryPolicyResponse{
|
||||
MaxAttempts: job.RetryPolicy.MaxAttempts,
|
||||
InitialBackoffSeconds: job.RetryPolicy.InitialBackoffSeconds,
|
||||
|
||||
@@ -31,233 +31,6 @@ func TestAIProviderResponseExposesOnlySecretPresence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesSQLiteSchemaProbeEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-probe",
|
||||
"leaseToken":"lease-probe",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"sqlite.schema-probe",
|
||||
"sqliteSchemaProbe":{
|
||||
"requestId":"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":"scum-database"},
|
||||
"status":"compatible",
|
||||
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"observedAt":"2026-08-12T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"limits":{"maxObjects":256,"maxColumnsPerObject":128,"maxIndexesPerObject":64,"maxForeignKeys":64,"maxCardinalityReads":64,"maxSampleRows":3,"timeoutMs":5000,"maxResultBytes":524288}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
probe := domainRequest.ExecutionResult.SQLiteSchemaProbe
|
||||
if probe == nil || probe.JobID != "job-probe" || probe.Binding.DatabaseIdentity != "scum-database" || probe.SourceFingerprint != "sha256:"+strings.Repeat("c", 64) || probe.ResultDigest != "sha256:"+strings.Repeat("b", 64) {
|
||||
t.Fatalf("sqliteSchemaProbe envelope did not parse: %+v", probe)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesSQLiteTemplateEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-query",
|
||||
"leaseToken":"lease-query",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"sqlite.template-query",
|
||||
"sqliteTemplate":{
|
||||
"requestId":"request-query",
|
||||
"jobId":"job-query",
|
||||
"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":"succeeded",
|
||||
"capability":"players.read",
|
||||
"targetKey":"scum-database",
|
||||
"templateKey":"players.active.v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"parameterDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
||||
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"rowCount":1,
|
||||
"rows":[{"externalPlayerId":"player-redacted","fame":12.5,"online":true,"squadId":null}],
|
||||
"limits":{"maxParameters":64,"maxRows":500,"timeoutMs":5000,"busyTimeoutMs":250,"maxResultBytes":1048576}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.SQLiteTemplate
|
||||
if result == nil || result.TemplateKey != "players.active.v1" || result.AssetDigest != "sha256:"+strings.Repeat("d", 64) || result.RowCount != 1 || result.Rows[0]["fame"].(float64) != 12.5 {
|
||||
t.Fatalf("sqliteTemplate envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesTypedRCONTemplateEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-rcon",
|
||||
"leaseToken":"lease-rcon",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"rcon.template-command",
|
||||
"rconTemplate":{
|
||||
"requestId":"request-rcon",
|
||||
"jobId":"job-rcon",
|
||||
"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":"succeeded",
|
||||
"capability":"economy-command.write",
|
||||
"transportKey":"scum-rcon",
|
||||
"targetKey":"scum-rcon",
|
||||
"templateKey":"economy.fame.set.v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"payloadDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
||||
"confirmationDigest":"sha256:` + strings.Repeat("f", 64) + `",
|
||||
"targetIdentityDigest":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"responseDigest":"sha256:` + strings.Repeat("1", 64) + `",
|
||||
"confirmationStatus":"confirmed",
|
||||
"confirmationDigestId":"sha256:` + strings.Repeat("2", 64) + `",
|
||||
"safeSummary":"confirmed by declared readback",
|
||||
"limits":{"maxPayloadBytes":2048,"timeoutMs":5000,"maxResponseBytes":16384,"maxConfirmRecords":16}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.RCONTemplate
|
||||
if result == nil || result.TemplateKey != "economy.fame.set.v1" || result.PayloadDigest != "sha256:"+strings.Repeat("e", 64) || result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed {
|
||||
t.Fatalf("rconTemplate envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesGuardedMutationEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-mutation",
|
||||
"leaseToken":"lease-mutation",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"sqlite.guarded-mutation",
|
||||
"guardedMutation":{
|
||||
"requestId":"request-mutation",
|
||||
"jobId":"job-mutation",
|
||||
"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":"succeeded",
|
||||
"capability":"profile-xml.write",
|
||||
"targetKey":"scum-mutation-db",
|
||||
"templateKey":"profile.attributes.patch.v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"targetIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `",
|
||||
"expectedRowDigest":"sha256:` + strings.Repeat("2", 64) + `",
|
||||
"expectedValueDigest":"sha256:` + strings.Repeat("3", 64) + `",
|
||||
"expectedXmlDigest":"sha256:` + strings.Repeat("4", 64) + `",
|
||||
"patchDigest":"sha256:` + strings.Repeat("5", 64) + `",
|
||||
"backupEvidenceDigest":"sha256:` + strings.Repeat("6", 64) + `",
|
||||
"offlineEvidenceDigest":"sha256:` + strings.Repeat("7", 64) + `",
|
||||
"dangerConfirmationDigest":"sha256:` + strings.Repeat("8", 64) + `",
|
||||
"readbackExpectationDigest":"sha256:` + strings.Repeat("9", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"beforeDigest":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"afterDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"readbackDigest":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"affectedRows":1,
|
||||
"readbackStatus":"confirmed",
|
||||
"safeSummary":"confirmed by declared readback",
|
||||
"limits":{"maxPayloadBytes":4096,"timeoutMs":5000,"busyTimeoutMs":250,"maxReadbackBytes":16384,"maxAffectedRows":1}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.GuardedMutation
|
||||
if result == nil || result.TemplateKey != "profile.attributes.patch.v1" || result.PatchDigest != "sha256:"+strings.Repeat("5", 64) || result.AffectedRows != 1 || result.ReadbackStatus != domain.SCUMMutationReadbackConfirmed {
|
||||
t.Fatalf("guardedMutation envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesParsedLogBatchEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-log",
|
||||
"leaseToken":"lease-log",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"log.parsed-events",
|
||||
"parsedLogBatch":{
|
||||
"requestId":"request-log",
|
||||
"jobId":"job-log",
|
||||
"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":"succeeded",
|
||||
"sourceKey":"scum-login-events",
|
||||
"streamKey":"scum.login",
|
||||
"parserKey":"scum-login-log-login-parser",
|
||||
"parserVersion":"scum-login-log-v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"parserDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"firstCursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},
|
||||
"lastCursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},
|
||||
"tailState":"rotated",
|
||||
"replay":true,
|
||||
"eventCount":1,
|
||||
"events":[{"eventType":"scum.login","occurredAt":"2026-08-13T00:00:00Z","cursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},"logicalEventDigest":"sha256:` + strings.Repeat("3", 64) + `","eventDigest":"sha256:` + strings.Repeat("4", 64) + `","payloadDigest":"sha256:` + strings.Repeat("5", 64) + `","payload":{"externalPlayerId":"player-redacted","displayName":"Known Player"}}],
|
||||
"safeSummary":"one sanitized login event parsed from declared source",
|
||||
"limits":{"maxEvents":256,"maxPayloadBytes":16384,"maxLineBytes":4096,"maxResultBytes":262144}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run parsed log result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.ParsedLogBatch
|
||||
if result == nil || result.ParserKey != "scum-login-log-login-parser" || result.ParserDigest != "sha256:"+strings.Repeat("e", 64) || result.EventCount != 1 || result.Events[0].LogicalEventDigest != "sha256:"+strings.Repeat("3", 64) {
|
||||
t.Fatalf("parsedLogBatch envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIProviderFromDomainCopiesModels(t *testing.T) {
|
||||
provider := domain.AIProvider{
|
||||
ID: "ai.openai",
|
||||
|
||||
@@ -127,18 +127,6 @@ type RuntimeTransportProfileBody struct {
|
||||
Capabilities []string `json:"capabilities"`
|
||||
}
|
||||
|
||||
type RuntimeDataTargetBody struct {
|
||||
Key string `json:"key"`
|
||||
Kind string `json:"kind"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
SourceRootKey string `json:"sourceRootKey"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
WorkspaceKey string `json:"workspaceKey"`
|
||||
RefreshPolicy string `json:"refreshPolicy"`
|
||||
MaxBytes int64 `json:"maxBytes,omitempty"`
|
||||
Platforms []string `json:"platforms,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeRepositoryBody struct {
|
||||
URL string `json:"url"`
|
||||
RevisionPolicy string `json:"revisionPolicy"`
|
||||
@@ -275,7 +263,6 @@ type GamePluginRuntimeProfilesBody struct {
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
LogEvents []RuntimeLogEventBody `json:"logEvents,omitempty"`
|
||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||
ClientManagers []RuntimeClientManagerProfileBody `json:"clientManagers,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
||||
}
|
||||
@@ -342,9 +329,6 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
||||
for _, item := range body.TransportProfiles {
|
||||
profiles.TransportProfiles = append(profiles.TransportProfiles, domain.RuntimeTransportProfile{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Capabilities: domain.CopyStringSlice(item.Capabilities)})
|
||||
}
|
||||
for _, item := range body.DataTargets {
|
||||
profiles.DataTargets = append(profiles.DataTargets, domain.RuntimeDataTarget{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||
}
|
||||
for _, item := range body.ClientManagers {
|
||||
manager := domain.RuntimeClientManagerProfile{
|
||||
Key: item.Key, DisplayName: item.DisplayName, Version: item.Version,
|
||||
|
||||
@@ -1,661 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type SCUMBindingIdentityDTO struct {
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
RunBindingID string `json:"runBindingId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
PluginVersion string `json:"pluginVersion"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
GameVersion string `json:"gameVersion"`
|
||||
DatabaseIdentity string `json:"databaseIdentity"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeBoundsDTO struct {
|
||||
MaxObjects int `json:"maxObjects"`
|
||||
MaxColumnsPerObject int `json:"maxColumnsPerObject"`
|
||||
MaxIndexesPerObject int `json:"maxIndexesPerObject"`
|
||||
MaxForeignKeys int `json:"maxForeignKeys"`
|
||||
MaxCardinalityReads int `json:"maxCardinalityReads"`
|
||||
MaxSampleRows int `json:"maxSampleRows"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
MaxResultBytes int `json:"maxResultBytes"`
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateBoundsDTO struct {
|
||||
MaxParameters int `json:"maxParameters"`
|
||||
MaxRows int `json:"maxRows"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
BusyTimeoutMS int `json:"busyTimeoutMs"`
|
||||
MaxResultBytes int `json:"maxResultBytes"`
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateBoundsDTO struct {
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
MaxResponseBytes int `json:"maxResponseBytes"`
|
||||
MaxConfirmRecords int `json:"maxConfirmRecords"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationBoundsDTO struct {
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
BusyTimeoutMS int `json:"busyTimeoutMs"`
|
||||
MaxReadbackBytes int `json:"maxReadbackBytes"`
|
||||
MaxAffectedRows int `json:"maxAffectedRows"`
|
||||
}
|
||||
|
||||
type SCUMParsedLogBatchBoundsDTO struct {
|
||||
MaxEvents int `json:"maxEvents"`
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
MaxLineBytes int `json:"maxLineBytes"`
|
||||
MaxResultBytes int `json:"maxResultBytes"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Bounds SCUMSchemaProbeBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
ParameterDigest string `json:"parameterDigest"`
|
||||
Parameters map[string]any `json:"parameters,omitempty"`
|
||||
Bounds SCUMSQLiteTemplateBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
PayloadDigest string `json:"payloadDigest"`
|
||||
ConfirmationDigest string `json:"confirmationDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Bounds SCUMTypedRCONTemplateBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Bounds SCUMGuardedMutationBoundsDTO `json:"bounds"`
|
||||
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 {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Retryable bool `json:"retryable"`
|
||||
}
|
||||
|
||||
type SCUMSchemaColumnEvidenceDTO struct {
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
DeclaredType string `json:"declaredType"`
|
||||
Nullable *bool `json:"nullable,omitempty"`
|
||||
PrimaryKey bool `json:"primaryKey"`
|
||||
Ordinal int `json:"ordinal"`
|
||||
}
|
||||
|
||||
type SCUMSchemaIndexEvidenceDTO struct {
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
Unique bool `json:"unique"`
|
||||
ColumnHashes []string `json:"columnHashes"`
|
||||
}
|
||||
|
||||
type SCUMSchemaForeignKeyEvidenceDTO struct {
|
||||
FromColumnHash string `json:"fromColumnHash"`
|
||||
ToObjectHash string `json:"toObjectHash"`
|
||||
ToColumnHash string `json:"toColumnHash"`
|
||||
}
|
||||
|
||||
type SCUMSchemaObjectEvidenceDTO struct {
|
||||
ObjectHash string `json:"objectHash"`
|
||||
Kind string `json:"kind"`
|
||||
NameFingerprint string `json:"nameFingerprint"`
|
||||
DeclaredColumns []SCUMSchemaColumnEvidenceDTO `json:"declaredColumns"`
|
||||
Indexes []SCUMSchemaIndexEvidenceDTO `json:"indexes"`
|
||||
ForeignKeys []SCUMSchemaForeignKeyEvidenceDTO `json:"foreignKeys"`
|
||||
ApproximateRows *int64 `json:"approximateRows,omitempty"`
|
||||
SampleFingerprints []string `json:"sampleFingerprints,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
SourceFingerprint string `json:"sourceFingerprint,omitempty"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest,omitempty"`
|
||||
Objects []SCUMSchemaObjectEvidenceDTO `json:"objects,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMSchemaProbeBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMSQLiteTemplateResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint,omitempty"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
ParameterDigest string `json:"parameterDigest"`
|
||||
SourceFingerprint string `json:"sourceFingerprint,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest,omitempty"`
|
||||
RowCount int `json:"rowCount"`
|
||||
Rows []map[string]any `json:"rows,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMSQLiteTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
Capability string `json:"capability"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint,omitempty"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
PayloadDigest string `json:"payloadDigest"`
|
||||
ConfirmationDigest string `json:"confirmationDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest"`
|
||||
ResponseDigest string `json:"responseDigest,omitempty"`
|
||||
ConfirmationStatus string `json:"confirmationStatus"`
|
||||
ConfirmationDigestID string `json:"confirmationDigestId,omitempty"`
|
||||
SafeSummary string `json:"safeSummary,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMTypedRCONTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
SourceFingerprint string `json:"sourceFingerprint,omitempty"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest"`
|
||||
BeforeDigest string `json:"beforeDigest,omitempty"`
|
||||
AfterDigest string `json:"afterDigest,omitempty"`
|
||||
ReadbackDigest string `json:"readbackDigest,omitempty"`
|
||||
AffectedRows int `json:"affectedRows"`
|
||||
ReadbackStatus string `json:"readbackStatus"`
|
||||
SafeSummary string `json:"safeSummary,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMGuardedMutationBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMParsedLogCursorDTO struct {
|
||||
SourceIdentityDigest string `json:"sourceIdentityDigest"`
|
||||
StreamGeneration string `json:"streamGeneration"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
}
|
||||
|
||||
type SCUMParsedLogEventDTO struct {
|
||||
EventType string `json:"eventType"`
|
||||
OccurredAt time.Time `json:"occurredAt"`
|
||||
Cursor SCUMParsedLogCursorDTO `json:"cursor"`
|
||||
LogicalEventDigest string `json:"logicalEventDigest"`
|
||||
EventDigest string `json:"eventDigest"`
|
||||
PayloadDigest string `json:"payloadDigest"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMParsedLogBatchResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
SourceKey string `json:"sourceKey"`
|
||||
StreamKey string `json:"streamKey"`
|
||||
ParserKey string `json:"parserKey"`
|
||||
ParserVersion string `json:"parserVersion"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
ParserDigest string `json:"parserDigest"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest"`
|
||||
FirstCursor SCUMParsedLogCursorDTO `json:"firstCursor"`
|
||||
LastCursor SCUMParsedLogCursorDTO `json:"lastCursor"`
|
||||
TailState string `json:"tailState"`
|
||||
PartialLineBuffered bool `json:"partialLineBuffered,omitempty"`
|
||||
Replay bool `json:"replay,omitempty"`
|
||||
EventCount int `json:"eventCount"`
|
||||
Events []SCUMParsedLogEventDTO `json:"events,omitempty"`
|
||||
SafeSummary string `json:"safeSummary,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMParsedLogBatchBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMCapabilityGateDTO struct {
|
||||
Capability string `json:"capability"`
|
||||
State string `json:"state"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ReasonCode string `json:"reasonCode"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type SCUMCapabilityNegotiationDTO struct {
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
RunBindingID string `json:"runBindingId,omitempty"`
|
||||
PluginID string `json:"pluginId"`
|
||||
PluginVersion string `json:"pluginVersion"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
GameVersion string `json:"gameVersion,omitempty"`
|
||||
DatabaseIdentity string `json:"databaseIdentity"`
|
||||
ProbeExecutorAvailable bool `json:"probeExecutorAvailable"`
|
||||
EvaluatedAt time.Time `json:"evaluatedAt"`
|
||||
Gates []SCUMCapabilityGateDTO `json:"gates"`
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeRequestToDomain(value SCUMSchemaProbeRequestDTO) domain.SCUMSchemaProbeRequest {
|
||||
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 SCUMSQLiteTemplateRequestToDomain(value SCUMSQLiteTemplateRequestDTO) domain.SCUMSQLiteTemplateRequest {
|
||||
return domain.SCUMSQLiteTemplateRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, Parameters: domain.CopySCUMValueMap(value.Parameters), Bounds: scumSQLiteTemplateBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateRequestFromDomain(value domain.SCUMSQLiteTemplateRequest) SCUMSQLiteTemplateRequestDTO {
|
||||
return SCUMSQLiteTemplateRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, Parameters: domain.CopySCUMValueMap(value.Parameters), Bounds: scumSQLiteTemplateBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateRequestPtrFromDomain(value *domain.SCUMSQLiteTemplateRequest) *SCUMSQLiteTemplateRequestDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSQLiteTemplateRequestFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateRequestToDomain(value SCUMTypedRCONTemplateRequestDTO) domain.SCUMTypedRCONTemplateRequest {
|
||||
return domain.SCUMTypedRCONTemplateRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Capability: domain.SCUMDataCapability(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumTypedRCONTemplateBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateRequestFromDomain(value domain.SCUMTypedRCONTemplateRequest) SCUMTypedRCONTemplateRequestDTO {
|
||||
return SCUMTypedRCONTemplateRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumTypedRCONTemplateBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateRequestPtrFromDomain(value *domain.SCUMTypedRCONTemplateRequest) *SCUMTypedRCONTemplateRequestDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMTypedRCONTemplateRequestFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestToDomain(value SCUMGuardedMutationRequestDTO) domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumGuardedMutationBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestFromDomain(value domain.SCUMGuardedMutationRequest) SCUMGuardedMutationRequestDTO {
|
||||
return SCUMGuardedMutationRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumGuardedMutationBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestPtrFromDomain(value *domain.SCUMGuardedMutationRequest) *SCUMGuardedMutationRequestDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationRequestFromDomain(*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 runSQLiteTemplateRequestPtrFromDomain(value *domain.SCUMSQLiteTemplateRequest) *RunSQLiteTemplateRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &RunSQLiteTemplateRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, Parameters: domain.CopySCUMValueMap(value.Parameters), Limits: scumSQLiteTemplateBoundsFromDomain(value.Bounds)}
|
||||
}
|
||||
|
||||
func runTypedRCONTemplateRequestPtrFromDomain(value *domain.SCUMTypedRCONTemplateRequest) *RunTypedRCONTemplateRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &RunTypedRCONTemplateRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Limits: scumTypedRCONTemplateBoundsFromDomain(value.Bounds)}
|
||||
}
|
||||
|
||||
func runGuardedMutationRequestPtrFromDomain(value *domain.SCUMGuardedMutationRequest) *RunGuardedMutationRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &RunGuardedMutationRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Limits: scumGuardedMutationBoundsFromDomain(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 {
|
||||
objects := make([]domain.SCUMSchemaObjectEvidence, len(value.Objects))
|
||||
for index, object := range value.Objects {
|
||||
columns := make([]domain.SCUMSchemaColumnEvidence, len(object.DeclaredColumns))
|
||||
for i, column := range object.DeclaredColumns {
|
||||
columns[i] = domain.SCUMSchemaColumnEvidence{NameFingerprint: column.NameFingerprint, DeclaredType: column.DeclaredType, Nullable: column.Nullable, PrimaryKey: column.PrimaryKey, Ordinal: column.Ordinal}
|
||||
}
|
||||
indexes := make([]domain.SCUMSchemaIndexEvidence, len(object.Indexes))
|
||||
for i, item := range object.Indexes {
|
||||
indexes[i] = domain.SCUMSchemaIndexEvidence{NameFingerprint: item.NameFingerprint, Unique: item.Unique, ColumnHashes: append([]string(nil), item.ColumnHashes...)}
|
||||
}
|
||||
foreignKeys := make([]domain.SCUMSchemaForeignKeyEvidence, len(object.ForeignKeys))
|
||||
for i, item := range object.ForeignKeys {
|
||||
foreignKeys[i] = domain.SCUMSchemaForeignKeyEvidence{FromColumnHash: item.FromColumnHash, ToObjectHash: item.ToObjectHash, ToColumnHash: item.ToColumnHash}
|
||||
}
|
||||
objects[index] = domain.SCUMSchemaObjectEvidence{ObjectHash: object.ObjectHash, Kind: object.Kind, NameFingerprint: object.NameFingerprint, DeclaredColumns: columns, Indexes: indexes, ForeignKeys: foreignKeys, ApproximateRows: object.ApproximateRows, SampleFingerprints: append([]string(nil), object.SampleFingerprints...)}
|
||||
}
|
||||
return domain.SCUMSchemaProbeResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMCapabilityEvidenceStatus(value.Status), SourceFingerprint: value.SourceFingerprint, SchemaFingerprint: value.SchemaFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, Objects: objects, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumProbeBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultPtrToDomain(value *SCUMSchemaProbeResultDTO) *domain.SCUMSchemaProbeResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSchemaProbeResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateResultToDomain(value SCUMSQLiteTemplateResultDTO) domain.SCUMSQLiteTemplateResult {
|
||||
return domain.SCUMSQLiteTemplateResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, SourceFingerprint: value.SourceFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, RowCount: value.RowCount, Rows: domain.CopySCUMRows(value.Rows), Truncated: value.Truncated, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumSQLiteTemplateBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateResultPtrToDomain(value *SCUMSQLiteTemplateResultDTO) *domain.SCUMSQLiteTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSQLiteTemplateResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateResultToDomain(value SCUMTypedRCONTemplateResultDTO) domain.SCUMTypedRCONTemplateResult {
|
||||
return domain.SCUMTypedRCONTemplateResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), Capability: domain.SCUMDataCapability(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, ResponseDigest: value.ResponseDigest, ConfirmationStatus: domain.SCUMRCONConfirmationStatus(value.ConfirmationStatus), ConfirmationDigestID: value.ConfirmationDigestID, SafeSummary: value.SafeSummary, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumTypedRCONTemplateBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateResultPtrToDomain(value *SCUMTypedRCONTemplateResultDTO) *domain.SCUMTypedRCONTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMTypedRCONTemplateResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultToDomain(value SCUMGuardedMutationResultDTO) domain.SCUMGuardedMutationResult {
|
||||
return domain.SCUMGuardedMutationResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, SourceFingerprint: value.SourceFingerprint, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, BeforeDigest: value.BeforeDigest, AfterDigest: value.AfterDigest, ReadbackDigest: value.ReadbackDigest, AffectedRows: value.AffectedRows, ReadbackStatus: domain.SCUMMutationReadbackStatus(value.ReadbackStatus), SafeSummary: value.SafeSummary, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumGuardedMutationBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultPtrToDomain(value *SCUMGuardedMutationResultDTO) *domain.SCUMGuardedMutationResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMParsedLogBatchResultToDomain(value SCUMParsedLogBatchResultDTO) domain.SCUMParsedLogBatchResult {
|
||||
events := make([]domain.SCUMParsedLogEvent, len(value.Events))
|
||||
for index, event := range value.Events {
|
||||
events[index] = domain.SCUMParsedLogEvent{EventType: event.EventType, OccurredAt: event.OccurredAt, Cursor: scumParsedLogCursorToDomain(event.Cursor), LogicalEventDigest: event.LogicalEventDigest, EventDigest: event.EventDigest, PayloadDigest: event.PayloadDigest, Payload: domain.CopySCUMValueMap(event.Payload)}
|
||||
}
|
||||
return domain.SCUMParsedLogBatchResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), SourceKey: value.SourceKey, StreamKey: value.StreamKey, ParserKey: value.ParserKey, ParserVersion: value.ParserVersion, AdapterVersion: value.AdapterVersion, AssetDigest: value.AssetDigest, ParserDigest: value.ParserDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, FirstCursor: scumParsedLogCursorToDomain(value.FirstCursor), LastCursor: scumParsedLogCursorToDomain(value.LastCursor), TailState: domain.SCUMLogTailState(value.TailState), PartialLineBuffered: value.PartialLineBuffered, Replay: value.Replay, EventCount: value.EventCount, Events: events, SafeSummary: value.SafeSummary, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumParsedLogBatchBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMParsedLogBatchResultPtrToDomain(value *SCUMParsedLogBatchResultDTO) *domain.SCUMParsedLogBatchResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMParsedLogBatchResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSchemaProbeResultDTO {
|
||||
value = domain.CopySCUMSchemaProbeResult(value)
|
||||
objects := make([]SCUMSchemaObjectEvidenceDTO, len(value.Objects))
|
||||
for index, object := range value.Objects {
|
||||
columns := make([]SCUMSchemaColumnEvidenceDTO, len(object.DeclaredColumns))
|
||||
for i, column := range object.DeclaredColumns {
|
||||
columns[i] = SCUMSchemaColumnEvidenceDTO{NameFingerprint: column.NameFingerprint, DeclaredType: column.DeclaredType, Nullable: column.Nullable, PrimaryKey: column.PrimaryKey, Ordinal: column.Ordinal}
|
||||
}
|
||||
indexes := make([]SCUMSchemaIndexEvidenceDTO, len(object.Indexes))
|
||||
for i, item := range object.Indexes {
|
||||
indexes[i] = SCUMSchemaIndexEvidenceDTO{NameFingerprint: item.NameFingerprint, Unique: item.Unique, ColumnHashes: append([]string(nil), item.ColumnHashes...)}
|
||||
}
|
||||
foreignKeys := make([]SCUMSchemaForeignKeyEvidenceDTO, len(object.ForeignKeys))
|
||||
for i, item := range object.ForeignKeys {
|
||||
foreignKeys[i] = SCUMSchemaForeignKeyEvidenceDTO{FromColumnHash: item.FromColumnHash, ToObjectHash: item.ToObjectHash, ToColumnHash: item.ToColumnHash}
|
||||
}
|
||||
objects[index] = SCUMSchemaObjectEvidenceDTO{ObjectHash: object.ObjectHash, Kind: object.Kind, NameFingerprint: object.NameFingerprint, DeclaredColumns: columns, Indexes: indexes, ForeignKeys: foreignKeys, ApproximateRows: object.ApproximateRows, SampleFingerprints: append([]string(nil), object.SampleFingerprints...)}
|
||||
}
|
||||
return SCUMSchemaProbeResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), SourceFingerprint: value.SourceFingerprint, SchemaFingerprint: value.SchemaFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, Objects: objects, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumProbeBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultPtrFromDomain(value *domain.SCUMSchemaProbeResult) *SCUMSchemaProbeResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSchemaProbeResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateResultFromDomain(value domain.SCUMSQLiteTemplateResult) SCUMSQLiteTemplateResultDTO {
|
||||
value = *domain.CopySCUMSQLiteTemplateResultPtr(&value)
|
||||
return SCUMSQLiteTemplateResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, SourceFingerprint: value.SourceFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, RowCount: value.RowCount, Rows: value.Rows, Truncated: value.Truncated, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumSQLiteTemplateBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMSQLiteTemplateResultPtrFromDomain(value *domain.SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMSQLiteTemplateResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateResultFromDomain(value domain.SCUMTypedRCONTemplateResult) SCUMTypedRCONTemplateResultDTO {
|
||||
return SCUMTypedRCONTemplateResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, ResponseDigest: value.ResponseDigest, ConfirmationStatus: string(value.ConfirmationStatus), ConfirmationDigestID: value.ConfirmationDigestID, SafeSummary: value.SafeSummary, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumTypedRCONTemplateBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMTypedRCONTemplateResultPtrFromDomain(value *domain.SCUMTypedRCONTemplateResult) *SCUMTypedRCONTemplateResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMTypedRCONTemplateResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultFromDomain(value domain.SCUMGuardedMutationResult) SCUMGuardedMutationResultDTO {
|
||||
return SCUMGuardedMutationResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, SourceFingerprint: value.SourceFingerprint, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, BeforeDigest: value.BeforeDigest, AfterDigest: value.AfterDigest, ReadbackDigest: value.ReadbackDigest, AffectedRows: value.AffectedRows, ReadbackStatus: string(value.ReadbackStatus), SafeSummary: value.SafeSummary, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumGuardedMutationBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultPtrFromDomain(value *domain.SCUMGuardedMutationResult) *SCUMGuardedMutationResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMParsedLogBatchResultFromDomain(value domain.SCUMParsedLogBatchResult) SCUMParsedLogBatchResultDTO {
|
||||
value = *domain.CopySCUMParsedLogBatchResultPtr(&value)
|
||||
events := make([]SCUMParsedLogEventDTO, len(value.Events))
|
||||
for index, event := range value.Events {
|
||||
events[index] = SCUMParsedLogEventDTO{EventType: event.EventType, OccurredAt: event.OccurredAt, Cursor: scumParsedLogCursorFromDomain(event.Cursor), LogicalEventDigest: event.LogicalEventDigest, EventDigest: event.EventDigest, PayloadDigest: event.PayloadDigest, Payload: event.Payload}
|
||||
}
|
||||
return SCUMParsedLogBatchResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), SourceKey: value.SourceKey, StreamKey: value.StreamKey, ParserKey: value.ParserKey, ParserVersion: value.ParserVersion, AdapterVersion: value.AdapterVersion, AssetDigest: value.AssetDigest, ParserDigest: value.ParserDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, FirstCursor: scumParsedLogCursorFromDomain(value.FirstCursor), LastCursor: scumParsedLogCursorFromDomain(value.LastCursor), TailState: string(value.TailState), PartialLineBuffered: value.PartialLineBuffered, Replay: value.Replay, EventCount: value.EventCount, Events: events, SafeSummary: value.SafeSummary, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumParsedLogBatchBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMParsedLogBatchResultPtrFromDomain(value *domain.SCUMParsedLogBatchResult) *SCUMParsedLogBatchResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMParsedLogBatchResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMCapabilityGateFromDomain(value domain.SCUMCapabilityGate) SCUMCapabilityGateDTO {
|
||||
return SCUMCapabilityGateDTO{Capability: string(value.Capability), State: string(value.State), Enabled: value.Enabled, ReasonCode: string(value.ReasonCode), Reason: value.Reason}
|
||||
}
|
||||
|
||||
func SCUMCapabilityNegotiationFromDomain(value domain.SCUMCapabilityNegotiation) SCUMCapabilityNegotiationDTO {
|
||||
gates := make([]SCUMCapabilityGateDTO, len(value.Gates))
|
||||
for index, gate := range value.Gates {
|
||||
gates[index] = SCUMCapabilityGateFromDomain(gate)
|
||||
}
|
||||
return SCUMCapabilityNegotiationDTO{ServerInstanceID: value.ServerInstanceID, RunEndpointID: value.RunEndpointID, RunBindingID: value.RunBindingID, PluginID: value.PluginID, PluginVersion: value.PluginVersion, AdapterVersion: value.AdapterVersion, GameVersion: value.GameVersion, DatabaseIdentity: value.DatabaseIdentity, ProbeExecutorAvailable: value.ProbeExecutorAvailable, EvaluatedAt: value.EvaluatedAt, Gates: gates}
|
||||
}
|
||||
|
||||
func scumBindingIdentityToDomain(value SCUMBindingIdentityDTO) domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: value.ServerInstanceID, RunBindingID: value.RunBindingID, RunEndpointID: value.RunEndpointID, PluginID: value.PluginID, PluginVersion: value.PluginVersion, AdapterVersion: value.AdapterVersion, GameVersion: value.GameVersion, DatabaseIdentity: value.DatabaseIdentity}
|
||||
}
|
||||
|
||||
func scumBindingIdentityFromDomain(value domain.SCUMBindingIdentity) SCUMBindingIdentityDTO {
|
||||
return SCUMBindingIdentityDTO{ServerInstanceID: value.ServerInstanceID, RunBindingID: value.RunBindingID, RunEndpointID: value.RunEndpointID, PluginID: value.PluginID, PluginVersion: value.PluginVersion, AdapterVersion: value.AdapterVersion, GameVersion: value.GameVersion, DatabaseIdentity: value.DatabaseIdentity}
|
||||
}
|
||||
|
||||
func scumProbeBoundsToDomain(value SCUMSchemaProbeBoundsDTO) domain.SCUMSchemaProbeBounds {
|
||||
return domain.SCUMSchemaProbeBounds{MaxObjects: value.MaxObjects, MaxColumnsPerObject: value.MaxColumnsPerObject, MaxIndexesPerObject: value.MaxIndexesPerObject, MaxForeignKeys: value.MaxForeignKeys, MaxCardinalityReads: value.MaxCardinalityReads, MaxSampleRows: value.MaxSampleRows, TimeoutMS: value.TimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumProbeBoundsFromDomain(value domain.SCUMSchemaProbeBounds) SCUMSchemaProbeBoundsDTO {
|
||||
return SCUMSchemaProbeBoundsDTO{MaxObjects: value.MaxObjects, MaxColumnsPerObject: value.MaxColumnsPerObject, MaxIndexesPerObject: value.MaxIndexesPerObject, MaxForeignKeys: value.MaxForeignKeys, MaxCardinalityReads: value.MaxCardinalityReads, MaxSampleRows: value.MaxSampleRows, TimeoutMS: value.TimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumSQLiteTemplateBoundsToDomain(value SCUMSQLiteTemplateBoundsDTO) domain.SCUMSQLiteTemplateBounds {
|
||||
return domain.SCUMSQLiteTemplateBounds{MaxParameters: value.MaxParameters, MaxRows: value.MaxRows, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumSQLiteTemplateBoundsFromDomain(value domain.SCUMSQLiteTemplateBounds) SCUMSQLiteTemplateBoundsDTO {
|
||||
return SCUMSQLiteTemplateBoundsDTO{MaxParameters: value.MaxParameters, MaxRows: value.MaxRows, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumTypedRCONTemplateBoundsToDomain(value SCUMTypedRCONTemplateBoundsDTO) domain.SCUMTypedRCONTemplateBounds {
|
||||
return domain.SCUMTypedRCONTemplateBounds{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, MaxResponseBytes: value.MaxResponseBytes, MaxConfirmRecords: value.MaxConfirmRecords}
|
||||
}
|
||||
|
||||
func scumTypedRCONTemplateBoundsFromDomain(value domain.SCUMTypedRCONTemplateBounds) SCUMTypedRCONTemplateBoundsDTO {
|
||||
return SCUMTypedRCONTemplateBoundsDTO{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, MaxResponseBytes: value.MaxResponseBytes, MaxConfirmRecords: value.MaxConfirmRecords}
|
||||
}
|
||||
|
||||
func scumGuardedMutationBoundsToDomain(value SCUMGuardedMutationBoundsDTO) domain.SCUMGuardedMutationBounds {
|
||||
return domain.SCUMGuardedMutationBounds{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxReadbackBytes: value.MaxReadbackBytes, MaxAffectedRows: value.MaxAffectedRows}
|
||||
}
|
||||
|
||||
func scumGuardedMutationBoundsFromDomain(value domain.SCUMGuardedMutationBounds) SCUMGuardedMutationBoundsDTO {
|
||||
return SCUMGuardedMutationBoundsDTO{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxReadbackBytes: value.MaxReadbackBytes, MaxAffectedRows: value.MaxAffectedRows}
|
||||
}
|
||||
|
||||
func scumParsedLogCursorToDomain(value SCUMParsedLogCursorDTO) domain.SCUMParsedLogCursor {
|
||||
return domain.SCUMParsedLogCursor{SourceIdentityDigest: value.SourceIdentityDigest, StreamGeneration: value.StreamGeneration, Sequence: value.Sequence}
|
||||
}
|
||||
|
||||
func scumParsedLogCursorFromDomain(value domain.SCUMParsedLogCursor) SCUMParsedLogCursorDTO {
|
||||
return SCUMParsedLogCursorDTO{SourceIdentityDigest: value.SourceIdentityDigest, StreamGeneration: value.StreamGeneration, Sequence: value.Sequence}
|
||||
}
|
||||
|
||||
func scumParsedLogBatchBoundsToDomain(value SCUMParsedLogBatchBoundsDTO) domain.SCUMParsedLogBatchBounds {
|
||||
return domain.SCUMParsedLogBatchBounds{MaxEvents: value.MaxEvents, MaxPayloadBytes: value.MaxPayloadBytes, MaxLineBytes: value.MaxLineBytes, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
|
||||
func scumParsedLogBatchBoundsFromDomain(value domain.SCUMParsedLogBatchBounds) SCUMParsedLogBatchBoundsDTO {
|
||||
return SCUMParsedLogBatchBoundsDTO{MaxEvents: value.MaxEvents, MaxPayloadBytes: value.MaxPayloadBytes, MaxLineBytes: value.MaxLineBytes, MaxResultBytes: value.MaxResultBytes}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package dto
|
||||
|
||||
import "browser.local/platform/domain"
|
||||
|
||||
type SCUMPlayerLiveStateListResponse struct {
|
||||
Items []domain.SCUMPlayerLiveState `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMSquadListResponse struct {
|
||||
Items []domain.SCUMSquad `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMSquadMemberListResponse struct {
|
||||
Items []domain.SCUMSquadMember `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMVehicleListResponse struct {
|
||||
Items []domain.SCUMVehicle `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMFlagListResponse struct {
|
||||
Items []domain.SCUMFlag `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMCurrentPositionListResponse struct {
|
||||
Items []domain.SCUMCurrentPosition `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func SCUMPlayerLiveStatesFromDomain(values []domain.SCUMPlayerLiveState) SCUMPlayerLiveStateListResponse {
|
||||
out := make([]domain.SCUMPlayerLiveState, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMPlayerLiveState(value)
|
||||
}
|
||||
return SCUMPlayerLiveStateListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMSquadsFromDomain(values []domain.SCUMSquad) SCUMSquadListResponse {
|
||||
out := make([]domain.SCUMSquad, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMSquad(value)
|
||||
}
|
||||
return SCUMSquadListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMSquadMembersFromDomain(values []domain.SCUMSquadMember) SCUMSquadMemberListResponse {
|
||||
out := make([]domain.SCUMSquadMember, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMSquadMember(value)
|
||||
}
|
||||
return SCUMSquadMemberListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMVehiclesFromDomain(values []domain.SCUMVehicle) SCUMVehicleListResponse {
|
||||
out := make([]domain.SCUMVehicle, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMVehicle(value)
|
||||
}
|
||||
return SCUMVehicleListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMFlagsFromDomain(values []domain.SCUMFlag) SCUMFlagListResponse {
|
||||
out := make([]domain.SCUMFlag, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMFlag(value)
|
||||
}
|
||||
return SCUMFlagListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
|
||||
func SCUMCurrentPositionsFromDomain(values []domain.SCUMCurrentPosition) SCUMCurrentPositionListResponse {
|
||||
out := make([]domain.SCUMCurrentPosition, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = domain.CopySCUMCurrentPosition(value)
|
||||
}
|
||||
return SCUMCurrentPositionListResponse{Items: out, Count: len(out)}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type SCUMSafeSummaryBody struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMDataObservationResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
Source string `json:"source"`
|
||||
QueryKey string `json:"queryKey,omitempty"`
|
||||
SubjectType string `json:"subjectType,omitempty"`
|
||||
SubjectID string `json:"subjectId,omitempty"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ReceivedAt time.Time `json:"receivedAt"`
|
||||
}
|
||||
|
||||
type SCUMProjectionFreshnessBody struct {
|
||||
Status string `json:"status"`
|
||||
ObservationID string `json:"observationId,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
QueryKey string `json:"queryKey,omitempty"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
StaleReason string `json:"staleReason,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt,omitempty"`
|
||||
ReceivedAt time.Time `json:"receivedAt,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMMutationGuardBody struct {
|
||||
FieldKey string `json:"fieldKey,omitempty"`
|
||||
Before any `json:"before,omitempty"`
|
||||
After any `json:"after,omitempty"`
|
||||
MaxRowsAffected int `json:"maxRowsAffected,omitempty"`
|
||||
SafetyWindow string `json:"safetyWindow,omitempty"`
|
||||
BackupRef string `json:"backupRef,omitempty"`
|
||||
RequiresOfflinePlayer bool `json:"requiresOfflinePlayer,omitempty"`
|
||||
RequiresMaintenance bool `json:"requiresMaintenance,omitempty"`
|
||||
RequiresBackup bool `json:"requiresBackup,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMOperationConfirmationBody struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
ObservationID string `json:"observationId,omitempty"`
|
||||
ConfirmedFields map[string]any `json:"confirmedFields,omitempty"`
|
||||
AffectedRows int `json:"affectedRows,omitempty"`
|
||||
MutationChecksum string `json:"mutationChecksum,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMOperationRequestBody struct {
|
||||
TemplateKey string `json:"templateKey"`
|
||||
PlayerID string `json:"playerId,omitempty"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
Guard SCUMMutationGuardBody `json:"guard,omitempty"`
|
||||
Reason string `json:"reason"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowCreateRequest struct {
|
||||
TemplateKey string `json:"templateKey"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Input map[string]any `json:"input,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMOperationResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
PlayerID string `json:"playerId,omitempty"`
|
||||
RequesterID string `json:"requesterId,omitempty"`
|
||||
ApproverID string `json:"approverId,omitempty"`
|
||||
ApprovalLevel string `json:"approvalLevel"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
Guard SCUMMutationGuardBody `json:"guard,omitempty"`
|
||||
Confirmation SCUMOperationConfirmationBody `json:"confirmation,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
RunJobID string `json:"runJobId,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
AuditReferences []string `json:"auditReferences,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ApprovedAt time.Time `json:"approvedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type SCUMOperationListResponse struct {
|
||||
Items []SCUMOperationResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
RequestedBy string `json:"requestedBy,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CurrentStepKey string `json:"currentStepKey,omitempty"`
|
||||
Input map[string]any `json:"input,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
BlockerReason string `json:"blockerReason,omitempty"`
|
||||
AuditReferences []string `json:"auditReferences,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowListResponse struct {
|
||||
Items []SCUMWorkflowResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowStepResponse struct {
|
||||
ID string `json:"id"`
|
||||
WorkflowID string `json:"workflowId"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
StepKey string `json:"stepKey"`
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
Status string `json:"status"`
|
||||
OperationKey string `json:"operationKey,omitempty"`
|
||||
QueryTemplateKey string `json:"queryTemplateKey,omitempty"`
|
||||
Capability string `json:"capability,omitempty"`
|
||||
TargetKey string `json:"targetKey,omitempty"`
|
||||
JobID string `json:"jobId,omitempty"`
|
||||
Attempt int `json:"attempt,omitempty"`
|
||||
MaxAttempts int `json:"maxAttempts,omitempty"`
|
||||
MutatesState bool `json:"mutatesState,omitempty"`
|
||||
Confirmation SCUMOperationConfirmationBody `json:"confirmation,omitempty"`
|
||||
SafeSummary SCUMSafeSummaryBody `json:"safeSummary,omitempty"`
|
||||
BlockerReason string `json:"blockerReason,omitempty"`
|
||||
AuditReferences []string `json:"auditReferences,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMWorkflowStepListResponse struct {
|
||||
Items []SCUMWorkflowStepResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func SCUMSafeSummaryFromDomain(value domain.SCUMSafeSummary) SCUMSafeSummaryBody {
|
||||
value = domain.CopySCUMSafeSummary(value)
|
||||
return SCUMSafeSummaryBody{Title: value.Title, Message: value.Message, Details: value.Details}
|
||||
}
|
||||
|
||||
func scumSafeSummaryToDomain(value SCUMSafeSummaryBody) domain.SCUMSafeSummary {
|
||||
return domain.SCUMSafeSummary{Title: value.Title, Message: value.Message, Details: domain.CopyStringMap(value.Details)}
|
||||
}
|
||||
|
||||
func SCUMDataObservationFromDomain(value domain.SCUMDataObservation) SCUMDataObservationResponse {
|
||||
value = domain.CopySCUMDataObservation(value)
|
||||
return SCUMDataObservationResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, Source: value.Source, QueryKey: value.QueryKey, SubjectType: value.SubjectType, SubjectID: value.SubjectID, Sequence: value.Sequence, Checksum: value.Checksum, Status: string(value.Status), ErrorCode: value.ErrorCode, SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary), ObservedAt: value.ObservedAt, ReceivedAt: value.ReceivedAt}
|
||||
}
|
||||
|
||||
func SCUMProjectionFreshnessFromDomain(value domain.SCUMProjectionFreshnessState) SCUMProjectionFreshnessBody {
|
||||
value = domain.CopySCUMProjectionFreshnessState(value)
|
||||
return SCUMProjectionFreshnessBody{Status: string(value.Status), ObservationID: value.ObservationID, Source: value.Source, QueryKey: value.QueryKey, Sequence: value.Sequence, Checksum: value.Checksum, StaleReason: value.StaleReason, ObservedAt: value.ObservedAt, ReceivedAt: value.ReceivedAt}
|
||||
}
|
||||
|
||||
func SCUMOperationRequestBodyToDomain(request SCUMOperationRequestBody) domain.SCUMOperationRequest {
|
||||
return domain.SCUMOperationRequest{TemplateKey: request.TemplateKey, PlayerID: request.PlayerID, Payload: domain.CopyGameClientBridgePayload(request.Payload), Guard: scumMutationGuardToDomain(request.Guard), Reason: request.Reason, IdempotencyKey: request.IdempotencyKey}
|
||||
}
|
||||
|
||||
func SCUMWorkflowCreateRequestToDomain(request SCUMWorkflowCreateRequest) domain.SCUMWorkflowInstance {
|
||||
return domain.SCUMWorkflowInstance{TemplateKey: request.TemplateKey, IdempotencyKey: request.IdempotencyKey, Input: domain.CopyGameClientBridgePayload(request.Input)}
|
||||
}
|
||||
|
||||
func SCUMOperationFromDomain(value domain.SCUMOperationRequest) SCUMOperationResponse {
|
||||
value = domain.CopySCUMOperationRequest(value)
|
||||
return SCUMOperationResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, TemplateKey: value.TemplateKey, PlayerID: value.PlayerID, RequesterID: value.RequesterID, ApproverID: value.ApproverID, ApprovalLevel: string(value.ApprovalLevel), Payload: value.Payload, Guard: scumMutationGuardFromDomain(value.Guard), Confirmation: scumOperationConfirmationFromDomain(value.Confirmation), Status: string(value.Status), Reason: value.Reason, RunJobID: value.RunJobID, SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary), AuditReferences: value.AuditReferences, CreatedAt: value.CreatedAt, ApprovedAt: value.ApprovedAt, CompletedAt: value.CompletedAt, UpdatedAt: value.UpdatedAt}
|
||||
}
|
||||
|
||||
func SCUMOperationsFromDomain(values []domain.SCUMOperationRequest) SCUMOperationListResponse {
|
||||
items := make([]SCUMOperationResponse, len(values))
|
||||
for index, value := range values {
|
||||
items[index] = SCUMOperationFromDomain(value)
|
||||
}
|
||||
return SCUMOperationListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMWorkflowFromDomain(value domain.SCUMWorkflowInstance) SCUMWorkflowResponse {
|
||||
value = domain.CopySCUMWorkflowInstance(value)
|
||||
return SCUMWorkflowResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, TemplateKey: value.TemplateKey, RequestedBy: value.RequestedBy, IdempotencyKey: value.IdempotencyKey, Status: string(value.Status), CurrentStepKey: value.CurrentStepKey, Input: value.Input, SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary), BlockerReason: value.BlockerReason, AuditReferences: value.AuditReferences, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, CompletedAt: value.CompletedAt}
|
||||
}
|
||||
|
||||
func SCUMWorkflowsFromDomain(values []domain.SCUMWorkflowInstance) SCUMWorkflowListResponse {
|
||||
items := make([]SCUMWorkflowResponse, len(values))
|
||||
for index, value := range values {
|
||||
items[index] = SCUMWorkflowFromDomain(value)
|
||||
}
|
||||
return SCUMWorkflowListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMWorkflowStepFromDomain(value domain.SCUMWorkflowStep) SCUMWorkflowStepResponse {
|
||||
value = domain.CopySCUMWorkflowStep(value)
|
||||
return SCUMWorkflowStepResponse{ID: value.ID, WorkflowID: value.WorkflowID, ServerInstanceID: value.ServerInstanceID, StepKey: value.StepKey, DependsOn: value.DependsOn, Status: string(value.Status), OperationKey: value.OperationKey, QueryTemplateKey: value.QueryTemplateKey, Capability: value.Capability, TargetKey: value.TargetKey, JobID: value.JobID, Attempt: value.Attempt, MaxAttempts: value.MaxAttempts, MutatesState: value.MutatesState, Confirmation: scumOperationConfirmationFromDomain(value.Confirmation), SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary), BlockerReason: value.BlockerReason, AuditReferences: value.AuditReferences, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, CompletedAt: value.CompletedAt}
|
||||
}
|
||||
|
||||
func SCUMWorkflowStepsFromDomain(values []domain.SCUMWorkflowStep) SCUMWorkflowStepListResponse {
|
||||
items := make([]SCUMWorkflowStepResponse, len(values))
|
||||
for index, value := range values {
|
||||
items[index] = SCUMWorkflowStepFromDomain(value)
|
||||
}
|
||||
return SCUMWorkflowStepListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func scumMutationGuardFromDomain(value domain.SCUMMutationGuard) SCUMMutationGuardBody {
|
||||
return SCUMMutationGuardBody{FieldKey: value.FieldKey, Before: value.Before, After: value.After, MaxRowsAffected: value.MaxRowsAffected, SafetyWindow: value.SafetyWindow, BackupRef: value.BackupRef, RequiresOfflinePlayer: value.RequiresOfflinePlayer, RequiresMaintenance: value.RequiresMaintenance, RequiresBackup: value.RequiresBackup}
|
||||
}
|
||||
|
||||
func scumMutationGuardToDomain(value SCUMMutationGuardBody) domain.SCUMMutationGuard {
|
||||
return domain.SCUMMutationGuard{FieldKey: value.FieldKey, Before: value.Before, After: value.After, MaxRowsAffected: value.MaxRowsAffected, SafetyWindow: value.SafetyWindow, BackupRef: value.BackupRef, RequiresOfflinePlayer: value.RequiresOfflinePlayer, RequiresMaintenance: value.RequiresMaintenance, RequiresBackup: value.RequiresBackup}
|
||||
}
|
||||
|
||||
func scumOperationConfirmationFromDomain(value domain.SCUMOperationConfirmation) SCUMOperationConfirmationBody {
|
||||
value = domain.CopySCUMOperationConfirmation(value)
|
||||
return SCUMOperationConfirmationBody{Status: value.Status, ObservationID: value.ObservationID, ConfirmedFields: value.ConfirmedFields, AffectedRows: value.AffectedRows, MutationChecksum: value.MutationChecksum, Checksum: value.Checksum, ObservedAt: value.ObservedAt, SafeSummary: SCUMSafeSummaryFromDomain(value.SafeSummary)}
|
||||
}
|
||||
|
||||
func scumOperationConfirmationToDomain(value SCUMOperationConfirmationBody) domain.SCUMOperationConfirmation {
|
||||
return domain.SCUMOperationConfirmation{Status: value.Status, ObservationID: value.ObservationID, ConfirmedFields: domain.CopyGameClientBridgePayload(value.ConfirmedFields), AffectedRows: value.AffectedRows, MutationChecksum: value.MutationChecksum, Checksum: value.Checksum, ObservedAt: value.ObservedAt, SafeSummary: scumSafeSummaryToDomain(value.SafeSummary)}
|
||||
}
|
||||
+13
-16
@@ -309,21 +309,18 @@ type JobExecutionInput struct {
|
||||
Deployment *domain.ServerDeploymentDefinition `json:"deployment,omitempty" db:"deployment"`
|
||||
// ServerDeploymentPlan is the legacy generic deployment-plan payload.
|
||||
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 {
|
||||
Kind string `json:"kind,omitempty" db:"kind"`
|
||||
ProcessState string `json:"processState,omitempty" db:"process_state"`
|
||||
ExitClassification string `json:"exitClassification,omitempty" db:"exit_classification"`
|
||||
ExitCode int `json:"exitCode,omitempty" db:"exit_code"`
|
||||
Version int `json:"version,omitempty" db:"version"`
|
||||
Checksum string `json:"checksum,omitempty" db:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty" db:"size_bytes"`
|
||||
AuditSummary string `json:"auditSummary,omitempty" db:"audit_summary"`
|
||||
Content string `json:"content,omitempty" db:"content"`
|
||||
SQLiteSchemaProbe *domain.SCUMSchemaProbeResult `json:"sqliteSchemaProbe,omitempty" db:"sqlite_schema_probe"`
|
||||
Kind string `json:"kind,omitempty" db:"kind"`
|
||||
ProcessState string `json:"processState,omitempty" db:"process_state"`
|
||||
ExitClassification string `json:"exitClassification,omitempty" db:"exit_classification"`
|
||||
ExitCode int `json:"exitCode,omitempty" db:"exit_code"`
|
||||
Version int `json:"version,omitempty" db:"version"`
|
||||
Checksum string `json:"checksum,omitempty" db:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty" db:"size_bytes"`
|
||||
AuditSummary string `json:"auditSummary,omitempty" db:"audit_summary"`
|
||||
Content string `json:"content,omitempty" db:"content"`
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
@@ -916,7 +913,7 @@ func executionInputFromDomain(input domain.JobExecutionInput) JobExecutionInput
|
||||
copy := domain.CopyServerDeploymentDefinition(*input.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), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(input.SQLiteSchemaProbe)}
|
||||
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)}
|
||||
}
|
||||
|
||||
func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
|
||||
@@ -925,15 +922,15 @@ func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
|
||||
copy := domain.CopyServerDeploymentDefinition(*input.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), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(input.SQLiteSchemaProbe)}
|
||||
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)}
|
||||
}
|
||||
|
||||
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, SQLiteSchemaProbe: domain.CopySCUMSchemaProbeResultPtr(result.SQLiteSchemaProbe)}
|
||||
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}
|
||||
}
|
||||
|
||||
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, SQLiteSchemaProbe: domain.CopySCUMSchemaProbeResultPtr(result.SQLiteSchemaProbe)}
|
||||
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}
|
||||
}
|
||||
|
||||
func (policy JobRetryPolicy) ToDomain() domain.JobRetryPolicy {
|
||||
|
||||
@@ -119,12 +119,6 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
||||
PluginID: "game.scum",
|
||||
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)
|
||||
@@ -136,13 +130,8 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
||||
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)
|
||||
}
|
||||
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.Deployment.CreateInputs["maxPlayers"] = "128"
|
||||
source.SQLiteSchemaProbe.Bounds.MaxObjects = domain.DefaultSCUMSchemaProbeBounds().MaxObjects
|
||||
row.Inputs["playerId"] = "row-mutated"
|
||||
if source.Inputs["playerId"] != "steam-123" {
|
||||
t.Fatalf("expected source execution inputs to be isolated from model mutation, source=%+v", source.Inputs)
|
||||
@@ -151,13 +140,8 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
||||
if source.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||
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.Deployment.CreateInputs["maxPlayers"] = "128"
|
||||
row.SQLiteSchemaProbe.Bounds.MaxObjects = domain.DefaultSCUMSchemaProbeBounds().MaxObjects
|
||||
|
||||
roundTrip := row.ToDomain()
|
||||
if !reflect.DeepEqual(roundTrip, source) {
|
||||
@@ -165,8 +149,7 @@ func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T)
|
||||
}
|
||||
roundTrip.Inputs["playerId"] = "mutated"
|
||||
roundTrip.Deployment.CreateInputs["maxPlayers"] = "16"
|
||||
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 {
|
||||
if source.Inputs["playerId"] != "steam-123" || row.Inputs["playerId"] != "steam-123" || source.Deployment.CreateInputs["maxPlayers"] != "128" || row.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||
t.Fatalf("expected execution inputs to round-trip without aliasing, source=%+v row=%+v", source, row)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +86,6 @@ Named log DTOs:
|
||||
|
||||
Log ingest supports bounded batches, sequence ranges, checksum validation, retry-safe duplicate acknowledgement, latest sequence tracking, cursor query, and browser SSE fan-out from already-ingested platform logs. Log payloads must not carry artifact chunks, host paths, raw credentials, direct sockets, or unbounded inline data.
|
||||
|
||||
Plugin-declared parsed-log batches use the signed job-result channel for typed `log.parsed-events` terminal envelopes when a bounded backfill/replay job is leased, while ordinary log bodies continue to use `/run/logs/batches`. The parsed-log envelope carries only logical source/stream/parser identity, redacted source identity digest, stream generation, sequence cursor, logical event digest, payload digest, sanitized scalar payload, tail state, safe error, and applied limits. It must not carry raw log lines, paths, globs, network identifiers, sockets, credentials, SQL, XML, or game-specific executor branch data.
|
||||
|
||||
Run-assigned Platform jobs use `job.<jobId>.<streamKey>` log stream IDs. Autonomous lifecycle bootstrap is Run-owned machine execution rather than a Platform job, so its durable process logs use `run.<runEndpointId>.<serverInstanceId>.<streamKey>`. Platform may auto-create those streams only after validating the active Run session and the server-to-Run binding. For retry compatibility, legacy spooled `job.autonomous-*.<streamKey>` batches are accepted as Run-owned autonomous streams without creating or completing a Platform job.
|
||||
|
||||
Log ingest is durable and independently retried. Artifact/file transfer backlog must not prevent log batch acknowledgement, duplicate acknowledgement, cursor state updates, or spool cleanup.
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# SCUM Live Data Contracts
|
||||
|
||||
This contract replaces SCUM projection/Workflow-facing reads with evidence-gated local management data. It is intentionally generic at the Run boundary: Platform and plugins may name SCUM capabilities, but Run receives only packaged generic SQLite probe/template/mutation work and never hardcodes SCUM table names, command keys, host paths, sockets, credentials, or browser-supplied SQL.
|
||||
|
||||
## Capability gate
|
||||
|
||||
Every database-backed SCUM read/write capability is disabled until all of the following are true for the active server binding:
|
||||
|
||||
- the bound Run advertises `remote.run.db.sqlite.probe`;
|
||||
- Platform has a current `SCUMCapabilityEvidence` row for the exact server instance, Run binding, Run endpoint, plugin id/version, adapter version, game version, and database identity;
|
||||
- evidence status is `compatible` for the requested capability;
|
||||
- the evidence schema fingerprint equals the versioned adapter requirement;
|
||||
- every required packaged asset digest is present in the evidence;
|
||||
- evidence has not expired or been invalidated by rebinding, database identity change, plugin version change, adapter version change, or schema fingerprint change.
|
||||
|
||||
If any condition fails, APIs and plugin pages receive a safe disabled state such as `probe_missing`, `probe_executor_absent`, `binding_mismatch`, `fingerprint_mismatch`, `digest_mismatch`, `schema_incompatible`, or `evidence_expired`. Disabled states are ordinary availability results, not projection/audit/workflow work items.
|
||||
|
||||
Platform evaluates the full capability set through a read-only negotiation step for the active server instance. The negotiation combines the current server/plugin/Run endpoint/runtime binding, the plugin's per-capability requirements, the bound Run capability list, and the latest accepted typed terminal evidence for the same binding. It returns independent gates for schema probe, read-only SQLite-backed player/squad/vehicle/flag/position reads, typed RCON economy/gift commands, and guarded XML mutations. The negotiation route never dispatches a Run job and never treats another Run binding, plugin version, adapter version, schema fingerprint, database identity, or asset digest as compatible evidence.
|
||||
|
||||
## Probe request
|
||||
|
||||
`SCUMSchemaProbeRequest` is a Platform durable-job payload addressed through the active authenticated Run binding.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `bounds`: `maxObjects`, `maxColumnsPerObject`, `maxIndexesPerObject`, `maxForeignKeys`, `maxCardinalityReads`, `maxSampleRows`, `timeoutMs`, `maxResultBytes`;
|
||||
- `requestedAt`.
|
||||
|
||||
The payload must not include a host database path, DSN, socket, credential, raw SQL text, raw rows, or SCUM-specific table names. Target resolution happens inside the active Run package from logical bindings only.
|
||||
|
||||
## Probe result
|
||||
|
||||
`SCUMSchemaProbeResult` returns only redacted schema evidence:
|
||||
|
||||
- request/job/binding identity;
|
||||
- status: `missing`, `compatible`, `incompatible`, or `failed`;
|
||||
- schema fingerprint and result digest;
|
||||
- bounded object metadata with object/name/column/index/fk fingerprints, declared types, nullable/primary-key flags, approximate row counts, and sample fingerprints;
|
||||
- safe error code/message when failed;
|
||||
- limits actually applied.
|
||||
|
||||
Samples are hashes/fingerprints only. Raw row content, XML payloads, SQL, paths, DSNs, sockets, credentials, host names, IPs, and RCON text are never returned to Platform Web, plugin pages, AI prompts, or safe diagnostic fields.
|
||||
|
||||
## SQLite template request
|
||||
|
||||
`SCUMSQLiteTemplateRequest` is the Platform durable-job payload for read-only plugin-owned query assets after a capability-specific gate is compatible. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `capability`, limited to database-backed read capabilities such as player, squad, vehicle, flag, and position reads;
|
||||
- logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, and canonical `parameterDigest`;
|
||||
- scalar `parameters` bounded by `maxParameters` and validated against the plugin-declared parameter schema;
|
||||
- `bounds`: `maxParameters`, `maxRows`, `timeoutMs`, `busyTimeoutMs`, and `maxResultBytes`;
|
||||
- `requestedAt`.
|
||||
|
||||
The request never contains raw SQL, host/database paths, DSNs, sockets, credentials, raw XML, RCON text, browser-supplied table names, or undeclared parameters. Run resolves the logical target and packaged template inside the generated Run package.
|
||||
|
||||
## SQLite template result
|
||||
|
||||
`SCUMSQLiteTemplateResult` is the terminal envelope for `sqlite.template-query` results. Required fields are request/job/binding identity, status (`succeeded`, `failed`, or `cancelled`), read capability, target/template key, adapter version, schema fingerprint, asset digest, parameter digest, source fingerprint, observed time, result digest, row count, bounded rows, truncation flag, safe error, and limits actually applied.
|
||||
|
||||
Platform accepts rows only when the terminal envelope matches the leased durable job's binding, template key, adapter/schema fingerprint, asset digest, and parameter digest. Late, duplicate, mismatched, stale, unsafe, over-limit, or schema-invalid results remain safe terminal failures and must not be converted into empty successful generations.
|
||||
|
||||
## Typed RCON template request
|
||||
|
||||
`SCUMTypedRCONTemplateRequest` is the Platform durable-job payload for plugin-owned command templates after a write capability is proven and reviewed. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `capability`, limited to verified typed RCON write capabilities such as economy-command or gift-command writes;
|
||||
- logical `transportKey`, `targetKey`, `templateKey`, `adapterVersion`, optional `requiredSchemaFingerprint`, immutable `assetDigest`, canonical `payloadDigest`, `confirmationDigest`, and `targetIdentityDigest`;
|
||||
- `idempotencyKey`, scalar `payload` validated against the plugin-declared payload schema, and safe `reviewReason`;
|
||||
- `bounds`: `maxPayloadBytes`, `timeoutMs`, `maxResponseBytes`, and `maxConfirmRecords`;
|
||||
- `requestedAt`.
|
||||
|
||||
The request never contains browser command text, raw RCON, SQL, XML, host/database paths, DSNs, sockets, credentials, or undeclared command keys. Run resolves the template and protected RCON transport inside the generated Run package.
|
||||
|
||||
## Typed RCON template result
|
||||
|
||||
`SCUMTypedRCONTemplateResult` is the terminal envelope for `rcon.template-command` results. Required fields are request/job/binding identity, status (`succeeded`, `failed`, or `cancelled`), write capability, transport/target/template key, adapter version, schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, observed time, result digest, response digest, confirmation status, confirmation digest id, safe summary, safe error, and limits actually applied.
|
||||
|
||||
Platform accepts write success only when the envelope matches the leased durable job and the declared confirmation status is conclusive. Missing, mismatched, stale, unsafe, partial, timed-out, cancelled, or schema-invalid confirmations remain failed or unknown outcomes; they must not update local verified facts or trigger automatic redelivery.
|
||||
|
||||
## Parsed log batch result
|
||||
|
||||
`SCUMParsedLogBatchResult` is the terminal envelope for `log.parsed-events` batches produced from plugin-declared log-source tailing or backfill. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity` for active-service fencing;
|
||||
- status (`succeeded`, `failed`, or `cancelled`), logical `sourceKey`, `streamKey`, `parserKey`, `parserVersion`, `adapterVersion`, immutable parser `assetDigest`, `parserDigest`, observed time, and `resultDigest`;
|
||||
- `firstCursor` and `lastCursor` containing `sourceIdentityDigest`, `streamGeneration`, and `sequence`;
|
||||
- `tailState`, limited to `advanced`, `rotated`, `truncated`, `restarted`, `partial-buffered`, or `replayed`, plus `partialLineBuffered` and `replay` flags;
|
||||
- `eventCount`, bounded sanitized events, safe summary, safe error, and limits: `maxEvents`, `maxPayloadBytes`, `maxLineBytes`, and `maxResultBytes`.
|
||||
|
||||
Each parsed event contains only event type, occurrence time, the transport cursor, privacy-safe `logicalEventDigest`, `eventDigest`, `payloadDigest`, and schema-safe scalar payload values. The envelope never contains raw log lines, raw IP/network identifiers, host paths, resolved file names, glob patterns, sockets, credentials, SQL, XML, or unredacted player/network identities.
|
||||
|
||||
Platform accepts a parsed-log success only when the envelope matches the leased job, server/Run binding, declared source/stream key, frozen parser key/version/digest when present, and a single redacted source identity/generation boundary. Replayed logical events are handled by the later ingestion layer through `logicalEventDigest`; transport cursor replay or rotation overlap must not by itself create duplicate players or sessions.
|
||||
|
||||
## Guarded mutation request
|
||||
|
||||
`SCUMGuardedMutationRequest` is the Platform durable-job payload for plugin-owned single-row SQLite/XML mutation templates after the mutation capability is proven, reviewed, and explicitly confirmed. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `capability`, limited to guarded database/XML write capabilities such as `profile-xml.write`;
|
||||
- logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, `targetIdentityDigest`, `expectedRowDigest`, `expectedValueDigest`, `expectedXmlDigest`, `patchDigest`, `backupEvidenceDigest`, `offlineEvidenceDigest`, `dangerConfirmationDigest`, and `readbackExpectationDigest`;
|
||||
- `idempotencyKey`, scalar `payload` validated against the plugin-declared payload schema, and safe `reviewReason`;
|
||||
- `bounds`: `maxPayloadBytes`, `timeoutMs`, `busyTimeoutMs`, `maxReadbackBytes`, and `maxAffectedRows`, which must equal `1`;
|
||||
- `requestedAt`.
|
||||
|
||||
The request never contains raw SQL, raw XML, browser mutation text, host/database paths, DSNs, sockets, credentials, table/column overrides, raw row payloads, `fieldKey=855`, `prisoner.value`, or undeclared patch fields. Run resolves the logical target and packaged preserving patch template inside the generated Run package.
|
||||
|
||||
## Guarded mutation result
|
||||
|
||||
`SCUMGuardedMutationResult` is the terminal envelope for `sqlite.guarded-mutation` results. Required fields are request/job/binding identity, status (`succeeded`, `failed`, or `cancelled`), write capability, target/template key, adapter version, schema fingerprint, asset digest, source fingerprint when succeeded, target identity digest, expected row/value/XML digests, patch digest, backup/offline/danger-confirmation digests, readback expectation digest, observed time, result digest, before/after/readback digests when succeeded, affected-row count, readback status, safe summary, safe error, and limits actually applied.
|
||||
|
||||
Platform accepts mutation success only when the terminal envelope matches the leased durable job and the declared binding/template/schema/asset/target/guard/patch/backup/offline/confirmation/readback digests, `affectedRows` is exactly `1`, and readback is `confirmed`. Zero rows, multiple rows, stale expected values, schema or source changes, malformed XML, absent named nodes, rollback, missing backup/offline/danger confirmation, missing readback, or unsafe summaries remain failed/conflict/unknown outcomes and must not update local verified facts.
|
||||
|
||||
## Release behavior
|
||||
|
||||
The first-party SCUM plugin declares `scumLiveData` with `remote.run.db.sqlite.probe` and per-capability gates. Until current-service evidence exists, all gates remain `disabled` with `evidenceStatus: missing`. Query assets, RCON templates, XML mutations, map transforms, and gift transports may be added only after current-service probe evidence proves their adapter requirements; unsupported or ambiguous capabilities stay disabled independently.
|
||||
@@ -1,77 +1,67 @@
|
||||
# SCUM Run Integration Contract
|
||||
|
||||
This repository owns the Platform/plugin side of SCUM real-data operations. The machine-side executor remains the independent `git@git.npc0.com:admin343/run.git` repository, and no `run/` source tree or SCUM-specific executor branch belongs in this repository.
|
||||
This repository defines the platform/plugin side of SCUM real-data operations. The executable machine-side implementation belongs in the independent `git@git.npc0.com:admin343/run.git` repository and must not be added here.
|
||||
|
||||
## Ownership Boundary
|
||||
|
||||
- Platform owns server instances, target-server authorization, durable jobs, local SCUM records, capability evidence, generated Run package inputs, safe browser APIs, idempotency, and internal write confirmation evidence.
|
||||
- The SCUM plugin owns versioned parser declarations, SQLite template assets, result schemas, schema-adapter compatibility, map metadata, typed command templates, gift catalogs, and guarded mutation declarations.
|
||||
- Run owns generic machine-side execution beside the current bound service: resolving package-scoped logical targets, enforcing declared capabilities, executing bounded jobs, supervising declared log sources, and returning terminal envelopes through the existing signed channels.
|
||||
- Platform owns server instances, authorization, audit, local projections, typed operation/workflow records, idempotency, approval state, and safe browser APIs.
|
||||
- The SCUM plugin owns query template keys, operation template keys, result schemas, safety rules, confirmation schemas, and lifecycle action assets.
|
||||
- Run owns local machine execution beside the current SCUM service: locating the declared logical SCUM.db/log/RCON targets from its scoped package, executing bounded jobs, and returning typed results through existing signed job channels.
|
||||
|
||||
Run and Platform Web must never receive or expose raw SQL, raw RCON text, raw XML, host/database paths, DSNs, sockets, credentials, raw row content, IP/network material, or arbitrary browser-supplied execution payloads.
|
||||
Run must never send host paths, DSNs, sockets, credentials, raw SQL, raw RCON text, or protected request bodies to browser/product APIs. Platform persists only safe job metadata, projection rows, checksums, confirmation summaries, and audit references.
|
||||
|
||||
## Capability Gate
|
||||
## Read Observation Jobs
|
||||
|
||||
Every database-backed SCUM read or write capability stays disabled until the active Run binding reports compatible current-service evidence for that exact server, endpoint, binding, plugin version, adapter version, game version, database identity, schema fingerprint, and asset digest set.
|
||||
Run must implement plugin-declared SQLite read templates for the current server binding and return rows matching the referenced schema files under `plugins/examples/scum-server-plugin/schemas/bridge/queries/`.
|
||||
|
||||
Disabled capability results are ordinary safe availability states such as `probe_executor_absent`, `probe_missing`, `schema_incompatible`, `binding_mismatch`, `fingerprint_mismatch`, `digest_mismatch`, or `evidence_expired`. They are not Workflow, observation, projection, audit-initiation, or manual-refresh states.
|
||||
Required template keys:
|
||||
|
||||
## Schema Probe Jobs
|
||||
| Key | Required behavior |
|
||||
| --- | --- |
|
||||
| `scum.player.profile` | Read player identity, profile ID, optional Steam/user ID, character/prisoner fields, economy balances, squad summary, and current coordinates where available. |
|
||||
| `scum.squads` | Read squad IDs, names, leader/profile references, and bounded member counts. |
|
||||
| `scum.squad-members` | Read roster membership, ranks, player/profile references, and unknown fields without fabricating missing identities. |
|
||||
| `scum.vehicles` | Read vehicle/entity rows and coordinates; unknown class/name mappings remain unknown. |
|
||||
| `scum.flags` | Read base flag/entity ownership, squad/player confidence, and coordinates where available. |
|
||||
| `scum.positions` | Read current player, vehicle, and flag coordinate projections. |
|
||||
|
||||
Platform may dispatch a schema probe only as a durable job through the active authenticated Run binding. The probe payload contains a logical target key, binding identity, timeout/row/result bounds, and no SCUM table names, database path, SQL text, row values, XML, credentials, or host identifiers.
|
||||
Each successful result must include the server binding, template key, observed time, monotonically comparable sequence, row count within manifest bounds, and `sha256:<hex>` checksum. Failures must return safe error codes such as missing database, locked database, schema mismatch, timeout, or row-bound exceeded; platform will mark affected projections stale while keeping last-known-good records.
|
||||
|
||||
Run executes the generic `remote.run.db.sqlite.probe` capability against the package-resolved current database or a short-lived read-only snapshot fenced to the same binding/database identity. The terminal result returns only redacted object, column, index, foreign-key, approximate cardinality, and sample fingerprints with the applied limits and a safe status.
|
||||
Login/logout evidence comes from plugin-declared log sources. A login line can create/update a local player/session projection; `last_save_time` is only freshness evidence and must not be treated as online-state proof by itself.
|
||||
|
||||
## Read-Only SQLite Template Jobs
|
||||
## Controlled Write Jobs
|
||||
|
||||
After probe evidence matches a plugin adapter, Platform can schedule plugin-owned read-only SQLite template jobs by template key, adapter/schema version, immutable asset digest, and bounded parameters. Platform does not build SQL strings, and the browser never submits query text or undeclared parameters.
|
||||
Run must execute only typed operations declared by the SCUM plugin manifest.
|
||||
|
||||
The leased Run assignment carries a typed `sqliteTemplate` request only. Required fields are `requestId`, server/plugin binding, read capability, logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, canonical `parameterDigest`, bounded scalar `parameters`, and `limits` containing `maxParameters`, `maxRows`, `timeoutMs`, `busyTimeoutMs`, and `maxResultBytes`. The payload carries no SQL text, table names from the browser, database path, DSN, socket, credential, raw XML, RCON text, or host identifier.
|
||||
| Operation key | Transport | Required behavior |
|
||||
| --- | --- | --- |
|
||||
| `player.fame.set` | RCON | Use the declared command template for fame and confirm through follow-up readback. |
|
||||
| `player.currency.normal.set` | RCON | Use the declared command template for normal currency and confirm through follow-up readback. |
|
||||
| `player.currency.gold.set` | RCON | Use the declared command template for gold and confirm through follow-up readback. |
|
||||
| `player.notify` | RCON/declared notification command | Deliver bounded player notification text and report unknown if delivery cannot be proven. |
|
||||
| `reward.deliver` | Declared reward transport | Deliver catalogued reward/notification only once per idempotency key and confirmation state. |
|
||||
| `player.attribute.855.set` | SQLite mutation | Execute the declared DB-only mutation with before-value guard, max affected rows = 1, maintenance/offline evidence, backup/snapshot reference, and confirmation query. |
|
||||
|
||||
Run verifies the packaged asset digest, adapter/schema fingerprint, canonical parameter digest, and active binding before opening a query-only SQLite connection or fenced short-lived read-only snapshot. It enforces one approved read-only statement or introspection boundary, bound parameters, short busy/operation timeouts, cancellation, row/result-byte limits, and rejects DDL, mutation, `ATTACH`, extension loading, write PRAGMAs, multi-statement input, and string-concatenated parameters.
|
||||
RCON-supported fame/currency writes must not be converted to DB mutations. DB-only mutations must fail safely when the current value differs from the approved `before` value, the affected row bound is exceeded, backup evidence is missing, or the player safety state is online/unknown.
|
||||
|
||||
The terminal `sqlite.template-query` envelope contains `requestId`, `jobId`, binding, status (`succeeded`, `failed`, or `cancelled`), capability, target/template key, adapter version, schema fingerprint, asset digest, parameter digest, source fingerprint, observed time, result digest, row count, bounded rows, truncation flag, safe error, and applied limits. Platform validates the envelope against the original durable job, lease attempt, binding, template key, schema fingerprint, asset digest, and parameter digest before any local SCUM generation can consume the rows.
|
||||
## Result And Confirmation Contract
|
||||
|
||||
## Parsed Log Event Jobs
|
||||
Run job results for SCUM reads, RCON writes, and SQLite mutations must return:
|
||||
|
||||
Login/logout ingestion starts from plugin-declared log sources and versioned parser assets. Each parsed event carries server/plugin/parser identity, transport cursor `(source identity, stream generation, sequence)`, a separate privacy-safe logical event identity, and occurrence time.
|
||||
- `kind` identifying the declared result type.
|
||||
- `checksum` as `sha256:<64 hex chars>`.
|
||||
- Bounded JSON content matching the plugin result/confirmation schema.
|
||||
- `affectedRows` for mutations and zero/one row confirmation details where applicable.
|
||||
- A safe audit summary that excludes raw SQL, raw RCON text, SCUM.db paths, host paths, tokens, sockets, and credentials.
|
||||
|
||||
Run and Platform discard raw IP addresses and other network identifiers before durable storage or logical fingerprinting. Malformed, failed-login, obsolete-binding, duplicate, or out-of-order events must not fabricate players or sessions.
|
||||
If execution may have happened but confirmation is missing, run should report an unknown/pending-confirmation state rather than success. Platform will read back before retrying so gifts, currency, fame, and DB fields are not duplicated or overwritten.
|
||||
|
||||
For plugin-declared file-tail backfill or replay, the leased Run assignment carries the frozen declared `logSource` and may additionally freeze `parserKey`, `parserVersion`, `parserDigest`, and `adapterVersion` as safe scalar execution inputs. The log source contains only package logical `sourceKey`/`targetKey`/`streamKey` metadata, cursor kind, and retention policy. It never contains the resolved host log path, glob, socket, credential, network endpoint, or raw line material.
|
||||
## External Run Tasks
|
||||
|
||||
The terminal `log.parsed-events` envelope contains request/job identity, server/plugin binding, status, source/stream key, parser key/version, adapter version, immutable parser asset digest, parser digest, observed time, result digest, first/last transport cursors with redacted `sourceIdentityDigest`, stream generation, sequence, tail state (`advanced`, `rotated`, `truncated`, `restarted`, `partial-buffered`, or `replayed`), partial-line and replay flags, event count, bounded sanitized events, safe summary, safe error, and applied limits. Each event carries event type, occurrence time, transport cursor, logical event digest, event digest, payload digest, and schema-safe scalar payload values only.
|
||||
The independent run repository needs implementation work for:
|
||||
|
||||
Platform accepts the parsed-log envelope only when it matches the leased `logs.backfill` job, active server/Run endpoint, plugin id/version when frozen, declared source/stream key, parser key/version/digest when frozen, and a single source identity/generation boundary. Parser digests, source identity, stream generation, logical event digest, and payload digest are fingerprints; raw log lines, IP/network values, paths, SQL, XML, sockets, credentials, and player identities not already redacted are rejected before local ingestion can use the batch.
|
||||
|
||||
## Typed RCON Template Jobs
|
||||
|
||||
SCUM command writes use only plugin-owned typed command templates. Platform dispatches a template key, adapter version, digest, target identity, idempotency key, validated parameters, and review reason through the durable job channel.
|
||||
|
||||
The leased Run assignment carries a typed `rconTemplate` request only. Required fields are `requestId`, server/plugin binding, write capability, logical `transportKey`, logical `targetKey`, `templateKey`, `adapterVersion`, optional `requiredSchemaFingerprint`, immutable `assetDigest`, canonical `payloadDigest`, `confirmationDigest`, `targetIdentityDigest`, idempotency key, bounded scalar payload, review reason, and limits containing `maxPayloadBytes`, `timeoutMs`, `maxResponseBytes`, and `maxConfirmRecords`. The payload carries no browser command text, raw RCON, SQL, XML, host path, socket, credential, or undeclared command key.
|
||||
|
||||
Run resolves the packaged command template and protected RCON transport from the generated Run package, verifies the asset/payload/confirmation digests and active binding, renders only the packaged template with bound scalar payload values, executes through generic protected RCON, and performs only the declared confirmation path. Run never accepts browser command text, exposes the rendered command in result envelopes, or branches on SCUM command names, SCUM keys, SCUM commands, SCUM tables, or gift/economy semantics.
|
||||
|
||||
The terminal `rcon.template-command` envelope contains `requestId`, `jobId`, binding, status (`succeeded`, `failed`, or `cancelled`), capability, transport/target/template key, adapter version, schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, observed time, result digest, response digest, confirmation status, confirmation digest id, safe summary, safe error, and applied limits. A write is successful only after the declared confirmation path returns schema-valid conclusive evidence; missing, partial, mismatched, cancelled, or timed-out confirmation is reported as failed, partial, or unknown rather than success.
|
||||
|
||||
## Guarded SQLite/XML Mutation Jobs
|
||||
|
||||
Database/XML writes are disabled until current-service evidence proves the source row, XML field, named attribute mapping, backup/offline safety requirements, and preserving patch contract. `855` is never an executable field key; it may only be a reviewed preset label that expands to explicit named attributes after the mapping is confirmed.
|
||||
|
||||
Platform dispatches guarded mutations only with effective `server.game-client.maintenance`, explicit danger confirmation, target identity, expected before values/checksum, same-instance backup evidence, idempotency key, reason, adapter/digest, and declared safety requirements. Run performs one bounded transaction, updates exactly one guarded row, preserves untargeted XML content, rolls back on zero/multiple affected rows or conflicts, and performs read-after-write confirmation before any success result.
|
||||
|
||||
The leased Run assignment carries a typed `guardedMutation` request only. Required fields are `requestId`, server/plugin binding, `profile-xml.write` capability, logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, `targetIdentityDigest`, `expectedRowDigest`, `expectedValueDigest`, `expectedXmlDigest`, `patchDigest`, `backupEvidenceDigest`, `offlineEvidenceDigest`, `dangerConfirmationDigest`, `readbackExpectationDigest`, idempotency key, bounded scalar payload, review reason, and limits containing `maxPayloadBytes`, `timeoutMs`, `busyTimeoutMs`, `maxReadbackBytes`, and `maxAffectedRows=1`. The payload carries no raw SQL, raw XML, database path, table/column override, `855` field key, browser mutation text, host path, socket, credential, or undeclared patch field.
|
||||
|
||||
The terminal `sqlite.guarded-mutation` envelope contains `requestId`, `jobId`, binding, status (`succeeded`, `failed`, or `cancelled`), capability, target/template key, adapter/schema fingerprint, asset digest, source fingerprint, target identity digest, expected row/value/XML digests, patch digest, backup/offline/danger-confirmation digests, readback expectation digest, observed time, result digest, before/after/readback digests, affected-row count, readback status, safe summary, safe error, and applied limits. Platform accepts success only when the envelope matches the leased job and binding, `affectedRows` is exactly `1`, and `readbackStatus` is `confirmed`; zero/multiple rows, guard mismatches, malformed XML, missing backup/offline/danger confirmation, missing readback, or stale schema remain safe failed/conflict/unknown results.
|
||||
|
||||
Saving attributes must never implicitly kill, respawn, kick, or otherwise activate destructive game behavior. Any verified required activation is a separate permission-checked and explicitly confirmed typed command.
|
||||
|
||||
## Terminal Result Envelope
|
||||
|
||||
Every probe, read template, typed command, parsed-log batch, or guarded mutation result returns a typed terminal envelope containing server/plugin binding, adapter/schema version, template/action/parser key, asset digest, job identity, observed time, checksum/result digest, row or affected-row count where applicable, and a stable safe result/error code.
|
||||
|
||||
Platform validates the envelope against the original durable job before updating local SCUM records or write-confirmation state. Late, duplicate, foreign, stale, incompatible, or unsafe results are rejected idempotently while preserving the last completed local generation.
|
||||
|
||||
## External Run Evidence Required
|
||||
|
||||
The independent Run repository still needs separately authorized implementation and verification evidence for generic schema probing, packaged SQLite-template execution, typed RCON execution, guarded SQLite/XML mutation execution, plugin-declared log-source tailing, and terminal-envelope fencing. This browser repository must record that tested Run commit/deployment evidence before enabling database-backed adapters, adding production query/mutation assets, or marking the real-service verification tasks complete.
|
||||
1. Resolve package-scoped logical SCUM.db and log targets from the generated run plan without exposing resolved host paths to Platform Web.
|
||||
2. Execute the six declared SQLite read templates with row/time bounds and schema-compatible JSON rows.
|
||||
3. Execute typed RCON operation templates for fame, currency, notification, and reward delivery without accepting arbitrary browser command text.
|
||||
4. Execute `player.attribute.855.set` through a guarded SQLite mutation with backup, maintenance/offline checks, before-value match, affected-row bound, and confirmation read.
|
||||
5. Report observation failures and write unknown states with safe codes and checksums so platform projections and workflows can reconcile deterministically.
|
||||
|
||||
@@ -53,6 +53,16 @@ type StoreSnapshot struct {
|
||||
GameGiftCatalogs []domain.GameGiftCatalog `json:"gameGiftCatalogs"`
|
||||
GameGiftRevisions []domain.GameGiftRevision `json:"gameGiftRevisions"`
|
||||
GameGiftGrants []domain.GameGiftGrant `json:"gameGiftGrants"`
|
||||
SCUMDataObservations []domain.SCUMDataObservation `json:"scumDataObservations"`
|
||||
SCUMPlayerLiveStates []domain.SCUMPlayerLiveState `json:"scumPlayerLiveStates"`
|
||||
SCUMSquads []domain.SCUMSquad `json:"scumSquads"`
|
||||
SCUMSquadMembers []domain.SCUMSquadMember `json:"scumSquadMembers"`
|
||||
SCUMVehicles []domain.SCUMVehicle `json:"scumVehicles"`
|
||||
SCUMFlags []domain.SCUMFlag `json:"scumFlags"`
|
||||
SCUMCurrentPositions []domain.SCUMCurrentPosition `json:"scumCurrentPositions"`
|
||||
SCUMOperationRequests []domain.SCUMOperationRequest `json:"scumOperationRequests"`
|
||||
SCUMWorkflowInstances []domain.SCUMWorkflowInstance `json:"scumWorkflowInstances"`
|
||||
SCUMWorkflowSteps []domain.SCUMWorkflowStep `json:"scumWorkflowSteps"`
|
||||
}
|
||||
|
||||
type FileStore struct {
|
||||
@@ -234,6 +244,36 @@ func (store *FileStore) GameGiftRevisions() GameGiftRevisionRepository {
|
||||
func (store *FileStore) GameGiftGrants() GameGiftGrantRepository {
|
||||
return &persistentRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]{repository: store.MemoryStore.gameGiftGrants, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMDataObservations() SCUMDataObservationRepository {
|
||||
return &persistentRepository[domain.SCUMDataObservation, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumDataObservations, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMPlayerLiveStates() SCUMPlayerLiveStateRepository {
|
||||
return &persistentRepository[domain.SCUMPlayerLiveState, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumPlayerLiveStates, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMSquads() SCUMSquadRepository {
|
||||
return &persistentRepository[domain.SCUMSquad, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumSquads, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMSquadMembers() SCUMSquadMemberRepository {
|
||||
return &persistentRepository[domain.SCUMSquadMember, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumSquadMembers, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMVehicles() SCUMVehicleRepository {
|
||||
return &persistentRepository[domain.SCUMVehicle, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumVehicles, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMFlags() SCUMFlagRepository {
|
||||
return &persistentRepository[domain.SCUMFlag, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumFlags, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMCurrentPositions() SCUMCurrentPositionRepository {
|
||||
return &persistentRepository[domain.SCUMCurrentPosition, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumCurrentPositions, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMOperationRequests() SCUMOperationRequestRepository {
|
||||
return &persistentRepository[domain.SCUMOperationRequest, domain.SCUMOperationRequestFilter]{repository: store.MemoryStore.scumOperationRequests, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMWorkflowInstances() SCUMWorkflowInstanceRepository {
|
||||
return &persistentRepository[domain.SCUMWorkflowInstance, domain.SCUMWorkflowInstanceFilter]{repository: store.MemoryStore.scumWorkflowInstances, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMWorkflowSteps() SCUMWorkflowStepRepository {
|
||||
return &persistentRepository[domain.SCUMWorkflowStep, domain.SCUMWorkflowStepFilter]{repository: store.MemoryStore.scumWorkflowSteps, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) load() error {
|
||||
data, err := os.ReadFile(store.path)
|
||||
@@ -307,7 +347,7 @@ func (store *FileStore) snapshot() StoreSnapshot {
|
||||
GameClientBridgeCommands: snapshotRepository(store.MemoryStore.bridgeCommands.memoryRepository),
|
||||
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
|
||||
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
|
||||
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
|
||||
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants), SCUMDataObservations: snapshotRepository(store.MemoryStore.scumDataObservations), SCUMPlayerLiveStates: snapshotRepository(store.MemoryStore.scumPlayerLiveStates), SCUMSquads: snapshotRepository(store.MemoryStore.scumSquads), SCUMSquadMembers: snapshotRepository(store.MemoryStore.scumSquadMembers), SCUMVehicles: snapshotRepository(store.MemoryStore.scumVehicles), SCUMFlags: snapshotRepository(store.MemoryStore.scumFlags), SCUMCurrentPositions: snapshotRepository(store.MemoryStore.scumCurrentPositions), SCUMOperationRequests: snapshotRepository(store.MemoryStore.scumOperationRequests), SCUMWorkflowInstances: snapshotRepository(store.MemoryStore.scumWorkflowInstances), SCUMWorkflowSteps: snapshotRepository(store.MemoryStore.scumWorkflowSteps),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +392,16 @@ func (store *FileStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.gameGiftCatalogs, snapshot.GameGiftCatalogs)
|
||||
loadRepository(store.MemoryStore.gameGiftRevisions, snapshot.GameGiftRevisions)
|
||||
loadRepository(store.MemoryStore.gameGiftGrants, snapshot.GameGiftGrants)
|
||||
loadRepository(store.MemoryStore.scumDataObservations, snapshot.SCUMDataObservations)
|
||||
loadRepository(store.MemoryStore.scumPlayerLiveStates, snapshot.SCUMPlayerLiveStates)
|
||||
loadRepository(store.MemoryStore.scumSquads, snapshot.SCUMSquads)
|
||||
loadRepository(store.MemoryStore.scumSquadMembers, snapshot.SCUMSquadMembers)
|
||||
loadRepository(store.MemoryStore.scumVehicles, snapshot.SCUMVehicles)
|
||||
loadRepository(store.MemoryStore.scumFlags, snapshot.SCUMFlags)
|
||||
loadRepository(store.MemoryStore.scumCurrentPositions, snapshot.SCUMCurrentPositions)
|
||||
loadRepository(store.MemoryStore.scumOperationRequests, snapshot.SCUMOperationRequests)
|
||||
loadRepository(store.MemoryStore.scumWorkflowInstances, snapshot.SCUMWorkflowInstances)
|
||||
loadRepository(store.MemoryStore.scumWorkflowSteps, snapshot.SCUMWorkflowSteps)
|
||||
}
|
||||
|
||||
type mutableRepository[T any, F any] interface {
|
||||
|
||||
@@ -204,6 +204,36 @@ func (store *MySQLStore) GameGiftRevisions() GameGiftRevisionRepository {
|
||||
func (store *MySQLStore) GameGiftGrants() GameGiftGrantRepository {
|
||||
return &persistentRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]{repository: store.MemoryStore.gameGiftGrants, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMDataObservations() SCUMDataObservationRepository {
|
||||
return &persistentRepository[domain.SCUMDataObservation, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumDataObservations, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMPlayerLiveStates() SCUMPlayerLiveStateRepository {
|
||||
return &persistentRepository[domain.SCUMPlayerLiveState, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumPlayerLiveStates, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMSquads() SCUMSquadRepository {
|
||||
return &persistentRepository[domain.SCUMSquad, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumSquads, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMSquadMembers() SCUMSquadMemberRepository {
|
||||
return &persistentRepository[domain.SCUMSquadMember, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumSquadMembers, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMVehicles() SCUMVehicleRepository {
|
||||
return &persistentRepository[domain.SCUMVehicle, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumVehicles, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMFlags() SCUMFlagRepository {
|
||||
return &persistentRepository[domain.SCUMFlag, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumFlags, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMCurrentPositions() SCUMCurrentPositionRepository {
|
||||
return &persistentRepository[domain.SCUMCurrentPosition, domain.SCUMProjectionFilter]{repository: store.MemoryStore.scumCurrentPositions, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMOperationRequests() SCUMOperationRequestRepository {
|
||||
return &persistentRepository[domain.SCUMOperationRequest, domain.SCUMOperationRequestFilter]{repository: store.MemoryStore.scumOperationRequests, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMWorkflowInstances() SCUMWorkflowInstanceRepository {
|
||||
return &persistentRepository[domain.SCUMWorkflowInstance, domain.SCUMWorkflowInstanceFilter]{repository: store.MemoryStore.scumWorkflowInstances, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMWorkflowSteps() SCUMWorkflowStepRepository {
|
||||
return &persistentRepository[domain.SCUMWorkflowStep, domain.SCUMWorkflowStepFilter]{repository: store.MemoryStore.scumWorkflowSteps, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) initialize() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -294,7 +324,7 @@ func (store *MySQLStore) snapshot() StoreSnapshot {
|
||||
GameClientBridgeCommands: snapshotRepository(store.MemoryStore.bridgeCommands.memoryRepository),
|
||||
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
|
||||
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
|
||||
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
|
||||
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants), SCUMDataObservations: snapshotRepository(store.MemoryStore.scumDataObservations), SCUMPlayerLiveStates: snapshotRepository(store.MemoryStore.scumPlayerLiveStates), SCUMSquads: snapshotRepository(store.MemoryStore.scumSquads), SCUMSquadMembers: snapshotRepository(store.MemoryStore.scumSquadMembers), SCUMVehicles: snapshotRepository(store.MemoryStore.scumVehicles), SCUMFlags: snapshotRepository(store.MemoryStore.scumFlags), SCUMCurrentPositions: snapshotRepository(store.MemoryStore.scumCurrentPositions), SCUMOperationRequests: snapshotRepository(store.MemoryStore.scumOperationRequests), SCUMWorkflowInstances: snapshotRepository(store.MemoryStore.scumWorkflowInstances), SCUMWorkflowSteps: snapshotRepository(store.MemoryStore.scumWorkflowSteps),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,4 +369,14 @@ func (store *MySQLStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.gameGiftCatalogs, snapshot.GameGiftCatalogs)
|
||||
loadRepository(store.MemoryStore.gameGiftRevisions, snapshot.GameGiftRevisions)
|
||||
loadRepository(store.MemoryStore.gameGiftGrants, snapshot.GameGiftGrants)
|
||||
loadRepository(store.MemoryStore.scumDataObservations, snapshot.SCUMDataObservations)
|
||||
loadRepository(store.MemoryStore.scumPlayerLiveStates, snapshot.SCUMPlayerLiveStates)
|
||||
loadRepository(store.MemoryStore.scumSquads, snapshot.SCUMSquads)
|
||||
loadRepository(store.MemoryStore.scumSquadMembers, snapshot.SCUMSquadMembers)
|
||||
loadRepository(store.MemoryStore.scumVehicles, snapshot.SCUMVehicles)
|
||||
loadRepository(store.MemoryStore.scumFlags, snapshot.SCUMFlags)
|
||||
loadRepository(store.MemoryStore.scumCurrentPositions, snapshot.SCUMCurrentPositions)
|
||||
loadRepository(store.MemoryStore.scumOperationRequests, snapshot.SCUMOperationRequests)
|
||||
loadRepository(store.MemoryStore.scumWorkflowInstances, snapshot.SCUMWorkflowInstances)
|
||||
loadRepository(store.MemoryStore.scumWorkflowSteps, snapshot.SCUMWorkflowSteps)
|
||||
}
|
||||
|
||||
@@ -295,6 +295,67 @@ type GameGiftGrantRepository interface {
|
||||
List(domain.GameGiftGrantFilter) ([]domain.GameGiftGrant, error)
|
||||
Update(domain.GameGiftGrant) error
|
||||
}
|
||||
type SCUMDataObservationRepository interface {
|
||||
Create(domain.SCUMDataObservation) error
|
||||
Get(string) (domain.SCUMDataObservation, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMDataObservation, error)
|
||||
Update(domain.SCUMDataObservation) error
|
||||
}
|
||||
type SCUMPlayerLiveStateRepository interface {
|
||||
Create(domain.SCUMPlayerLiveState) error
|
||||
Get(string) (domain.SCUMPlayerLiveState, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMPlayerLiveState, error)
|
||||
Update(domain.SCUMPlayerLiveState) error
|
||||
}
|
||||
type SCUMSquadRepository interface {
|
||||
Create(domain.SCUMSquad) error
|
||||
Get(string) (domain.SCUMSquad, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMSquad, error)
|
||||
Update(domain.SCUMSquad) error
|
||||
}
|
||||
type SCUMSquadMemberRepository interface {
|
||||
Create(domain.SCUMSquadMember) error
|
||||
Get(string) (domain.SCUMSquadMember, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMSquadMember, error)
|
||||
Update(domain.SCUMSquadMember) error
|
||||
}
|
||||
type SCUMVehicleRepository interface {
|
||||
Create(domain.SCUMVehicle) error
|
||||
Get(string) (domain.SCUMVehicle, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMVehicle, error)
|
||||
Update(domain.SCUMVehicle) error
|
||||
}
|
||||
type SCUMFlagRepository interface {
|
||||
Create(domain.SCUMFlag) error
|
||||
Get(string) (domain.SCUMFlag, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMFlag, error)
|
||||
Update(domain.SCUMFlag) error
|
||||
}
|
||||
type SCUMCurrentPositionRepository interface {
|
||||
Create(domain.SCUMCurrentPosition) error
|
||||
Get(string) (domain.SCUMCurrentPosition, error)
|
||||
List(domain.SCUMProjectionFilter) ([]domain.SCUMCurrentPosition, error)
|
||||
Update(domain.SCUMCurrentPosition) error
|
||||
}
|
||||
type SCUMOperationRequestRepository interface {
|
||||
Create(domain.SCUMOperationRequest) error
|
||||
Get(string) (domain.SCUMOperationRequest, error)
|
||||
List(domain.SCUMOperationRequestFilter) ([]domain.SCUMOperationRequest, error)
|
||||
Update(domain.SCUMOperationRequest) error
|
||||
}
|
||||
type SCUMWorkflowInstanceRepository interface {
|
||||
Create(domain.SCUMWorkflowInstance) error
|
||||
Get(string) (domain.SCUMWorkflowInstance, error)
|
||||
List(domain.SCUMWorkflowInstanceFilter) ([]domain.SCUMWorkflowInstance, error)
|
||||
Update(domain.SCUMWorkflowInstance) error
|
||||
}
|
||||
type SCUMWorkflowStepRepository interface {
|
||||
Create(domain.SCUMWorkflowStep) error
|
||||
Get(string) (domain.SCUMWorkflowStep, error)
|
||||
List(domain.SCUMWorkflowStepFilter) ([]domain.SCUMWorkflowStep, error)
|
||||
Update(domain.SCUMWorkflowStep) error
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Users() UserRepository
|
||||
AuthSessions() AuthSessionRepository
|
||||
@@ -336,6 +397,16 @@ type Store interface {
|
||||
GameGiftCatalogs() GameGiftCatalogRepository
|
||||
GameGiftRevisions() GameGiftRevisionRepository
|
||||
GameGiftGrants() GameGiftGrantRepository
|
||||
SCUMDataObservations() SCUMDataObservationRepository
|
||||
SCUMPlayerLiveStates() SCUMPlayerLiveStateRepository
|
||||
SCUMSquads() SCUMSquadRepository
|
||||
SCUMSquadMembers() SCUMSquadMemberRepository
|
||||
SCUMVehicles() SCUMVehicleRepository
|
||||
SCUMFlags() SCUMFlagRepository
|
||||
SCUMCurrentPositions() SCUMCurrentPositionRepository
|
||||
SCUMOperationRequests() SCUMOperationRequestRepository
|
||||
SCUMWorkflowInstances() SCUMWorkflowInstanceRepository
|
||||
SCUMWorkflowSteps() SCUMWorkflowStepRepository
|
||||
}
|
||||
|
||||
type MemoryStore struct {
|
||||
@@ -379,6 +450,16 @@ type MemoryStore struct {
|
||||
gameGiftCatalogs *memoryRepository[domain.GameGiftCatalog, domain.GameGiftCatalogFilter]
|
||||
gameGiftRevisions *memoryRepository[domain.GameGiftRevision, domain.GameGiftRevisionFilter]
|
||||
gameGiftGrants *memoryRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]
|
||||
scumDataObservations *memoryRepository[domain.SCUMDataObservation, domain.SCUMProjectionFilter]
|
||||
scumPlayerLiveStates *memoryRepository[domain.SCUMPlayerLiveState, domain.SCUMProjectionFilter]
|
||||
scumSquads *memoryRepository[domain.SCUMSquad, domain.SCUMProjectionFilter]
|
||||
scumSquadMembers *memoryRepository[domain.SCUMSquadMember, domain.SCUMProjectionFilter]
|
||||
scumVehicles *memoryRepository[domain.SCUMVehicle, domain.SCUMProjectionFilter]
|
||||
scumFlags *memoryRepository[domain.SCUMFlag, domain.SCUMProjectionFilter]
|
||||
scumCurrentPositions *memoryRepository[domain.SCUMCurrentPosition, domain.SCUMProjectionFilter]
|
||||
scumOperationRequests *memoryRepository[domain.SCUMOperationRequest, domain.SCUMOperationRequestFilter]
|
||||
scumWorkflowInstances *memoryRepository[domain.SCUMWorkflowInstance, domain.SCUMWorkflowInstanceFilter]
|
||||
scumWorkflowSteps *memoryRepository[domain.SCUMWorkflowStep, domain.SCUMWorkflowStepFilter]
|
||||
}
|
||||
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
@@ -527,6 +608,16 @@ func NewMemoryStore() *MemoryStore {
|
||||
gameGiftCatalogs: newMemoryRepository(func(v domain.GameGiftCatalog) string { return v.ID }, domain.CopyGameGiftCatalog, matchGameGiftCatalog),
|
||||
gameGiftRevisions: newMemoryRepository(func(v domain.GameGiftRevision) string { return v.ID }, domain.CopyGameGiftRevision, matchGameGiftRevision),
|
||||
gameGiftGrants: newMemoryRepository(func(v domain.GameGiftGrant) string { return v.ID }, domain.CopyGameGiftGrant, matchGameGiftGrant),
|
||||
scumDataObservations: newMemoryRepository(func(v domain.SCUMDataObservation) string { return v.ID }, domain.CopySCUMDataObservation, matchSCUMDataObservation),
|
||||
scumPlayerLiveStates: newMemoryRepository(func(v domain.SCUMPlayerLiveState) string { return v.ID }, domain.CopySCUMPlayerLiveState, matchSCUMPlayerLiveState),
|
||||
scumSquads: newMemoryRepository(func(v domain.SCUMSquad) string { return v.ID }, domain.CopySCUMSquad, matchSCUMSquad),
|
||||
scumSquadMembers: newMemoryRepository(func(v domain.SCUMSquadMember) string { return v.ID }, domain.CopySCUMSquadMember, matchSCUMSquadMember),
|
||||
scumVehicles: newMemoryRepository(func(v domain.SCUMVehicle) string { return v.ID }, domain.CopySCUMVehicle, matchSCUMVehicle),
|
||||
scumFlags: newMemoryRepository(func(v domain.SCUMFlag) string { return v.ID }, domain.CopySCUMFlag, matchSCUMFlag),
|
||||
scumCurrentPositions: newMemoryRepository(func(v domain.SCUMCurrentPosition) string { return v.ID }, domain.CopySCUMCurrentPosition, matchSCUMCurrentPosition),
|
||||
scumOperationRequests: newMemoryRepository(func(v domain.SCUMOperationRequest) string { return v.ID }, domain.CopySCUMOperationRequest, matchSCUMOperationRequest),
|
||||
scumWorkflowInstances: newMemoryRepository(func(v domain.SCUMWorkflowInstance) string { return v.ID }, domain.CopySCUMWorkflowInstance, matchSCUMWorkflowInstance),
|
||||
scumWorkflowSteps: newMemoryRepository(func(v domain.SCUMWorkflowStep) string { return v.ID }, domain.CopySCUMWorkflowStep, matchSCUMWorkflowStep),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,6 +697,30 @@ func (store *MemoryStore) GameGiftRevisions() GameGiftRevisionRepository {
|
||||
return store.gameGiftRevisions
|
||||
}
|
||||
func (store *MemoryStore) GameGiftGrants() GameGiftGrantRepository { return store.gameGiftGrants }
|
||||
func (store *MemoryStore) SCUMDataObservations() SCUMDataObservationRepository {
|
||||
return store.scumDataObservations
|
||||
}
|
||||
func (store *MemoryStore) SCUMPlayerLiveStates() SCUMPlayerLiveStateRepository {
|
||||
return store.scumPlayerLiveStates
|
||||
}
|
||||
func (store *MemoryStore) SCUMSquads() SCUMSquadRepository { return store.scumSquads }
|
||||
func (store *MemoryStore) SCUMSquadMembers() SCUMSquadMemberRepository {
|
||||
return store.scumSquadMembers
|
||||
}
|
||||
func (store *MemoryStore) SCUMVehicles() SCUMVehicleRepository { return store.scumVehicles }
|
||||
func (store *MemoryStore) SCUMFlags() SCUMFlagRepository { return store.scumFlags }
|
||||
func (store *MemoryStore) SCUMCurrentPositions() SCUMCurrentPositionRepository {
|
||||
return store.scumCurrentPositions
|
||||
}
|
||||
func (store *MemoryStore) SCUMOperationRequests() SCUMOperationRequestRepository {
|
||||
return store.scumOperationRequests
|
||||
}
|
||||
func (store *MemoryStore) SCUMWorkflowInstances() SCUMWorkflowInstanceRepository {
|
||||
return store.scumWorkflowInstances
|
||||
}
|
||||
func (store *MemoryStore) SCUMWorkflowSteps() SCUMWorkflowStepRepository {
|
||||
return store.scumWorkflowSteps
|
||||
}
|
||||
|
||||
type memoryRepository[T any, F any] struct {
|
||||
mu sync.RWMutex
|
||||
@@ -940,3 +1055,100 @@ func matchGameGiftRevision(v domain.GameGiftRevision, f domain.GameGiftRevisionF
|
||||
func matchGameGiftGrant(v domain.GameGiftGrant, f domain.GameGiftGrantFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) && (f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) && (f.IdempotencyKey == "" || v.IdempotencyKey == f.IdempotencyKey)
|
||||
}
|
||||
|
||||
func matchSCUMDataObservation(v domain.SCUMDataObservation, f domain.SCUMProjectionFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.SubjectType == "" || v.SubjectType == string(f.SubjectType)) &&
|
||||
(f.GamePlayerRecordID == "" || v.SubjectID == f.GamePlayerRecordID) &&
|
||||
(f.QueryKey == "" || v.QueryKey == f.QueryKey) &&
|
||||
(f.Freshness == "" || domain.SCUMProjectionFreshness(v.Status) == f.Freshness)
|
||||
}
|
||||
|
||||
func matchSCUMPlayerLiveState(v domain.SCUMPlayerLiveState, f domain.SCUMProjectionFilter) bool {
|
||||
search := strings.ToLower(strings.TrimSpace(f.Search))
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.GamePlayerID == "" || v.GamePlayerID == f.GamePlayerID) &&
|
||||
(f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) &&
|
||||
(f.UserProfileID == "" || v.UserProfileID == f.UserProfileID) &&
|
||||
(f.SteamID == "" || v.SteamID == f.SteamID) &&
|
||||
(f.SquadID == "" || v.SquadID == f.SquadID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness) &&
|
||||
(search == "" || strings.Contains(strings.ToLower(v.DisplayName), search) || strings.Contains(strings.ToLower(v.GamePlayerID), search) || strings.Contains(strings.ToLower(v.UserProfileID), search) || strings.Contains(strings.ToLower(v.SteamID), search))
|
||||
}
|
||||
|
||||
func matchSCUMSquad(v domain.SCUMSquad, f domain.SCUMProjectionFilter) bool {
|
||||
search := strings.ToLower(strings.TrimSpace(f.Search))
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.SquadID == "" || v.SquadID == f.SquadID) &&
|
||||
(f.UserProfileID == "" || v.LeaderProfileID == f.UserProfileID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness) &&
|
||||
(search == "" || strings.Contains(strings.ToLower(v.Name), search) || strings.Contains(strings.ToLower(v.SquadID), search))
|
||||
}
|
||||
|
||||
func matchSCUMSquadMember(v domain.SCUMSquadMember, f domain.SCUMProjectionFilter) bool {
|
||||
search := strings.ToLower(strings.TrimSpace(f.Search))
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.SquadID == "" || v.SquadID == f.SquadID) &&
|
||||
(f.GamePlayerID == "" || v.GamePlayerID == f.GamePlayerID) &&
|
||||
(f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) &&
|
||||
(f.UserProfileID == "" || v.UserProfileID == f.UserProfileID) &&
|
||||
(f.SteamID == "" || v.SteamID == f.SteamID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness) &&
|
||||
(search == "" || strings.Contains(strings.ToLower(v.DisplayName), search) || strings.Contains(strings.ToLower(v.GamePlayerID), search) || strings.Contains(strings.ToLower(v.UserProfileID), search))
|
||||
}
|
||||
|
||||
func matchSCUMVehicle(v domain.SCUMVehicle, f domain.SCUMProjectionFilter) bool {
|
||||
search := strings.ToLower(strings.TrimSpace(f.Search))
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.VehicleID == "" || v.VehicleID == f.VehicleID) &&
|
||||
(f.UserProfileID == "" || v.OwnerProfileID == f.UserProfileID) &&
|
||||
(f.GamePlayerID == "" || v.OwnerPlayerID == f.GamePlayerID) &&
|
||||
(f.SquadID == "" || v.SquadID == f.SquadID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness) &&
|
||||
(search == "" || strings.Contains(strings.ToLower(v.Label), search) || strings.Contains(strings.ToLower(v.ClassName), search) || strings.Contains(strings.ToLower(v.VehicleID), search))
|
||||
}
|
||||
|
||||
func matchSCUMFlag(v domain.SCUMFlag, f domain.SCUMProjectionFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.FlagID == "" || v.FlagID == f.FlagID) &&
|
||||
(f.UserProfileID == "" || v.OwnerProfileID == f.UserProfileID) &&
|
||||
(f.GamePlayerID == "" || v.OwnerPlayerID == f.GamePlayerID) &&
|
||||
(f.SquadID == "" || v.OwnerSquadID == f.SquadID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness)
|
||||
}
|
||||
|
||||
func matchSCUMCurrentPosition(v domain.SCUMCurrentPosition, f domain.SCUMProjectionFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.SubjectType == "" || v.SubjectType == f.SubjectType) &&
|
||||
(f.GamePlayerID == "" || v.GamePlayerID == f.GamePlayerID) &&
|
||||
(f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) &&
|
||||
(f.VehicleID == "" || v.VehicleID == f.VehicleID) &&
|
||||
(f.Freshness == "" || v.Freshness.Status == f.Freshness)
|
||||
}
|
||||
|
||||
func matchSCUMOperationRequest(v domain.SCUMOperationRequest, f domain.SCUMOperationRequestFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.PluginID == "" || v.PluginID == f.PluginID) &&
|
||||
(f.TemplateKey == "" || v.TemplateKey == f.TemplateKey) &&
|
||||
(f.PlayerID == "" || v.PlayerID == f.PlayerID) &&
|
||||
(f.RequesterID == "" || v.RequesterID == f.RequesterID) &&
|
||||
(f.Status == "" || v.Status == f.Status) &&
|
||||
(f.IdempotencyKey == "" || v.IdempotencyKey == f.IdempotencyKey)
|
||||
}
|
||||
|
||||
func matchSCUMWorkflowInstance(v domain.SCUMWorkflowInstance, f domain.SCUMWorkflowInstanceFilter) bool {
|
||||
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.PluginID == "" || v.PluginID == f.PluginID) &&
|
||||
(f.TemplateKey == "" || v.TemplateKey == f.TemplateKey) &&
|
||||
(f.RequestedBy == "" || v.RequestedBy == f.RequestedBy) &&
|
||||
(f.Status == "" || v.Status == f.Status) &&
|
||||
(f.IdempotencyKey == "" || v.IdempotencyKey == f.IdempotencyKey)
|
||||
}
|
||||
|
||||
func matchSCUMWorkflowStep(v domain.SCUMWorkflowStep, f domain.SCUMWorkflowStepFilter) bool {
|
||||
return (f.WorkflowID == "" || v.WorkflowID == f.WorkflowID) &&
|
||||
(f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) &&
|
||||
(f.StepKey == "" || v.StepKey == f.StepKey) &&
|
||||
(f.Status == "" || v.Status == f.Status) &&
|
||||
(f.MutatesState == nil || v.MutatesState == *f.MutatesState)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestSCUMProjectionRepositoriesCopyFilterAndPersist(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "metadata.json")
|
||||
store, err := NewFileStore(path)
|
||||
if err != nil {
|
||||
t.Fatalf("create file store: %v", err)
|
||||
}
|
||||
stamp := time.Date(2026, 8, 10, 9, 0, 0, 0, time.UTC)
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionFresh, ObservationID: "obs-1", Source: "run", QueryKey: "scum.player.profile", Sequence: 7, Checksum: "sha256:projection", ObservedAt: stamp, ReceivedAt: stamp.Add(time.Second)}
|
||||
state := domain.SCUMPlayerLiveState{ID: "state-1", ServerInstanceID: "server-1", GamePlayerRecordID: "game-player-1", GamePlayerID: "steam-1", UserProfileID: "profile-1", SteamID: "steam-1", DisplayName: "Moon", SquadID: "squad-1", UnknownFields: map[string]any{"schemaField": "kept"}, Freshness: freshness, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := store.SCUMPlayerLiveStates().Create(state); err != nil {
|
||||
t.Fatalf("create state: %v", err)
|
||||
}
|
||||
got, err := store.SCUMPlayerLiveStates().Get(state.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get state: %v", err)
|
||||
}
|
||||
got.UnknownFields["schemaField"] = "mutated"
|
||||
again, err := store.SCUMPlayerLiveStates().Get(state.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get state again: %v", err)
|
||||
}
|
||||
if again.UnknownFields["schemaField"] != "kept" {
|
||||
t.Fatalf("state was not copy-isolated: %+v", again.UnknownFields)
|
||||
}
|
||||
filtered, err := store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", UserProfileID: "profile-1", Search: "moon"})
|
||||
if err != nil || len(filtered) != 1 {
|
||||
t.Fatalf("filter states=%+v err=%v", filtered, err)
|
||||
}
|
||||
if err := store.SCUMSquads().Create(domain.SCUMSquad{ID: "squad-1", ServerInstanceID: "server-1", SquadID: "squad-1", Name: "Crystal", Freshness: freshness}); err != nil {
|
||||
t.Fatalf("create squad: %v", err)
|
||||
}
|
||||
if err := store.SCUMVehicles().Create(domain.SCUMVehicle{ID: "vehicle-1", ServerInstanceID: "server-1", VehicleID: "veh-1", Label: "Unknown vehicle", Freshness: freshness}); err != nil {
|
||||
t.Fatalf("create vehicle: %v", err)
|
||||
}
|
||||
if err := store.SCUMFlags().Create(domain.SCUMFlag{ID: "flag-1", ServerInstanceID: "server-1", FlagID: "flag-1", OwnerSquadID: "squad-1", Freshness: freshness}); err != nil {
|
||||
t.Fatalf("create flag: %v", err)
|
||||
}
|
||||
if err := store.SCUMCurrentPositions().Create(domain.SCUMCurrentPosition{ID: "position-1", ServerInstanceID: "server-1", SubjectType: domain.SCUMProjectionSubjectPlayer, SubjectID: "steam-1", GamePlayerRecordID: "game-player-1", X: 1, Y: 2, HasCoordinates: true, Freshness: freshness}); err != nil {
|
||||
t.Fatalf("create position: %v", err)
|
||||
}
|
||||
reloaded, err := NewFileStore(path)
|
||||
if err != nil {
|
||||
t.Fatalf("reload file store: %v", err)
|
||||
}
|
||||
reloadedStates, err := reloaded.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", SquadID: "squad-1"})
|
||||
if err != nil || len(reloadedStates) != 1 || reloadedStates[0].Freshness.QueryKey != "scum.player.profile" {
|
||||
t.Fatalf("unexpected reloaded states=%+v err=%v", reloadedStates, err)
|
||||
}
|
||||
vehicles, err := reloaded.SCUMVehicles().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", Search: "unknown"})
|
||||
if err != nil || len(vehicles) != 1 {
|
||||
t.Fatalf("unexpected reloaded vehicles=%+v err=%v", vehicles, err)
|
||||
}
|
||||
positions, err := reloaded.SCUMCurrentPositions().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", SubjectType: domain.SCUMProjectionSubjectPlayer})
|
||||
if err != nil || len(positions) != 1 || !positions[0].HasCoordinates {
|
||||
t.Fatalf("unexpected reloaded positions=%+v err=%v", positions, err)
|
||||
}
|
||||
}
|
||||
@@ -268,11 +268,6 @@ func runAutonomousLifecyclePlan(distribution domain.RunDistribution, instance do
|
||||
plan.LogSources = append(plan.LogSources, autonomousLogSource(source))
|
||||
}
|
||||
}
|
||||
for _, target := range plugin.RuntimeProfiles.DataTargets {
|
||||
if runtimePlatformsContain(target.Platforms, distribution.TargetOS) {
|
||||
plan.DataTargets = append(plan.DataTargets, autonomousDataTarget(target))
|
||||
}
|
||||
}
|
||||
if hasProfile && len(profile.DLLExtensionRefs) > 0 {
|
||||
endpoint := domain.RunEndpoint{ID: distribution.RunEndpointID, Platform: distribution.TargetOS, Architecture: distribution.TargetArch}
|
||||
extensions, err := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint)
|
||||
@@ -331,10 +326,6 @@ func autonomousDLLExtension(extension domain.RuntimeDLLExtensionPlan) domain.Run
|
||||
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
||||
}
|
||||
|
||||
func autonomousDataTarget(target domain.RuntimeDataTarget) domain.RunAutonomousDataTarget {
|
||||
return domain.RunAutonomousDataTarget{Key: target.Key, Kind: target.Kind, TransportKey: target.TransportKey, SourceRootKey: target.SourceRootKey, SourcePath: target.SourcePath, WorkspaceKey: target.WorkspaceKey, RefreshPolicy: target.RefreshPolicy, MaxBytes: target.MaxBytes, Platforms: domain.CopyStringSlice(target.Platforms)}
|
||||
}
|
||||
|
||||
func autonomousDeploymentFromDefinition(definition domain.ServerDeploymentDefinition, profileKey string, bindings map[string]string) *domain.RunAutonomousDeployment {
|
||||
if definition.Mode == "" {
|
||||
return nil
|
||||
|
||||
@@ -45,13 +45,6 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14},
|
||||
domain.RuntimeLogSource{Key: "server-events", Kind: "file.tail", TargetKey: "logs/server", StreamKey: "scum.server", CursorKind: "fingerprint", RetentionDays: 90},
|
||||
)
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles,
|
||||
domain.RuntimeTransportProfile{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||
domain.RuntimeTransportProfile{Key: "world-db", Kind: "sqlite", TargetKey: "world-db", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
)
|
||||
plugin.RuntimeProfiles.DataTargets = append(plugin.RuntimeProfiles.DataTargets,
|
||||
domain.RuntimeDataTarget{Key: "world-db", Kind: "sqlite.snapshot", TransportKey: "world-db", SourceRootKey: "server-root", SourcePath: "world/current.db", WorkspaceKey: "databases/world-db", RefreshPolicy: "on-demand-snapshot", MaxBytes: 128 * 1024 * 1024, Platforms: []string{"linux"}},
|
||||
)
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("seed plugin lifecycle assets: %v", err)
|
||||
}
|
||||
@@ -97,14 +90,14 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
if plan == nil || plan.SchemaVersion != "1" || plan.ServerInstanceID != instance.ID || plan.PluginID != plugin.ID || plan.ProfileKey != "local" || plan.Bootstrap == nil || plan.Bootstrap.Action != domain.ServerLifecycleActionStart || plan.Bootstrap.TargetKey != "actions/start.json" {
|
||||
t.Fatalf("platform builder received incomplete autonomous lifecycle plan: %+v", plan)
|
||||
}
|
||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 3 || !hasAutonomousLogSource(plan.LogSources, "process.stdout", "console") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "scum.server") || len(plan.DataTargets) != 1 || plan.DataTargets[0].WorkspaceKey != "databases/world-db" || plan.DataTargets[0].SourcePath != "world/current.db" || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 3 || !hasAutonomousLogSource(plan.LogSources, "process.stdout", "console") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "scum.server") || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||
t.Fatalf("autonomous lifecycle plan lost plugin runtime declarations: %+v", plan)
|
||||
}
|
||||
var seededPlan domain.RunAutonomousLifecyclePlan
|
||||
if err := json.Unmarshal([]byte(seedFiles[2].Content), &seededPlan); err != nil {
|
||||
t.Fatalf("unmarshal seeded autonomous lifecycle plan: %v", err)
|
||||
}
|
||||
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey || len(seededPlan.DataTargets) != 1 || seededPlan.DataTargets[0].WorkspaceKey != "databases/world-db" {
|
||||
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey {
|
||||
t.Fatalf("seeded lifecycle plan differs from build input: seed=%+v input=%+v", seededPlan, plan)
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
|
||||
@@ -764,7 +764,7 @@ func (svc *CoreService) QueueLogBackfillForSession(sessionID string, request dom
|
||||
InputRef: request.CheckpointRef,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "historical log backfill queued"},
|
||||
ExecutionInput: domain.JobExecutionInput{PluginID: plugin.ID, TargetVersion: plugin.Version, LogSource: &source},
|
||||
ExecutionInput: domain.JobExecutionInput{LogSource: &source},
|
||||
})
|
||||
if err != nil {
|
||||
_ = svc.recordAuditEvent(user.ID, "logs.backfill.denied", "server-instance", instance.ID, domain.AuditResultDenied, "log backfill denied: endpoint unsupported or offline")
|
||||
|
||||
@@ -125,6 +125,9 @@ func (svc *CoreService) projectGamePlayerEvent(batch domain.LogBatchIngest, entr
|
||||
if eventType == "scum.login" {
|
||||
outcome := strings.TrimSpace(fields["outcome"])
|
||||
if outcome == "accepted" {
|
||||
if err := svc.projectSCUMLoginLiveState(player, batch, entry, occurred, true, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.recordSuccessfulGameAccess(player, batch, entry, occurred, strings.TrimSpace(fields["networkFingerprint"])); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -132,6 +135,9 @@ func (svc *CoreService) projectGamePlayerEvent(batch domain.LogBatchIngest, entr
|
||||
}
|
||||
return svc.recordFailedGameAccess(player, batch, entry, occurred, strings.TrimSpace(fields["networkFingerprint"]))
|
||||
}
|
||||
if err := svc.projectSCUMLoginLiveState(player, batch, entry, occurred, false, strings.TrimSpace(fields["reason"])); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.closeGamePlayerSession(player, sourceSession, occurred, strings.TrimSpace(fields["reason"]))
|
||||
}
|
||||
|
||||
|
||||
@@ -288,9 +288,6 @@ func (svc *CoreService) CompleteRunJob(result domain.RunJobResult) (domain.RunJo
|
||||
}
|
||||
|
||||
func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) error {
|
||||
if result.ExecutionResult.ParsedLogBatch != nil && job.Capability != domain.JobCapabilityLogsBackfill {
|
||||
return validationError("parsed log batch result is allowed only for logs.backfill jobs")
|
||||
}
|
||||
if definition := job.ExecutionInput.Deployment; definition != nil {
|
||||
receipt := result.ExecutionResult.DeploymentReceipt
|
||||
if result.State == domain.JobStateSucceeded && definition.Mode == domain.ServerDeploymentModeCustom && receipt == nil {
|
||||
@@ -337,72 +334,6 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "run.update.staged" {
|
||||
return validationError("Run self-update result type is invalid")
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunDBSQLiteProbe:
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumSchemaProbeExecutionKind {
|
||||
return validationError("SQLite schema probe result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.SQLiteSchemaProbe == nil {
|
||||
return validationError("SQLite schema probe terminal result is required")
|
||||
}
|
||||
if err := validateSCUMSchemaProbeResultForJob(job, *result.ExecutionResult.SQLiteSchemaProbe); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunDBSQLiteQuery:
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumSQLiteTemplateExecutionKind {
|
||||
return validationError("SQLite template query result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.SQLiteTemplate == nil {
|
||||
return validationError("SQLite template terminal result is required")
|
||||
}
|
||||
if err := validateSCUMSQLiteTemplateResultForJob(job, *result.ExecutionResult.SQLiteTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunProtectedRCON:
|
||||
if job.ExecutionInput.RCONTemplate == nil {
|
||||
break
|
||||
}
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumRCONTemplateExecutionKind {
|
||||
return validationError("typed RCON template result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.RCONTemplate == nil {
|
||||
return validationError("typed RCON template terminal result is required")
|
||||
}
|
||||
if err := validateSCUMTypedRCONTemplateResultForJob(job, *result.ExecutionResult.RCONTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunProtectedSQL:
|
||||
if job.ExecutionInput.GuardedMutation == nil {
|
||||
break
|
||||
}
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumGuardedMutationExecutionKind {
|
||||
return validationError("guarded mutation result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.GuardedMutation == nil {
|
||||
return validationError("guarded mutation terminal result is required")
|
||||
}
|
||||
if err := validateSCUMGuardedMutationResultForJob(job, *result.ExecutionResult.GuardedMutation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityLogsBackfill:
|
||||
if result.ExecutionResult.ParsedLogBatch == nil {
|
||||
break
|
||||
}
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumParsedLogBatchExecutionKind {
|
||||
return validationError("parsed log batch result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if err := validateSCUMParsedLogBatchResultForJob(job, *result.ExecutionResult.ParsedLogBatch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityClientManagerDeploy:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" {
|
||||
return validationError("client-manager deploy result type is invalid")
|
||||
@@ -694,7 +625,7 @@ func firstEligibleSupportedJob(jobs []domain.Job, capabilities []string, stamp t
|
||||
|
||||
func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignment {
|
||||
fencingToken := uint64(0)
|
||||
if isProtectedRequestCapability(job.Capability) || job.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe || job.Capability == domain.JobCapabilityRemoteRunDBSQLiteQuery {
|
||||
if isProtectedRequestCapability(job.Capability) {
|
||||
fencingToken = uint64(job.Attempt)
|
||||
}
|
||||
return domain.RunJobAssignment{
|
||||
@@ -708,7 +639,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
|
||||
State: job.State,
|
||||
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
|
||||
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), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: domain.CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate), RCONTemplate: domain.CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate), GuardedMutation: domain.CopySCUMGuardedMutationRequestPtr(job.ExecutionInput.GuardedMutation)},
|
||||
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)},
|
||||
LeaseToken: leaseToken,
|
||||
Attempt: job.Attempt,
|
||||
FencingToken: fencingToken,
|
||||
@@ -786,27 +717,7 @@ func jobRetryBackoff(policy domain.JobRetryPolicy, attempt int) time.Duration {
|
||||
}
|
||||
|
||||
func terminalFingerprint(result domain.RunJobResult) string {
|
||||
schemaProbeFingerprint := ""
|
||||
if result.ExecutionResult.SQLiteSchemaProbe != nil {
|
||||
schemaProbeFingerprint = result.ExecutionResult.SQLiteSchemaProbe.ResultDigest
|
||||
}
|
||||
sqliteTemplateFingerprint := ""
|
||||
if result.ExecutionResult.SQLiteTemplate != nil {
|
||||
sqliteTemplateFingerprint = fmt.Sprintf("%s|%s|%s|%d", result.ExecutionResult.SQLiteTemplate.ResultDigest, result.ExecutionResult.SQLiteTemplate.AssetDigest, result.ExecutionResult.SQLiteTemplate.ParameterDigest, result.ExecutionResult.SQLiteTemplate.RowCount)
|
||||
}
|
||||
rconTemplateFingerprint := ""
|
||||
if result.ExecutionResult.RCONTemplate != nil {
|
||||
rconTemplateFingerprint = fmt.Sprintf("%s|%s|%s|%s|%s", result.ExecutionResult.RCONTemplate.ResultDigest, result.ExecutionResult.RCONTemplate.AssetDigest, result.ExecutionResult.RCONTemplate.PayloadDigest, result.ExecutionResult.RCONTemplate.ConfirmationDigest, result.ExecutionResult.RCONTemplate.ConfirmationStatus)
|
||||
}
|
||||
guardedMutationFingerprint := ""
|
||||
if result.ExecutionResult.GuardedMutation != nil {
|
||||
guardedMutationFingerprint = fmt.Sprintf("%s|%s|%s|%d|%s", result.ExecutionResult.GuardedMutation.ResultDigest, result.ExecutionResult.GuardedMutation.AssetDigest, result.ExecutionResult.GuardedMutation.PatchDigest, result.ExecutionResult.GuardedMutation.AffectedRows, result.ExecutionResult.GuardedMutation.ReadbackStatus)
|
||||
}
|
||||
parsedLogBatchFingerprint := ""
|
||||
if result.ExecutionResult.ParsedLogBatch != nil {
|
||||
parsedLogBatchFingerprint = fmt.Sprintf("%s|%s|%s|%s|%d|%s", result.ExecutionResult.ParsedLogBatch.ResultDigest, result.ExecutionResult.ParsedLogBatch.AssetDigest, result.ExecutionResult.ParsedLogBatch.ParserDigest, result.ExecutionResult.ParsedLogBatch.FirstCursor.StreamGeneration, result.ExecutionResult.ParsedLogBatch.EventCount, result.ExecutionResult.ParsedLogBatch.TailState)
|
||||
}
|
||||
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t|%s|%s|%s|%s|%s|%s|%s", result.State, result.Progress.Percent, result.ResultRef, result.Message, result.ErrorCode, result.Progress.Message, result.Retryable, result.ExecutionResult.Kind, result.ExecutionResult.Checksum, schemaProbeFingerprint, sqliteTemplateFingerprint, rconTemplateFingerprint, guardedMutationFingerprint, parsedLogBatchFingerprint)
|
||||
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t", result.State, result.Progress.Percent, result.ResultRef, result.Message, result.ErrorCode, result.Progress.Message, result.Retryable)
|
||||
}
|
||||
|
||||
func terminalMessage(result domain.RunJobResult) string {
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
@@ -213,147 +212,6 @@ func TestCoreServiceRunJobTerminalResultIsIdempotent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobSQLiteTemplateEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
request := domain.SCUMSQLiteTemplateRequest{RequestID: "request-query", JobID: "job-query", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players.active.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), ParameterDigest: scumTemplateTestHash(), Parameters: map[string]any{"limit": 100.0}, Bounds: domain.DefaultSCUMSQLiteTemplateBounds(), RequestedAt: time.Now()}
|
||||
createSCUMTemplateServerFixture(t, svc, request.Binding)
|
||||
job, err := svc.CreateJob(domain.Job{ID: request.JobID, ServerInstanceID: request.Binding.ServerInstanceID, RunEndpointID: request.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunDBSQLiteQuery, TargetKey: request.TargetKey, InputRef: "input://sqlite-template/request-query", IdempotencyKey: "idem-query", ExecutionInput: domain.JobExecutionInput{SQLiteTemplate: &request}})
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlite query job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.SQLiteTemplate == nil {
|
||||
t.Fatalf("claim sqlite query job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumTemplateTestResult(request)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "query complete"}, Message: "query complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &result, AuditSummary: "redacted sqlite template query"}}); err != nil {
|
||||
t.Fatalf("complete matching sqlite query: %v", err)
|
||||
}
|
||||
|
||||
badRequest := request
|
||||
badRequest.RequestID = "request-query-bad"
|
||||
badRequest.JobID = "job-query-bad"
|
||||
if _, err := svc.CreateJob(domain.Job{ID: badRequest.JobID, ServerInstanceID: badRequest.Binding.ServerInstanceID, RunEndpointID: badRequest.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunDBSQLiteQuery, TargetKey: badRequest.TargetKey, InputRef: "input://sqlite-template/request-query-bad", IdempotencyKey: "idem-query-bad", ExecutionInput: domain.JobExecutionInput{SQLiteTemplate: &badRequest}}); err != nil {
|
||||
t.Fatalf("create bad sqlite query job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim bad sqlite query job: %v", err)
|
||||
}
|
||||
badResult := scumTemplateTestResult(badRequest)
|
||||
badResult.AssetDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "query complete"}, Message: "query complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &badResult, AuditSummary: "redacted sqlite template query"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "digest") {
|
||||
t.Fatalf("expected digest-fenced query result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobTypedRCONTemplateEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
request := scumTypedRCONTemplateTestRequest()
|
||||
createSCUMTemplateServerFixture(t, svc, request.Binding)
|
||||
job, err := svc.CreateJob(domain.Job{ID: request.JobID, ServerInstanceID: request.Binding.ServerInstanceID, RunEndpointID: request.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedRCON, TargetKey: request.TargetKey, InputRef: "input://rcon-template/request-rcon", IdempotencyKey: request.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-rcon", SourceRCON: scumTypedRCONSourcePlan(), RCONTemplate: &request}})
|
||||
if err != nil {
|
||||
t.Fatalf("create typed RCON job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedRCON}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.RCONTemplate == nil || claim.Job.ExecutionInput.RCONTemplate.Payload["absoluteValue"].(float64) != 100.0 {
|
||||
t.Fatalf("claim typed RCON job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumTypedRCONTemplateTestResult(request)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "rcon complete"}, Message: "rcon complete", ExecutionResult: domain.JobExecutionResult{Kind: scumRCONTemplateExecutionKind, RCONTemplate: &result, AuditSummary: "redacted typed RCON template command"}}); err != nil {
|
||||
t.Fatalf("complete matching typed RCON job: %v", err)
|
||||
}
|
||||
|
||||
badRequest := request
|
||||
badRequest.RequestID = "request-rcon-bad"
|
||||
badRequest.JobID = "job-rcon-bad"
|
||||
badRequest.IdempotencyKey = "idem-rcon-bad"
|
||||
if _, err := svc.CreateJob(domain.Job{ID: badRequest.JobID, ServerInstanceID: badRequest.Binding.ServerInstanceID, RunEndpointID: badRequest.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedRCON, TargetKey: badRequest.TargetKey, InputRef: "input://rcon-template/request-rcon-bad", IdempotencyKey: badRequest.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-rcon", SourceRCON: scumTypedRCONSourcePlan(), RCONTemplate: &badRequest}}); err != nil {
|
||||
t.Fatalf("create bad typed RCON job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedRCON}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim bad typed RCON job: %v", err)
|
||||
}
|
||||
badResult := scumTypedRCONTemplateTestResult(badRequest)
|
||||
badResult.PayloadDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "rcon complete"}, Message: "rcon complete", ExecutionResult: domain.JobExecutionResult{Kind: scumRCONTemplateExecutionKind, RCONTemplate: &badResult, AuditSummary: "redacted typed RCON template command"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "digest") {
|
||||
t.Fatalf("expected digest-fenced typed RCON result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobGuardedMutationEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
request := scumGuardedMutationTestRequest()
|
||||
createSCUMTemplateServerFixture(t, svc, request.Binding)
|
||||
job, err := svc.CreateJob(domain.Job{ID: request.JobID, ServerInstanceID: request.Binding.ServerInstanceID, RunEndpointID: request.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedSQL, TargetKey: request.TargetKey, InputRef: "input://guarded-mutation/request-mutation", IdempotencyKey: request.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-sql", RemoteAdapterKey: request.TargetKey, GuardedMutation: &request}})
|
||||
if err != nil {
|
||||
t.Fatalf("create guarded mutation job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.GuardedMutation == nil || claim.Job.ExecutionInput.GuardedMutation.Payload["attributeKey"].(string) != "Strength" {
|
||||
t.Fatalf("claim guarded mutation job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumGuardedMutationTestResult(request)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "mutation complete"}, Message: "mutation complete", ExecutionResult: domain.JobExecutionResult{Kind: scumGuardedMutationExecutionKind, GuardedMutation: &result, AuditSummary: "redacted guarded mutation"}}); err != nil {
|
||||
t.Fatalf("complete matching guarded mutation job: %v", err)
|
||||
}
|
||||
|
||||
badRequest := request
|
||||
badRequest.RequestID = "request-mutation-bad"
|
||||
badRequest.JobID = "job-mutation-bad"
|
||||
badRequest.IdempotencyKey = "idem-mutation-bad"
|
||||
if _, err := svc.CreateJob(domain.Job{ID: badRequest.JobID, ServerInstanceID: badRequest.Binding.ServerInstanceID, RunEndpointID: badRequest.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedSQL, TargetKey: badRequest.TargetKey, InputRef: "input://guarded-mutation/request-mutation-bad", IdempotencyKey: badRequest.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-sql", RemoteAdapterKey: badRequest.TargetKey, GuardedMutation: &badRequest}}); err != nil {
|
||||
t.Fatalf("create bad guarded mutation job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim bad guarded mutation job: %v", err)
|
||||
}
|
||||
badResult := scumGuardedMutationTestResult(badRequest)
|
||||
badResult.PatchDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "mutation complete"}, Message: "mutation complete", ExecutionResult: domain.JobExecutionResult{Kind: scumGuardedMutationExecutionKind, GuardedMutation: &badResult, AuditSummary: "redacted guarded mutation"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "digest") {
|
||||
t.Fatalf("expected digest-fenced guarded mutation result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobParsedLogBatchEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
binding := scumTemplateTestBinding()
|
||||
createSCUMTemplateServerFixture(t, svc, binding)
|
||||
source := &domain.RuntimeLogSource{Key: "scum-login-events", Kind: "file.tail", TargetKey: "logs/login", StreamKey: "scum.login", CursorKind: "fingerprint", RetentionDays: 90}
|
||||
job, err := svc.CreateJob(domain.Job{ID: "job-log", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, Capability: domain.JobCapabilityLogsBackfill, TargetKey: "logs/scum-login-events", InputRef: "artifact://logs/checkpoint/1", IdempotencyKey: "idem-log", ExecutionInput: domain.JobExecutionInput{PluginID: binding.PluginID, TargetVersion: binding.PluginVersion, LogSource: source, Inputs: map[string]string{"parserKey": "scum-login-log-login-parser", "parserVersion": "scum-login-log-v1", "parserDigest": scumTemplateTestHash(), "adapterVersion": binding.AdapterVersion}}})
|
||||
if err != nil {
|
||||
t.Fatalf("create parsed log backfill job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityLogsBackfill}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.LogSource == nil {
|
||||
t.Fatalf("claim parsed log job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumParsedLogBatchTestResult(binding, job.ID, *source)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "log parse complete"}, Message: "log parse complete", ExecutionResult: domain.JobExecutionResult{Kind: scumParsedLogBatchExecutionKind, ParsedLogBatch: &result, AuditSummary: "redacted parsed log batch"}}); err != nil {
|
||||
t.Fatalf("complete matching parsed log job: %v", err)
|
||||
}
|
||||
|
||||
badJob, err := svc.CreateJob(domain.Job{ID: "job-log-bad", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, Capability: domain.JobCapabilityLogsBackfill, TargetKey: "logs/scum-login-events", InputRef: "artifact://logs/checkpoint/2", IdempotencyKey: "idem-log-bad", ExecutionInput: domain.JobExecutionInput{PluginID: binding.PluginID, TargetVersion: binding.PluginVersion, LogSource: source, Inputs: map[string]string{"parserKey": "scum-login-log-login-parser", "parserVersion": "scum-login-log-v1", "parserDigest": scumTemplateTestHash(), "adapterVersion": binding.AdapterVersion}}})
|
||||
if err != nil {
|
||||
t.Fatalf("create bad parsed log job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityLogsBackfill}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !badClaim.HasJob || badClaim.Job.JobID != badJob.ID {
|
||||
t.Fatalf("claim bad parsed log job: claim=%+v err=%v", badClaim, err)
|
||||
}
|
||||
badResult := scumParsedLogBatchTestResult(binding, badJob.ID, *source)
|
||||
badResult.ParserDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "log parse complete"}, Message: "log parse complete", ExecutionResult: domain.JobExecutionResult{Kind: scumParsedLogBatchExecutionKind, ParsedLogBatch: &badResult, AuditSummary: "redacted parsed log batch"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "parser identity") {
|
||||
t.Fatalf("expected parser-fenced parsed log result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobReconcile(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
createQueuedRunJob(t, svc, "job-1", "idem-1")
|
||||
@@ -384,7 +242,7 @@ func newRegisteredRunJobService(t *testing.T) (*CoreService, string) {
|
||||
t.Helper()
|
||||
svc := newTestCoreService()
|
||||
helloRequest := validRunControlHello()
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start", domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityLogsBackfill)
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start")
|
||||
helloRequest.CapabilityReport.Fingerprint = "cap-jobs"
|
||||
hello, err := svc.RegisterRunHello(helloRequest)
|
||||
if err != nil {
|
||||
@@ -393,51 +251,6 @@ func newRegisteredRunJobService(t *testing.T) (*CoreService, string) {
|
||||
return svc, hello.SessionToken
|
||||
}
|
||||
|
||||
func scumTemplateTestHash() string { return "sha256:" + strings.Repeat("a", 64) }
|
||||
|
||||
func scumTemplateTestBinding() domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: "server-scum", RunBindingID: "binding-scum", RunEndpointID: "run-local", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "scum-database"}
|
||||
}
|
||||
|
||||
func scumTypedRCONTemplateTestRequest() domain.SCUMTypedRCONTemplateRequest {
|
||||
return domain.SCUMTypedRCONTemplateRequest{RequestID: "request-rcon", JobID: "job-rcon", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityEconomyCommand, TransportKey: "scum-rcon", TargetKey: "scum-rcon", TemplateKey: "economy.fame.set.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), PayloadDigest: scumTemplateTestHash(), ConfirmationDigest: scumTemplateTestHash(), TargetIdentityDigest: scumTemplateTestHash(), IdempotencyKey: "idem-rcon", Payload: map[string]any{"externalPlayerId": "player-redacted", "absoluteValue": 100.0}, ReviewReason: "operator reviewed absolute fame update", Bounds: domain.DefaultSCUMTypedRCONTemplateBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func scumGuardedMutationTestRequest() domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: "request-mutation", JobID: "job-mutation", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityProfileXMLWrite, TargetKey: "scum-mutation-db", TemplateKey: "profile.attributes.patch.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), TargetIdentityDigest: scumTemplateTestHash(), ExpectedRowDigest: scumTemplateTestHash(), ExpectedValueDigest: scumTemplateTestHash(), ExpectedXMLDigest: scumTemplateTestHash(), PatchDigest: scumTemplateTestHash(), BackupEvidenceDigest: scumTemplateTestHash(), OfflineEvidenceDigest: scumTemplateTestHash(), DangerConfirmationDigest: scumTemplateTestHash(), ReadbackExpectationDigest: scumTemplateTestHash(), IdempotencyKey: "idem-mutation", Payload: map[string]any{"attributeKey": "Strength", "absoluteValue": 8.5}, ReviewReason: "operator confirmed offline profile attribute patch", Bounds: domain.DefaultSCUMGuardedMutationBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func scumTypedRCONSourcePlan() *domain.RuntimeSourceRCONPlan {
|
||||
return &domain.RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/scum-rcon/release.json", Port: 27015}
|
||||
}
|
||||
|
||||
func createSCUMTemplateServerFixture(t *testing.T, svc *CoreService, binding domain.SCUMBindingIdentity) {
|
||||
t.Helper()
|
||||
if _, err := svc.CreateGamePlugin(domain.GamePlugin{ID: binding.PluginID, Name: "SCUM", Version: binding.PluginVersion, ServerType: "scum", ManifestRef: "artifact://manifests/game.scum/0.1.6", CreateFormSchemaRef: "artifact://schemas/game.scum/create-form/0.1.6", RequiredRunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityLogsBackfill}, DeclaredPermissions: []string{"server.remote.access", "server.game-client.command", "server.game-client.maintenance", "server.logs.read"}, Permissions: domain.PluginPermissions{RemoteAccess: true}}); err != nil {
|
||||
t.Fatalf("create SCUM plugin fixture: %v", err)
|
||||
}
|
||||
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: binding.ServerInstanceID, PluginID: binding.PluginID, RunEndpointID: binding.RunEndpointID, Name: "SCUM"}); err != nil {
|
||||
t.Fatalf("create SCUM server fixture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func scumTemplateTestResult(request domain.SCUMSQLiteTemplateRequest) domain.SCUMSQLiteTemplateResult {
|
||||
return domain.SCUMSQLiteTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, ParameterDigest: request.ParameterDigest, SourceFingerprint: scumTemplateTestHash(), ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), RowCount: 1, Rows: []map[string]any{{"externalPlayerId": "player-redacted", "displayName": "Known Player"}}, Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func scumTypedRCONTemplateTestResult(request domain.SCUMTypedRCONTemplateRequest) domain.SCUMTypedRCONTemplateResult {
|
||||
return domain.SCUMTypedRCONTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TransportKey: request.TransportKey, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, PayloadDigest: request.PayloadDigest, ConfirmationDigest: request.ConfirmationDigest, TargetIdentityDigest: request.TargetIdentityDigest, ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), ResponseDigest: scumTemplateTestHash(), ConfirmationStatus: domain.SCUMRCONConfirmationConfirmed, ConfirmationDigestID: scumTemplateTestHash(), SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func scumGuardedMutationTestResult(request domain.SCUMGuardedMutationRequest) domain.SCUMGuardedMutationResult {
|
||||
return domain.SCUMGuardedMutationResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, SourceFingerprint: scumTemplateTestHash(), TargetIdentityDigest: request.TargetIdentityDigest, ExpectedRowDigest: request.ExpectedRowDigest, ExpectedValueDigest: request.ExpectedValueDigest, ExpectedXMLDigest: request.ExpectedXMLDigest, PatchDigest: request.PatchDigest, BackupEvidenceDigest: request.BackupEvidenceDigest, OfflineEvidenceDigest: request.OfflineEvidenceDigest, DangerConfirmationDigest: request.DangerConfirmationDigest, ReadbackExpectationDigest: request.ReadbackExpectationDigest, ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), BeforeDigest: scumTemplateTestHash(), AfterDigest: scumTemplateTestHash(), ReadbackDigest: scumTemplateTestHash(), AffectedRows: 1, ReadbackStatus: domain.SCUMMutationReadbackConfirmed, SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func scumParsedLogBatchTestResult(binding domain.SCUMBindingIdentity, jobID string, source domain.RuntimeLogSource) domain.SCUMParsedLogBatchResult {
|
||||
cursor := domain.SCUMParsedLogCursor{SourceIdentityDigest: scumTemplateTestHash(), StreamGeneration: scumTemplateTestHash(), Sequence: 7}
|
||||
return domain.SCUMParsedLogBatchResult{RequestID: "request-log", JobID: jobID, Binding: binding, Status: domain.SCUMTerminalResultSucceeded, SourceKey: source.Key, StreamKey: source.StreamKey, ParserKey: "scum-login-log-login-parser", ParserVersion: "scum-login-log-v1", AdapterVersion: binding.AdapterVersion, AssetDigest: scumTemplateTestHash(), ParserDigest: scumTemplateTestHash(), ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), FirstCursor: cursor, LastCursor: cursor, TailState: domain.SCUMLogTailRotated, Replay: true, EventCount: 1, Events: []domain.SCUMParsedLogEvent{{EventType: "scum.login", OccurredAt: time.Now(), Cursor: cursor, LogicalEventDigest: scumTemplateTestHash(), EventDigest: scumTemplateTestHash(), PayloadDigest: scumTemplateTestHash(), Payload: map[string]any{"externalPlayerId": "player-redacted", "displayName": "Known Player", "profileLocalId": "profile-redacted"}}}, SafeSummary: "one sanitized login event parsed from declared source", Limits: domain.DefaultSCUMParsedLogBatchBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func createQueuedRunJob(t *testing.T, svc *CoreService, id string, idempotencyKey string) domain.Job {
|
||||
t.Helper()
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
|
||||
@@ -44,9 +44,6 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
|
||||
if err := validator.ValidateRemoteAdapterRequest(request); err != nil {
|
||||
return domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
if request.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe && !request.PlatformScheduled {
|
||||
return domain.RemoteAdapterResult{}, forbiddenError("schema probe is scheduled by Platform and is not a direct remote-adapter request")
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.RemoteAdapterResult{}, err
|
||||
@@ -86,30 +83,20 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
|
||||
return domain.RemoteAdapterResult{}, validationError("remote adapter timeout or retry exceeds declaration")
|
||||
}
|
||||
inputRef := request.InputRef
|
||||
isSchemaProbe := request.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe && request.PlatformScheduled && request.SQLiteSchemaProbe != nil
|
||||
if inputRef == "" && !isSchemaProbe {
|
||||
if inputRef == "" {
|
||||
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{
|
||||
ID: jobIDFromParts("job-remote-adapter", instance.ID, request.IdempotencyKey),
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: request.Capability,
|
||||
TargetKey: targetKey,
|
||||
TargetKey: request.TargetKey,
|
||||
InputRef: inputRef,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "scoped remote adapter queued"},
|
||||
RetryPolicy: domain.JobRetryPolicy{MaxAttempts: attempts, InitialBackoffSeconds: 2, MaxBackoffSeconds: 30},
|
||||
ExecutionInput: executionInput,
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: selected.Key, RemoteAdapterKind: string(selected.Kind), TimeoutSeconds: timeout, Inputs: domain.CopyStringMap(request.Inputs)},
|
||||
}
|
||||
created, err := svc.CreateJob(job)
|
||||
if err != nil {
|
||||
@@ -126,14 +113,6 @@ 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
|
||||
}
|
||||
|
||||
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 {
|
||||
result := make([]string, 0, len(profile))
|
||||
for _, capability := range profile {
|
||||
@@ -150,7 +129,7 @@ func isRemoteAdapterCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand:
|
||||
return true
|
||||
default:
|
||||
@@ -187,7 +166,7 @@ func remoteAdapterKindForCapability(capability string) domain.RemoteAdapterKind
|
||||
return domain.RemoteAdapterRunFile
|
||||
case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop:
|
||||
return domain.RemoteAdapterRunProcess
|
||||
case domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery:
|
||||
case domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery:
|
||||
return domain.RemoteAdapterDatabase
|
||||
case domain.JobCapabilityRemoteRunRCONCommand:
|
||||
return domain.RemoteAdapterRCON
|
||||
|
||||
@@ -2,7 +2,6 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
@@ -87,125 +86,3 @@ func TestRemoteAdapterRequestPropagatesTypedInputsToRunJob(t *testing.T) {
|
||||
t.Fatal("real Run claim aliases persisted remote inputs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMSchemaProbeDispatchIsPlatformScheduledAndFenced(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
plugin.Permissions.RemoteAccess = true
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
plugin.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, DatabaseEngines: []string{"sqlite"}}
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles,
|
||||
domain.RuntimeTransportProfile{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||
domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
)
|
||||
plugin.RuntimeProfiles.DataTargets = append(plugin.RuntimeProfiles.DataTargets, domain.RuntimeDataTarget{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}})
|
||||
plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys = append(plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys, "scum-database")
|
||||
plugin.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 := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session := createServiceUserAndLogin(t, svc, domain.User{ID: "user-scum-probe-owner", DisplayName: "SCUM Probe Owner", Email: "scum-probe-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "server-scum-probe", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Probe"})
|
||||
if err != nil {
|
||||
t.Fatalf("create server instance: %v", err)
|
||||
}
|
||||
createCompleteRuntimeBinding(t, svc, instance, "local")
|
||||
|
||||
_, err = svc.RequestRemoteAdapterForSession(session, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: "scum-database", TargetKey: "scum-database", Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, IdempotencyKey: "direct-probe-denied", InputRef: "input://scum-schema-probe/direct-probe-denied"})
|
||||
if err == nil || !strings.Contains(err.Error(), "scheduled by Platform") {
|
||||
t.Fatalf("expected public probe request denial, got %v", err)
|
||||
}
|
||||
endpoint.Capabilities = withoutCapability(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err = svc.RequestSCUMSchemaProbeForSession(session, instance.ID, "probe-missing-run-capability"); err == nil || !strings.Contains(err.Error(), "does not expose") {
|
||||
t.Fatalf("expected missing active Run capability, got %v", err)
|
||||
}
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("missing probe executor must not create jobs: len=%d err=%v", len(jobs), err)
|
||||
}
|
||||
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
probeRequest, queued, err := svc.RequestSCUMSchemaProbeForSession(session, instance.ID, "probe-current-schema")
|
||||
if err != nil {
|
||||
t.Fatalf("queue SCUM schema probe: %v", err)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(queued.RequestID)
|
||||
if err != nil {
|
||||
t.Fatalf("get probe job: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 job did not include typed SQLite schema probe request: %+v", job.ExecutionInput.SQLiteSchemaProbe)
|
||||
}
|
||||
|
||||
helloRequest := validRunControlHello()
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
helloRequest.CapabilityReport.Fingerprint = "cap-scum-probe"
|
||||
hello, err := svc.RegisterRunHello(helloRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("register Run hello: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim probe job: %v", err)
|
||||
}
|
||||
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 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.RunBindingID = "runtime-binding-other"
|
||||
badProbe := domain.SCUMSchemaProbeResult{RequestID: probeRequest.RequestID, JobID: probeRequest.JobID, Binding: badBinding, Status: domain.SCUMCapabilityEvidenceCompatible, SourceFingerprint: "sha256:" + strings.Repeat("c", 64), SchemaFingerprint: "sha256:" + strings.Repeat("a", 64), ObservedAt: fixedTime, ResultDigest: "sha256:" + strings.Repeat("b", 64), Limits: probeRequest.Bounds}
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "probe complete"}, Message: "probe complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &badProbe, AuditSummary: "redacted schema probe"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "binding identity") {
|
||||
t.Fatalf("expected binding mismatch rejection, got %v", err)
|
||||
}
|
||||
goodProbe := badProbe
|
||||
goodProbe.Binding = probeRequest.Binding
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "probe complete"}, Message: "probe complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &goodProbe, AuditSummary: "redacted schema probe"}}); err != nil {
|
||||
t.Fatalf("complete fenced probe job: %v", err)
|
||||
}
|
||||
stored, err := svc.store.Jobs().Get(job.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get completed probe job: %v", err)
|
||||
}
|
||||
if stored.ExecutionResult.SQLiteSchemaProbe == nil || stored.ExecutionResult.SQLiteSchemaProbe.SourceFingerprint != goodProbe.SourceFingerprint || stored.ExecutionResult.SQLiteSchemaProbe.ResultDigest != goodProbe.ResultDigest || stored.ExecutionResult.SQLiteSchemaProbe.Binding.RunBindingID != probeRequest.Binding.RunBindingID {
|
||||
t.Fatalf("typed probe result was not persisted safely: %+v", stored.ExecutionResult.SQLiteSchemaProbe)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +124,6 @@ type Core interface {
|
||||
ListBackupsForSession(string, domain.BackupFilter) ([]domain.BackupRecord, error)
|
||||
ListRemoteAdapterDeclarationsForSession(string, string) ([]domain.RemoteAdapterDeclaration, error)
|
||||
RequestRemoteAdapterForSession(string, domain.RemoteAdapterRequest) (domain.RemoteAdapterResult, error)
|
||||
NegotiateSCUMCapabilitiesForSession(string, string) (domain.SCUMCapabilityNegotiation, error)
|
||||
RequestSCUMSchemaProbeForSession(string, string, string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error)
|
||||
GetServerConfigForSession(string, string) (domain.ServerConfig, error)
|
||||
GetDeclaredFileReadSnapshotForSession(string, string, string) (domain.DeclaredFileReadSnapshot, error)
|
||||
PreviewServerConfigWriteForSession(string, domain.ServerConfigDiffRequest) (domain.ServerConfigDiffPreview, error)
|
||||
@@ -227,6 +225,20 @@ type Core interface {
|
||||
RequestGameGiftGrantForSession(string, string, domain.GameGiftGrantRequest) (domain.GameGiftGrant, error)
|
||||
ApproveGameGiftGrantForSession(string, string) (domain.GameGiftGrant, error)
|
||||
ListGameGiftGrantsForSession(string, string) ([]domain.GameGiftGrant, error)
|
||||
ListSCUMPlayerLiveStatesForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMPlayerLiveState, error)
|
||||
ListSCUMSquadsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMSquad, error)
|
||||
ListSCUMSquadMembersForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMSquadMember, error)
|
||||
ListSCUMVehiclesForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMVehicle, error)
|
||||
ListSCUMFlagsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMFlag, error)
|
||||
ListSCUMCurrentPositionsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMCurrentPosition, error)
|
||||
RequestSCUMOperationForSession(string, string, domain.SCUMOperationRequest) (domain.SCUMOperationRequest, error)
|
||||
ListSCUMOperationsForSession(string, domain.SCUMOperationRequestFilter) ([]domain.SCUMOperationRequest, error)
|
||||
ApproveSCUMOperationForSession(string, string) (domain.SCUMOperationRequest, error)
|
||||
ReconcileSCUMOperation(string) (domain.SCUMOperationRequest, error)
|
||||
ConfirmSCUMOperation(string, domain.SCUMOperationConfirmation) (domain.SCUMOperationRequest, error)
|
||||
CreateSCUMWorkflowForSession(string, string, domain.SCUMWorkflowInstance) (domain.SCUMWorkflowInstance, error)
|
||||
ListSCUMWorkflowsForSession(string, domain.SCUMWorkflowInstanceFilter) ([]domain.SCUMWorkflowInstance, error)
|
||||
ListSCUMWorkflowStepsForSession(string, domain.SCUMWorkflowStepFilter) ([]domain.SCUMWorkflowStep, error)
|
||||
CreateAuditEvent(domain.AuditEvent) (domain.AuditEvent, error)
|
||||
GetAuditEvent(string) (domain.AuditEvent, error)
|
||||
ListAuditEvents(domain.AuditEventFilter) ([]domain.AuditEvent, error)
|
||||
@@ -814,7 +826,6 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
|
||||
RemoteAccess: manifest.RemoteAccess,
|
||||
RuntimeProfiles: manifest.RuntimeProfiles,
|
||||
GameClientBridge: manifest.GameClientBridge,
|
||||
SCUMLiveData: manifest.SCUMLiveData,
|
||||
MapTrajectories: manifest.MapTrajectories,
|
||||
Status: domain.GamePluginStatusInstalled,
|
||||
}
|
||||
@@ -1169,11 +1180,6 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "requested remote capability is not declared by plugin"}
|
||||
return base
|
||||
}
|
||||
if capability == domain.JobCapabilityRemoteRunDBSQLiteProbe {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "schema probe is scheduled by Platform and is not a plugin page action"}
|
||||
return base
|
||||
}
|
||||
declarationKey := strings.TrimSpace(payload["declarationKey"])
|
||||
if declarationKey == "" {
|
||||
for _, profile := range plugin.RuntimeProfiles.TransportProfiles {
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
const (
|
||||
scumSchemaProbeExecutionKind = "sqlite.schema-probe"
|
||||
scumSQLiteTemplateExecutionKind = "sqlite.template-query"
|
||||
scumRCONTemplateExecutionKind = "rcon.template-command"
|
||||
scumGuardedMutationExecutionKind = "sqlite.guarded-mutation"
|
||||
scumParsedLogBatchExecutionKind = "log.parsed-events"
|
||||
)
|
||||
|
||||
func (svc *CoreService) NegotiateSCUMCapabilitiesForSession(sessionID, serverInstanceID string) (domain.SCUMCapabilityNegotiation, error) {
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMCapabilityNegotiation{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMCapabilityNegotiation{}, err
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.SCUMCapabilityNegotiation{}, err
|
||||
}
|
||||
adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData)
|
||||
active := domain.SCUMBindingIdentity{ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, PluginID: plugin.ID, PluginVersion: plugin.Version, AdapterVersion: adapterVersion, DatabaseIdentity: scumSchemaProbeDatabaseIdentity(plugin.SCUMLiveData.Probe.TargetKey)}
|
||||
binding, bindingErr := svc.runtimeBindingForServer(instance.ID)
|
||||
if bindingErr == nil {
|
||||
binding, bindingErr = normalizeRuntimeBinding(plugin, binding)
|
||||
}
|
||||
if bindingErr == nil {
|
||||
active.RunBindingID = binding.ID
|
||||
}
|
||||
probeExecutorAvailable := scumRunCapabilityAvailable(endpoint, domain.SCUMDataCapabilitySchemaProbe)
|
||||
negotiation := domain.SCUMCapabilityNegotiation{ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, RunBindingID: active.RunBindingID, PluginID: plugin.ID, PluginVersion: plugin.Version, AdapterVersion: adapterVersion, GameVersion: active.GameVersion, DatabaseIdentity: active.DatabaseIdentity, ProbeExecutorAvailable: probeExecutorAvailable, EvaluatedAt: svc.now()}
|
||||
evidenceByCapability := svc.latestSCUMCapabilityEvidenceByCapability(instance.ID)
|
||||
for _, declaration := range plugin.SCUMLiveData.CapabilityGates {
|
||||
gate := domain.SCUMCapabilityGate{Capability: declaration.Capability, State: domain.SCUMCapabilityGateDisabled, ReasonCode: domain.SCUMSafeErrorProbeMissing, Reason: safeSCUMGateReason(declaration.SafeReason, "current-service evidence is required before this SCUM capability can run")}
|
||||
if requiredRunCapability := scumRequiredRunCapabilityForDataCapability(declaration.Capability); requiredRunCapability != "" && !containsString(endpoint.Capabilities, requiredRunCapability) {
|
||||
gate.ReasonCode = domain.SCUMSafeErrorProbeExecutorAbsent
|
||||
gate.Reason = "bound Run does not expose the generic executor required for this SCUM capability"
|
||||
negotiation.Gates = append(negotiation.Gates, gate)
|
||||
continue
|
||||
}
|
||||
if bindingErr != nil || binding.Status != domain.RuntimeBindingStatusComplete || binding.PluginVersion != plugin.Version {
|
||||
gate.ReasonCode = domain.SCUMSafeErrorBindingMismatch
|
||||
gate.Reason = "active runtime binding is missing, incomplete, or stale for this plugin version"
|
||||
negotiation.Gates = append(negotiation.Gates, gate)
|
||||
continue
|
||||
}
|
||||
if declaration.Gate != domain.SCUMCapabilityGateEnabled {
|
||||
gate.ReasonCode = scumReasonCodeForEvidenceStatus(declaration.EvidenceStatus)
|
||||
negotiation.Gates = append(negotiation.Gates, gate)
|
||||
continue
|
||||
}
|
||||
requirement := domain.SCUMCapabilityRequirement{Capability: declaration.Capability, AdapterVersion: declaration.AdapterVersion, SchemaFingerprint: declaration.RequiredSchemaFingerprint, AssetDigests: domain.CopyStringSlice(declaration.RequiredAssetDigests)}
|
||||
gate = domain.EvaluateSCUMCapabilityGate(requirement, evidenceByCapability[declaration.Capability], active, probeExecutorAvailable, negotiation.EvaluatedAt)
|
||||
negotiation.Gates = append(negotiation.Gates, gate)
|
||||
}
|
||||
return negotiation, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) latestSCUMCapabilityEvidenceByCapability(serverInstanceID string) map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence {
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: serverInstanceID})
|
||||
if err != nil {
|
||||
return map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence{}
|
||||
}
|
||||
evidence := map[domain.SCUMDataCapability]domain.SCUMCapabilityEvidence{}
|
||||
for _, job := range jobs {
|
||||
for _, candidate := range scumCapabilityEvidenceFromJob(job) {
|
||||
current, exists := evidence[candidate.Capability]
|
||||
if !exists || current.ObservedAt.Before(candidate.ObservedAt) {
|
||||
evidence[candidate.Capability] = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
return evidence
|
||||
}
|
||||
|
||||
func scumCapabilityEvidenceFromJob(job domain.Job) []domain.SCUMCapabilityEvidence {
|
||||
var evidence []domain.SCUMCapabilityEvidence
|
||||
if result := job.ExecutionResult.SQLiteSchemaProbe; result != nil {
|
||||
status := domain.SCUMCapabilityEvidenceFailed
|
||||
if result.Status == domain.SCUMSchemaProbeStatusSucceeded || result.Status == domain.SCUMCapabilityEvidenceCompatible {
|
||||
status = domain.SCUMCapabilityEvidenceCompatible
|
||||
} else if result.Status == domain.SCUMCapabilityEvidenceIncompatible {
|
||||
status = domain.SCUMCapabilityEvidenceIncompatible
|
||||
}
|
||||
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: domain.SCUMDataCapabilitySchemaProbe, Status: status, Binding: result.Binding, AdapterVersion: result.Binding.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
|
||||
}
|
||||
if result := job.ExecutionResult.SQLiteTemplate; result != nil {
|
||||
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
|
||||
}
|
||||
if result := job.ExecutionResult.RCONTemplate; result != nil {
|
||||
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
|
||||
}
|
||||
if result := job.ExecutionResult.GuardedMutation; result != nil {
|
||||
evidence = append(evidence, domain.SCUMCapabilityEvidence{Capability: result.Capability, Status: scumTerminalEvidenceStatus(result.Status), Binding: result.Binding, AdapterVersion: result.AdapterVersion, SchemaFingerprint: result.SchemaFingerprint, ProbeResultDigest: result.ResultDigest, AssetDigests: []string{result.AssetDigest}, ObservedAt: result.ObservedAt, SafeError: result.SafeError})
|
||||
}
|
||||
return evidence
|
||||
}
|
||||
|
||||
func scumTerminalEvidenceStatus(status domain.SCUMTerminalResultStatus) domain.SCUMCapabilityEvidenceStatus {
|
||||
if status == domain.SCUMTerminalResultSucceeded {
|
||||
return domain.SCUMCapabilityEvidenceCompatible
|
||||
}
|
||||
return domain.SCUMCapabilityEvidenceFailed
|
||||
}
|
||||
|
||||
func scumRunCapabilityAvailable(endpoint domain.RunEndpoint, capability domain.SCUMDataCapability) bool {
|
||||
return containsString(endpoint.Capabilities, scumRequiredRunCapabilityForDataCapability(capability))
|
||||
}
|
||||
|
||||
func scumRequiredRunCapabilityForDataCapability(capability domain.SCUMDataCapability) string {
|
||||
switch capability {
|
||||
case domain.SCUMDataCapabilitySchemaProbe:
|
||||
return domain.JobCapabilityRemoteRunDBSQLiteProbe
|
||||
case domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead:
|
||||
return domain.JobCapabilityRemoteRunDBSQLiteQuery
|
||||
case domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return domain.JobCapabilityRemoteRunProtectedRCON
|
||||
case domain.SCUMDataCapabilityProfileXMLWrite:
|
||||
return domain.JobCapabilityRemoteRunProtectedSQL
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func scumReasonCodeForEvidenceStatus(status domain.SCUMCapabilityEvidenceStatus) domain.SCUMSafeErrorCode {
|
||||
switch status {
|
||||
case domain.SCUMCapabilityEvidenceFailed:
|
||||
return domain.SCUMSafeErrorProbeFailed
|
||||
case domain.SCUMCapabilityEvidenceIncompatible:
|
||||
return domain.SCUMSafeErrorSchemaIncompatible
|
||||
default:
|
||||
return domain.SCUMSafeErrorProbeMissing
|
||||
}
|
||||
}
|
||||
|
||||
func safeSCUMGateReason(value, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
|
||||
idempotencyKey = strings.TrimSpace(idempotencyKey)
|
||||
if idempotencyKey == "" {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("idempotencyKey is required")
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
probe := plugin.SCUMLiveData.Probe
|
||||
if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin")
|
||||
}
|
||||
if !scumSchemaProbeHasDataTarget(plugin.RuntimeProfiles, probe.TargetKey) {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe target is not declared as a generated Run data target")
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run is not online for SCUM schema probe")
|
||||
}
|
||||
if !containsString(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run does not expose the generic SQLite schema-probe executor")
|
||||
}
|
||||
binding, err := svc.runtimeBindingForServer(instance.ID)
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("runtime binding is required before SCUM schema probe")
|
||||
}
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData)
|
||||
if adapterVersion == "" {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema-probe adapter version is not declared")
|
||||
}
|
||||
bounds := probe.Bounds
|
||||
if bounds.MaxObjects == 0 {
|
||||
bounds = domain.DefaultSCUMSchemaProbeBounds()
|
||||
}
|
||||
jobID := jobIDFromParts("job-remote-adapter", instance.ID, idempotencyKey)
|
||||
request := domain.SCUMSchemaProbeRequest{
|
||||
RequestID: jobID,
|
||||
JobID: jobID,
|
||||
Binding: domain.SCUMBindingIdentity{
|
||||
ServerInstanceID: instance.ID,
|
||||
RunBindingID: binding.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
PluginID: plugin.ID,
|
||||
PluginVersion: plugin.Version,
|
||||
AdapterVersion: adapterVersion,
|
||||
DatabaseIdentity: scumSchemaProbeDatabaseIdentity(probe.TargetKey),
|
||||
},
|
||||
Bounds: bounds,
|
||||
RequestedAt: svc.now(),
|
||||
}
|
||||
if err := validator.ValidateSCUMSchemaProbeRequest(request); err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
DeclarationKey: probe.TargetKey,
|
||||
TargetKey: probe.TargetKey,
|
||||
Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe,
|
||||
TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds),
|
||||
MaxAttempts: 1,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
PlatformScheduled: true,
|
||||
SQLiteSchemaProbe: &request,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||
}
|
||||
return request, result, nil
|
||||
}
|
||||
|
||||
func scumSchemaProbeDatabaseIdentity(targetKey string) string {
|
||||
trimmed := strings.TrimSpace(targetKey)
|
||||
return strings.TrimPrefix(trimmed, "databases/")
|
||||
}
|
||||
|
||||
func scumSchemaProbeHasDataTarget(profiles domain.GamePluginRuntimeProfiles, targetKey string) bool {
|
||||
expectedWorkspaceKey := "databases/" + scumSchemaProbeDatabaseIdentity(targetKey)
|
||||
for _, target := range profiles.DataTargets {
|
||||
if target.Key == targetKey && target.Kind == "sqlite.snapshot" && target.WorkspaceKey == expectedWorkspaceKey && target.RefreshPolicy == "on-demand-snapshot" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
|
||||
for _, gate := range manifest.CapabilityGates {
|
||||
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
|
||||
return strings.TrimSpace(gate.AdapterVersion)
|
||||
}
|
||||
}
|
||||
for _, gate := range manifest.CapabilityGates {
|
||||
if strings.TrimSpace(gate.AdapterVersion) != "" {
|
||||
return strings.TrimSpace(gate.AdapterVersion)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func scumSchemaProbeTimeoutSeconds(bounds domain.SCUMSchemaProbeBounds) int {
|
||||
if bounds.TimeoutMS <= 0 {
|
||||
return 1
|
||||
}
|
||||
seconds := (bounds.TimeoutMS + 999) / 1000
|
||||
if seconds <= 0 {
|
||||
return 1
|
||||
}
|
||||
return seconds
|
||||
}
|
||||
|
||||
func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error {
|
||||
if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.SQLiteSchemaProbe
|
||||
if expected == nil {
|
||||
return validationError("SQLite schema probe request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("SQLite schema probe result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("SQLite schema probe result does not match leased binding identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMSQLiteTemplateResultForJob(job domain.Job, result domain.SCUMSQLiteTemplateResult) error {
|
||||
if err := validator.ValidateSCUMSQLiteTemplateResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.SQLiteTemplate
|
||||
if expected == nil {
|
||||
return validationError("SQLite template request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("SQLite template result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("SQLite template result does not match leased binding identity")
|
||||
}
|
||||
if result.Capability != expected.Capability || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.SchemaFingerprint != expected.RequiredSchemaFingerprint || result.AssetDigest != expected.AssetDigest || result.ParameterDigest != expected.ParameterDigest {
|
||||
return validationError("SQLite template result does not match leased template, adapter, digest, or parameter identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMTypedRCONTemplateResultForJob(job domain.Job, result domain.SCUMTypedRCONTemplateResult) error {
|
||||
if err := validator.ValidateSCUMTypedRCONTemplateResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.RCONTemplate
|
||||
if expected == nil {
|
||||
return validationError("typed RCON template request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("typed RCON template result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("typed RCON template result does not match leased binding identity")
|
||||
}
|
||||
if result.Capability != expected.Capability || result.TransportKey != expected.TransportKey || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.AssetDigest != expected.AssetDigest || result.PayloadDigest != expected.PayloadDigest || result.ConfirmationDigest != expected.ConfirmationDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest {
|
||||
return validationError("typed RCON template result does not match leased template, target, digest, or payload identity")
|
||||
}
|
||||
if expected.RequiredSchemaFingerprint != "" && result.SchemaFingerprint != expected.RequiredSchemaFingerprint {
|
||||
return validationError("typed RCON template result does not match leased schema fingerprint")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationResultForJob(job domain.Job, result domain.SCUMGuardedMutationResult) error {
|
||||
if err := validator.ValidateSCUMGuardedMutationResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.GuardedMutation
|
||||
if expected == nil {
|
||||
return validationError("guarded mutation request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("guarded mutation result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("guarded mutation result does not match leased binding identity")
|
||||
}
|
||||
if result.Capability != expected.Capability || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.SchemaFingerprint != expected.RequiredSchemaFingerprint || result.AssetDigest != expected.AssetDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest || result.ExpectedRowDigest != expected.ExpectedRowDigest || result.ExpectedValueDigest != expected.ExpectedValueDigest || result.ExpectedXMLDigest != expected.ExpectedXMLDigest || result.PatchDigest != expected.PatchDigest || result.BackupEvidenceDigest != expected.BackupEvidenceDigest || result.OfflineEvidenceDigest != expected.OfflineEvidenceDigest || result.DangerConfirmationDigest != expected.DangerConfirmationDigest || result.ReadbackExpectationDigest != expected.ReadbackExpectationDigest {
|
||||
return validationError("guarded mutation result does not match leased template, target, guard, digest, or readback identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMParsedLogBatchResultForJob(job domain.Job, result domain.SCUMParsedLogBatchResult) error {
|
||||
if err := validator.ValidateSCUMParsedLogBatchResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.LogSource
|
||||
if expected == nil {
|
||||
return validationError("parsed log batch request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID {
|
||||
return validationError("parsed log batch result does not match leased job identity")
|
||||
}
|
||||
if result.Binding.ServerInstanceID != job.ServerInstanceID || result.Binding.RunEndpointID != job.RunEndpointID {
|
||||
return validationError("parsed log batch result does not match leased server or Run endpoint")
|
||||
}
|
||||
if result.SourceKey != expected.Key || result.StreamKey != expected.StreamKey {
|
||||
return validationError("parsed log batch result does not match leased log source identity")
|
||||
}
|
||||
for key, value := range map[string]string{
|
||||
"parserKey": result.ParserKey,
|
||||
"parserVersion": result.ParserVersion,
|
||||
"parserDigest": result.ParserDigest,
|
||||
"adapterVersion": result.AdapterVersion,
|
||||
} {
|
||||
if expectedValue := strings.TrimSpace(job.ExecutionInput.Inputs[key]); expectedValue != "" && value != expectedValue {
|
||||
return validationError("parsed log batch result does not match leased parser identity")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.PluginID != "" && result.Binding.PluginID != job.ExecutionInput.PluginID {
|
||||
return validationError("parsed log batch result does not match leased plugin identity")
|
||||
}
|
||||
if job.ExecutionInput.TargetVersion != "" && result.Binding.PluginVersion != job.ExecutionInput.TargetVersion {
|
||||
return validationError("parsed log batch result does not match leased plugin version")
|
||||
}
|
||||
if result.FirstCursor.SourceIdentityDigest != result.LastCursor.SourceIdentityDigest || result.FirstCursor.StreamGeneration != result.LastCursor.StreamGeneration {
|
||||
return validationError("parsed log batch result crosses source identity or generation boundaries")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sameSCUMSchemaProbeBinding(a, b domain.SCUMBindingIdentity) bool {
|
||||
return a.ServerInstanceID == b.ServerInstanceID && a.RunBindingID == b.RunBindingID && a.RunEndpointID == b.RunEndpointID && a.PluginID == b.PluginID && a.PluginVersion == b.PluginVersion && a.AdapterVersion == b.AdapterVersion && a.GameVersion == b.GameVersion && a.DatabaseIdentity == b.DatabaseIdentity
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
func TestSCUMCapabilityNegotiationEvaluatesActiveBindingEvidenceIndependently(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
svc := NewCoreService(store)
|
||||
now := time.Date(2026, 8, 13, 7, 0, 0, 0, time.UTC)
|
||||
svc.now = func() time.Time { return now }
|
||||
sessionID := seedSCUMCapabilityNegotiationFixture(t, svc, store)
|
||||
binding := scumCapabilityNegotiationBinding()
|
||||
schema := scumNegotiationDigest("a")
|
||||
playerAsset := scumNegotiationDigest("b")
|
||||
|
||||
if err := store.Jobs().Create(domain.Job{ID: "job-scum-probe", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &domain.SCUMSchemaProbeResult{RequestID: "job-scum-probe", JobID: "job-scum-probe", Binding: binding, Status: domain.SCUMSchemaProbeStatusSucceeded, SourceFingerprint: scumNegotiationDigest("c"), SchemaFingerprint: schema, ObservedAt: now.Add(-2 * time.Minute), ResultDigest: scumNegotiationDigest("d"), Limits: domain.DefaultSCUMSchemaProbeBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
|
||||
t.Fatalf("create probe evidence job: %v", err)
|
||||
}
|
||||
if err := store.Jobs().Create(domain.Job{ID: "job-scum-player-query", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &domain.SCUMSQLiteTemplateResult{RequestID: "job-scum-player-query", JobID: "job-scum-player-query", Binding: binding, Status: domain.SCUMTerminalResultSucceeded, Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players-read", AdapterVersion: binding.AdapterVersion, SchemaFingerprint: schema, AssetDigest: playerAsset, ParameterDigest: scumNegotiationDigest("e"), SourceFingerprint: scumNegotiationDigest("f"), ObservedAt: now.Add(-time.Minute), ResultDigest: scumNegotiationDigest("1"), RowCount: 0, Limits: domain.DefaultSCUMSQLiteTemplateBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
|
||||
t.Fatalf("create player evidence job: %v", err)
|
||||
}
|
||||
|
||||
negotiation, err := svc.NegotiateSCUMCapabilitiesForSession(sessionID, binding.ServerInstanceID)
|
||||
if err != nil {
|
||||
t.Fatalf("negotiate capabilities: %v", err)
|
||||
}
|
||||
gates := map[domain.SCUMDataCapability]domain.SCUMCapabilityGate{}
|
||||
for _, gate := range negotiation.Gates {
|
||||
gates[gate.Capability] = gate
|
||||
}
|
||||
if !negotiation.ProbeExecutorAvailable || negotiation.RunBindingID != binding.RunBindingID || negotiation.DatabaseIdentity != binding.DatabaseIdentity {
|
||||
t.Fatalf("unexpected negotiation identity: %+v", negotiation)
|
||||
}
|
||||
if gate := gates[domain.SCUMDataCapabilitySchemaProbe]; !gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorNone {
|
||||
t.Fatalf("schema probe should be enabled from accepted probe evidence, got %+v", gate)
|
||||
}
|
||||
if gate := gates[domain.SCUMDataCapabilityPlayerRead]; !gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorNone {
|
||||
t.Fatalf("players.read should be enabled from matching template evidence, got %+v", gate)
|
||||
}
|
||||
if gate := gates[domain.SCUMDataCapabilitySquadRead]; gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorProbeMissing {
|
||||
t.Fatalf("squads.read should remain independently disabled without template evidence, got %+v", gate)
|
||||
}
|
||||
if gate := gates[domain.SCUMDataCapabilityEconomyCommand]; gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorProbeExecutorAbsent {
|
||||
t.Fatalf("economy-command.write should remain disabled when Run lacks protected RCON, got %+v", gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMCapabilityNegotiationRejectsEvidenceFromAnotherBinding(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
svc := NewCoreService(store)
|
||||
now := time.Date(2026, 8, 13, 7, 30, 0, 0, time.UTC)
|
||||
svc.now = func() time.Time { return now }
|
||||
sessionID := seedSCUMCapabilityNegotiationFixture(t, svc, store)
|
||||
binding := scumCapabilityNegotiationBinding()
|
||||
stale := binding
|
||||
stale.RunBindingID = "runtime-binding-old"
|
||||
if err := store.Jobs().Create(domain.Job{ID: "job-stale-player-query", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &domain.SCUMSQLiteTemplateResult{RequestID: "job-stale-player-query", JobID: "job-stale-player-query", Binding: stale, Status: domain.SCUMTerminalResultSucceeded, Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players-read", AdapterVersion: binding.AdapterVersion, SchemaFingerprint: scumNegotiationDigest("a"), AssetDigest: scumNegotiationDigest("b"), ParameterDigest: scumNegotiationDigest("e"), SourceFingerprint: scumNegotiationDigest("f"), ObservedAt: now, ResultDigest: scumNegotiationDigest("1"), RowCount: 0, Limits: domain.DefaultSCUMSQLiteTemplateBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
|
||||
t.Fatalf("create stale evidence job: %v", err)
|
||||
}
|
||||
|
||||
negotiation, err := svc.NegotiateSCUMCapabilitiesForSession(sessionID, binding.ServerInstanceID)
|
||||
if err != nil {
|
||||
t.Fatalf("negotiate capabilities: %v", err)
|
||||
}
|
||||
for _, gate := range negotiation.Gates {
|
||||
if gate.Capability == domain.SCUMDataCapabilityPlayerRead {
|
||||
if gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorBindingMismatch {
|
||||
t.Fatalf("players.read should reject stale binding evidence, got %+v", gate)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("players.read gate missing")
|
||||
}
|
||||
|
||||
func seedSCUMCapabilityNegotiationFixture(t *testing.T, svc *CoreService, store *repo.MemoryStore) string {
|
||||
t.Helper()
|
||||
if _, err := svc.CreateUser(domain.User{ID: "scum-negotiation-owner", DisplayName: "SCUM Negotiation Owner", Email: "scum-negotiation@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
auth, err := svc.LoginUser(domain.UserLogin{Account: "scum-negotiation@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatalf("login: %v", err)
|
||||
}
|
||||
plugin := domain.GamePlugin{ID: "game.scum", Version: "1.0.0", Status: domain.GamePluginStatusInstalled, 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.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "schema probe evidence is compatible"},
|
||||
{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("b")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "players query evidence is compatible"},
|
||||
{Capability: domain.SCUMDataCapabilitySquadRead, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("2")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "squad query evidence is compatible"},
|
||||
{Capability: domain.SCUMDataCapabilityEconomyCommand, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("3")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "economy command evidence is compatible"},
|
||||
{Capability: domain.SCUMDataCapabilityGiftCommand, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "gift command evidence is missing"},
|
||||
{Capability: domain.SCUMDataCapabilityProfileXMLWrite, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "XML mutation evidence is missing"},
|
||||
}}, RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process"}}}}
|
||||
if err := store.GamePlugins().Create(plugin); err != nil {
|
||||
t.Fatalf("create plugin: %v", err)
|
||||
}
|
||||
endpoint := domain.RunEndpoint{ID: "run-scum-negotiation", DisplayName: "Run SCUM Negotiation", Version: "0.1.0", Status: domain.RunEndpointStatusOnline, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery}}
|
||||
if err := store.RunEndpoints().Create(endpoint); err != nil {
|
||||
t.Fatalf("create endpoint: %v", err)
|
||||
}
|
||||
if err := store.ServerInstances().Create(domain.ServerInstance{ID: "server-scum-negotiation", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: endpoint.ID, Name: "SCUM Negotiation", OwnerUserID: "scum-negotiation-owner", State: domain.ServerInstanceStateRunning}); err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
if _, err := svc.UpdateServerRuntimeBindingForSession(auth.SessionID, "server-scum-negotiation", domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{}}); err != nil {
|
||||
t.Fatalf("create runtime binding: %v", err)
|
||||
}
|
||||
return auth.SessionID
|
||||
}
|
||||
|
||||
func scumCapabilityNegotiationBinding() domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: "server-scum-negotiation", RunBindingID: "runtime-binding-server-scum-negotiation", RunEndpointID: "run-scum-negotiation", PluginID: "game.scum", PluginVersion: "1.0.0", AdapterVersion: "adapter-1", DatabaseIdentity: "scum-database"}
|
||||
}
|
||||
|
||||
func scumNegotiationDigest(char string) string {
|
||||
return "sha256:" + strings.Repeat(char, 64)
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
type scumSQLiteMutationJobResult struct {
|
||||
Outcome string `json:"outcome"`
|
||||
AffectedRows int `json:"affectedRows"`
|
||||
MutationChecksum string `json:"mutationChecksum"`
|
||||
ConfirmationRows []map[string]any `json:"confirmationRows"`
|
||||
SafeMessage string `json:"safeMessage"`
|
||||
}
|
||||
|
||||
func (svc *CoreService) RequestSCUMOperationForSession(sessionID, serverID string, request domain.SCUMOperationRequest) (domain.SCUMOperationRequest, error) {
|
||||
request = domain.CopySCUMOperationRequest(request)
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, serverID); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(serverID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
template, ok := scumOperationTemplate(plugin, request.TemplateKey)
|
||||
if !ok {
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation template is not declared")
|
||||
}
|
||||
if !containsString(plugin.DeclaredPermissions, template.Permission) {
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation permission is not declared")
|
||||
}
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" || len(request.IdempotencyKey) > 120 {
|
||||
return domain.SCUMOperationRequest{}, validationError("operation idempotency key is required")
|
||||
}
|
||||
existing, err := svc.store.SCUMOperationRequests().List(domain.SCUMOperationRequestFilter{ServerInstanceID: serverID, IdempotencyKey: request.IdempotencyKey})
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if len(existing) > 0 {
|
||||
return domain.CopySCUMOperationRequest(existing[0]), nil
|
||||
}
|
||||
playerID := coalesceString(request.PlayerID, firstString(request.Payload, "playerId", "steamId"))
|
||||
if playerID == "" && request.TemplateKey != "server.reward.command.deliver" {
|
||||
return domain.SCUMOperationRequest{}, validationError("operation playerId is required")
|
||||
}
|
||||
summary := operationSafeSummary(request.TemplateKey, playerID, request.Payload)
|
||||
switch template.Kind {
|
||||
case domain.GameClientBridgeOperationKindRCON:
|
||||
if err := validateSCUMRCONOperationPayload(request.TemplateKey, playerID, request.Payload); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
case domain.GameClientBridgeOperationKindSQLiteMutation:
|
||||
guard, payload, err := normalizeSCUMSQLiteMutationRequest(template, playerID, request.Payload, request.Guard)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
request.Guard = guard
|
||||
request.Payload = payload
|
||||
summary = scumSQLiteMutationSafeSummary(request.TemplateKey, playerID, guard)
|
||||
default:
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation kind is unsupported")
|
||||
}
|
||||
stamp := svc.now()
|
||||
operation := domain.SCUMOperationRequest{ID: "scum-operation-" + fingerprintID(serverID, request.IdempotencyKey), ServerInstanceID: serverID, PluginID: instance.PluginID, TemplateKey: request.TemplateKey, PlayerID: playerID, RequesterID: user.ID, ApprovalLevel: template.ApprovalLevel, Payload: domain.CopyGameClientBridgePayload(request.Payload), Guard: request.Guard, Status: domain.SCUMWorkflowStepWaiting, Reason: bounded(request.Reason, 240), IdempotencyKey: request.IdempotencyKey, SafeSummary: summary, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := svc.store.SCUMOperationRequests().Create(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
_, err = svc.recordAuditEventWithID(user.ID, "scum.operation.request", "scum-operation", operation.ID, domain.AuditResultQueued, "typed SCUM operation awaiting approval")
|
||||
return domain.CopySCUMOperationRequest(operation), err
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMOperationsForSession(sessionID string, filter domain.SCUMOperationRequestFilter) ([]domain.SCUMOperationRequest, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMOperationRequests().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ApproveSCUMOperationForSession(sessionID, operationID string) (domain.SCUMOperationRequest, error) {
|
||||
operation, err := svc.store.SCUMOperationRequests().Get(operationID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, operation.ServerInstanceID); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if operation.ApprovalLevel == domain.GameClientBridgeApprovalLevelPlatformAdmin && !isPlatformAdmin(user) {
|
||||
return domain.SCUMOperationRequest{}, ErrForbidden
|
||||
}
|
||||
if operation.Status != domain.SCUMWorkflowStepWaiting {
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation is not awaiting approval")
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(operation.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
template, ok := scumOperationTemplate(plugin, operation.TemplateKey)
|
||||
if !ok {
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation template is not declared")
|
||||
}
|
||||
var jobID string
|
||||
var auditSummary string
|
||||
switch template.Kind {
|
||||
case domain.GameClientBridgeOperationKindRCON:
|
||||
request, err := svc.sourceRCONRequestForSCUMOperation(operation)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
dispatch, err := svc.DispatchSourceRCONCommandForSession(sessionID, request)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
jobID = dispatch.JobID
|
||||
auditSummary = "typed SCUM operation dispatched through transient RCON input"
|
||||
case domain.GameClientBridgeOperationKindSQLiteMutation:
|
||||
gated, ready, err := svc.applySCUMSQLiteMutationApprovalGate(operation, template)
|
||||
if err != nil || !ready {
|
||||
return gated, err
|
||||
}
|
||||
operation = gated
|
||||
job, err := svc.dispatchSCUMSQLiteMutationOperation(operation, template)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
jobID = job.ID
|
||||
auditSummary = "typed SCUM DB mutation dispatched through template-bound Run job"
|
||||
default:
|
||||
return domain.SCUMOperationRequest{}, validationError("SCUM operation kind is unsupported")
|
||||
}
|
||||
stamp := svc.now()
|
||||
operation.ApproverID = user.ID
|
||||
operation.ApprovedAt = stamp
|
||||
operation.Status = domain.SCUMWorkflowStepQueued
|
||||
operation.RunJobID = jobID
|
||||
operation.UpdatedAt = stamp
|
||||
operation.AuditReferences = append(operation.AuditReferences, "job:"+jobID)
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
_, err = svc.recordAuditEventWithID(user.ID, "scum.operation.approve", "scum-operation", operation.ID, domain.AuditResultQueued, auditSummary)
|
||||
return domain.CopySCUMOperationRequest(operation), err
|
||||
}
|
||||
|
||||
func (svc *CoreService) ReconcileSCUMOperation(operationID string) (domain.SCUMOperationRequest, error) {
|
||||
operation, err := svc.store.SCUMOperationRequests().Get(operationID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
if strings.TrimSpace(operation.RunJobID) == "" {
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(operation.RunJobID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(operation.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
template, _ := scumOperationTemplate(plugin, operation.TemplateKey)
|
||||
stamp := svc.now()
|
||||
switch job.State {
|
||||
case domain.JobStateSucceeded:
|
||||
if template.Kind == domain.GameClientBridgeOperationKindSQLiteMutation {
|
||||
if updated, terminal := reconcileSCUMSQLiteMutationJobResult(operation, template, job); terminal {
|
||||
operation = updated
|
||||
} else {
|
||||
operation = updated
|
||||
operation.Status = domain.SCUMWorkflowStepConfirming
|
||||
}
|
||||
} else if operation.Confirmation.Status == "confirmed" {
|
||||
operation.Status = domain.SCUMWorkflowStepConfirmed
|
||||
} else {
|
||||
operation.Status = domain.SCUMWorkflowStepConfirming
|
||||
}
|
||||
case domain.JobStateFailed:
|
||||
if strings.Contains(strings.ToLower(job.ExecutionResult.Kind), "unknown") || strings.Contains(strings.ToLower(job.ExecutionResult.AuditSummary), "unknown") {
|
||||
operation.Status = domain.SCUMWorkflowStepUnknown
|
||||
} else {
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
}
|
||||
operation.CompletedAt = stamp
|
||||
case domain.JobStateCancelled:
|
||||
operation.Status = domain.SCUMWorkflowStepUnknown
|
||||
operation.CompletedAt = stamp
|
||||
}
|
||||
operation.UpdatedAt = stamp
|
||||
if (operation.Status == domain.SCUMWorkflowStepConfirmed || operation.Status == domain.SCUMWorkflowStepFailed || operation.Status == domain.SCUMWorkflowStepUnknown) && operation.CompletedAt.IsZero() {
|
||||
operation.CompletedAt = stamp
|
||||
}
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ConfirmSCUMOperation(operationID string, confirmation domain.SCUMOperationConfirmation) (domain.SCUMOperationRequest, error) {
|
||||
operation, err := svc.store.SCUMOperationRequests().Get(operationID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(operation.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
template, _ := scumOperationTemplate(plugin, operation.TemplateKey)
|
||||
confirmation = domain.CopySCUMOperationConfirmation(confirmation)
|
||||
stamp := svc.now()
|
||||
if confirmation.Status != "confirmed" {
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation = confirmation
|
||||
operation.CompletedAt = stamp
|
||||
operation.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
if template.Kind == domain.GameClientBridgeOperationKindSQLiteMutation && !scumSQLiteMutationConfirmationMatches(operation, confirmation.ConfirmedFields) {
|
||||
confirmation.Status = "failed"
|
||||
confirmation.SafeSummary = domain.SCUMSafeSummary{Title: "DB mutation confirmation mismatch", Message: "Run readback did not prove the requested SCUM player field value."}
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation = confirmation
|
||||
operation.CompletedAt = stamp
|
||||
operation.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
operation.Confirmation = confirmation
|
||||
operation.Status = domain.SCUMWorkflowStepConfirmed
|
||||
operation.CompletedAt = stamp
|
||||
operation.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) sourceRCONRequestForSCUMOperation(operation domain.SCUMOperationRequest) (domain.SourceRCONCommandRequest, error) {
|
||||
command, chat, err := scumRCONCommandForOperation(operation)
|
||||
if err != nil {
|
||||
return domain.SourceRCONCommandRequest{}, err
|
||||
}
|
||||
request := domain.SourceRCONCommandRequest{ServerInstanceID: operation.ServerInstanceID, IdempotencyKey: "scum-operation-" + operation.IdempotencyKey}
|
||||
if chat != "" {
|
||||
request.Kind = domain.SourceRCONCommandKindChat
|
||||
request.ChatType = 4
|
||||
request.TargetSteamID = operation.PlayerID
|
||||
request.Message = chat
|
||||
return request, nil
|
||||
}
|
||||
request.Kind = domain.SourceRCONCommandKindCommand
|
||||
request.Command = command
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func scumRCONCommandForOperation(operation domain.SCUMOperationRequest) (command string, chat string, err error) {
|
||||
playerID := operation.PlayerID
|
||||
switch operation.TemplateKey {
|
||||
case "player.fame.set":
|
||||
amount, ok := operationInteger(operation.Payload, "fame", "amount", "value")
|
||||
if !ok {
|
||||
return "", "", validationError("fame amount is required")
|
||||
}
|
||||
return fmt.Sprintf("#SetFamePoints %d %q", amount, playerID), "", nil
|
||||
case "player.currency.normal.set":
|
||||
amount, ok := operationInteger(operation.Payload, "amount", "balance", "normalBalance")
|
||||
if !ok {
|
||||
return "", "", validationError("normal currency amount is required")
|
||||
}
|
||||
return fmt.Sprintf("#SetCurrencyBalance Normal %d %q", amount, playerID), "", nil
|
||||
case "player.currency.gold.set":
|
||||
amount, ok := operationInteger(operation.Payload, "amount", "balance", "goldBalance")
|
||||
if !ok {
|
||||
return "", "", validationError("gold currency amount is required")
|
||||
}
|
||||
return fmt.Sprintf("#SetCurrencyBalance Gold %d %q", amount, playerID), "", nil
|
||||
case "player.notify":
|
||||
message := strings.TrimSpace(firstString(operation.Payload, "message", "notice"))
|
||||
if message == "" || len(message) > 200 {
|
||||
return "", "", validationError("notification message is required")
|
||||
}
|
||||
return "", message, nil
|
||||
default:
|
||||
return "", "", validationError("unsupported SCUM RCON operation template")
|
||||
}
|
||||
}
|
||||
|
||||
func validateSCUMRCONOperationPayload(templateKey, playerID string, payload map[string]any) error {
|
||||
operation := domain.SCUMOperationRequest{TemplateKey: templateKey, PlayerID: playerID, Payload: payload}
|
||||
command, chat, err := scumRCONCommandForOperation(operation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ContainsAny(command, "\r\n") || strings.ContainsAny(chat, "\r\n") {
|
||||
return validationError("operation payload contains invalid control characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeSCUMSQLiteMutationRequest(template domain.GameClientBridgeOperationTemplateDeclaration, playerID string, payload map[string]any, guard domain.SCUMMutationGuard) (domain.SCUMMutationGuard, map[string]any, error) {
|
||||
lowerKey := strings.ToLower(template.Key)
|
||||
if strings.Contains(lowerKey, "fame") || strings.Contains(lowerKey, "currency") {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM fame and currency edits must use RCON operation templates")
|
||||
}
|
||||
if template.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation requires platform-admin approval")
|
||||
}
|
||||
if template.Mutation.FieldKey == "" || template.Mutation.ConfirmationQueryKey == "" || template.Mutation.TableKey == "" || template.Mutation.IdentityKey == "" || template.Mutation.ValueKey == "" {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation metadata is incomplete")
|
||||
}
|
||||
if template.MaxRowsAffected < 1 {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation row bound is required")
|
||||
}
|
||||
payload = domain.CopyGameClientBridgePayload(payload)
|
||||
if guard.FieldKey == "" {
|
||||
guard.FieldKey = coalesceString(firstString(payload, "fieldKey"), template.Mutation.FieldKey)
|
||||
}
|
||||
if guard.Before == nil {
|
||||
guard.Before = payload["before"]
|
||||
}
|
||||
if guard.After == nil {
|
||||
guard.After = payload["after"]
|
||||
if guard.After == nil {
|
||||
guard.After = payload["value"]
|
||||
}
|
||||
}
|
||||
if guard.MaxRowsAffected == 0 {
|
||||
guard.MaxRowsAffected = template.MaxRowsAffected
|
||||
}
|
||||
guard.SafetyWindow = coalesceString(guard.SafetyWindow, firstString(payload, "safetyWindow", "maintenanceWindow"))
|
||||
guard.BackupRef = coalesceString(guard.BackupRef, firstString(payload, "backupRef", "snapshotRef"))
|
||||
guard.RequiresOfflinePlayer = template.Safety.RequiresOfflinePlayer
|
||||
guard.RequiresMaintenance = template.Safety.RequiresMaintenanceWindow
|
||||
guard.RequiresBackup = template.Safety.BackupRequired
|
||||
if playerID == "" {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation playerId is required")
|
||||
}
|
||||
if guard.FieldKey != template.Mutation.FieldKey {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation field key does not match template")
|
||||
}
|
||||
if guard.Before == nil || guard.After == nil {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation before and after values are required")
|
||||
}
|
||||
if guard.MaxRowsAffected < 1 || guard.MaxRowsAffected > template.MaxRowsAffected {
|
||||
return domain.SCUMMutationGuard{}, nil, validationError("SCUM DB mutation maxRowsAffected exceeds template bound")
|
||||
}
|
||||
if err := validateSCUMMutationValue(template, guard.Before, "before"); err != nil {
|
||||
return domain.SCUMMutationGuard{}, nil, err
|
||||
}
|
||||
if err := validateSCUMMutationValue(template, guard.After, "after"); err != nil {
|
||||
return domain.SCUMMutationGuard{}, nil, err
|
||||
}
|
||||
for key, value := range map[string]any{"playerId": playerID, "fieldKey": guard.FieldKey, "before": guard.Before, "after": guard.After, "safetyWindow": guard.SafetyWindow, "backupRef": guard.BackupRef} {
|
||||
if value != nil && value != "" {
|
||||
payload[key] = value
|
||||
}
|
||||
}
|
||||
return guard, payload, nil
|
||||
}
|
||||
|
||||
func validateSCUMMutationValue(template domain.GameClientBridgeOperationTemplateDeclaration, value any, label string) error {
|
||||
switch template.Mutation.AllowedValueType {
|
||||
case "integer":
|
||||
parsed, ok := anyInt64(value)
|
||||
if !ok {
|
||||
return validationError("SCUM DB mutation " + label + " value must be an integer")
|
||||
}
|
||||
if template.Mutation.MinValue != 0 && float64(parsed) < template.Mutation.MinValue || template.Mutation.MaxValue != 0 && float64(parsed) > template.Mutation.MaxValue {
|
||||
return validationError("SCUM DB mutation " + label + " value is outside the template range")
|
||||
}
|
||||
case "number":
|
||||
parsed, ok := anyFloat64(value)
|
||||
if !ok {
|
||||
return validationError("SCUM DB mutation " + label + " value must be numeric")
|
||||
}
|
||||
if template.Mutation.MinValue != 0 && parsed < template.Mutation.MinValue || template.Mutation.MaxValue != 0 && parsed > template.Mutation.MaxValue {
|
||||
return validationError("SCUM DB mutation " + label + " value is outside the template range")
|
||||
}
|
||||
case "string":
|
||||
if strings.TrimSpace(fmt.Sprint(value)) == "" || strings.ContainsAny(fmt.Sprint(value), "\r\n") {
|
||||
return validationError("SCUM DB mutation " + label + " value is invalid")
|
||||
}
|
||||
case "boolean":
|
||||
if _, ok := value.(bool); !ok {
|
||||
return validationError("SCUM DB mutation " + label + " value must be boolean")
|
||||
}
|
||||
default:
|
||||
return validationError("SCUM DB mutation value type is unsupported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMSQLiteMutationApprovalGate(operation domain.SCUMOperationRequest, template domain.GameClientBridgeOperationTemplateDeclaration) (domain.SCUMOperationRequest, bool, error) {
|
||||
state, err := svc.latestSCUMPlayerLiveState(operation.ServerInstanceID, operation.PlayerID)
|
||||
if err != nil {
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "等待真实玩家投影", "需要先从当前服务的登录日志或 SCUM.db 读取玩家数据。")
|
||||
}
|
||||
return domain.SCUMOperationRequest{}, false, err
|
||||
}
|
||||
if state.Freshness.Status != domain.SCUMProjectionFresh {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "等待新鲜投影", "玩家投影不是 fresh,需先刷新 SCUM.db/readback。")
|
||||
}
|
||||
if template.Safety.RequiresOfflinePlayer && state.Online {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "等待玩家离线", "DB-only 玩家字段修改必须等玩家离线或进入维护窗口。")
|
||||
}
|
||||
if template.Safety.RequiresMaintenanceWindow && strings.TrimSpace(operation.Guard.SafetyWindow) == "" {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "缺少维护窗口", "DB mutation 需要记录维护窗口/离线安全证据。")
|
||||
}
|
||||
if template.Safety.BackupRequired && strings.TrimSpace(operation.Guard.BackupRef) == "" {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "缺少备份快照", "DB mutation 需要 run 或管理员提供 backup/snapshot evidence。")
|
||||
}
|
||||
current, ok := scumCurrentMutationFieldValue(state, operation.Guard.FieldKey)
|
||||
if !ok {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepWaiting, "等待字段读回", "当前投影没有该 DB-only 字段,需先执行确认查询。")
|
||||
}
|
||||
if !scumScalarEqual(current, operation.Guard.Before) {
|
||||
return svc.updateSCUMOperationGate(operation, domain.SCUMWorkflowStepBlocked, "before value 已过期", "当前投影值与审批时 before guard 不一致,已阻止写入。")
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), true, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) updateSCUMOperationGate(operation domain.SCUMOperationRequest, status domain.SCUMWorkflowStepStatus, title string, message string) (domain.SCUMOperationRequest, bool, error) {
|
||||
operation.Status = status
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: title, Message: message, Details: map[string]string{"template": operation.TemplateKey, "playerId": operation.PlayerID}}
|
||||
operation.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMOperationRequests().Update(operation); err != nil {
|
||||
return domain.SCUMOperationRequest{}, false, err
|
||||
}
|
||||
return domain.CopySCUMOperationRequest(operation), false, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) latestSCUMPlayerLiveState(serverID, playerID string) (domain.SCUMPlayerLiveState, error) {
|
||||
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: serverID, GamePlayerID: playerID})
|
||||
if err != nil {
|
||||
return domain.SCUMPlayerLiveState{}, err
|
||||
}
|
||||
if len(states) == 0 {
|
||||
states, err = svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: serverID, SteamID: playerID})
|
||||
if err != nil {
|
||||
return domain.SCUMPlayerLiveState{}, err
|
||||
}
|
||||
}
|
||||
if len(states) == 0 {
|
||||
return domain.SCUMPlayerLiveState{}, repo.ErrNotFound
|
||||
}
|
||||
best := states[0]
|
||||
for _, state := range states[1:] {
|
||||
if state.Freshness.ObservedAt.After(best.Freshness.ObservedAt) || state.UpdatedAt.After(best.UpdatedAt) {
|
||||
best = state
|
||||
}
|
||||
}
|
||||
return domain.CopySCUMPlayerLiveState(best), nil
|
||||
}
|
||||
|
||||
func scumCurrentMutationFieldValue(state domain.SCUMPlayerLiveState, fieldKey string) (any, bool) {
|
||||
if state.UnknownFields != nil {
|
||||
for _, key := range []string{fieldKey, "field" + fieldKey, "attribute" + fieldKey, "attribute_" + fieldKey, "stat" + fieldKey, "stat_" + fieldKey} {
|
||||
if value, ok := state.UnknownFields[key]; ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (svc *CoreService) dispatchSCUMSQLiteMutationOperation(operation domain.SCUMOperationRequest, template domain.GameClientBridgeOperationTemplateDeclaration) (domain.Job, error) {
|
||||
instance, err := svc.store.ServerInstances().Get(operation.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
jobID := jobIDFromParts("job-scum-sqlite-mutation", instance.ID, operation.IdempotencyKey)
|
||||
job := domain.Job{ID: jobID, ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedSQL, TargetKey: template.TargetKey, InputRef: "input://scum-operation/" + operation.ID, IdempotencyKey: "scum-sqlite-mutation:" + operation.IdempotencyKey, Progress: domain.JobProgress{Percent: 0, Message: "typed SCUM DB mutation queued"}, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: template.TransportKey, RemoteAdapterKind: "protected-sql", TimeoutSeconds: template.TimeoutSeconds, PluginID: operation.PluginID, Inputs: scumSQLiteMutationJobInputs(operation, template)}}
|
||||
created, err := svc.CreateJob(job)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
if created.ID != jobID || created.Capability != domain.JobCapabilityRemoteRunProtectedSQL || created.TargetKey != template.TargetKey || created.ExecutionInput.RemoteAdapterKey != template.TransportKey {
|
||||
return domain.Job{}, validationError("SCUM DB mutation idempotency key is already bound")
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func scumSQLiteMutationJobInputs(operation domain.SCUMOperationRequest, template domain.GameClientBridgeOperationTemplateDeclaration) map[string]string {
|
||||
return map[string]string{
|
||||
"operationId": operation.ID,
|
||||
"templateKey": operation.TemplateKey,
|
||||
"playerId": operation.PlayerID,
|
||||
"fieldKey": operation.Guard.FieldKey,
|
||||
"tableKey": template.Mutation.TableKey,
|
||||
"identityKey": template.Mutation.IdentityKey,
|
||||
"valueKey": template.Mutation.ValueKey,
|
||||
"before": scumScalarString(operation.Guard.Before),
|
||||
"after": scumScalarString(operation.Guard.After),
|
||||
"maxRowsAffected": strconv.Itoa(operation.Guard.MaxRowsAffected),
|
||||
"confirmationQueryKey": template.Mutation.ConfirmationQueryKey,
|
||||
"safetyWindow": operation.Guard.SafetyWindow,
|
||||
"backupRef": operation.Guard.BackupRef,
|
||||
}
|
||||
}
|
||||
|
||||
func reconcileSCUMSQLiteMutationJobResult(operation domain.SCUMOperationRequest, template domain.GameClientBridgeOperationTemplateDeclaration, job domain.Job) (domain.SCUMOperationRequest, bool) {
|
||||
result, ok := parseSCUMSQLiteMutationJobResult(job.ExecutionResult.Content)
|
||||
if !ok || result.Outcome == "unknown" || strings.Contains(strings.ToLower(job.ExecutionResult.Kind), "unknown") {
|
||||
operation.Status = domain.SCUMWorkflowStepUnknown
|
||||
operation.Confirmation = domain.SCUMOperationConfirmation{Status: "unknown", SafeSummary: domain.SCUMSafeSummary{Title: "DB mutation state unknown", Message: "Run did not return a valid bounded mutation result."}}
|
||||
return operation, true
|
||||
}
|
||||
operation.Confirmation.AffectedRows = result.AffectedRows
|
||||
operation.Confirmation.MutationChecksum = result.MutationChecksum
|
||||
operation.Confirmation.Checksum = coalesceString(operation.Confirmation.Checksum, coalesceString(result.MutationChecksum, job.ExecutionResult.Checksum))
|
||||
if result.Outcome == "stale-before" {
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation.Status = "failed"
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: "before value 已过期", Message: "Run 在写入前发现当前 DB 值与 approved before guard 不一致。"}
|
||||
return operation, true
|
||||
}
|
||||
if result.Outcome != "succeeded" || result.AffectedRows < 1 {
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation.Status = "failed"
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: "DB mutation failed", Message: bounded(coalesceString(result.SafeMessage, "Run reported the mutation did not succeed."), 240)}
|
||||
return operation, true
|
||||
}
|
||||
if result.AffectedRows > template.MaxRowsAffected || result.AffectedRows > operation.Guard.MaxRowsAffected || strings.TrimSpace(result.MutationChecksum) == "" {
|
||||
operation.Status = domain.SCUMWorkflowStepUnknown
|
||||
operation.Confirmation.Status = "unknown"
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: "DB mutation row bound unknown", Message: "Run result exceeded declared row bounds or omitted mutation checksum."}
|
||||
return operation, true
|
||||
}
|
||||
if len(result.ConfirmationRows) > 0 {
|
||||
for _, row := range result.ConfirmationRows {
|
||||
if scumSQLiteMutationConfirmationMatches(operation, row) {
|
||||
operation.Status = domain.SCUMWorkflowStepConfirmed
|
||||
operation.Confirmation.Status = "confirmed"
|
||||
operation.Confirmation.ConfirmedFields = domain.CopyGameClientBridgePayload(row)
|
||||
return operation, true
|
||||
}
|
||||
}
|
||||
operation.Status = domain.SCUMWorkflowStepFailed
|
||||
operation.Confirmation.Status = "failed"
|
||||
operation.SafeSummary = domain.SCUMSafeSummary{Title: "DB mutation confirmation mismatch", Message: "Run confirmation rows did not match the requested after value."}
|
||||
return operation, true
|
||||
}
|
||||
operation.Confirmation.Status = "executed"
|
||||
return operation, false
|
||||
}
|
||||
|
||||
func parseSCUMSQLiteMutationJobResult(content string) (scumSQLiteMutationJobResult, bool) {
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return scumSQLiteMutationJobResult{}, false
|
||||
}
|
||||
var result scumSQLiteMutationJobResult
|
||||
if err := json.Unmarshal([]byte(content), &result); err != nil {
|
||||
return scumSQLiteMutationJobResult{}, false
|
||||
}
|
||||
result.Outcome = strings.TrimSpace(result.Outcome)
|
||||
return result, result.Outcome != ""
|
||||
}
|
||||
|
||||
func scumSQLiteMutationConfirmationMatches(operation domain.SCUMOperationRequest, row map[string]any) bool {
|
||||
if row == nil {
|
||||
return false
|
||||
}
|
||||
rowPlayerID := firstString(row, "playerId", "gamePlayerId", "steamId", "steam_id")
|
||||
if rowPlayerID != "" && rowPlayerID != operation.PlayerID {
|
||||
return false
|
||||
}
|
||||
if field := firstString(row, "fieldKey", "field", "attributeKey"); field != "" && field != operation.Guard.FieldKey {
|
||||
return false
|
||||
}
|
||||
for _, key := range []string{"value", "after", operation.Guard.FieldKey, "field" + operation.Guard.FieldKey, "attribute" + operation.Guard.FieldKey, "attribute_" + operation.Guard.FieldKey} {
|
||||
if value, ok := row[key]; ok && scumScalarEqual(value, operation.Guard.After) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scumSQLiteMutationSafeSummary(templateKey, playerID string, guard domain.SCUMMutationGuard) domain.SCUMSafeSummary {
|
||||
details := map[string]string{"template": templateKey, "fieldKey": guard.FieldKey, "maxRowsAffected": strconv.Itoa(guard.MaxRowsAffected)}
|
||||
if playerID != "" {
|
||||
details["playerId"] = playerID
|
||||
}
|
||||
if guard.SafetyWindow != "" {
|
||||
details["safetyWindow"] = guard.SafetyWindow
|
||||
}
|
||||
if guard.BackupRef != "" {
|
||||
details["backupRef"] = guard.BackupRef
|
||||
}
|
||||
return domain.SCUMSafeSummary{Title: "Typed SCUM DB mutation", Message: "Run executes this through a declared mutation template with before-value and row-bound guards; raw SQL is not stored.", Details: details}
|
||||
}
|
||||
|
||||
func operationInteger(payload map[string]any, keys ...string) (int64, bool) {
|
||||
for _, key := range keys {
|
||||
value, exists := payload[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return int64(typed), true
|
||||
case int64:
|
||||
return typed, true
|
||||
case uint64:
|
||||
if typed > uint64(^uint64(0)>>1) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(typed), true
|
||||
case float64:
|
||||
if typed == float64(int64(typed)) {
|
||||
return int64(typed), true
|
||||
}
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func anyInt64(value any) (int64, bool) {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return int64(typed), true
|
||||
case int8:
|
||||
return int64(typed), true
|
||||
case int16:
|
||||
return int64(typed), true
|
||||
case int32:
|
||||
return int64(typed), true
|
||||
case int64:
|
||||
return typed, true
|
||||
case uint:
|
||||
return int64(typed), true
|
||||
case uint8:
|
||||
return int64(typed), true
|
||||
case uint16:
|
||||
return int64(typed), true
|
||||
case uint32:
|
||||
return int64(typed), true
|
||||
case uint64:
|
||||
if typed > uint64(^uint64(0)>>1) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(typed), true
|
||||
case float64:
|
||||
if typed == float64(int64(typed)) {
|
||||
return int64(typed), true
|
||||
}
|
||||
case float32:
|
||||
if typed == float32(int64(typed)) {
|
||||
return int64(typed), true
|
||||
}
|
||||
case json.Number:
|
||||
parsed, err := typed.Int64()
|
||||
return parsed, err == nil
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
return parsed, err == nil
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func anyFloat64(value any) (float64, bool) {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return float64(typed), true
|
||||
case int64:
|
||||
return float64(typed), true
|
||||
case uint64:
|
||||
return float64(typed), true
|
||||
case float64:
|
||||
return typed, true
|
||||
case float32:
|
||||
return float64(typed), true
|
||||
case json.Number:
|
||||
parsed, err := typed.Float64()
|
||||
return parsed, err == nil
|
||||
case string:
|
||||
parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
|
||||
return parsed, err == nil
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func scumScalarEqual(left any, right any) bool {
|
||||
if leftInt, ok := anyInt64(left); ok {
|
||||
if rightInt, rightOK := anyInt64(right); rightOK {
|
||||
return leftInt == rightInt
|
||||
}
|
||||
}
|
||||
if leftFloat, ok := anyFloat64(left); ok {
|
||||
if rightFloat, rightOK := anyFloat64(right); rightOK {
|
||||
return leftFloat == rightFloat
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprint(left)) == strings.TrimSpace(fmt.Sprint(right))
|
||||
}
|
||||
|
||||
func scumScalarString(value any) string {
|
||||
if parsed, ok := anyInt64(value); ok {
|
||||
return strconv.FormatInt(parsed, 10)
|
||||
}
|
||||
if parsed, ok := anyFloat64(value); ok {
|
||||
return strconv.FormatFloat(parsed, 'f', -1, 64)
|
||||
}
|
||||
if typed, ok := value.(bool); ok {
|
||||
return strconv.FormatBool(typed)
|
||||
}
|
||||
return bounded(strings.TrimSpace(fmt.Sprint(value)), 512)
|
||||
}
|
||||
|
||||
func operationSafeSummary(templateKey, playerID string, payload map[string]any) domain.SCUMSafeSummary {
|
||||
details := map[string]string{"template": templateKey}
|
||||
if playerID != "" {
|
||||
details["playerId"] = playerID
|
||||
}
|
||||
if amount, ok := operationInteger(payload, "fame", "amount", "balance", "value", "normalBalance", "goldBalance"); ok {
|
||||
details["value"] = fmt.Sprintf("%d", amount)
|
||||
}
|
||||
return domain.SCUMSafeSummary{Title: "Typed SCUM operation", Message: "RCON text is generated server-side and is not stored in the operation record.", Details: details}
|
||||
}
|
||||
|
||||
func scumOperationTemplate(plugin domain.GamePlugin, key string) (domain.GameClientBridgeOperationTemplateDeclaration, bool) {
|
||||
for _, template := range plugin.GameClientBridge.OperationTemplates {
|
||||
if template.Key == key {
|
||||
return template, true
|
||||
}
|
||||
}
|
||||
return domain.GameClientBridgeOperationTemplateDeclaration{}, false
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestSCUMRCONOperationApprovalDispatchesTransientCommandAndConfirms(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
request := domain.SCUMOperationRequest{TemplateKey: "player.fame.set", PlayerID: "76561198000000001", Payload: map[string]any{"fame": 123}, Reason: "restore fame", IdempotencyKey: "fame-restore-1"}
|
||||
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, request)
|
||||
if err != nil || operation.Status != domain.SCUMWorkflowStepWaiting {
|
||||
t.Fatalf("request operation=%+v err=%v", operation, err)
|
||||
}
|
||||
duplicate, err := svc.RequestSCUMOperationForSession(session, instance.ID, request)
|
||||
if err != nil || duplicate.ID != operation.ID {
|
||||
t.Fatalf("duplicate should return original operation: duplicate=%+v err=%v", duplicate, err)
|
||||
}
|
||||
approved, err := svc.ApproveSCUMOperationForSession(session, operation.ID)
|
||||
if err != nil || approved.Status != domain.SCUMWorkflowStepQueued || approved.RunJobID == "" {
|
||||
t.Fatalf("approve operation=%+v err=%v", approved, err)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(approved.RunJobID)
|
||||
if err != nil {
|
||||
t.Fatalf("get operation job: %v", err)
|
||||
}
|
||||
serializedOperation, _ := json.Marshal(approved)
|
||||
serializedJob, _ := json.Marshal(job)
|
||||
for _, forbidden := range []string{"#SetFamePoints", "SetCurrencyBalance", "password="} {
|
||||
if strings.Contains(string(serializedOperation), forbidden) || strings.Contains(string(serializedJob), forbidden) {
|
||||
t.Fatalf("operation/job persisted raw RCON text %q: operation=%s job=%s", forbidden, serializedOperation, serializedJob)
|
||||
}
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
|
||||
t.Fatalf("claim operation RCON job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"})
|
||||
if err != nil || !ack.Accepted {
|
||||
t.Fatalf("ack operation RCON job: ack=%+v err=%v", ack, err)
|
||||
}
|
||||
input, err := svc.GetSourceRCONExecutionInput(domain.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt})
|
||||
if err != nil {
|
||||
t.Fatalf("read transient operation command: %v", err)
|
||||
}
|
||||
if input.Command != "#SetFamePoints 123 \"76561198000000001\"" {
|
||||
t.Fatalf("unexpected generated RCON command: %q", input.Command)
|
||||
}
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "source-rcon.succeeded", AuditSummary: "typed RCON delivered"}}); err != nil {
|
||||
t.Fatalf("complete operation job: %v", err)
|
||||
}
|
||||
reconciled, err := svc.ReconcileSCUMOperation(approved.ID)
|
||||
if err != nil || reconciled.Status != domain.SCUMWorkflowStepConfirming {
|
||||
t.Fatalf("expected confirming after delivery before readback: %+v err=%v", reconciled, err)
|
||||
}
|
||||
confirmed, err := svc.ConfirmSCUMOperation(approved.ID, domain.SCUMOperationConfirmation{Status: "confirmed", ConfirmedFields: map[string]any{"fame": 123}, ObservedAt: fixedTime.Add(time.Minute)})
|
||||
if err != nil || confirmed.Status != domain.SCUMWorkflowStepConfirmed || confirmed.CompletedAt.IsZero() {
|
||||
t.Fatalf("confirm operation=%+v err=%v", confirmed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMRCONOperationPermissionUnknownAndConfirmationFailure(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
adminOnly, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.currency.gold.set", PlayerID: "76561198000000002", Payload: map[string]any{"amount": 9}, Reason: "admin-only", IdempotencyKey: "gold-admin-only"})
|
||||
if err != nil {
|
||||
t.Fatalf("request admin-only operation: %v", err)
|
||||
}
|
||||
if _, err := svc.ApproveSCUMOperationForSession(session, adminOnly.ID); err != ErrForbidden {
|
||||
t.Fatalf("expected platform-admin approval denial, got %v", err)
|
||||
}
|
||||
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.currency.normal.set", PlayerID: "76561198000000002", Payload: map[string]any{"amount": 500}, Reason: "repair balance", IdempotencyKey: "normal-unknown"})
|
||||
if err != nil {
|
||||
t.Fatalf("request normal currency operation: %v", err)
|
||||
}
|
||||
approved, err := svc.ApproveSCUMOperationForSession(session, operation.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("approve normal currency operation: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
|
||||
t.Fatalf("claim normal currency job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt})
|
||||
if err != nil {
|
||||
t.Fatalf("ack normal currency job: %v", err)
|
||||
}
|
||||
if _, err := svc.GetSourceRCONExecutionInput(domain.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt}); err != nil {
|
||||
t.Fatalf("consume normal currency command: %v", err)
|
||||
}
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateFailed, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "source-rcon.unknown", AuditSummary: "unknown command state"}}); err != nil {
|
||||
t.Fatalf("complete unknown operation job: %v", err)
|
||||
}
|
||||
unknown, err := svc.ReconcileSCUMOperation(approved.ID)
|
||||
if err != nil || unknown.Status != domain.SCUMWorkflowStepUnknown {
|
||||
t.Fatalf("expected unknown terminal state: %+v err=%v", unknown, err)
|
||||
}
|
||||
failure, err := svc.ConfirmSCUMOperation(operation.ID, domain.SCUMOperationConfirmation{Status: "failed", SafeSummary: domain.SCUMSafeSummary{Title: "Readback mismatch", Message: "Projection did not match expected currency."}, ObservedAt: time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC)})
|
||||
if err != nil || failure.Status != domain.SCUMWorkflowStepFailed {
|
||||
t.Fatalf("expected confirmation failure: %+v err=%v", failure, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMSQLiteMutationOperationSafetyGatesAndDispatchesTypedJob(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-online", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "76561198000000855", "displayName": "Attribute Tester", "online": true, "855": 100}}}); err != nil {
|
||||
t.Fatalf("seed online projection: %v", err)
|
||||
}
|
||||
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "76561198000000855", Payload: map[string]any{"fieldKey": "855", "before": 100, "after": 150, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/20260810"}, Reason: "repair attribute 855", IdempotencyKey: "attribute-855-1"})
|
||||
if err != nil || operation.Status != domain.SCUMWorkflowStepWaiting {
|
||||
t.Fatalf("request sqlite mutation=%+v err=%v", operation, err)
|
||||
}
|
||||
waiting, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
|
||||
if err != nil || waiting.Status != domain.SCUMWorkflowStepWaiting || waiting.RunJobID != "" || !strings.Contains(waiting.SafeSummary.Title, "离线") {
|
||||
t.Fatalf("online player should block dispatch: %+v err=%v", waiting, err)
|
||||
}
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 2, Checksum: "sha256:profile-offline", ObservedAt: fixedTime.Add(time.Minute), Rows: []map[string]any{{"gamePlayerId": "76561198000000855", "displayName": "Attribute Tester", "online": false, "855": 100}}}); err != nil {
|
||||
t.Fatalf("seed offline projection: %v", err)
|
||||
}
|
||||
approved, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
|
||||
if err != nil || approved.Status != domain.SCUMWorkflowStepQueued || approved.RunJobID == "" {
|
||||
t.Fatalf("approve sqlite mutation=%+v err=%v", approved, err)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(approved.RunJobID)
|
||||
if err != nil {
|
||||
t.Fatalf("get sqlite mutation job: %v", err)
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunProtectedSQL || job.ExecutionInput.Inputs["fieldKey"] != "855" || job.ExecutionInput.Inputs["before"] != "100" || job.ExecutionInput.Inputs["after"] != "150" || job.ExecutionInput.Inputs["maxRowsAffected"] != "1" {
|
||||
t.Fatalf("unexpected typed mutation job: %+v", job)
|
||||
}
|
||||
serializedOperation, _ := json.Marshal(approved)
|
||||
serializedJob, _ := json.Marshal(job)
|
||||
for _, forbidden := range []string{"UPDATE ", "DELETE ", "INSERT ", "SELECT ", "SCUM.db", "/Saved/", "requestText"} {
|
||||
if strings.Contains(strings.ToUpper(string(serializedOperation)), strings.ToUpper(forbidden)) || strings.Contains(strings.ToUpper(string(serializedJob)), strings.ToUpper(forbidden)) {
|
||||
t.Fatalf("operation/job persisted raw DB material %q: operation=%s job=%s", forbidden, serializedOperation, serializedJob)
|
||||
}
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
|
||||
t.Fatalf("claim sqlite mutation job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"})
|
||||
if err != nil || !ack.Accepted {
|
||||
t.Fatalf("ack sqlite mutation job: ack=%+v err=%v", ack, err)
|
||||
}
|
||||
mutationChecksum := "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
content := mustJSON(t, map[string]any{"outcome": "succeeded", "affectedRows": 1, "mutationChecksum": mutationChecksum, "confirmationRows": []map[string]any{{"playerId": "76561198000000855", "fieldKey": "855", "value": 150}}})
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "scum.sqlite-mutation.succeeded", Checksum: mutationChecksum, AuditSummary: "typed SCUM DB mutation result", Content: content}}); err != nil {
|
||||
t.Fatalf("complete sqlite mutation job: %v", err)
|
||||
}
|
||||
confirmed, err := svc.ReconcileSCUMOperation(approved.ID)
|
||||
if err != nil || confirmed.Status != domain.SCUMWorkflowStepConfirmed || confirmed.Confirmation.AffectedRows != 1 || confirmed.Confirmation.MutationChecksum != mutationChecksum {
|
||||
t.Fatalf("expected confirmed sqlite mutation: %+v err=%v", confirmed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMSQLiteMutationBlocksMissingSafetyAndStaleBefore(t *testing.T) {
|
||||
svc, session, _, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-855", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "steam-855", "displayName": "Guarded", "online": false, "855": 100}}}); err != nil {
|
||||
t.Fatalf("seed projection: %v", err)
|
||||
}
|
||||
missingSafety, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-855", Payload: map[string]any{"fieldKey": "855", "before": 100, "after": 101}, Reason: "missing maintenance", IdempotencyKey: "attribute-855-missing-safety"})
|
||||
if err != nil {
|
||||
t.Fatalf("request missing safety mutation: %v", err)
|
||||
}
|
||||
waiting, err := svc.ApproveSCUMOperationForSession(adminSession, missingSafety.ID)
|
||||
if err != nil || waiting.Status != domain.SCUMWorkflowStepWaiting || waiting.RunJobID != "" || !strings.Contains(waiting.SafeSummary.Title, "维护") {
|
||||
t.Fatalf("expected missing maintenance/backup wait: %+v err=%v", waiting, err)
|
||||
}
|
||||
stale, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-855", Payload: map[string]any{"fieldKey": "855", "before": 99, "after": 101, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/stale"}, Reason: "stale before", IdempotencyKey: "attribute-855-stale-before"})
|
||||
if err != nil {
|
||||
t.Fatalf("request stale mutation: %v", err)
|
||||
}
|
||||
blocked, err := svc.ApproveSCUMOperationForSession(adminSession, stale.ID)
|
||||
if err != nil || blocked.Status != domain.SCUMWorkflowStepBlocked || blocked.RunJobID != "" || !strings.Contains(blocked.SafeSummary.Title, "before") {
|
||||
t.Fatalf("expected stale before block: %+v err=%v", blocked, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMSQLiteMutationResultValidationRejectsOverBoundRows(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
seedSCUMOperationTemplates(t, svc, instance.PluginID)
|
||||
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-overbound", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "steam-overbound", "online": false, "855": 10}}}); err != nil {
|
||||
t.Fatalf("seed projection: %v", err)
|
||||
}
|
||||
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-overbound", Payload: map[string]any{"fieldKey": "855", "before": 10, "after": 11, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/overbound"}, Reason: "overbound test", IdempotencyKey: "attribute-855-overbound"})
|
||||
if err != nil {
|
||||
t.Fatalf("request overbound mutation: %v", err)
|
||||
}
|
||||
approved, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
|
||||
if err != nil || approved.RunJobID == "" {
|
||||
t.Fatalf("approve overbound mutation=%+v err=%v", approved, err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil {
|
||||
t.Fatalf("claim overbound job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt})
|
||||
if err != nil {
|
||||
t.Fatalf("ack overbound job: %v", err)
|
||||
}
|
||||
content := mustJSON(t, map[string]any{"outcome": "succeeded", "affectedRows": 2, "mutationChecksum": "sha256:mutation-overbound"})
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "scum.sqlite-mutation.succeeded", Content: content, AuditSummary: "typed SCUM DB mutation result"}}); err != nil {
|
||||
t.Fatalf("complete overbound job: %v", err)
|
||||
}
|
||||
unknown, err := svc.ReconcileSCUMOperation(approved.ID)
|
||||
if err != nil || unknown.Status != domain.SCUMWorkflowStepUnknown {
|
||||
t.Fatalf("expected over-bound rows to become unknown: %+v err=%v", unknown, err)
|
||||
}
|
||||
}
|
||||
|
||||
func seedSCUMOperationTemplates(t *testing.T, svc *CoreService, pluginID string) {
|
||||
t.Helper()
|
||||
plugin, err := svc.store.GamePlugins().Get(pluginID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.command")
|
||||
plugin.GameClientBridge.OperationTemplates = []domain.GameClientBridgeOperationTemplateDeclaration{
|
||||
{Key: "player.fame.set", Title: "Set player fame", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-fame-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
|
||||
{Key: "player.currency.normal.set", Title: "Set player normal currency", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-currency-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
|
||||
{Key: "player.currency.gold.set", Title: "Set player gold currency", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-currency-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
|
||||
}
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin operation templates: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func enableSCUMSQLiteMutationOperationSupport(t *testing.T, svc *CoreService, instance domain.ServerInstance) string {
|
||||
t.Helper()
|
||||
adminSession := createServiceUserAndLogin(t, svc, domain.User{ID: "platform-admin-scum", DisplayName: "SCUM Admin", Email: "scum-admin@example.test", Roles: []string{"platform-admin"}, PasswordHash: "secret-password"})
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.maintenance")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
plugin.RemoteAccess.RunCapabilities = append(plugin.RemoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
plugin.RemoteAccess.DatabaseEngines = append(plugin.RemoteAccess.DatabaseEngines, "sqlite")
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}})
|
||||
plugin.GameClientBridge.OperationTemplates = append(plugin.GameClientBridge.OperationTemplates, domain.GameClientBridgeOperationTemplateDeclaration{Key: "player.attribute.855.set", Title: "Set player attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindSQLiteMutation, TransportKey: "scum-database", TargetKey: "scum-database", PayloadSchemaRef: "schemas/bridge/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1, Mutation: domain.GameClientBridgeOperationMutationDeclaration{FieldKey: "855", TableKey: "prisoner", IdentityKey: "user_profile_id", ValueKey: "value", ConfirmationQueryKey: "scum.player.profile", AllowedValueType: "integer", MinValue: 0, MaxValue: 100000}, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresMaintenanceWindow: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true}})
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update SCUM DB mutation plugin: %v", err)
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("update SCUM DB mutation endpoint: %v", err)
|
||||
}
|
||||
return adminSession
|
||||
}
|
||||
|
||||
func mustJSON(t *testing.T, value any) string {
|
||||
t.Helper()
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal test JSON: %v", err)
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
@@ -0,0 +1,803 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
func (svc *CoreService) ApplySCUMObservationResult(result domain.SCUMObservationResult) (domain.SCUMDataObservation, error) {
|
||||
result = domain.CopySCUMObservationResult(result)
|
||||
if strings.TrimSpace(result.ServerInstanceID) == "" {
|
||||
return domain.SCUMDataObservation{}, validationError("serverInstanceId is required")
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(result.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
if strings.TrimSpace(result.PluginID) == "" {
|
||||
result.PluginID = instance.PluginID
|
||||
}
|
||||
if result.PluginID != instance.PluginID {
|
||||
return domain.SCUMDataObservation{}, validationError("pluginId must match server instance")
|
||||
}
|
||||
if strings.TrimSpace(result.QueryKey) == "" {
|
||||
return domain.SCUMDataObservation{}, validationError("queryKey is required")
|
||||
}
|
||||
if result.ReceivedAt.IsZero() {
|
||||
result.ReceivedAt = svc.now()
|
||||
}
|
||||
if result.ObservedAt.IsZero() {
|
||||
result.ObservedAt = result.ReceivedAt
|
||||
}
|
||||
if result.Status == "" {
|
||||
result.Status = domain.SCUMObservationAccepted
|
||||
}
|
||||
latest, err := svc.latestSCUMObservation(result.ServerInstanceID, result.PluginID, result.QueryKey)
|
||||
if err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
if result.Status == domain.SCUMObservationAccepted && !latest.ObservedAt.IsZero() && scumObservationOlder(result, latest) {
|
||||
result.Status = domain.SCUMObservationStale
|
||||
result.ErrorCode = "older_observation"
|
||||
result.SafeSummary = domain.SCUMSafeSummary{Title: "旧观察已忽略", Message: "Run 返回的 SCUM.db 观察早于当前本地投影,未覆盖 last-known-good 数据。"}
|
||||
}
|
||||
observation := domain.SCUMDataObservation{ID: scumObservationID(result), ServerInstanceID: result.ServerInstanceID, PluginID: result.PluginID, Source: result.Source, QueryKey: result.QueryKey, Sequence: result.Sequence, Checksum: result.Checksum, Status: result.Status, ErrorCode: result.ErrorCode, SafeSummary: result.SafeSummary, ObservedAt: result.ObservedAt, ReceivedAt: result.ReceivedAt}
|
||||
if err := svc.upsertSCUMObservation(observation); err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
if result.Status != domain.SCUMObservationAccepted {
|
||||
if result.Status == domain.SCUMObservationFailed {
|
||||
return observation, svc.markSCUMQueryStale(result, "observation_failed")
|
||||
}
|
||||
return observation, nil
|
||||
}
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionFresh, ObservationID: observation.ID, Source: observation.Source, QueryKey: observation.QueryKey, Sequence: observation.Sequence, Checksum: observation.Checksum, ObservedAt: observation.ObservedAt, ReceivedAt: observation.ReceivedAt}
|
||||
if err := svc.applySCUMRows(result.QueryKey, result.ServerInstanceID, result.Rows, freshness); err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
return observation, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMPlayerLiveStatesForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMPlayerLiveState, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMPlayerLiveStates().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMSquadsForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMSquad, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMSquads().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMSquadMembersForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMSquadMember, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMSquadMembers().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMVehiclesForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMVehicle, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMVehicles().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMFlagsForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMFlag, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMFlags().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMCurrentPositionsForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMCurrentPosition, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMCurrentPositions().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) latestSCUMObservation(serverID, pluginID, queryKey string) (domain.SCUMDataObservation, error) {
|
||||
observations, err := svc.store.SCUMDataObservations().List(domain.SCUMProjectionFilter{ServerInstanceID: serverID, QueryKey: queryKey})
|
||||
if err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
var latest domain.SCUMDataObservation
|
||||
for _, observation := range observations {
|
||||
if pluginID != "" && observation.PluginID != pluginID {
|
||||
continue
|
||||
}
|
||||
if latest.ObservedAt.IsZero() || observation.Sequence > latest.Sequence || (observation.Sequence == latest.Sequence && observation.ObservedAt.After(latest.ObservedAt)) {
|
||||
latest = observation
|
||||
}
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func scumObservationOlder(next domain.SCUMObservationResult, latest domain.SCUMDataObservation) bool {
|
||||
if next.Sequence > 0 && latest.Sequence > 0 && next.Sequence <= latest.Sequence {
|
||||
return true
|
||||
}
|
||||
return !next.ObservedAt.IsZero() && !latest.ObservedAt.IsZero() && next.ObservedAt.Before(latest.ObservedAt)
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertSCUMObservation(observation domain.SCUMDataObservation) error {
|
||||
if existing, err := svc.store.SCUMDataObservations().Get(observation.ID); err == nil {
|
||||
existing.Status = observation.Status
|
||||
existing.ErrorCode = observation.ErrorCode
|
||||
existing.SafeSummary = observation.SafeSummary
|
||||
existing.ReceivedAt = observation.ReceivedAt
|
||||
return svc.store.SCUMDataObservations().Update(existing)
|
||||
} else if err != repo.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
return svc.store.SCUMDataObservations().Create(observation)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMRows(queryKey, serverID string, rows []map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
lower := strings.ToLower(queryKey)
|
||||
if strings.Contains(lower, "player") || strings.Contains(lower, "profile") || strings.Contains(lower, "economy") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMPlayerRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "squad-member") || strings.Contains(lower, "squad.member") || strings.Contains(lower, "member") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMSquadMemberRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(lower, "squad") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMSquadRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "vehicle") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMVehicleRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "flag") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMFlagRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "position") || strings.Contains(lower, "coordinate") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMPositionRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMPlayerRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
gamePlayerID := firstString(row, "gamePlayerId", "playerId", "steamId", "steam_id")
|
||||
profileID := firstString(row, "userProfileId", "user_profile_id", "profileId")
|
||||
steamID := firstString(row, "steamId", "steam_id")
|
||||
name := firstString(row, "displayName", "name", "playerName")
|
||||
if gamePlayerID == "" && steamID != "" {
|
||||
gamePlayerID = steamID
|
||||
}
|
||||
if gamePlayerID == "" && profileID == "" {
|
||||
return nil
|
||||
}
|
||||
playerRecordID := ""
|
||||
if gamePlayerID != "" {
|
||||
playerRecordID = gamePlayerRecordID(serverID, gamePlayerID)
|
||||
if err := svc.upsertSCUMGamePlayer(serverID, playerRecordID, gamePlayerID, name, freshness.ObservedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
idSource := gamePlayerID
|
||||
if idSource == "" {
|
||||
idSource = "profile-" + profileID
|
||||
}
|
||||
id := scumProjectionID("player-live", serverID, idSource)
|
||||
state, err := svc.store.SCUMPlayerLiveStates().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
state = domain.SCUMPlayerLiveState{ID: id, ServerInstanceID: serverID, GamePlayerRecordID: playerRecordID, GamePlayerID: gamePlayerID, UserProfileID: profileID, SteamID: steamID, DisplayName: name, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, state.Freshness) {
|
||||
return nil
|
||||
}
|
||||
state.GamePlayerRecordID = coalesceString(playerRecordID, state.GamePlayerRecordID)
|
||||
state.GamePlayerID = coalesceString(gamePlayerID, state.GamePlayerID)
|
||||
state.UserProfileID = coalesceString(profileID, state.UserProfileID)
|
||||
state.SteamID = coalesceString(steamID, state.SteamID)
|
||||
state.DisplayName = coalesceString(name, state.DisplayName)
|
||||
state.SquadID = coalesceString(firstString(row, "squadId", "squad_id"), state.SquadID)
|
||||
state.SquadName = coalesceString(firstString(row, "squadName", "squad_name"), state.SquadName)
|
||||
if value, ok := firstFloat(row, "famePoints", "fame_points", "fame"); ok {
|
||||
state.FamePoints = value
|
||||
}
|
||||
if value, ok := firstFloat(row, "normalBalance", "currencyNormal", "money", "normal_balance"); ok {
|
||||
state.NormalBalance = value
|
||||
}
|
||||
if value, ok := firstFloat(row, "goldBalance", "currencyGold", "gold", "gold_balance"); ok {
|
||||
state.GoldBalance = value
|
||||
}
|
||||
if value, ok := firstBool(row, "online", "isOnline"); ok {
|
||||
state.Online = value
|
||||
}
|
||||
state.LastLoginAt = coalesceTime(firstTime(row, "lastLoginAt", "last_login_at"), state.LastLoginAt)
|
||||
state.LastLogoutAt = coalesceTime(firstTime(row, "lastLogoutAt", "last_logout_at"), state.LastLogoutAt)
|
||||
state.LastSaveTime = coalesceTime(firstTime(row, "lastSaveTime", "last_save_time"), state.LastSaveTime)
|
||||
if position, ok := scumPositionFromRow(serverID, domain.SCUMProjectionSubjectPlayer, gamePlayerID, row, freshness); ok {
|
||||
position.GamePlayerRecordID = playerRecordID
|
||||
position.GamePlayerID = gamePlayerID
|
||||
state.Position = position
|
||||
if err := svc.upsertSCUMPosition(position); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
state.UnknownFields = unknownRowFields(row, "gamePlayerId", "playerId", "steamId", "steam_id", "userProfileId", "user_profile_id", "profileId", "displayName", "name", "playerName", "squadId", "squad_id", "squadName", "squad_name", "famePoints", "fame_points", "fame", "normalBalance", "currencyNormal", "money", "normal_balance", "goldBalance", "currencyGold", "gold", "gold_balance", "online", "isOnline", "lastLoginAt", "last_login_at", "lastLogoutAt", "last_logout_at", "lastSaveTime", "last_save_time", "x", "y", "z", "worldX", "worldY", "worldZ", "mapId", "mapVersion")
|
||||
state.Freshness = freshness
|
||||
state.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMPlayerLiveStates().Create(state)
|
||||
}
|
||||
return svc.store.SCUMPlayerLiveStates().Update(state)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMSquadRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
squadID := firstString(row, "squadId", "squad_id", "id")
|
||||
if squadID == "" {
|
||||
return nil
|
||||
}
|
||||
id := scumProjectionID("squad", serverID, squadID)
|
||||
value, err := svc.store.SCUMSquads().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMSquad{ID: id, ServerInstanceID: serverID, SquadID: squadID, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.Name = coalesceString(firstString(row, "name", "squadName", "squad_name"), value.Name)
|
||||
value.LeaderProfileID = coalesceString(firstString(row, "leaderProfileId", "leader_profile_id"), value.LeaderProfileID)
|
||||
value.LeaderPlayerID = coalesceString(firstString(row, "leaderPlayerId", "leader_player_id", "leaderSteamId"), value.LeaderPlayerID)
|
||||
if memberCount, ok := firstInt(row, "memberCount", "member_count"); ok {
|
||||
value.MemberCount = memberCount
|
||||
}
|
||||
if score, ok := firstFloat(row, "score", "fame", "points"); ok {
|
||||
value.Score = score
|
||||
}
|
||||
value.UnknownFields = unknownRowFields(row, "squadId", "squad_id", "id", "name", "squadName", "squad_name", "leaderProfileId", "leader_profile_id", "leaderPlayerId", "leader_player_id", "leaderSteamId", "memberCount", "member_count", "score", "fame", "points")
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMSquads().Create(value)
|
||||
}
|
||||
return svc.store.SCUMSquads().Update(value)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMSquadMemberRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
squadID := firstString(row, "squadId", "squad_id")
|
||||
profileID := firstString(row, "userProfileId", "user_profile_id", "profileId")
|
||||
gamePlayerID := firstString(row, "gamePlayerId", "playerId", "steamId", "steam_id")
|
||||
if squadID == "" || (profileID == "" && gamePlayerID == "") {
|
||||
return nil
|
||||
}
|
||||
playerRecordID := ""
|
||||
if gamePlayerID != "" {
|
||||
playerRecordID = gamePlayerRecordID(serverID, gamePlayerID)
|
||||
if err := svc.upsertSCUMGamePlayer(serverID, playerRecordID, gamePlayerID, firstString(row, "displayName", "name", "playerName"), freshness.ObservedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
id := scumProjectionID("squad-member", serverID, squadID+"/"+coalesceString(profileID, gamePlayerID))
|
||||
value, err := svc.store.SCUMSquadMembers().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMSquadMember{ID: id, ServerInstanceID: serverID, SquadID: squadID, UserProfileID: profileID, GamePlayerRecordID: playerRecordID, GamePlayerID: gamePlayerID, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.UserProfileID = coalesceString(profileID, value.UserProfileID)
|
||||
value.GamePlayerRecordID = coalesceString(playerRecordID, value.GamePlayerRecordID)
|
||||
value.GamePlayerID = coalesceString(gamePlayerID, value.GamePlayerID)
|
||||
value.SteamID = coalesceString(firstString(row, "steamId", "steam_id"), value.SteamID)
|
||||
value.DisplayName = coalesceString(firstString(row, "displayName", "name", "playerName"), value.DisplayName)
|
||||
value.Rank = coalesceString(firstString(row, "rank", "role"), value.Rank)
|
||||
if isLeader, ok := firstBool(row, "isLeader", "leader"); ok {
|
||||
value.IsLeader = isLeader
|
||||
}
|
||||
value.JoinedAt = coalesceTime(firstTime(row, "joinedAt", "joined_at"), value.JoinedAt)
|
||||
value.UnknownFields = unknownRowFields(row, "squadId", "squad_id", "userProfileId", "user_profile_id", "profileId", "gamePlayerId", "playerId", "steamId", "steam_id", "displayName", "name", "playerName", "rank", "role", "isLeader", "leader", "joinedAt", "joined_at")
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMSquadMembers().Create(value)
|
||||
}
|
||||
return svc.store.SCUMSquadMembers().Update(value)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMVehicleRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
vehicleID := firstString(row, "vehicleId", "vehicle_id", "id")
|
||||
entityID := firstString(row, "entityId", "entity_id")
|
||||
if vehicleID == "" && entityID != "" {
|
||||
vehicleID = entityID
|
||||
}
|
||||
if vehicleID == "" {
|
||||
return nil
|
||||
}
|
||||
id := scumProjectionID("vehicle", serverID, vehicleID)
|
||||
value, err := svc.store.SCUMVehicles().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMVehicle{ID: id, ServerInstanceID: serverID, VehicleID: vehicleID, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.EntityID = coalesceString(entityID, value.EntityID)
|
||||
value.ClassName = coalesceString(firstString(row, "className", "class", "type"), value.ClassName)
|
||||
value.Label = coalesceString(firstString(row, "label", "vehicleName", "name"), value.Label)
|
||||
if value.Label == "" {
|
||||
value.Label = coalesceString(value.ClassName, "Unknown vehicle")
|
||||
}
|
||||
value.OwnerProfileID = coalesceString(firstString(row, "ownerProfileId", "owner_profile_id", "userProfileId", "user_profile_id"), value.OwnerProfileID)
|
||||
value.OwnerPlayerID = coalesceString(firstString(row, "ownerPlayerId", "owner_player_id", "steamId", "steam_id"), value.OwnerPlayerID)
|
||||
value.SquadID = coalesceString(firstString(row, "squadId", "squad_id"), value.SquadID)
|
||||
if position, ok := scumPositionFromRow(serverID, domain.SCUMProjectionSubjectVehicle, vehicleID, row, freshness); ok {
|
||||
position.VehicleID = vehicleID
|
||||
position.EntityID = entityID
|
||||
value.Position = position
|
||||
if err := svc.upsertSCUMPosition(position); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
value.UnknownFields = unknownRowFields(row, "vehicleId", "vehicle_id", "id", "entityId", "entity_id", "className", "class", "type", "label", "vehicleName", "name", "ownerProfileId", "owner_profile_id", "userProfileId", "user_profile_id", "ownerPlayerId", "owner_player_id", "steamId", "steam_id", "squadId", "squad_id", "x", "y", "z", "worldX", "worldY", "worldZ", "mapId", "mapVersion")
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMVehicles().Create(value)
|
||||
}
|
||||
return svc.store.SCUMVehicles().Update(value)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMFlagRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
flagID := firstString(row, "flagId", "flag_id", "baseElementId", "base_element_id", "id")
|
||||
entityID := firstString(row, "entityId", "entity_id")
|
||||
if flagID == "" && entityID != "" {
|
||||
flagID = entityID
|
||||
}
|
||||
if flagID == "" {
|
||||
return nil
|
||||
}
|
||||
id := scumProjectionID("flag", serverID, flagID)
|
||||
value, err := svc.store.SCUMFlags().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMFlag{ID: id, ServerInstanceID: serverID, FlagID: flagID, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.EntityID = coalesceString(entityID, value.EntityID)
|
||||
value.OwnerProfileID = coalesceString(firstString(row, "ownerProfileId", "owner_profile_id", "userProfileId", "user_profile_id"), value.OwnerProfileID)
|
||||
value.OwnerPlayerID = coalesceString(firstString(row, "ownerPlayerId", "owner_player_id", "steamId", "steam_id"), value.OwnerPlayerID)
|
||||
value.OwnerSquadID = coalesceString(firstString(row, "ownerSquadId", "owner_squad_id", "squadId", "squad_id"), value.OwnerSquadID)
|
||||
value.OwnerSquadName = coalesceString(firstString(row, "ownerSquadName", "owner_squad_name", "squadName", "squad_name"), value.OwnerSquadName)
|
||||
value.OwnershipConfidence = coalesceString(firstString(row, "ownershipConfidence", "ownership_confidence"), value.OwnershipConfidence)
|
||||
if value.OwnershipConfidence == "" {
|
||||
value.OwnershipConfidence = "unknown"
|
||||
}
|
||||
if position, ok := scumPositionFromRow(serverID, domain.SCUMProjectionSubjectFlag, flagID, row, freshness); ok {
|
||||
position.EntityID = entityID
|
||||
value.Position = position
|
||||
if err := svc.upsertSCUMPosition(position); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
value.UnknownFields = unknownRowFields(row, "flagId", "flag_id", "baseElementId", "base_element_id", "id", "entityId", "entity_id", "ownerProfileId", "owner_profile_id", "userProfileId", "user_profile_id", "ownerPlayerId", "owner_player_id", "steamId", "steam_id", "ownerSquadId", "owner_squad_id", "squadId", "squad_id", "ownerSquadName", "owner_squad_name", "squadName", "squad_name", "ownershipConfidence", "ownership_confidence", "x", "y", "z", "worldX", "worldY", "worldZ", "mapId", "mapVersion")
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMFlags().Create(value)
|
||||
}
|
||||
return svc.store.SCUMFlags().Update(value)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMPositionRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
subjectType := domain.SCUMProjectionSubject(firstString(row, "subjectType", "subject_type"))
|
||||
if subjectType == "" {
|
||||
if firstString(row, "vehicleId", "vehicle_id") != "" {
|
||||
subjectType = domain.SCUMProjectionSubjectVehicle
|
||||
} else {
|
||||
subjectType = domain.SCUMProjectionSubjectPlayer
|
||||
}
|
||||
}
|
||||
subjectID := firstString(row, "subjectId", "subject_id", "gamePlayerId", "playerId", "vehicleId", "flagId", "entityId", "id")
|
||||
position, ok := scumPositionFromRow(serverID, subjectType, subjectID, row, freshness)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
position.GamePlayerID = firstString(row, "gamePlayerId", "playerId", "steamId", "steam_id")
|
||||
if position.GamePlayerID != "" {
|
||||
position.GamePlayerRecordID = gamePlayerRecordID(serverID, position.GamePlayerID)
|
||||
}
|
||||
position.VehicleID = firstString(row, "vehicleId", "vehicle_id")
|
||||
position.EntityID = firstString(row, "entityId", "entity_id")
|
||||
return svc.upsertSCUMPosition(position)
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertSCUMGamePlayer(serverID, recordID, gamePlayerID, displayName string, observedAt time.Time) error {
|
||||
if gamePlayerID == "" || recordID == "" {
|
||||
return nil
|
||||
}
|
||||
if observedAt.IsZero() {
|
||||
observedAt = svc.now()
|
||||
}
|
||||
player, err := svc.store.GamePlayers().Get(recordID)
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.GamePlayers().Create(domain.GamePlayer{ID: recordID, ServerInstanceID: serverID, GamePlayerID: gamePlayerID, DisplayName: displayName, FirstSeenAt: observedAt, LastSeenAt: observedAt, LastEventAt: observedAt, CreatedAt: svc.now(), UpdatedAt: svc.now()})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if observedAt.Before(player.LastEventAt) {
|
||||
return nil
|
||||
}
|
||||
player.DisplayName = coalesceString(displayName, player.DisplayName)
|
||||
player.LastSeenAt = maxTime(player.LastSeenAt, observedAt)
|
||||
player.LastEventAt = observedAt
|
||||
player.UpdatedAt = svc.now()
|
||||
return svc.store.GamePlayers().Update(player)
|
||||
}
|
||||
|
||||
func (svc *CoreService) projectSCUMLoginLiveState(player domain.GamePlayer, batch domain.LogBatchIngest, entry domain.LogEntry, observedAt time.Time, online bool, reason string) error {
|
||||
if player.ID == "" || player.GamePlayerID == "" {
|
||||
return nil
|
||||
}
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionFresh, ObservationID: entryID(batch.LogStreamID, entry.Seq), Source: "login-log", QueryKey: strings.TrimSpace(entry.Fields["eventType"]), Sequence: entry.Seq, Checksum: validator.LogLineChecksum(entry.Line), ObservedAt: observedAt, ReceivedAt: svc.now()}
|
||||
id := scumProjectionID("player-live", player.ServerInstanceID, player.GamePlayerID)
|
||||
state, err := svc.store.SCUMPlayerLiveStates().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
state = domain.SCUMPlayerLiveState{ID: id, ServerInstanceID: player.ServerInstanceID, GamePlayerRecordID: player.ID, GamePlayerID: player.GamePlayerID, DisplayName: player.DisplayName, Freshness: domain.SCUMProjectionStateUnknown(), CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, state.Freshness) {
|
||||
return nil
|
||||
}
|
||||
state.GamePlayerRecordID = player.ID
|
||||
state.GamePlayerID = player.GamePlayerID
|
||||
state.DisplayName = player.DisplayName
|
||||
state.Online = online
|
||||
if online {
|
||||
state.LastLoginAt = observedAt
|
||||
} else {
|
||||
state.LastLogoutAt = observedAt
|
||||
}
|
||||
state.Freshness = freshness
|
||||
if reason != "" {
|
||||
state.UnknownFields = domain.CopyGameClientBridgePayload(map[string]any{"lastLogoutReason": bounded(reason, 80)})
|
||||
}
|
||||
state.UpdatedAt = svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMPlayerLiveStates().Create(state)
|
||||
}
|
||||
return svc.store.SCUMPlayerLiveStates().Update(state)
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertSCUMPosition(position domain.SCUMCurrentPosition) error {
|
||||
existing, err := svc.store.SCUMCurrentPositions().Get(position.ID)
|
||||
if err == repo.ErrNotFound {
|
||||
position.CreatedAt = svc.now()
|
||||
position.UpdatedAt = svc.now()
|
||||
return svc.store.SCUMCurrentPositions().Create(position)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(position.Freshness, existing.Freshness) {
|
||||
return nil
|
||||
}
|
||||
position.CreatedAt = existing.CreatedAt
|
||||
position.UpdatedAt = svc.now()
|
||||
return svc.store.SCUMCurrentPositions().Update(position)
|
||||
}
|
||||
|
||||
func (svc *CoreService) markSCUMQueryStale(result domain.SCUMObservationResult, reason string) error {
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionStale, ObservationID: scumObservationID(result), Source: result.Source, QueryKey: result.QueryKey, Sequence: result.Sequence, Checksum: result.Checksum, StaleReason: reason, ObservedAt: result.ObservedAt, ReceivedAt: result.ReceivedAt}
|
||||
lower := strings.ToLower(result.QueryKey)
|
||||
if strings.Contains(lower, "player") || strings.Contains(lower, "profile") || strings.Contains(lower, "economy") {
|
||||
values, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMPlayerLiveStates().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "squad") {
|
||||
values, err := svc.store.SCUMSquads().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMSquads().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "vehicle") {
|
||||
values, err := svc.store.SCUMVehicles().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMVehicles().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "flag") {
|
||||
values, err := svc.store.SCUMFlags().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness = freshness
|
||||
value.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMFlags().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scumPositionFromRow(serverID string, subjectType domain.SCUMProjectionSubject, subjectID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) (domain.SCUMCurrentPosition, bool) {
|
||||
x, hasX := firstFloat(row, "x", "worldX", "world_x", "locationX")
|
||||
y, hasY := firstFloat(row, "y", "worldY", "world_y", "locationY")
|
||||
z, hasZ := firstFloat(row, "z", "worldZ", "world_z", "locationZ")
|
||||
if !hasX || !hasY {
|
||||
return domain.SCUMCurrentPosition{}, false
|
||||
}
|
||||
if subjectID == "" {
|
||||
return domain.SCUMCurrentPosition{}, false
|
||||
}
|
||||
position := domain.SCUMCurrentPosition{ID: scumProjectionID("position-"+string(subjectType), serverID, subjectID), ServerInstanceID: serverID, SubjectType: subjectType, SubjectID: subjectID, MapID: coalesceString(firstString(row, "mapId", "map_id"), domain.SCUMMapTrajectoryMapID), MapVersion: coalesceString(firstString(row, "mapVersion", "map_version"), "0.9"), X: x, Y: y, HasCoordinates: true, LastSaveTime: firstTime(row, "lastSaveTime", "last_save_time"), Freshness: freshness}
|
||||
if hasZ && !math.IsNaN(z) {
|
||||
position.Z = z
|
||||
}
|
||||
return position, true
|
||||
}
|
||||
|
||||
func isProjectionOlder(next, current domain.SCUMProjectionFreshnessState) bool {
|
||||
if current.Status == "" || current.Status == domain.SCUMProjectionUnknown {
|
||||
return false
|
||||
}
|
||||
if next.Source == current.Source && next.QueryKey == current.QueryKey && next.Sequence > 0 && current.Sequence > 0 && next.Sequence < current.Sequence {
|
||||
return true
|
||||
}
|
||||
return !next.ObservedAt.IsZero() && !current.ObservedAt.IsZero() && next.ObservedAt.Before(current.ObservedAt)
|
||||
}
|
||||
|
||||
func scumObservationID(result domain.SCUMObservationResult) string {
|
||||
seed := fmt.Sprintf("%s/%s/%s/%d/%s", result.ServerInstanceID, result.PluginID, result.QueryKey, result.Sequence, result.Checksum)
|
||||
if result.Checksum == "" {
|
||||
seed = fmt.Sprintf("%s/%s/%s/%d/%s", result.ServerInstanceID, result.PluginID, result.QueryKey, result.Sequence, result.ObservedAt.Format(time.RFC3339Nano))
|
||||
}
|
||||
return "scum-observation-" + fingerprintID(result.ServerInstanceID, seed)
|
||||
}
|
||||
|
||||
func scumProjectionID(kind, serverID, subject string) string {
|
||||
return "scum-" + kind + "-" + fingerprintID(serverID, subject)
|
||||
}
|
||||
|
||||
func firstString(row map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value, ok := row[key]; ok {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if trimmed := strings.TrimSpace(typed); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
case fmt.Stringer:
|
||||
if trimmed := strings.TrimSpace(typed.String()); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
case int, int64, uint64, float64:
|
||||
return fmt.Sprint(typed)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstFloat(row map[string]any, keys ...string) (float64, bool) {
|
||||
for _, key := range keys {
|
||||
if value, ok := row[key]; ok {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
return typed, true
|
||||
case float32:
|
||||
return float64(typed), true
|
||||
case int:
|
||||
return float64(typed), true
|
||||
case int64:
|
||||
return float64(typed), true
|
||||
case uint64:
|
||||
return float64(typed), true
|
||||
case string:
|
||||
parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func firstInt(row map[string]any, keys ...string) (int, bool) {
|
||||
value, ok := firstFloat(row, keys...)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return int(value), true
|
||||
}
|
||||
|
||||
func firstBool(row map[string]any, keys ...string) (bool, bool) {
|
||||
for _, key := range keys {
|
||||
if value, ok := row[key]; ok {
|
||||
switch typed := value.(type) {
|
||||
case bool:
|
||||
return typed, true
|
||||
case string:
|
||||
parsed, err := strconv.ParseBool(strings.TrimSpace(typed))
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
case int:
|
||||
return typed != 0, true
|
||||
case int64:
|
||||
return typed != 0, true
|
||||
case float64:
|
||||
return typed != 0, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func firstTime(row map[string]any, keys ...string) time.Time {
|
||||
for _, key := range keys {
|
||||
if value, ok := row[key]; ok {
|
||||
switch typed := value.(type) {
|
||||
case time.Time:
|
||||
return typed
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(typed)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339Nano, trimmed); err == nil {
|
||||
return parsed
|
||||
}
|
||||
if parsed, err := time.Parse("2006-01-02 15:04:05", trimmed); err == nil {
|
||||
return parsed.UTC()
|
||||
}
|
||||
case int64:
|
||||
return time.Unix(typed, 0).UTC()
|
||||
case float64:
|
||||
return time.Unix(int64(typed), 0).UTC()
|
||||
}
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func unknownRowFields(row map[string]any, known ...string) map[string]any {
|
||||
knownSet := map[string]struct{}{}
|
||||
for _, key := range known {
|
||||
knownSet[key] = struct{}{}
|
||||
}
|
||||
unknown := map[string]any{}
|
||||
for key, value := range row {
|
||||
if _, ok := knownSet[key]; ok {
|
||||
continue
|
||||
}
|
||||
unknown[key] = value
|
||||
}
|
||||
if len(unknown) == 0 {
|
||||
return nil
|
||||
}
|
||||
return domain.CopyGameClientBridgePayload(unknown)
|
||||
}
|
||||
|
||||
func coalesceString(next, current string) string {
|
||||
if strings.TrimSpace(next) != "" {
|
||||
return strings.TrimSpace(next)
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func coalesceTime(next, current time.Time) time.Time {
|
||||
if !next.IsZero() {
|
||||
return next
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func limitSCUMProjectionSlice[T any](values *[]T, limit int) {
|
||||
if limit > 0 && len(*values) > limit {
|
||||
*values = (*values)[:limit]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestSCUMObservationProjectsRealRowsAndSeparatesProfileFromSteamID(t *testing.T) {
|
||||
svc, _ := newRegisteredLogIngestService(t)
|
||||
observed := time.Date(2026, 8, 10, 9, 0, 0, 0, time.UTC)
|
||||
observation, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{
|
||||
ServerInstanceID: "server-1",
|
||||
PluginID: "server.scum",
|
||||
Source: "run.sqlite.read",
|
||||
QueryKey: "scum.player.profile",
|
||||
Sequence: 10,
|
||||
Checksum: "sha256:profile-10",
|
||||
ObservedAt: observed,
|
||||
Rows: []map[string]any{{
|
||||
"gamePlayerId": "steam-1",
|
||||
"userProfileId": "profile-99",
|
||||
"steamId": "steam-1",
|
||||
"displayName": "Moon",
|
||||
"squadId": "squad-1",
|
||||
"famePoints": 42,
|
||||
"normalBalance": 500.0,
|
||||
"goldBalance": 7.0,
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"z": 30,
|
||||
"lastSaveTime": observed.Add(-time.Minute).Format(time.RFC3339),
|
||||
"future_column": "preserved",
|
||||
}},
|
||||
})
|
||||
if err != nil || observation.Status != domain.SCUMObservationAccepted {
|
||||
t.Fatalf("apply observation=%+v err=%v", observation, err)
|
||||
}
|
||||
player, err := svc.store.GamePlayers().Get(gamePlayerRecordID("server-1", "steam-1"))
|
||||
if err != nil || player.DisplayName != "Moon" {
|
||||
t.Fatalf("expected game player from real row: player=%+v err=%v", player, err)
|
||||
}
|
||||
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", UserProfileID: "profile-99"})
|
||||
if err != nil || len(states) != 1 {
|
||||
t.Fatalf("states=%+v err=%v", states, err)
|
||||
}
|
||||
state := states[0]
|
||||
if state.GamePlayerID != "steam-1" || state.UserProfileID != "profile-99" || state.SteamID != "steam-1" || state.NormalBalance != 500 || state.Online {
|
||||
t.Fatalf("identity/economy projection mixed IDs or inferred online incorrectly: %+v", state)
|
||||
}
|
||||
if !state.Position.HasCoordinates || state.Position.X != 100 || state.Position.Y != 200 || state.UnknownFields["future_column"] != "preserved" {
|
||||
t.Fatalf("position/unknown fields not projected safely: %+v", state)
|
||||
}
|
||||
stale, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 9, Checksum: "sha256:profile-9", ObservedAt: observed.Add(-time.Hour), Rows: []map[string]any{{"gamePlayerId": "steam-1", "userProfileId": "profile-99", "displayName": "Old", "normalBalance": 9999}}})
|
||||
if err != nil || stale.Status != domain.SCUMObservationStale || stale.ErrorCode != "older_observation" {
|
||||
t.Fatalf("expected older observation stale, got %+v err=%v", stale, err)
|
||||
}
|
||||
again, err := svc.store.SCUMPlayerLiveStates().Get(state.ID)
|
||||
if err != nil || again.DisplayName != "Moon" || again.NormalBalance != 500 {
|
||||
t.Fatalf("older observation overwrote last-known-good: %+v err=%v", again, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMFailedObservationMarksStaleWithoutOverwritingProjection(t *testing.T) {
|
||||
svc, _ := newRegisteredLogIngestService(t)
|
||||
observed := time.Date(2026, 8, 10, 10, 0, 0, 0, time.UTC)
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:ok", ObservedAt: observed, Rows: []map[string]any{{"gamePlayerId": "steam-2", "userProfileId": "profile-2", "displayName": "Nova", "normalBalance": 125}}}); err != nil {
|
||||
t.Fatalf("apply initial observation: %v", err)
|
||||
}
|
||||
failed, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 2, Checksum: "sha256:failed", Status: domain.SCUMObservationFailed, ErrorCode: "sqlite_busy", ObservedAt: observed.Add(time.Minute)})
|
||||
if err != nil || failed.Status != domain.SCUMObservationFailed {
|
||||
t.Fatalf("failed observation=%+v err=%v", failed, err)
|
||||
}
|
||||
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-2"})
|
||||
if err != nil || len(states) != 1 {
|
||||
t.Fatalf("states=%+v err=%v", states, err)
|
||||
}
|
||||
if states[0].NormalBalance != 125 || states[0].Freshness.Status != domain.SCUMProjectionStale || states[0].Freshness.StaleReason != "observation_failed" {
|
||||
t.Fatalf("failed query did not preserve values and mark stale: %+v", states[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMLoginLogsProjectLiveStateAndDatabaseSaveTimeDoesNotProveOnline(t *testing.T) {
|
||||
svc, token := newRegisteredLogIngestService(t)
|
||||
createLogStreamFixture(t, svc)
|
||||
base := time.Date(2026, 8, 10, 11, 0, 0, 0, time.UTC)
|
||||
login := gamePlayerBatch(t, token, 1, []domain.LogEntry{{Seq: 1, Timestamp: base, Line: "login accepted", Fields: map[string]string{"eventType": "scum.login", "playerId": "steam-3", "playerName": "Comet", "sessionId": "session-3", "outcome": "accepted"}}})
|
||||
if _, err := svc.IngestLogBatch(login); err != nil {
|
||||
t.Fatalf("ingest login: %v", err)
|
||||
}
|
||||
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-3"})
|
||||
if err != nil || len(states) != 1 || !states[0].Online {
|
||||
t.Fatalf("login did not mark live state online: states=%+v err=%v", states, err)
|
||||
}
|
||||
logout := gamePlayerBatch(t, token, 2, []domain.LogEntry{{Seq: 2, Timestamp: base.Add(time.Minute), Line: "logout", Fields: map[string]string{"eventType": "scum.logout", "playerId": "steam-3", "playerName": "Comet", "sessionId": "session-3", "reason": "disconnect"}}})
|
||||
if _, err := svc.IngestLogBatch(logout); err != nil {
|
||||
t.Fatalf("ingest logout: %v", err)
|
||||
}
|
||||
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 3, Checksum: "sha256:save-time", ObservedAt: base.Add(2 * time.Minute), Rows: []map[string]any{{"gamePlayerId": "steam-3", "userProfileId": "profile-3", "displayName": "Comet", "lastSaveTime": base.Add(90 * time.Second).Format(time.RFC3339)}}}); err != nil {
|
||||
t.Fatalf("apply save-time observation: %v", err)
|
||||
}
|
||||
states, err = svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-3"})
|
||||
if err != nil || len(states) != 1 {
|
||||
t.Fatalf("states=%+v err=%v", states, err)
|
||||
}
|
||||
if states[0].Online || states[0].LastSaveTime.IsZero() {
|
||||
t.Fatalf("last_save_time was incorrectly treated as online proof: %+v", states[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
type scumWorkflowTemplateDefinition struct {
|
||||
Key string
|
||||
Title string
|
||||
Steps []scumWorkflowStepDefinition
|
||||
}
|
||||
|
||||
type scumWorkflowStepDefinition struct {
|
||||
Key string
|
||||
DependsOn []string
|
||||
OperationKey string
|
||||
QueryTemplateKey string
|
||||
Capability string
|
||||
TargetKey string
|
||||
MutatesState bool
|
||||
MaxAttempts int
|
||||
Summary string
|
||||
}
|
||||
|
||||
func (svc *CoreService) CreateSCUMWorkflowForSession(sessionID, serverID string, request domain.SCUMWorkflowInstance) (domain.SCUMWorkflowInstance, error) {
|
||||
request = domain.CopySCUMWorkflowInstance(request)
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, serverID); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(serverID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
template, ok := scumWorkflowTemplates()[request.TemplateKey]
|
||||
if !ok {
|
||||
return domain.SCUMWorkflowInstance{}, validationError("SCUM workflow template is not declared")
|
||||
}
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" || len(request.IdempotencyKey) > 120 {
|
||||
return domain.SCUMWorkflowInstance{}, validationError("workflow idempotency key is required")
|
||||
}
|
||||
if existing, err := svc.store.SCUMWorkflowInstances().List(domain.SCUMWorkflowInstanceFilter{ServerInstanceID: serverID, IdempotencyKey: request.IdempotencyKey}); err == nil && len(existing) > 0 {
|
||||
return domain.CopySCUMWorkflowInstance(existing[0]), nil
|
||||
} else if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
stamp := svc.now()
|
||||
workflow := domain.SCUMWorkflowInstance{ID: "scum-workflow-" + fingerprintID(serverID, request.IdempotencyKey), ServerInstanceID: serverID, PluginID: plugin.ID, TemplateKey: template.Key, RequestedBy: user.ID, IdempotencyKey: request.IdempotencyKey, Status: domain.SCUMWorkflowQueued, Input: domain.CopyGameClientBridgePayload(request.Input), SafeSummary: domain.SCUMSafeSummary{Title: template.Title, Message: "SCUM workflow queued with typed steps and safe summaries."}, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := svc.store.SCUMWorkflowInstances().Create(workflow); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
for index, step := range template.Steps {
|
||||
maxAttempts := step.MaxAttempts
|
||||
if maxAttempts == 0 {
|
||||
maxAttempts = 1
|
||||
}
|
||||
record := domain.SCUMWorkflowStep{ID: fmt.Sprintf("%s.step.%02d.%s", workflow.ID, index+1, step.Key), WorkflowID: workflow.ID, ServerInstanceID: serverID, StepKey: step.Key, DependsOn: domain.CopyStringSlice(step.DependsOn), Status: domain.SCUMWorkflowStepQueued, OperationKey: step.OperationKey, QueryTemplateKey: step.QueryTemplateKey, Capability: step.Capability, TargetKey: step.TargetKey, MaxAttempts: maxAttempts, MutatesState: step.MutatesState, SafeSummary: domain.SCUMSafeSummary{Title: step.Key, Message: step.Summary}, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := svc.store.SCUMWorkflowSteps().Create(record); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
}
|
||||
_, err = svc.recordAuditEventWithID(user.ID, "scum.workflow.create", "scum-workflow", workflow.ID, domain.AuditResultQueued, "typed SCUM workflow queued")
|
||||
return domain.CopySCUMWorkflowInstance(workflow), err
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMWorkflowsForSession(sessionID string, filter domain.SCUMWorkflowInstanceFilter) ([]domain.SCUMWorkflowInstance, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMWorkflowInstances().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMWorkflowStepsForSession(sessionID string, filter domain.SCUMWorkflowStepFilter) ([]domain.SCUMWorkflowStep, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMWorkflowSteps().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) DispatchNextSCUMWorkflowSteps(serverID string, limit int) ([]domain.SCUMWorkflowStep, error) {
|
||||
if limit <= 0 {
|
||||
limit = 1
|
||||
}
|
||||
workflows, err := svc.store.SCUMWorkflowInstances().List(domain.SCUMWorkflowInstanceFilter{ServerInstanceID: serverID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(workflows, func(i, j int) bool {
|
||||
if workflows[i].CreatedAt.Equal(workflows[j].CreatedAt) {
|
||||
return workflows[i].IdempotencyKey < workflows[j].IdempotencyKey
|
||||
}
|
||||
return workflows[i].CreatedAt.Before(workflows[j].CreatedAt)
|
||||
})
|
||||
dispatched := []domain.SCUMWorkflowStep{}
|
||||
activeMutating, err := svc.hasActiveSCUMMutatingStep(serverID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, workflow := range workflows {
|
||||
if !scumWorkflowRunnable(workflow.Status) || len(dispatched) >= limit {
|
||||
continue
|
||||
}
|
||||
steps, err := svc.sortedSCUMWorkflowSteps(workflow.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, step := range steps {
|
||||
if len(dispatched) >= limit || !scumWorkflowStepRunnable(step.Status) || !scumWorkflowDependenciesConfirmed(step, steps) {
|
||||
continue
|
||||
}
|
||||
if step.MutatesState && activeMutating {
|
||||
return dispatched, nil
|
||||
}
|
||||
if blocked, err := svc.blockSCUMStepIfRunUnavailable(workflow, step); err != nil || blocked.ID != "" {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dispatched = append(dispatched, blocked)
|
||||
return dispatched, nil
|
||||
}
|
||||
step.Status = domain.SCUMWorkflowStepRunning
|
||||
step.Attempt++
|
||||
step.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workflow.Status = domain.SCUMWorkflowRunning
|
||||
workflow.CurrentStepKey = step.StepKey
|
||||
workflow.UpdatedAt = step.UpdatedAt
|
||||
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dispatched = append(dispatched, domain.CopySCUMWorkflowStep(step))
|
||||
if step.MutatesState {
|
||||
activeMutating = true
|
||||
return dispatched, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return dispatched, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) CompleteSCUMWorkflowStep(stepID string, status domain.SCUMWorkflowStepStatus, confirmation domain.SCUMOperationConfirmation) (domain.SCUMWorkflowInstance, error) {
|
||||
step, err := svc.store.SCUMWorkflowSteps().Get(stepID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
workflow, err := svc.store.SCUMWorkflowInstances().Get(step.WorkflowID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
if !scumWorkflowStepTerminal(status) {
|
||||
return domain.SCUMWorkflowInstance{}, validationError("SCUM workflow step completion status must be terminal")
|
||||
}
|
||||
stamp := svc.now()
|
||||
step.Status = status
|
||||
step.Confirmation = domain.CopySCUMOperationConfirmation(confirmation)
|
||||
step.CompletedAt = stamp
|
||||
step.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
return svc.refreshSCUMWorkflowStatus(workflow)
|
||||
}
|
||||
|
||||
func (svc *CoreService) RetrySCUMWorkflowStep(stepID string) (domain.SCUMWorkflowStep, error) {
|
||||
step, err := svc.store.SCUMWorkflowSteps().Get(stepID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
workflow, err := svc.store.SCUMWorkflowInstances().Get(step.WorkflowID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
if step.Attempt >= step.MaxAttempts {
|
||||
return domain.SCUMWorkflowStep{}, validationError("SCUM workflow step retry limit reached")
|
||||
}
|
||||
if step.MutatesState && step.Status == domain.SCUMWorkflowStepUnknown && step.Confirmation.Status != "confirmed" {
|
||||
step.SafeSummary = domain.SCUMSafeSummary{Title: "确认后才能重试", Message: "State-changing SCUM step is unknown; workflow must run confirmation/readback before retry to avoid duplicate effects."}
|
||||
step.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
return domain.CopySCUMWorkflowStep(step), nil
|
||||
}
|
||||
step.Status = domain.SCUMWorkflowStepQueued
|
||||
step.Confirmation = domain.SCUMOperationConfirmation{}
|
||||
step.CompletedAt = time.Time{}
|
||||
step.UpdatedAt = svc.now()
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
workflow.Status = domain.SCUMWorkflowQueued
|
||||
workflow.BlockerReason = ""
|
||||
workflow.UpdatedAt = step.UpdatedAt
|
||||
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
return domain.CopySCUMWorkflowStep(step), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) sortedSCUMWorkflowSteps(workflowID string) ([]domain.SCUMWorkflowStep, error) {
|
||||
steps, err := svc.store.SCUMWorkflowSteps().List(domain.SCUMWorkflowStepFilter{WorkflowID: workflowID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(steps, func(i, j int) bool {
|
||||
if steps[i].CreatedAt.Equal(steps[j].CreatedAt) {
|
||||
return steps[i].ID < steps[j].ID
|
||||
}
|
||||
return steps[i].CreatedAt.Before(steps[j].CreatedAt)
|
||||
})
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) hasActiveSCUMMutatingStep(serverID string) (bool, error) {
|
||||
mutates := true
|
||||
for _, status := range []domain.SCUMWorkflowStepStatus{domain.SCUMWorkflowStepRunning, domain.SCUMWorkflowStepConfirming} {
|
||||
steps, err := svc.store.SCUMWorkflowSteps().List(domain.SCUMWorkflowStepFilter{ServerInstanceID: serverID, Status: status, MutatesState: &mutates})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(steps) > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) blockSCUMStepIfRunUnavailable(workflow domain.SCUMWorkflowInstance, step domain.SCUMWorkflowStep) (domain.SCUMWorkflowStep, error) {
|
||||
if strings.TrimSpace(step.Capability) == "" {
|
||||
return domain.SCUMWorkflowStep{}, nil
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(workflow.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.blockSCUMWorkflowStep(workflow, step, "Run unavailable", "No bound run endpoint is available for this typed SCUM workflow step.")
|
||||
}
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
if err := svc.validateRunnableEndpoint(endpoint, step.Capability); err != nil {
|
||||
return svc.blockSCUMWorkflowStep(workflow, step, "Run unavailable", "Bound run cannot currently claim the declared workflow capability.")
|
||||
}
|
||||
return domain.SCUMWorkflowStep{}, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) blockSCUMWorkflowStep(workflow domain.SCUMWorkflowInstance, step domain.SCUMWorkflowStep, title string, message string) (domain.SCUMWorkflowStep, error) {
|
||||
stamp := svc.now()
|
||||
step.Status = domain.SCUMWorkflowStepBlocked
|
||||
step.SafeSummary = domain.SCUMSafeSummary{Title: title, Message: message, Details: map[string]string{"stepKey": step.StepKey, "capability": step.Capability}}
|
||||
step.UpdatedAt = stamp
|
||||
workflow.Status = domain.SCUMWorkflowBlocked
|
||||
workflow.CurrentStepKey = step.StepKey
|
||||
workflow.BlockerReason = title
|
||||
workflow.SafeSummary = step.SafeSummary
|
||||
workflow.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
||||
return domain.SCUMWorkflowStep{}, err
|
||||
}
|
||||
return domain.CopySCUMWorkflowStep(step), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) refreshSCUMWorkflowStatus(workflow domain.SCUMWorkflowInstance) (domain.SCUMWorkflowInstance, error) {
|
||||
steps, err := svc.sortedSCUMWorkflowSteps(workflow.ID)
|
||||
if err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
allConfirmed := len(steps) > 0
|
||||
stamp := svc.now()
|
||||
for _, step := range steps {
|
||||
switch step.Status {
|
||||
case domain.SCUMWorkflowStepFailed:
|
||||
workflow.Status = domain.SCUMWorkflowFailed
|
||||
case domain.SCUMWorkflowStepUnknown:
|
||||
workflow.Status = domain.SCUMWorkflowUnknown
|
||||
case domain.SCUMWorkflowStepCancelled:
|
||||
workflow.Status = domain.SCUMWorkflowCancelled
|
||||
case domain.SCUMWorkflowStepConfirmed:
|
||||
default:
|
||||
allConfirmed = false
|
||||
}
|
||||
if workflow.Status == domain.SCUMWorkflowFailed || workflow.Status == domain.SCUMWorkflowUnknown || workflow.Status == domain.SCUMWorkflowCancelled {
|
||||
workflow.CurrentStepKey = step.StepKey
|
||||
workflow.CompletedAt = stamp
|
||||
workflow.UpdatedAt = stamp
|
||||
return domain.CopySCUMWorkflowInstance(workflow), svc.store.SCUMWorkflowInstances().Update(workflow)
|
||||
}
|
||||
}
|
||||
if allConfirmed {
|
||||
workflow.Status = domain.SCUMWorkflowConfirmed
|
||||
workflow.CurrentStepKey = ""
|
||||
workflow.CompletedAt = stamp
|
||||
} else {
|
||||
workflow.Status = domain.SCUMWorkflowQueued
|
||||
workflow.CurrentStepKey = ""
|
||||
}
|
||||
workflow.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
||||
return domain.SCUMWorkflowInstance{}, err
|
||||
}
|
||||
return domain.CopySCUMWorkflowInstance(workflow), nil
|
||||
}
|
||||
|
||||
func scumWorkflowDependenciesConfirmed(step domain.SCUMWorkflowStep, steps []domain.SCUMWorkflowStep) bool {
|
||||
if len(step.DependsOn) == 0 {
|
||||
return true
|
||||
}
|
||||
statuses := map[string]domain.SCUMWorkflowStepStatus{}
|
||||
for _, candidate := range steps {
|
||||
statuses[candidate.StepKey] = candidate.Status
|
||||
}
|
||||
for _, dependency := range step.DependsOn {
|
||||
if statuses[dependency] != domain.SCUMWorkflowStepConfirmed {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func scumWorkflowRunnable(status domain.SCUMWorkflowStatus) bool {
|
||||
switch status {
|
||||
case domain.SCUMWorkflowQueued, domain.SCUMWorkflowRunning, domain.SCUMWorkflowWaiting:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scumWorkflowStepRunnable(status domain.SCUMWorkflowStepStatus) bool {
|
||||
switch status {
|
||||
case domain.SCUMWorkflowStepQueued, domain.SCUMWorkflowStepWaiting:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scumWorkflowStepTerminal(status domain.SCUMWorkflowStepStatus) bool {
|
||||
switch status {
|
||||
case domain.SCUMWorkflowStepConfirmed, domain.SCUMWorkflowStepFailed, domain.SCUMWorkflowStepUnknown, domain.SCUMWorkflowStepCancelled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scumWorkflowTemplates() map[string]scumWorkflowTemplateDefinition {
|
||||
read := domain.JobCapabilityRemoteRunDBSQLiteQuery
|
||||
logs := domain.JobCapabilityRemoteRunLogsTransfer
|
||||
protectedSQL := domain.JobCapabilityRemoteRunProtectedSQL
|
||||
rcon := domain.JobCapabilityRemoteRunRCONCommand
|
||||
return map[string]scumWorkflowTemplateDefinition{
|
||||
"scum.bootstrap-real-data": {Key: "scum.bootstrap-real-data", Title: "Bootstrap SCUM real data", Steps: []scumWorkflowStepDefinition{{Key: "verify-run-binding", Capability: read, TargetKey: "scum-database", Summary: "Verify run binding and SCUM.db query capability."}, {Key: "schema-probe", DependsOn: []string{"verify-run-binding"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.schema.probe", Summary: "Probe SCUM.db schema before projection refresh."}, {Key: "login-cursor", DependsOn: []string{"schema-probe"}, Capability: logs, TargetKey: "scum-login", Summary: "Initialize login log observation cursor."}}},
|
||||
"scum.player-refresh": {Key: "scum.player-refresh", Title: "Refresh SCUM player", Steps: []scumWorkflowStepDefinition{{Key: "login-evidence", Capability: logs, TargetKey: "scum-login", Summary: "Sync login/logout evidence."}, {Key: "player-profile", DependsOn: []string{"login-evidence"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Read player profile/economy facts."}, {Key: "position-read", DependsOn: []string{"player-profile"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", Summary: "Read current player coordinates."}}},
|
||||
"scum.world-refresh": {Key: "scum.world-refresh", Title: "Refresh SCUM world", Steps: []scumWorkflowStepDefinition{{Key: "squad-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.squads", MaxAttempts: 2, Summary: "Refresh squads."}, {Key: "vehicle-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.vehicles", MaxAttempts: 2, Summary: "Refresh vehicles."}, {Key: "flag-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.flags", MaxAttempts: 2, Summary: "Refresh flags."}, {Key: "position-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", MaxAttempts: 2, Summary: "Refresh map positions."}}},
|
||||
"scum.player-correction": {Key: "scum.player-correction", Title: "SCUM player correction", Steps: []scumWorkflowStepDefinition{{Key: "safety-check", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Verify current projection, before value, offline state, and backup evidence."}, {Key: "apply-operation", DependsOn: []string{"safety-check"}, Capability: protectedSQL, TargetKey: "scum-database", OperationKey: "player.attribute.855.set", MutatesState: true, Summary: "Apply the approved typed operation through Run."}, {Key: "confirmation-read", DependsOn: []string{"apply-operation"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Confirm the requested value by readback."}}},
|
||||
"scum.gift-delivery": {Key: "scum.gift-delivery", Title: "SCUM gift delivery", Steps: []scumWorkflowStepDefinition{{Key: "eligibility-check", Summary: "Evaluate gift eligibility and idempotency."}, {Key: "deliver-reward", DependsOn: []string{"eligibility-check"}, Capability: rcon, TargetKey: "scum-management", OperationKey: "reward.deliver", MutatesState: true, MaxAttempts: 2, Summary: "Deliver approved reward through typed operation."}, {Key: "notify-player", DependsOn: []string{"deliver-reward"}, Capability: rcon, TargetKey: "scum-management", OperationKey: "player.notify", MutatesState: true, Summary: "Notify the player after delivery."}, {Key: "confirmation-read", DependsOn: []string{"notify-player"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Confirm grant state/readback before marking delivered."}}},
|
||||
"scum.territory-audit": {Key: "scum.territory-audit", Title: "SCUM territory audit", Steps: []scumWorkflowStepDefinition{{Key: "squad-roster", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.squad-members", Summary: "Refresh squad rosters."}, {Key: "flag-ownership", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.flags", Summary: "Refresh flag ownership."}, {Key: "risk-signal", DependsOn: []string{"squad-roster", "flag-ownership"}, Summary: "Project stale owner/member risk signals."}}},
|
||||
"scum.vehicle-audit": {Key: "scum.vehicle-audit", Title: "SCUM vehicle audit", Steps: []scumWorkflowStepDefinition{{Key: "vehicle-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.vehicles", Summary: "Refresh vehicle inventory."}, {Key: "vehicle-map", DependsOn: []string{"vehicle-read"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", Summary: "Refresh vehicle map overlays."}}},
|
||||
"scum.ai-assist": {Key: "scum.ai-assist", Title: "SCUM AI assist", Steps: []scumWorkflowStepDefinition{{Key: "collect-allowed-fields", Summary: "Collect plugin-declared config fields and workflow inputs."}, {Key: "draft-review", DependsOn: []string{"collect-allowed-fields"}, Summary: "Create a reviewable typed diff or workflow draft."}, {Key: "approved-dispatch", DependsOn: []string{"draft-review"}, MutatesState: true, Summary: "Dispatch only after human approval through typed paths."}}},
|
||||
"scum.product-cleanup": {Key: "scum.product-cleanup", Title: "SCUM product cleanup", Steps: []scumWorkflowStepDefinition{{Key: "remove-raw-routes", Summary: "Remove raw logs, terminal, config, and operation-history product routes."}, {Key: "publish-safe-status", DependsOn: []string{"remove-raw-routes"}, Summary: "Route users to safe workflow/status surfaces."}}},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
func TestSCUMWorkflowDispatchesReadStepsWithBoundedConcurrencyAndIdempotency(t *testing.T) {
|
||||
svc, session, instance := newSCUMWorkflowFixture(t, true)
|
||||
workflow, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.world-refresh", IdempotencyKey: "world-refresh-1", Input: map[string]any{"scope": "world"}})
|
||||
if err != nil || workflow.Status != domain.SCUMWorkflowQueued {
|
||||
t.Fatalf("create world workflow=%+v err=%v", workflow, err)
|
||||
}
|
||||
duplicate, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.world-refresh", IdempotencyKey: "world-refresh-1"})
|
||||
if err != nil || duplicate.ID != workflow.ID {
|
||||
t.Fatalf("expected idempotent workflow create: duplicate=%+v err=%v", duplicate, err)
|
||||
}
|
||||
dispatched, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 3)
|
||||
if err != nil || len(dispatched) != 3 {
|
||||
t.Fatalf("expected three bounded read steps dispatched: steps=%+v err=%v", dispatched, err)
|
||||
}
|
||||
for _, step := range dispatched {
|
||||
if step.MutatesState || step.Status != domain.SCUMWorkflowStepRunning || step.Attempt != 1 {
|
||||
t.Fatalf("unexpected read step dispatch: %+v", step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMWorkflowSerializesMutatingStepsPerServer(t *testing.T) {
|
||||
svc, session, instance := newSCUMWorkflowFixture(t, true)
|
||||
first, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.gift-delivery", IdempotencyKey: "gift-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("create first gift workflow: %v", err)
|
||||
}
|
||||
if _, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.gift-delivery", IdempotencyKey: "gift-2"}); err != nil {
|
||||
t.Fatalf("create second gift workflow: %v", err)
|
||||
}
|
||||
steps, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 || steps[0].StepKey != "eligibility-check" {
|
||||
t.Fatalf("expected first eligibility step: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
if _, err := svc.CompleteSCUMWorkflowStep(steps[0].ID, domain.SCUMWorkflowStepConfirmed, domain.SCUMOperationConfirmation{Status: "confirmed"}); err != nil {
|
||||
t.Fatalf("complete eligibility: %v", err)
|
||||
}
|
||||
steps, err = svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 || steps[0].StepKey != "deliver-reward" || !steps[0].MutatesState {
|
||||
t.Fatalf("expected first mutating reward step: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
if steps[0].WorkflowID != first.ID {
|
||||
t.Fatalf("expected first workflow to keep the mutation slot: step=%+v first=%+v", steps[0], first)
|
||||
}
|
||||
blockedByActiveMutation, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch while mutation active: %v", err)
|
||||
}
|
||||
for _, step := range blockedByActiveMutation {
|
||||
if step.MutatesState {
|
||||
t.Fatalf("second state-changing step should wait for first terminal state: steps=%+v", blockedByActiveMutation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMWorkflowBlocksWhenRunUnavailable(t *testing.T) {
|
||||
svc, session, instance := newSCUMWorkflowFixture(t, false)
|
||||
workflow, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.player-refresh", IdempotencyKey: "player-refresh-blocked"})
|
||||
if err != nil {
|
||||
t.Fatalf("create player refresh workflow: %v", err)
|
||||
}
|
||||
steps, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 || steps[0].Status != domain.SCUMWorkflowStepBlocked {
|
||||
t.Fatalf("expected blocked run step: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
updated, err := svc.store.SCUMWorkflowInstances().Get(workflow.ID)
|
||||
if err != nil || updated.Status != domain.SCUMWorkflowBlocked || strings.Contains(updated.SafeSummary.Message, "/") || strings.Contains(strings.ToLower(updated.SafeSummary.Message), "token") {
|
||||
t.Fatalf("workflow blocker should be safe: workflow=%+v err=%v", updated, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMWorkflowRetryRequiresConfirmationAfterUnknownMutation(t *testing.T) {
|
||||
svc, session, instance := newSCUMWorkflowFixture(t, true)
|
||||
if _, err := svc.CreateSCUMWorkflowForSession(session, instance.ID, domain.SCUMWorkflowInstance{TemplateKey: "scum.gift-delivery", IdempotencyKey: "gift-unknown"}); err != nil {
|
||||
t.Fatalf("create gift workflow: %v", err)
|
||||
}
|
||||
steps, err := svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 {
|
||||
t.Fatalf("dispatch eligibility: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
if _, err := svc.CompleteSCUMWorkflowStep(steps[0].ID, domain.SCUMWorkflowStepConfirmed, domain.SCUMOperationConfirmation{Status: "confirmed"}); err != nil {
|
||||
t.Fatalf("complete eligibility: %v", err)
|
||||
}
|
||||
steps, err = svc.DispatchNextSCUMWorkflowSteps(instance.ID, 1)
|
||||
if err != nil || len(steps) != 1 || !steps[0].MutatesState {
|
||||
t.Fatalf("dispatch mutating reward: steps=%+v err=%v", steps, err)
|
||||
}
|
||||
if _, err := svc.CompleteSCUMWorkflowStep(steps[0].ID, domain.SCUMWorkflowStepUnknown, domain.SCUMOperationConfirmation{Status: "unknown"}); err != nil {
|
||||
t.Fatalf("complete unknown mutation: %v", err)
|
||||
}
|
||||
retry, err := svc.RetrySCUMWorkflowStep(steps[0].ID)
|
||||
if err != nil || retry.Status != domain.SCUMWorkflowStepUnknown || !strings.Contains(retry.SafeSummary.Title, "确认") {
|
||||
t.Fatalf("unknown mutating retry should require confirmation: step=%+v err=%v", retry, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newSCUMWorkflowFixture(t *testing.T, runAvailable bool) (*CoreService, string, domain.ServerInstance) {
|
||||
t.Helper()
|
||||
svc := newCoreService(repo.NewMemoryStore(), func() time.Time { return fixedTime })
|
||||
capabilities := []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunRCONCommand}
|
||||
plugin, err := svc.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: capabilities, DeclaredPermissions: []string{"server.game-client.read", "server.game-client.command", "server.game-client.maintenance"}, Permissions: domain.PluginPermissions{Jobs: true, RemoteAccess: true}, RemoteAccess: domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: capabilities, DatabaseEngines: []string{"sqlite"}, RCON: true, LogTransfer: true}, LifecycleActions: domain.PluginLifecycleActions{Start: "actions/start.json"}, RuntimeProfiles: domain.GamePluginRuntimeProfiles{TransportProfiles: []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedSQL}}, {Key: "scum-management", Kind: "rcon", TargetKey: "scum-management", Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}}}}})
|
||||
if err != nil {
|
||||
t.Fatalf("create workflow plugin: %v", err)
|
||||
}
|
||||
endpoint, err := svc.CreateRunEndpoint(domain.RunEndpoint{ID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Platform: "windows", Architecture: "amd64", Status: domain.RunEndpointStatusOnline, Capabilities: capabilities, Capacity: domain.RunCapacity{MaxJobs: 4}, LastHeartbeatAt: fixedTime})
|
||||
if err != nil {
|
||||
t.Fatalf("create workflow endpoint: %v", err)
|
||||
}
|
||||
session := createServiceUserAndLogin(t, svc, domain.User{ID: "workflow-owner", DisplayName: "Workflow Owner", Email: "workflow-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "server-workflow", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Workflow Server", State: domain.ServerInstanceStateRunning})
|
||||
if err != nil {
|
||||
t.Fatalf("create workflow server: %v", err)
|
||||
}
|
||||
if !runAvailable {
|
||||
endpoint.Status = domain.RunEndpointStatusOffline
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("mark workflow endpoint offline: %v", err)
|
||||
}
|
||||
}
|
||||
return svc, session, instance
|
||||
}
|
||||
@@ -51,31 +51,6 @@ func ValidateRunJobResult(result domain.RunJobResult) error {
|
||||
if result.ExecutionResult.Checksum != "" && !validSHA256Checksum(result.ExecutionResult.Checksum) {
|
||||
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
|
||||
}
|
||||
if result.ExecutionResult.SQLiteSchemaProbe != nil {
|
||||
if err := ValidateSCUMSchemaProbeResult(*result.ExecutionResult.SQLiteSchemaProbe); err != nil {
|
||||
violations = append(violations, "executionResult.sqliteSchemaProbe: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.SQLiteTemplate != nil {
|
||||
if err := ValidateSCUMSQLiteTemplateResult(*result.ExecutionResult.SQLiteTemplate); err != nil {
|
||||
violations = append(violations, "executionResult.sqliteTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.RCONTemplate != nil {
|
||||
if err := ValidateSCUMTypedRCONTemplateResult(*result.ExecutionResult.RCONTemplate); err != nil {
|
||||
violations = append(violations, "executionResult.rconTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationResult(*result.ExecutionResult.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionResult.guardedMutation: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.ParsedLogBatch != nil {
|
||||
if err := ValidateSCUMParsedLogBatchResult(*result.ExecutionResult.ParsedLogBatch); err != nil {
|
||||
violations = append(violations, "executionResult.parsedLogBatch: "+err.Error())
|
||||
}
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,6 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
|
||||
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(plugin.RuntimeProfiles, plugin.RequiredRunCapabilities)...)
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("runtimeProfiles.logEvents", plugin.RuntimeProfiles, plugin.DeclaredPermissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateSCUMLiveDataManifest("scumLiveData", plugin.SCUMLiveData, plugin.RequiredRunCapabilities, plugin.RemoteAccess, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("mapTrajectories", plugin.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...)
|
||||
violations = append(violations, validatePluginAssetFiles("lifecycleAssets", plugin.LifecycleAssets)...)
|
||||
@@ -230,7 +229,6 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
|
||||
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(manifest.RuntimeProfiles, manifest.Capabilities)...)
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateSCUMLiveDataManifest("manifest.scumLiveData", manifest.SCUMLiveData, manifest.Capabilities, manifest.RemoteAccess, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("manifest.mapTrajectories", manifest.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginAssetFileDeclarations("manifest.assetFiles", manifest.AssetFiles)...)
|
||||
violations = append(violations, validatePluginAssetFiles("assetFiles", registration.AssetFiles)...)
|
||||
@@ -1533,39 +1531,6 @@ func ValidateJob(job domain.Job) error {
|
||||
violations = append(violations, "executionInput.sourceRcon must not persist adapter inputs")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.SQLiteTemplate != nil {
|
||||
if err := ValidateSCUMSQLiteTemplateRequest(*job.ExecutionInput.SQLiteTemplate); err != nil {
|
||||
violations = append(violations, "executionInput.sqliteTemplate: "+err.Error())
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteQuery {
|
||||
violations = append(violations, "executionInput.sqliteTemplate is allowed only for sqlite query jobs")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.RCONTemplate != nil {
|
||||
if err := ValidateSCUMTypedRCONTemplateRequest(*job.ExecutionInput.RCONTemplate); err != nil {
|
||||
violations = append(violations, "executionInput.rconTemplate: "+err.Error())
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunProtectedRCON {
|
||||
violations = append(violations, "executionInput.rconTemplate is allowed only for protected rcon jobs")
|
||||
}
|
||||
if job.ExecutionInput.SourceRCON == nil || job.ExecutionInput.RemoteAdapterKind != "protected-rcon" {
|
||||
violations = append(violations, "executionInput.rconTemplate requires a protected rcon transport plan")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationRequest(*job.ExecutionInput.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionInput.guardedMutation: "+err.Error())
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunProtectedSQL {
|
||||
violations = append(violations, "executionInput.guardedMutation is allowed only for protected sql jobs")
|
||||
}
|
||||
if job.ExecutionInput.RemoteAdapterKind != "protected-sql" {
|
||||
violations = append(violations, "executionInput.guardedMutation requires a protected sql transport plan")
|
||||
}
|
||||
if len(job.ExecutionInput.Inputs) != 0 || job.ExecutionInput.Content != "" {
|
||||
violations = append(violations, "executionInput.guardedMutation must not persist raw adapter inputs")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...)
|
||||
if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) {
|
||||
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
|
||||
@@ -1576,31 +1541,6 @@ func ValidateJob(job domain.Job) error {
|
||||
if len(job.ExecutionResult.AuditSummary) > maxAuditSummaryLength {
|
||||
violations = append(violations, "executionResult.auditSummary is too long")
|
||||
}
|
||||
if job.ExecutionResult.SQLiteSchemaProbe != nil {
|
||||
if err := ValidateSCUMSchemaProbeResult(*job.ExecutionResult.SQLiteSchemaProbe); err != nil {
|
||||
violations = append(violations, "executionResult.sqliteSchemaProbe: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.SQLiteTemplate != nil {
|
||||
if err := ValidateSCUMSQLiteTemplateResult(*job.ExecutionResult.SQLiteTemplate); err != nil {
|
||||
violations = append(violations, "executionResult.sqliteTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.RCONTemplate != nil {
|
||||
if err := ValidateSCUMTypedRCONTemplateResult(*job.ExecutionResult.RCONTemplate); err != nil {
|
||||
violations = append(violations, "executionResult.rconTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationResult(*job.ExecutionResult.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionResult.guardedMutation: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.ParsedLogBatch != nil {
|
||||
if err := ValidateSCUMParsedLogBatchResult(*job.ExecutionResult.ParsedLogBatch); err != nil {
|
||||
violations = append(violations, "executionResult.parsedLogBatch: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite {
|
||||
if job.ServerInstanceID == "" {
|
||||
violations = append(violations, "serverInstanceId is required for scoped file jobs")
|
||||
@@ -2259,7 +2199,7 @@ func validPluginRunCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||
domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram,
|
||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||
|
||||
@@ -295,29 +295,6 @@ func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermi
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationValidatesSCUMLiveDataGate(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
registration.Manifest.ID = "game.scum"
|
||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunFilesRead)
|
||||
registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}
|
||||
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{
|
||||
{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||
{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||
}
|
||||
registration.Manifest.RuntimeProfiles.DataTargets = []domain.RuntimeDataTarget{{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}}}
|
||||
registration.Manifest.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "等待当前服务证据。"}}}
|
||||
|
||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||
t.Fatalf("expected disabled live-data gate to validate, got %v", err)
|
||||
}
|
||||
|
||||
registration.Manifest.SCUMLiveData.CapabilityGates[0].Gate = domain.SCUMCapabilityGateEnabled
|
||||
err := ValidateGamePluginManifestRegistration(registration)
|
||||
if err == nil || !strings.Contains(err.Error(), "evidenceStatus must be compatible") || !strings.Contains(err.Error(), "requiredSchemaFingerprint") {
|
||||
t.Fatalf("expected enabled gate evidence violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServerInstanceDependencies(t *testing.T) {
|
||||
instance := domain.ServerInstance{
|
||||
ID: "server-1",
|
||||
|
||||
@@ -34,8 +34,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
logSourceRetentions := map[string]int{}
|
||||
logEventKeys := map[string]struct{}{}
|
||||
logEventTypes := map[string]struct{}{}
|
||||
dataTargetKeys := map[string]struct{}{}
|
||||
transportTargetKeys := map[string]struct{}{}
|
||||
|
||||
for i, probe := range profiles.Discovery {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.discovery[%d]", i)
|
||||
@@ -303,7 +301,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
}
|
||||
if transport.TargetKey != "" {
|
||||
violations = append(violations, validateProfileKey(prefix+".targetKey", transport.TargetKey)...)
|
||||
transportTargetKeys[transport.TargetKey] = struct{}{}
|
||||
}
|
||||
if len(transport.Capabilities) == 0 {
|
||||
violations = append(violations, prefix+".capabilities must not be empty")
|
||||
@@ -315,35 +312,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
}
|
||||
violations = append(violations, duplicateViolations(prefix+".capabilities", transport.Capabilities)...)
|
||||
}
|
||||
for i, target := range profiles.DataTargets {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.dataTargets[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", target.Key)...)
|
||||
violations = append(violations, recordRuntimeProfileKey(dataTargetKeys, prefix+".key", target.Key)...)
|
||||
if target.Kind != "sqlite.snapshot" {
|
||||
violations = append(violations, prefix+".kind is invalid")
|
||||
}
|
||||
violations = append(violations, validateProfileKey(prefix+".transportKey", target.TransportKey)...)
|
||||
transport, transportExists := transportByKey(profiles.TransportProfiles, target.TransportKey)
|
||||
if !transportExists || transport.Kind != "sqlite" {
|
||||
violations = append(violations, prefix+".transportKey must reference a declared sqlite transport")
|
||||
}
|
||||
violations = append(violations, validateProfileKey(prefix+".sourceRootKey", target.SourceRootKey)...)
|
||||
if _, exists := transportTargetKeys[target.SourceRootKey]; !exists {
|
||||
violations = append(violations, prefix+".sourceRootKey must reference a declared runtime transport target")
|
||||
}
|
||||
violations = append(violations, validateSafeRelativeRuntimePath(prefix+".sourcePath", target.SourcePath)...)
|
||||
violations = append(violations, validateSafeRelativeRuntimePath(prefix+".workspaceKey", target.WorkspaceKey)...)
|
||||
if !strings.HasPrefix(target.WorkspaceKey, "databases/") {
|
||||
violations = append(violations, prefix+".workspaceKey must live under databases/")
|
||||
}
|
||||
if target.RefreshPolicy != "on-demand-snapshot" {
|
||||
violations = append(violations, prefix+".refreshPolicy is invalid")
|
||||
}
|
||||
if target.MaxBytes < 1 || target.MaxBytes > 1024*1024*1024 {
|
||||
violations = append(violations, prefix+".maxBytes is invalid")
|
||||
}
|
||||
violations = append(violations, validateRuntimePlatforms(prefix+".platforms", target.Platforms)...)
|
||||
}
|
||||
for i, manager := range profiles.ClientManagers {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.clientManagers[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", manager.Key)...)
|
||||
@@ -688,15 +656,6 @@ func recordRuntimeProfileKey(seen map[string]struct{}, field, key string) []stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func transportByKey(transports []domain.RuntimeTransportProfile, key string) (domain.RuntimeTransportProfile, bool) {
|
||||
for _, transport := range transports {
|
||||
if transport.Key == key {
|
||||
return transport, true
|
||||
}
|
||||
}
|
||||
return domain.RuntimeTransportProfile{}, false
|
||||
}
|
||||
|
||||
func validateRuntimeProfileCapabilityDeclarations(profiles domain.GamePluginRuntimeProfiles, declared []string) []string {
|
||||
declaredSet := map[string]struct{}{}
|
||||
for _, capability := range declared {
|
||||
|
||||
@@ -1,981 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSCUMProbeObjects = 512
|
||||
maxSCUMProbeColumnsPerObject = 256
|
||||
maxSCUMProbeIndexesPerObject = 128
|
||||
maxSCUMProbeForeignKeys = 128
|
||||
maxSCUMProbeSamples = 3
|
||||
maxSCUMProbeTimeoutMS = 10000
|
||||
maxSCUMProbeResultBytes = 1024 * 1024
|
||||
maxSCUMTemplateParameters = 64
|
||||
maxSCUMTemplateRows = 1000
|
||||
maxSCUMTemplateBusyTimeoutMS = 1000
|
||||
maxSCUMTemplateValueBytes = 4096
|
||||
maxSCUMRCONPayloadBytes = 4096
|
||||
maxSCUMRCONResponseBytes = 64 * 1024
|
||||
maxSCUMRCONConfirmRecords = 128
|
||||
maxSCUMMutationPayloadBytes = 4096
|
||||
maxSCUMMutationReadbackBytes = 64 * 1024
|
||||
maxSCUMParsedLogEvents = 1024
|
||||
maxSCUMParsedLogPayloadBytes = 64 * 1024
|
||||
maxSCUMParsedLogLineBytes = 64 * 1024
|
||||
maxSCUMParsedLogResultBytes = 1024 * 1024
|
||||
)
|
||||
|
||||
var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`)
|
||||
var scumDataTargetSafeErrorPattern = regexp.MustCompile(`^data_target_[a-z0-9_]{1,80}$`)
|
||||
var scumTemplateKeyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_.-]{0,127}$`)
|
||||
|
||||
func ValidateSCUMSchemaProbeRequest(request domain.SCUMSchemaProbeRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds("bounds", request.Bounds)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSQLiteTemplateRequest(request domain.SCUMSQLiteTemplateRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
if !validSCUMReadCapability(request.Capability) {
|
||||
violations = append(violations, "capability must be a read capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
|
||||
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if !validSCUMDigest(request.AssetDigest) {
|
||||
violations = append(violations, "assetDigest must be sha256 digest")
|
||||
}
|
||||
if !validSCUMDigest(request.ParameterDigest) {
|
||||
violations = append(violations, "parameterDigest must be sha256 digest")
|
||||
}
|
||||
violations = append(violations, validateSCUMSQLiteTemplateBounds("bounds", request.Bounds)...)
|
||||
violations = append(violations, validateSCUMValueMap("parameters", request.Parameters, request.Bounds.MaxParameters)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMTypedRCONTemplateRequest(request domain.SCUMTypedRCONTemplateRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
if !validSCUMRCONWriteCapability(request.Capability) {
|
||||
violations = append(violations, "capability must be a typed RCON write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("transportKey", request.TransportKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
|
||||
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if request.RequiredSchemaFingerprint != "" && !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"assetDigest", request.AssetDigest}, {"payloadDigest", request.PayloadDigest}, {"confirmationDigest", request.ConfirmationDigest}, {"targetIdentityDigest", request.TargetIdentityDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("idempotencyKey", request.IdempotencyKey)...)
|
||||
if strings.TrimSpace(request.ReviewReason) == "" || len(request.ReviewReason) > 320 || containsSCUMProtectedMaterial(request.ReviewReason) {
|
||||
violations = append(violations, "reviewReason is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMTypedRCONTemplateBounds("bounds", request.Bounds)...)
|
||||
violations = append(violations, validateSCUMValueMap("payload", request.Payload, 64)...)
|
||||
violations = append(violations, validateSCUMJSONSize("payload", request.Payload, request.Bounds.MaxPayloadBytes)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMGuardedMutationRequest(request domain.SCUMGuardedMutationRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
if !validSCUMGuardedMutationCapability(request.Capability) {
|
||||
violations = append(violations, "capability must be a guarded database/XML write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
|
||||
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range scumGuardedMutationRequestDigests(request) {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("idempotencyKey", request.IdempotencyKey)...)
|
||||
if strings.TrimSpace(request.ReviewReason) == "" || len(request.ReviewReason) > 320 || containsSCUMProtectedMaterial(request.ReviewReason) || containsSCUMRawXML(request.ReviewReason) {
|
||||
violations = append(violations, "reviewReason is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMGuardedMutationBounds("bounds", request.Bounds)...)
|
||||
violations = append(violations, validateSCUMGuardedMutationPayload("payload", request.Payload)...)
|
||||
violations = append(violations, validateSCUMJSONSize("payload", request.Payload, request.Bounds.MaxPayloadBytes)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMSchemaProbeResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if result.SourceFingerprint != "" && !validSCUMFingerprint(result.SourceFingerprint) {
|
||||
violations = append(violations, "sourceFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if result.ResultDigest != "" && !validSCUMFingerprint(result.ResultDigest) {
|
||||
violations = append(violations, "resultDigest must be a digest/fingerprint")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds("limits", result.Limits)...)
|
||||
if len(result.Objects) > result.Limits.MaxObjects && result.Limits.MaxObjects > 0 {
|
||||
violations = append(violations, "objects exceeds declared limit")
|
||||
}
|
||||
for i, object := range result.Objects {
|
||||
field := fmt.Sprintf("objects[%d]", i)
|
||||
violations = append(violations, validateSCUMSchemaObjectEvidence(field, object)...)
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSQLiteTemplateResult(result domain.SCUMSQLiteTemplateResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validSCUMReadCapability(result.Capability) {
|
||||
violations = append(violations, "capability must be a read capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
|
||||
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if !validSCUMDigest(result.AssetDigest) {
|
||||
violations = append(violations, "assetDigest must be sha256 digest")
|
||||
}
|
||||
if !validSCUMDigest(result.ParameterDigest) {
|
||||
violations = append(violations, "parameterDigest must be sha256 digest")
|
||||
}
|
||||
if result.SourceFingerprint != "" && !validSCUMFingerprint(result.SourceFingerprint) {
|
||||
violations = append(violations, "sourceFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if !validSCUMFingerprint(result.ResultDigest) {
|
||||
violations = append(violations, "resultDigest must be a digest/fingerprint")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMSQLiteTemplateBounds("limits", result.Limits)...)
|
||||
if result.RowCount != len(result.Rows) {
|
||||
violations = append(violations, "rowCount must match returned rows")
|
||||
}
|
||||
if len(result.Rows) > result.Limits.MaxRows && result.Limits.MaxRows > 0 {
|
||||
violations = append(violations, "rows exceeds declared limit")
|
||||
}
|
||||
for i, row := range result.Rows {
|
||||
violations = append(violations, validateSCUMValueMap(fmt.Sprintf("rows[%d]", i), row, 256)...)
|
||||
}
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.SchemaFingerprint == "" || result.SourceFingerprint == "" {
|
||||
violations = append(violations, "succeeded result requires schema/source fingerprints")
|
||||
}
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMTypedRCONTemplateResult(result domain.SCUMTypedRCONTemplateResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validSCUMRCONWriteCapability(result.Capability) {
|
||||
violations = append(violations, "capability must be a typed RCON write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("transportKey", result.TransportKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
|
||||
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"payloadDigest", result.PayloadDigest}, {"confirmationDigest", result.ConfirmationDigest}, {"targetIdentityDigest", result.TargetIdentityDigest}, {"resultDigest", result.ResultDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
if result.ResponseDigest != "" && !validSCUMDigest(result.ResponseDigest) {
|
||||
violations = append(violations, "responseDigest must be sha256 digest")
|
||||
}
|
||||
if result.ConfirmationDigestID != "" && !validSCUMDigest(result.ConfirmationDigestID) {
|
||||
violations = append(violations, "confirmationDigestId must be sha256 digest")
|
||||
}
|
||||
if !validSCUMRCONConfirmationStatus(result.ConfirmationStatus) {
|
||||
violations = append(violations, "confirmationStatus is invalid")
|
||||
}
|
||||
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) {
|
||||
violations = append(violations, "safeSummary is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMTypedRCONTemplateBounds("limits", result.Limits)...)
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed || result.ResponseDigest == "" || result.ConfirmationDigestID == "" {
|
||||
violations = append(violations, "succeeded result requires confirmed response and confirmation digests")
|
||||
}
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMGuardedMutationResult(result domain.SCUMGuardedMutationResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validSCUMGuardedMutationCapability(result.Capability) {
|
||||
violations = append(violations, "capability must be a guarded database/XML write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
|
||||
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if result.SourceFingerprint != "" && !validSCUMFingerprint(result.SourceFingerprint) {
|
||||
violations = append(violations, "sourceFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range scumGuardedMutationResultDigests(result) {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
if !validSCUMMutationReadbackStatus(result.ReadbackStatus) {
|
||||
violations = append(violations, "readbackStatus is invalid")
|
||||
}
|
||||
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) || containsSCUMRawXML(result.SafeSummary) {
|
||||
violations = append(violations, "safeSummary is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMGuardedMutationBounds("limits", result.Limits)...)
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.AffectedRows != 1 || result.Limits.MaxAffectedRows != 1 {
|
||||
violations = append(violations, "succeeded result requires exactly one affected row")
|
||||
}
|
||||
if result.ReadbackStatus != domain.SCUMMutationReadbackConfirmed || result.SourceFingerprint == "" || result.BeforeDigest == "" || result.AfterDigest == "" || result.ReadbackDigest == "" {
|
||||
violations = append(violations, "succeeded result requires conclusive before/after/readback digests")
|
||||
}
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMParsedLogBatchResult(result domain.SCUMParsedLogBatchResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"sourceKey", result.SourceKey}, {"streamKey", result.StreamKey}, {"parserKey", result.ParserKey}, {"parserVersion", result.ParserVersion}, {"adapterVersion", result.AdapterVersion}} {
|
||||
violations = append(violations, validateSCUMTemplateKey(item.name, item.value)...)
|
||||
if containsSCUMProtectedMaterial(item.value) || containsSCUMNetworkMaterial(item.value) {
|
||||
violations = append(violations, item.name+" contains protected material")
|
||||
}
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"parserDigest", result.ParserDigest}, {"resultDigest", result.ResultDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMParsedLogCursor("firstCursor", result.FirstCursor)...)
|
||||
violations = append(violations, validateSCUMParsedLogCursor("lastCursor", result.LastCursor)...)
|
||||
if !validSCUMLogTailState(result.TailState) {
|
||||
violations = append(violations, "tailState is invalid")
|
||||
}
|
||||
if result.EventCount != len(result.Events) {
|
||||
violations = append(violations, "eventCount must match returned events")
|
||||
}
|
||||
violations = append(violations, validateSCUMParsedLogBatchBounds("limits", result.Limits)...)
|
||||
if len(result.Events) > result.Limits.MaxEvents && result.Limits.MaxEvents > 0 {
|
||||
violations = append(violations, "events exceeds declared limit")
|
||||
}
|
||||
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) || containsSCUMNetworkMaterial(result.SafeSummary) || containsSCUMRawXML(result.SafeSummary) {
|
||||
violations = append(violations, "safeSummary is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
if containsSCUMNetworkMaterial(result.SafeError.Message) || containsSCUMRawXML(result.SafeError.Message) {
|
||||
violations = append(violations, "safeError.message is unsafe")
|
||||
}
|
||||
seenLogical := map[string]struct{}{}
|
||||
seenTransport := map[string]struct{}{}
|
||||
for i, event := range result.Events {
|
||||
field := fmt.Sprintf("events[%d]", i)
|
||||
violations = append(violations, validateSCUMParsedLogEvent(field, event, result)...)
|
||||
if event.LogicalEventDigest != "" {
|
||||
if _, exists := seenLogical[event.LogicalEventDigest]; exists {
|
||||
violations = append(violations, field+".logicalEventDigest is duplicated")
|
||||
}
|
||||
seenLogical[event.LogicalEventDigest] = struct{}{}
|
||||
}
|
||||
transport := fmt.Sprintf("%s|%s|%d", event.Cursor.SourceIdentityDigest, event.Cursor.StreamGeneration, event.Cursor.Sequence)
|
||||
if _, exists := seenTransport[transport]; exists {
|
||||
violations = append(violations, field+".cursor is duplicated")
|
||||
}
|
||||
seenTransport[transport] = struct{}{}
|
||||
}
|
||||
if result.EventCount > 0 && result.LastCursor.Sequence < result.FirstCursor.Sequence {
|
||||
violations = append(violations, "lastCursor.sequence must not be before firstCursor.sequence")
|
||||
}
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func scumGuardedMutationRequestDigests(request domain.SCUMGuardedMutationRequest) []struct{ name, value string } {
|
||||
return []struct{ name, value string }{{"assetDigest", request.AssetDigest}, {"targetIdentityDigest", request.TargetIdentityDigest}, {"expectedRowDigest", request.ExpectedRowDigest}, {"expectedValueDigest", request.ExpectedValueDigest}, {"expectedXmlDigest", request.ExpectedXMLDigest}, {"patchDigest", request.PatchDigest}, {"backupEvidenceDigest", request.BackupEvidenceDigest}, {"offlineEvidenceDigest", request.OfflineEvidenceDigest}, {"dangerConfirmationDigest", request.DangerConfirmationDigest}, {"readbackExpectationDigest", request.ReadbackExpectationDigest}}
|
||||
}
|
||||
|
||||
func scumGuardedMutationResultDigests(result domain.SCUMGuardedMutationResult) []struct{ name, value string } {
|
||||
items := []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"targetIdentityDigest", result.TargetIdentityDigest}, {"expectedRowDigest", result.ExpectedRowDigest}, {"expectedValueDigest", result.ExpectedValueDigest}, {"expectedXmlDigest", result.ExpectedXMLDigest}, {"patchDigest", result.PatchDigest}, {"backupEvidenceDigest", result.BackupEvidenceDigest}, {"offlineEvidenceDigest", result.OfflineEvidenceDigest}, {"dangerConfirmationDigest", result.DangerConfirmationDigest}, {"readbackExpectationDigest", result.ReadbackExpectationDigest}, {"resultDigest", result.ResultDigest}}
|
||||
for _, item := range []struct{ name, value string }{{"beforeDigest", result.BeforeDigest}, {"afterDigest", result.AfterDigest}, {"readbackDigest", result.ReadbackDigest}} {
|
||||
if item.value != "" {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error {
|
||||
var violations []string
|
||||
if !validSCUMDataCapability(evidence.Capability) {
|
||||
violations = append(violations, "capability is invalid")
|
||||
}
|
||||
if !validSCUMCapabilityEvidenceStatus(evidence.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", evidence.Binding)...)
|
||||
violations = appendRequired(violations, "adapterVersion", evidence.AdapterVersion)
|
||||
if evidence.SchemaFingerprint != "" && !validSCUMFingerprint(evidence.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if evidence.ProbeResultDigest != "" && !validSCUMFingerprint(evidence.ProbeResultDigest) {
|
||||
violations = append(violations, "probeResultDigest must be a digest/fingerprint")
|
||||
}
|
||||
for i, digest := range evidence.AssetDigests {
|
||||
if !validSCUMDigest(digest) {
|
||||
violations = append(violations, fmt.Sprintf("assetDigests[%d] must be sha256 digest", i))
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", evidence.SafeError)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validateSCUMLiveDataManifest(prefix string, value domain.SCUMLiveDataManifest, capabilities []string, remoteAccess domain.GamePluginRemoteAccess, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
|
||||
if value.SchemaVersion == "" && value.Probe.Capability == "" && len(value.CapabilityGates) == 0 {
|
||||
return nil
|
||||
}
|
||||
var violations []string
|
||||
if value.SchemaVersion != "1" {
|
||||
violations = append(violations, prefix+".schemaVersion must be 1")
|
||||
}
|
||||
if value.Probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe {
|
||||
violations = append(violations, prefix+".probe.capability must be "+domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
}
|
||||
if !containsString(capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) || !containsString(remoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
violations = append(violations, prefix+".probe requires declared remote.run.db.sqlite.probe capability")
|
||||
}
|
||||
probeTransportFound := false
|
||||
probeDataTargetFound := false
|
||||
expectedWorkspaceKey := "databases/" + strings.TrimPrefix(value.Probe.TargetKey, "databases/")
|
||||
for _, transport := range runtimeProfiles.TransportProfiles {
|
||||
if transport.Key != value.Probe.TargetKey && transport.TargetKey != value.Probe.TargetKey {
|
||||
continue
|
||||
}
|
||||
probeTransportFound = true
|
||||
if transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference sqlite transport with remote.run.db.sqlite.probe")
|
||||
}
|
||||
}
|
||||
if !probeTransportFound {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference a declared transport")
|
||||
}
|
||||
for _, target := range runtimeProfiles.DataTargets {
|
||||
if target.Key != value.Probe.TargetKey {
|
||||
continue
|
||||
}
|
||||
probeDataTargetFound = true
|
||||
transport, ok := transportByKey(runtimeProfiles.TransportProfiles, target.TransportKey)
|
||||
if target.Kind != "sqlite.snapshot" || target.WorkspaceKey != expectedWorkspaceKey || !ok || transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference a sqlite snapshot data target for the generated Run workspace")
|
||||
}
|
||||
}
|
||||
if !probeDataTargetFound {
|
||||
violations = append(violations, prefix+".probe.targetKey must reference a declared runtime data target")
|
||||
}
|
||||
violations = append(violations, validateSCUMSchemaProbeBounds(prefix+".probe.bounds", value.Probe.Bounds)...)
|
||||
seen := map[domain.SCUMDataCapability]struct{}{}
|
||||
for i, gate := range value.CapabilityGates {
|
||||
field := fmt.Sprintf("%s.capabilityGates[%d]", prefix, i)
|
||||
if !validSCUMDataCapability(gate.Capability) {
|
||||
violations = append(violations, field+".capability is invalid")
|
||||
}
|
||||
if _, exists := seen[gate.Capability]; exists {
|
||||
violations = append(violations, field+".capability is duplicated")
|
||||
}
|
||||
seen[gate.Capability] = struct{}{}
|
||||
if gate.Gate != domain.SCUMCapabilityGateDisabled && gate.Gate != domain.SCUMCapabilityGateEnabled {
|
||||
violations = append(violations, field+".gate is invalid")
|
||||
}
|
||||
violations = appendRequired(violations, field+".adapterVersion", gate.AdapterVersion)
|
||||
if !validSCUMCapabilityEvidenceStatus(gate.EvidenceStatus) {
|
||||
violations = append(violations, field+".evidenceStatus is invalid")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(gate.SafeReason) || len(gate.SafeReason) > 240 || strings.TrimSpace(gate.SafeReason) == "" {
|
||||
violations = append(violations, field+".safeReason is unsafe")
|
||||
}
|
||||
if gate.Gate == domain.SCUMCapabilityGateEnabled {
|
||||
if gate.EvidenceStatus != domain.SCUMCapabilityEvidenceCompatible {
|
||||
violations = append(violations, field+".evidenceStatus must be compatible when enabled")
|
||||
}
|
||||
if !validSCUMFingerprint(gate.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, field+".requiredSchemaFingerprint is required when enabled")
|
||||
}
|
||||
if gate.Capability != domain.SCUMDataCapabilitySchemaProbe && len(gate.RequiredAssetDigests) == 0 {
|
||||
violations = append(violations, field+".requiredAssetDigests is required when enabled")
|
||||
}
|
||||
}
|
||||
if gate.Gate == domain.SCUMCapabilityGateDisabled && gate.EvidenceStatus == domain.SCUMCapabilityEvidenceCompatible {
|
||||
violations = append(violations, field+".evidenceStatus must not claim compatibility while disabled")
|
||||
}
|
||||
for digestIndex, digest := range gate.RequiredAssetDigests {
|
||||
if !validSCUMDigest(digest) {
|
||||
violations = append(violations, fmt.Sprintf("%s.requiredAssetDigests[%d] must be sha256 digest", field, digestIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMBindingIdentity(prefix string, value domain.SCUMBindingIdentity) []string {
|
||||
var violations []string
|
||||
for _, item := range []struct{ name, value string }{{"serverInstanceId", value.ServerInstanceID}, {"runBindingId", value.RunBindingID}, {"runEndpointId", value.RunEndpointID}, {"pluginId", value.PluginID}, {"pluginVersion", value.PluginVersion}, {"adapterVersion", value.AdapterVersion}, {"databaseIdentity", value.DatabaseIdentity}} {
|
||||
violations = appendRequired(violations, prefix+"."+item.name, item.value)
|
||||
if containsSCUMProtectedMaterial(item.value) {
|
||||
violations = append(violations, prefix+"."+item.name+" contains protected connection material")
|
||||
}
|
||||
}
|
||||
if value.GameVersion != "" && containsSCUMProtectedMaterial(value.GameVersion) {
|
||||
violations = append(violations, prefix+".gameVersion contains protected connection material")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSchemaProbeBounds(prefix string, value domain.SCUMSchemaProbeBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxObjects < 1 || value.MaxObjects > maxSCUMProbeObjects {
|
||||
violations = append(violations, prefix+".maxObjects is out of bounds")
|
||||
}
|
||||
if value.MaxColumnsPerObject < 1 || value.MaxColumnsPerObject > maxSCUMProbeColumnsPerObject {
|
||||
violations = append(violations, prefix+".maxColumnsPerObject is out of bounds")
|
||||
}
|
||||
if value.MaxIndexesPerObject < 0 || value.MaxIndexesPerObject > maxSCUMProbeIndexesPerObject {
|
||||
violations = append(violations, prefix+".maxIndexesPerObject is out of bounds")
|
||||
}
|
||||
if value.MaxForeignKeys < 0 || value.MaxForeignKeys > maxSCUMProbeForeignKeys {
|
||||
violations = append(violations, prefix+".maxForeignKeys is out of bounds")
|
||||
}
|
||||
if value.MaxCardinalityReads < 0 || value.MaxCardinalityReads > maxSCUMProbeObjects {
|
||||
violations = append(violations, prefix+".maxCardinalityReads is out of bounds")
|
||||
}
|
||||
if value.MaxSampleRows < 0 || value.MaxSampleRows > maxSCUMProbeSamples {
|
||||
violations = append(violations, prefix+".maxSampleRows is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMProbeResultBytes {
|
||||
violations = append(violations, prefix+".maxResultBytes is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSQLiteTemplateBounds(prefix string, value domain.SCUMSQLiteTemplateBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxParameters < 0 || value.MaxParameters > maxSCUMTemplateParameters {
|
||||
violations = append(violations, prefix+".maxParameters is out of bounds")
|
||||
}
|
||||
if value.MaxRows < 1 || value.MaxRows > maxSCUMTemplateRows {
|
||||
violations = append(violations, prefix+".maxRows is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.BusyTimeoutMS < 0 || value.BusyTimeoutMS > maxSCUMTemplateBusyTimeoutMS {
|
||||
violations = append(violations, prefix+".busyTimeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMProbeResultBytes {
|
||||
violations = append(violations, prefix+".maxResultBytes is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMTypedRCONTemplateBounds(prefix string, value domain.SCUMTypedRCONTemplateBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMRCONPayloadBytes {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxResponseBytes < 1 || value.MaxResponseBytes > maxSCUMRCONResponseBytes {
|
||||
violations = append(violations, prefix+".maxResponseBytes is out of bounds")
|
||||
}
|
||||
if value.MaxConfirmRecords < 1 || value.MaxConfirmRecords > maxSCUMRCONConfirmRecords {
|
||||
violations = append(violations, prefix+".maxConfirmRecords is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationBounds(prefix string, value domain.SCUMGuardedMutationBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMMutationPayloadBytes {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.BusyTimeoutMS < 0 || value.BusyTimeoutMS > maxSCUMTemplateBusyTimeoutMS {
|
||||
violations = append(violations, prefix+".busyTimeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxReadbackBytes < 1 || value.MaxReadbackBytes > maxSCUMMutationReadbackBytes {
|
||||
violations = append(violations, prefix+".maxReadbackBytes is out of bounds")
|
||||
}
|
||||
if value.MaxAffectedRows != 1 {
|
||||
violations = append(violations, prefix+".maxAffectedRows must be 1")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMParsedLogBatchBounds(prefix string, value domain.SCUMParsedLogBatchBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxEvents < 0 || value.MaxEvents > maxSCUMParsedLogEvents {
|
||||
violations = append(violations, prefix+".maxEvents is out of bounds")
|
||||
}
|
||||
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMParsedLogPayloadBytes {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
|
||||
}
|
||||
if value.MaxLineBytes < 1 || value.MaxLineBytes > maxSCUMParsedLogLineBytes {
|
||||
violations = append(violations, prefix+".maxLineBytes is out of bounds")
|
||||
}
|
||||
if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMParsedLogResultBytes {
|
||||
violations = append(violations, prefix+".maxResultBytes is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMParsedLogCursor(prefix string, value domain.SCUMParsedLogCursor) []string {
|
||||
var violations []string
|
||||
if !validSCUMFingerprint(value.SourceIdentityDigest) || containsSCUMProtectedMaterial(value.SourceIdentityDigest) || containsSCUMNetworkMaterial(value.SourceIdentityDigest) {
|
||||
violations = append(violations, prefix+".sourceIdentityDigest must be a redacted fingerprint")
|
||||
}
|
||||
if !validSCUMFingerprint(value.StreamGeneration) || containsSCUMProtectedMaterial(value.StreamGeneration) || containsSCUMNetworkMaterial(value.StreamGeneration) {
|
||||
violations = append(violations, prefix+".streamGeneration must be a redacted fingerprint")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMParsedLogEvent(prefix string, event domain.SCUMParsedLogEvent, result domain.SCUMParsedLogBatchResult) []string {
|
||||
var violations []string
|
||||
violations = append(violations, validateSCUMTemplateKey(prefix+".eventType", event.EventType)...)
|
||||
violations = append(violations, validateSCUMParsedLogCursor(prefix+".cursor", event.Cursor)...)
|
||||
for _, item := range []struct{ name, value string }{{"logicalEventDigest", event.LogicalEventDigest}, {"eventDigest", event.EventDigest}, {"payloadDigest", event.PayloadDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, prefix+"."+item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
if result.EventCount > 0 && event.Cursor.Sequence < result.FirstCursor.Sequence || result.EventCount > 0 && event.Cursor.Sequence > result.LastCursor.Sequence {
|
||||
violations = append(violations, prefix+".cursor.sequence is outside batch range")
|
||||
}
|
||||
if event.Cursor.SourceIdentityDigest != result.FirstCursor.SourceIdentityDigest || event.Cursor.StreamGeneration != result.FirstCursor.StreamGeneration {
|
||||
violations = append(violations, prefix+".cursor must match batch source identity and generation")
|
||||
}
|
||||
violations = append(violations, validateSCUMValueMap(prefix+".payload", event.Payload, 64)...)
|
||||
violations = append(violations, validateSCUMJSONSize(prefix+".payload", event.Payload, result.Limits.MaxPayloadBytes)...)
|
||||
for key, value := range event.Payload {
|
||||
if strings.Contains(strings.ToLower(key), "raw") || containsSCUMProtectedMaterial(key) || containsSCUMNetworkMaterial(key) {
|
||||
violations = append(violations, prefix+".payload key is unsafe")
|
||||
}
|
||||
if text, ok := value.(string); ok && (containsSCUMProtectedMaterial(text) || containsSCUMNetworkMaterial(text) || containsSCUMRawXML(text)) {
|
||||
violations = append(violations, prefix+".payload value is unsafe")
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMTemplateKey(prefix, value string) []string {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, prefix, value)
|
||||
if value != "" && (!scumTemplateKeyPattern.MatchString(value) || strings.Contains(value, "..")) {
|
||||
violations = append(violations, prefix+" is unsafe")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMValueMap(prefix string, values map[string]any, maxItems int) []string {
|
||||
var violations []string
|
||||
if maxItems >= 0 && len(values) > maxItems {
|
||||
violations = append(violations, prefix+" exceeds declared limit")
|
||||
}
|
||||
for key, value := range values {
|
||||
loweredKey := strings.ToLower(key)
|
||||
if !scumTemplateKeyPattern.MatchString(key) || containsSCUMProtectedMaterial(key) || strings.Contains(key, "..") || strings.Contains(loweredKey, "command") || strings.Contains(loweredKey, "rcon") {
|
||||
violations = append(violations, prefix+" key is unsafe")
|
||||
}
|
||||
field := prefix + ".value"
|
||||
violations = append(violations, validateSCUMValue(field, value)...)
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationPayload(prefix string, values map[string]any) []string {
|
||||
violations := validateSCUMValueMap(prefix, values, 64)
|
||||
for key, value := range values {
|
||||
loweredKey := strings.ToLower(key)
|
||||
if strings.Contains(loweredKey, "sql") || strings.Contains(loweredKey, "xml") || strings.Contains(loweredKey, "path") || strings.Contains(loweredKey, "table") || strings.Contains(loweredKey, "column") || strings.Contains(loweredKey, "query") || strings.Contains(loweredKey, "raw") || strings.Contains(loweredKey, "855") {
|
||||
violations = append(violations, prefix+" key is unsafe")
|
||||
}
|
||||
if text, ok := value.(string); ok {
|
||||
trimmed := strings.TrimSpace(strings.ToLower(text))
|
||||
if containsSCUMRawXML(text) || trimmed == "855" || strings.Contains(trimmed, "fieldkey=855") || strings.Contains(trimmed, "prisoner.value") {
|
||||
violations = append(violations, prefix+" value is unsafe")
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMValue(prefix string, value any) []string {
|
||||
var violations []string
|
||||
switch item := value.(type) {
|
||||
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return nil
|
||||
case string:
|
||||
if len([]byte(item)) > maxSCUMTemplateValueBytes {
|
||||
violations = append(violations, prefix+" is too large")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(item) {
|
||||
violations = append(violations, prefix+" contains protected material")
|
||||
}
|
||||
default:
|
||||
violations = append(violations, prefix+" must be a scalar value")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMJSONSize(prefix string, value any, maxBytes int) []string {
|
||||
if maxBytes <= 0 {
|
||||
return []string{prefix + " max byte limit is required"}
|
||||
}
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return []string{prefix + " must be JSON serializable"}
|
||||
}
|
||||
if len(payload) > maxBytes {
|
||||
return []string{prefix + " exceeds declared byte limit"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMSchemaObjectEvidence(prefix string, value domain.SCUMSchemaObjectEvidence) []string {
|
||||
var violations []string
|
||||
if !validSCUMFingerprint(value.ObjectHash) {
|
||||
violations = append(violations, prefix+".objectHash must be a digest/fingerprint")
|
||||
}
|
||||
if value.Kind != "table" && value.Kind != "view" && value.Kind != "index" && value.Kind != "trigger" {
|
||||
violations = append(violations, prefix+".kind is invalid")
|
||||
}
|
||||
if !validSCUMFingerprint(value.NameFingerprint) || containsSCUMProtectedMaterial(value.NameFingerprint) {
|
||||
violations = append(violations, prefix+".nameFingerprint must be redacted")
|
||||
}
|
||||
for i, column := range value.DeclaredColumns {
|
||||
field := fmt.Sprintf("%s.declaredColumns[%d]", prefix, i)
|
||||
if !validSCUMFingerprint(column.NameFingerprint) || containsSCUMProtectedMaterial(column.NameFingerprint) {
|
||||
violations = append(violations, field+".nameFingerprint must be redacted")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(column.DeclaredType) {
|
||||
violations = append(violations, field+".declaredType contains protected material")
|
||||
}
|
||||
}
|
||||
for i, index := range value.Indexes {
|
||||
field := fmt.Sprintf("%s.indexes[%d]", prefix, i)
|
||||
if !validSCUMFingerprint(index.NameFingerprint) || containsSCUMProtectedMaterial(index.NameFingerprint) {
|
||||
violations = append(violations, field+".nameFingerprint must be redacted")
|
||||
}
|
||||
for columnIndex, hash := range index.ColumnHashes {
|
||||
if !validSCUMFingerprint(hash) {
|
||||
violations = append(violations, fmt.Sprintf("%s.columnHashes[%d] must be a digest/fingerprint", field, columnIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, fk := range value.ForeignKeys {
|
||||
field := fmt.Sprintf("%s.foreignKeys[%d]", prefix, i)
|
||||
for _, item := range []struct{ name, value string }{{"fromColumnHash", fk.FromColumnHash}, {"toObjectHash", fk.ToObjectHash}, {"toColumnHash", fk.ToColumnHash}} {
|
||||
if !validSCUMFingerprint(item.value) {
|
||||
violations = append(violations, field+"."+item.name+" must be a digest/fingerprint")
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, sample := range value.SampleFingerprints {
|
||||
if !validSCUMFingerprint(sample) || containsSCUMProtectedMaterial(sample) {
|
||||
violations = append(violations, fmt.Sprintf("%s.sampleFingerprints[%d] must be a redacted fingerprint", prefix, i))
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMSafeError(prefix string, value domain.SCUMSafeError) []string {
|
||||
var violations []string
|
||||
if !validSCUMSafeErrorCode(value.Code) {
|
||||
violations = append(violations, prefix+".code is invalid")
|
||||
}
|
||||
if containsSCUMProtectedMaterial(value.Message) {
|
||||
violations = append(violations, prefix+".message contains protected material")
|
||||
}
|
||||
if len(value.Message) > 320 {
|
||||
violations = append(violations, prefix+".message is too long")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validSCUMDataCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilitySchemaProbe, domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead, domain.SCUMDataCapabilityProfileXMLWrite, domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMReadCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMRCONWriteCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMGuardedMutationCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityProfileXMLWrite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMTerminalResultSucceeded, domain.SCUMTerminalResultFailed, domain.SCUMTerminalResultCancelled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMRCONConfirmationStatus(value domain.SCUMRCONConfirmationStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMRCONConfirmationConfirmed, domain.SCUMRCONConfirmationFailed, domain.SCUMRCONConfirmationUnknown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMMutationReadbackStatus(value domain.SCUMMutationReadbackStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMMutationReadbackConfirmed, domain.SCUMMutationReadbackFailed, domain.SCUMMutationReadbackConflict, domain.SCUMMutationReadbackUnknown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMLogTailState(value domain.SCUMLogTailState) bool {
|
||||
switch value {
|
||||
case domain.SCUMLogTailAdvanced, domain.SCUMLogTailRotated, domain.SCUMLogTailTruncated, domain.SCUMLogTailRestarted, domain.SCUMLogTailPartial, domain.SCUMLogTailReplayed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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, domain.SCUMSafeErrorInvalidRequest, domain.SCUMSafeErrorTargetUnavailable, domain.SCUMSafeErrorSourceUnavailable, domain.SCUMSafeErrorSQLiteOpenFailed, domain.SCUMSafeErrorSQLiteReadFailed, domain.SCUMSafeErrorDatabaseBusy, domain.SCUMSafeErrorTimeout, domain.SCUMSafeErrorCancelled, domain.SCUMSafeErrorSourceChanged, domain.SCUMSafeErrorResultLimitExceeded, domain.SCUMSafeErrorTemplateMissing, domain.SCUMSafeErrorTemplateMismatch, domain.SCUMSafeErrorParameterInvalid, domain.SCUMSafeErrorRowLimitExceeded, domain.SCUMSafeErrorResultSchemaInvalid, domain.SCUMSafeErrorMutationGuardMismatch, domain.SCUMSafeErrorMutationBackupUnavailable, domain.SCUMSafeErrorMutationOfflineRequired, domain.SCUMSafeErrorMutationConfirmationMissing, domain.SCUMSafeErrorMutationPatchInvalid, domain.SCUMSafeErrorAffectedRowsMismatch, domain.SCUMSafeErrorReadbackMismatch, domain.SCUMSafeErrorRollbackFailed:
|
||||
return true
|
||||
default:
|
||||
return scumDataTargetSafeErrorPattern.MatchString(string(value))
|
||||
}
|
||||
}
|
||||
|
||||
func containsSCUMRawXML(value string) bool {
|
||||
return regexp.MustCompile(`<\s*/?\s*[A-Za-z][^>]*>`).MatchString(value)
|
||||
}
|
||||
|
||||
func validSCUMFingerprint(value string) bool { return scumHashPattern.MatchString(value) }
|
||||
func validSCUMDigest(value string) bool {
|
||||
return regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$`).MatchString(value)
|
||||
}
|
||||
|
||||
func containsSCUMProtectedMaterial(value string) bool {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
lowered := strings.ToLower(trimmed)
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lowered, "select ") || strings.Contains(lowered, "insert into") || strings.Contains(lowered, "update ") || strings.Contains(lowered, "delete from") || strings.Contains(lowered, "pragma ") || strings.Contains(lowered, "attach database") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lowered, "dsn") || strings.Contains(lowered, "password") || strings.Contains(lowered, "credential") || strings.Contains(lowered, "token") || strings.Contains(lowered, "socket") || strings.Contains(lowered, "rcon") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(lowered, "sqlite://") || strings.HasPrefix(lowered, "mysql://") || strings.HasPrefix(lowered, "file://") || strings.HasPrefix(lowered, "tcp://") || strings.HasPrefix(lowered, "unix://") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\\\") || regexp.MustCompile(`[A-Za-z]:[\\/]`).MatchString(trimmed) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsSCUMNetworkMaterial(value string) bool {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
lowered := strings.ToLower(trimmed)
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lowered, "ip=") || strings.Contains(lowered, "addr=") || strings.Contains(lowered, "endpoint=") || strings.Contains(lowered, "port=") {
|
||||
return true
|
||||
}
|
||||
if regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`).MatchString(trimmed) {
|
||||
return true
|
||||
}
|
||||
for _, token := range regexp.MustCompile(`[\s,;()\[\]{}'"]+`).Split(trimmed, -1) {
|
||||
if strings.Contains(token, ":") && net.ParseIP(strings.Trim(token, "<>")) != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
const scumProbeHash = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
func TestValidateSCUMSchemaProbeRequestAllowsBoundedGenericProbe(t *testing.T) {
|
||||
request := domain.SCUMSchemaProbeRequest{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Bounds: domain.DefaultSCUMSchemaProbeBounds(), RequestedAt: time.Now()}
|
||||
|
||||
if err := ValidateSCUMSchemaProbeRequest(request); err != nil {
|
||||
t.Fatalf("expected valid probe request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeRequestRejectsHostPathsAndLooseBounds(t *testing.T) {
|
||||
request := domain.SCUMSchemaProbeRequest{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Bounds: domain.DefaultSCUMSchemaProbeBounds()}
|
||||
request.Binding.DatabaseIdentity = `C:\SCUM\Saved\SaveFiles\SCUM.db`
|
||||
request.Bounds.MaxSampleRows = 25
|
||||
|
||||
err := ValidateSCUMSchemaProbeRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "protected connection material") || !strings.Contains(err.Error(), "maxSampleRows") {
|
||||
t.Fatalf("expected protected material and bound violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeResultRejectsRawSQLAndRows(t *testing.T) {
|
||||
result := domain.SCUMSchemaProbeResult{
|
||||
RequestID: "probe-1",
|
||||
JobID: "job-1",
|
||||
Binding: validatorSCUMBinding(),
|
||||
Status: domain.SCUMCapabilityEvidenceCompatible,
|
||||
SchemaFingerprint: scumProbeHash,
|
||||
ObservedAt: time.Now(),
|
||||
ResultDigest: scumProbeHash,
|
||||
Limits: domain.DefaultSCUMSchemaProbeBounds(),
|
||||
Objects: []domain.SCUMSchemaObjectEvidence{{
|
||||
ObjectHash: scumProbeHash,
|
||||
Kind: "table",
|
||||
NameFingerprint: "select * from players",
|
||||
DeclaredColumns: []domain.SCUMSchemaColumnEvidence{{NameFingerprint: scumProbeHash, DeclaredType: "TEXT", Ordinal: 1}},
|
||||
SampleFingerprints: []string{"{\"raw\":\"row\"}"},
|
||||
}},
|
||||
}
|
||||
|
||||
err := ValidateSCUMSchemaProbeResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "nameFingerprint must be redacted") || !strings.Contains(err.Error(), "sampleFingerprints") {
|
||||
t.Fatalf("expected redaction violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeResultAcceptsRunTerminalStatuses(t *testing.T) {
|
||||
succeeded := domain.SCUMSchemaProbeResult{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Status: domain.SCUMSchemaProbeStatusSucceeded, SourceFingerprint: scumProbeHash, 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)
|
||||
}
|
||||
|
||||
succeeded.SourceFingerprint = "C:/db/SCUM.db"
|
||||
if err := ValidateSCUMSchemaProbeResult(succeeded); err == nil || !strings.Contains(err.Error(), "sourceFingerprint must be a digest/fingerprint") {
|
||||
t.Fatalf("expected raw source fingerprint rejection, got %v", err)
|
||||
}
|
||||
succeeded.SourceFingerprint = scumProbeHash
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
failed.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode("data_target_plan_invalid"), Retryable: false}
|
||||
if err := ValidateSCUMSchemaProbeResult(failed); err != nil {
|
||||
t.Fatalf("expected generic Run data-target failure to validate, got %v", err)
|
||||
}
|
||||
|
||||
failed.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode("data_target_invalid path"), Retryable: false}
|
||||
if err := ValidateSCUMSchemaProbeResult(failed); err == nil || !strings.Contains(err.Error(), "safeError.code is invalid") {
|
||||
t.Fatalf("expected unsafe data-target code rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSQLiteTemplateRequestAllowsBoundedGenericTemplate(t *testing.T) {
|
||||
request := validatorSCUMSQLiteTemplateRequest()
|
||||
|
||||
if err := ValidateSCUMSQLiteTemplateRequest(request); err != nil {
|
||||
t.Fatalf("expected valid SQLite template request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSQLiteTemplateRequestRejectsSQLPathsAndLooseBounds(t *testing.T) {
|
||||
request := validatorSCUMSQLiteTemplateRequest()
|
||||
request.TemplateKey = "select * from players"
|
||||
request.Parameters = map[string]any{"profilePath": `C:\SCUM\Saved\SCUM.db`}
|
||||
request.Bounds.MaxRows = 50000
|
||||
|
||||
err := ValidateSCUMSQLiteTemplateRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "templateKey is unsafe") || !strings.Contains(err.Error(), "protected material") || !strings.Contains(err.Error(), "maxRows") {
|
||||
t.Fatalf("expected template/key/bounds violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSQLiteTemplateResultAcceptsTypedRows(t *testing.T) {
|
||||
result := validatorSCUMSQLiteTemplateResult()
|
||||
|
||||
if err := ValidateSCUMSQLiteTemplateResult(result); err != nil {
|
||||
t.Fatalf("expected valid SQLite template result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSQLiteTemplateResultRejectsUnsafeRowsAndMismatchedCounts(t *testing.T) {
|
||||
result := validatorSCUMSQLiteTemplateResult()
|
||||
result.RowCount = 2
|
||||
result.Rows[0]["displayName"] = "select * from user_profile"
|
||||
|
||||
err := ValidateSCUMSQLiteTemplateResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "rowCount") || !strings.Contains(err.Error(), "protected material") {
|
||||
t.Fatalf("expected row-count and protected-row violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMTypedRCONTemplateRequestAllowsBoundedGenericTemplate(t *testing.T) {
|
||||
request := validatorSCUMTypedRCONTemplateRequest()
|
||||
|
||||
if err := ValidateSCUMTypedRCONTemplateRequest(request); err != nil {
|
||||
t.Fatalf("expected valid typed RCON template request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMTypedRCONTemplateRequestRejectsRawCommandAndLooseBounds(t *testing.T) {
|
||||
request := validatorSCUMTypedRCONTemplateRequest()
|
||||
request.Payload = map[string]any{"rawCommand": "#SetFamePoints 7 100"}
|
||||
request.ReviewReason = `use C:\SCUM\secret.txt`
|
||||
request.Bounds.MaxPayloadBytes = 100000
|
||||
|
||||
err := ValidateSCUMTypedRCONTemplateRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "payload key is unsafe") || !strings.Contains(err.Error(), "reviewReason is unsafe") || !strings.Contains(err.Error(), "maxPayloadBytes") {
|
||||
t.Fatalf("expected raw command/reason/bounds violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMTypedRCONTemplateResultAcceptsConfirmedEnvelope(t *testing.T) {
|
||||
result := validatorSCUMTypedRCONTemplateResult()
|
||||
|
||||
if err := ValidateSCUMTypedRCONTemplateResult(result); err != nil {
|
||||
t.Fatalf("expected valid typed RCON template result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMTypedRCONTemplateResultRejectsUnconfirmedSuccessAndUnsafeSummary(t *testing.T) {
|
||||
result := validatorSCUMTypedRCONTemplateResult()
|
||||
result.ConfirmationStatus = domain.SCUMRCONConfirmationUnknown
|
||||
result.SafeSummary = "rcon password leaked"
|
||||
|
||||
err := ValidateSCUMTypedRCONTemplateResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "confirmed") || !strings.Contains(err.Error(), "safeSummary") {
|
||||
t.Fatalf("expected confirmation and summary violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationRequestAllowsBoundedGenericTemplate(t *testing.T) {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
|
||||
if err := ValidateSCUMGuardedMutationRequest(request); err != nil {
|
||||
t.Fatalf("expected valid guarded mutation request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationRequestRejectsRawXMLSQL855AndMissingGuards(t *testing.T) {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
request.Payload = map[string]any{"rawXml": "<CharacterTemplate><Attribute name=\"Strength\" value=\"9\" /></CharacterTemplate>", "fieldKey855": "855"}
|
||||
request.BackupEvidenceDigest = ""
|
||||
request.OfflineEvidenceDigest = ""
|
||||
request.DangerConfirmationDigest = ""
|
||||
request.ReadbackExpectationDigest = ""
|
||||
request.ReviewReason = "update sqlite:///private/tmp/SCUM.db directly"
|
||||
request.Bounds.MaxAffectedRows = 2
|
||||
|
||||
err := ValidateSCUMGuardedMutationRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "payload key is unsafe") || !strings.Contains(err.Error(), "payload value is unsafe") || !strings.Contains(err.Error(), "backupEvidenceDigest") || !strings.Contains(err.Error(), "offlineEvidenceDigest") || !strings.Contains(err.Error(), "dangerConfirmationDigest") || !strings.Contains(err.Error(), "readbackExpectationDigest") || !strings.Contains(err.Error(), "maxAffectedRows") || !strings.Contains(err.Error(), "reviewReason") {
|
||||
t.Fatalf("expected guarded mutation safety violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationResultAcceptsConfirmedSingleRowEnvelope(t *testing.T) {
|
||||
result := validatorSCUMGuardedMutationResult()
|
||||
|
||||
if err := ValidateSCUMGuardedMutationResult(result); err != nil {
|
||||
t.Fatalf("expected valid guarded mutation result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationResultRejectsMultiRowMissingReadbackAndUnsafeSummary(t *testing.T) {
|
||||
result := validatorSCUMGuardedMutationResult()
|
||||
result.AffectedRows = 2
|
||||
result.ReadbackStatus = domain.SCUMMutationReadbackUnknown
|
||||
result.ReadbackDigest = ""
|
||||
result.SafeSummary = "raw <CharacterTemplate /> leaked"
|
||||
|
||||
err := ValidateSCUMGuardedMutationResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "exactly one affected row") || !strings.Contains(err.Error(), "readback") || !strings.Contains(err.Error(), "safeSummary") {
|
||||
t.Fatalf("expected affected-row/readback/summary violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMParsedLogBatchResultAcceptsRotationSafeEnvelope(t *testing.T) {
|
||||
result := validatorSCUMParsedLogBatchResult()
|
||||
|
||||
if err := ValidateSCUMParsedLogBatchResult(result); err != nil {
|
||||
t.Fatalf("expected valid parsed log batch result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMParsedLogBatchResultRejectsRawLineNetworkMaterialAndLooseBounds(t *testing.T) {
|
||||
result := validatorSCUMParsedLogBatchResult()
|
||||
result.ParserDigest = ""
|
||||
result.FirstCursor.SourceIdentityDigest = `C:\SCUM\Saved\Logs\login.log`
|
||||
result.Events[0].Payload = map[string]any{"rawLine": "2026.08.13: '203.0.113.10 player' logged in"}
|
||||
result.Limits.MaxLineBytes = 1000000
|
||||
|
||||
err := ValidateSCUMParsedLogBatchResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "parserDigest") || !strings.Contains(err.Error(), "sourceIdentityDigest") || !strings.Contains(err.Error(), "payload key is unsafe") || !strings.Contains(err.Error(), "payload value is unsafe") || !strings.Contains(err.Error(), "maxLineBytes") {
|
||||
t.Fatalf("expected parsed-log safety violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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}}
|
||||
if err := ValidateSCUMCapabilityEvidence(evidence); err != nil {
|
||||
t.Fatalf("expected valid evidence, got %v", err)
|
||||
}
|
||||
|
||||
evidence.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorProbeFailed, Message: "sqlite:///private/tmp/SCUM.db locked"}
|
||||
if err := ValidateSCUMCapabilityEvidence(evidence); err == nil || !strings.Contains(err.Error(), "protected material") {
|
||||
t.Fatalf("expected safe error redaction violation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validatorSCUMBinding() domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "db-fingerprint-1"}
|
||||
}
|
||||
|
||||
func validatorSCUMSQLiteTemplateRequest() domain.SCUMSQLiteTemplateRequest {
|
||||
return domain.SCUMSQLiteTemplateRequest{RequestID: "request-1", JobID: "job-1", Binding: validatorSCUMBinding(), Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players.active.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumProbeHash, AssetDigest: scumProbeHash, ParameterDigest: scumProbeHash, Parameters: map[string]any{"cursor": "", "limit": 100.0}, Bounds: domain.DefaultSCUMSQLiteTemplateBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func validatorSCUMSQLiteTemplateResult() domain.SCUMSQLiteTemplateResult {
|
||||
request := validatorSCUMSQLiteTemplateRequest()
|
||||
return domain.SCUMSQLiteTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, ParameterDigest: request.ParameterDigest, SourceFingerprint: scumProbeHash, ObservedAt: time.Now(), ResultDigest: scumProbeHash, RowCount: 1, Rows: []map[string]any{{"externalPlayerId": "player-redacted", "displayName": "Known Player", "fame": 12.5, "online": true, "squadId": nil}}, Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func validatorSCUMTypedRCONTemplateRequest() domain.SCUMTypedRCONTemplateRequest {
|
||||
return domain.SCUMTypedRCONTemplateRequest{RequestID: "request-rcon-1", JobID: "job-rcon-1", Binding: validatorSCUMBinding(), Capability: domain.SCUMDataCapabilityEconomyCommand, TransportKey: "scum-rcon", TargetKey: "scum-rcon", TemplateKey: "economy.fame.set.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumProbeHash, AssetDigest: scumProbeHash, PayloadDigest: scumProbeHash, ConfirmationDigest: scumProbeHash, TargetIdentityDigest: scumProbeHash, IdempotencyKey: "idem-rcon-1", Payload: map[string]any{"externalPlayerId": "player-redacted", "absoluteValue": 100.0}, ReviewReason: "operator reviewed absolute fame update", Bounds: domain.DefaultSCUMTypedRCONTemplateBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func validatorSCUMTypedRCONTemplateResult() domain.SCUMTypedRCONTemplateResult {
|
||||
request := validatorSCUMTypedRCONTemplateRequest()
|
||||
return domain.SCUMTypedRCONTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TransportKey: request.TransportKey, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, PayloadDigest: request.PayloadDigest, ConfirmationDigest: request.ConfirmationDigest, TargetIdentityDigest: request.TargetIdentityDigest, ObservedAt: time.Now(), ResultDigest: scumProbeHash, ResponseDigest: scumProbeHash, ConfirmationStatus: domain.SCUMRCONConfirmationConfirmed, ConfirmationDigestID: scumProbeHash, SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func validatorSCUMGuardedMutationRequest() domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: "request-mutation-1", JobID: "job-mutation-1", Binding: validatorSCUMBinding(), Capability: domain.SCUMDataCapabilityProfileXMLWrite, TargetKey: "scum-mutation-db", TemplateKey: "profile.attributes.patch.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumProbeHash, AssetDigest: scumProbeHash, TargetIdentityDigest: scumProbeHash, ExpectedRowDigest: scumProbeHash, ExpectedValueDigest: scumProbeHash, ExpectedXMLDigest: scumProbeHash, PatchDigest: scumProbeHash, BackupEvidenceDigest: scumProbeHash, OfflineEvidenceDigest: scumProbeHash, DangerConfirmationDigest: scumProbeHash, ReadbackExpectationDigest: scumProbeHash, IdempotencyKey: "idem-mutation-1", Payload: map[string]any{"attributeKey": "Strength", "absoluteValue": 8.5}, ReviewReason: "operator confirmed offline profile attribute patch", Bounds: domain.DefaultSCUMGuardedMutationBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func validatorSCUMGuardedMutationResult() domain.SCUMGuardedMutationResult {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
return domain.SCUMGuardedMutationResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, SourceFingerprint: scumProbeHash, TargetIdentityDigest: request.TargetIdentityDigest, ExpectedRowDigest: request.ExpectedRowDigest, ExpectedValueDigest: request.ExpectedValueDigest, ExpectedXMLDigest: request.ExpectedXMLDigest, PatchDigest: request.PatchDigest, BackupEvidenceDigest: request.BackupEvidenceDigest, OfflineEvidenceDigest: request.OfflineEvidenceDigest, DangerConfirmationDigest: request.DangerConfirmationDigest, ReadbackExpectationDigest: request.ReadbackExpectationDigest, ObservedAt: time.Now(), ResultDigest: scumProbeHash, BeforeDigest: scumProbeHash, AfterDigest: scumProbeHash, ReadbackDigest: scumProbeHash, AffectedRows: 1, ReadbackStatus: domain.SCUMMutationReadbackConfirmed, SafeSummary: "confirmed by declared readback", SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}, Limits: request.Bounds}
|
||||
}
|
||||
|
||||
func validatorSCUMParsedLogBatchResult() domain.SCUMParsedLogBatchResult {
|
||||
cursor := domain.SCUMParsedLogCursor{SourceIdentityDigest: scumProbeHash, StreamGeneration: scumProbeHash, Sequence: 7}
|
||||
return domain.SCUMParsedLogBatchResult{
|
||||
RequestID: "request-log-1",
|
||||
JobID: "job-log-1",
|
||||
Binding: validatorSCUMBinding(),
|
||||
Status: domain.SCUMTerminalResultSucceeded,
|
||||
SourceKey: "scum-login-events",
|
||||
StreamKey: "scum.login",
|
||||
ParserKey: "scum-login-log-login-parser",
|
||||
ParserVersion: "scum-login-log-v1",
|
||||
AdapterVersion: "adapter-1",
|
||||
AssetDigest: scumProbeHash,
|
||||
ParserDigest: scumProbeHash,
|
||||
ObservedAt: time.Now(),
|
||||
ResultDigest: scumProbeHash,
|
||||
FirstCursor: cursor,
|
||||
LastCursor: cursor,
|
||||
TailState: domain.SCUMLogTailRotated,
|
||||
Replay: true,
|
||||
EventCount: 1,
|
||||
SafeSummary: "one sanitized login event parsed from declared source",
|
||||
SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone},
|
||||
Limits: domain.DefaultSCUMParsedLogBatchBounds(),
|
||||
Events: []domain.SCUMParsedLogEvent{{
|
||||
EventType: "scum.login",
|
||||
OccurredAt: time.Now(),
|
||||
Cursor: cursor,
|
||||
LogicalEventDigest: scumProbeHash,
|
||||
EventDigest: scumProbeHash,
|
||||
PayloadDigest: scumProbeHash,
|
||||
Payload: map[string]any{"externalPlayerId": "player-redacted", "displayName": "Known Player", "profileLocalId": "profile-redacted"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user