Freeze SCUM SQLite template run contract

This commit is contained in:
npc0-hue
2026-08-13 13:05:39 +08:00
parent 36ed8822c3
commit 467d5c3a8c
17 changed files with 683 additions and 12 deletions
+1
View File
@@ -436,6 +436,7 @@ func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
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)
return assignment
}
+4
View File
@@ -1157,6 +1157,7 @@ type JobExecutionInput struct {
Deployment *ServerDeploymentDefinition
ServerDeploymentPlan *ServerDeploymentPlan
SQLiteSchemaProbe *SCUMSchemaProbeRequest
SQLiteTemplate *SCUMSQLiteTemplateRequest
}
type ServerDeploymentPlan struct {
@@ -1200,6 +1201,7 @@ type JobExecutionResult struct {
AuditSummary string
Content string
SQLiteSchemaProbe *SCUMSchemaProbeResult
SQLiteTemplate *SCUMSQLiteTemplateResult
ServerDeploymentEvidence *ServerDeploymentEvidence
DeploymentReceipt *ServerDeploymentExecutionReceipt
}
@@ -2045,7 +2047,9 @@ func CopyJob(job Job) Job {
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.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
job.ExecutionResult.SQLiteTemplate = CopySCUMSQLiteTemplateResultPtr(job.ExecutionResult.SQLiteTemplate)
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
if job.ExecutionInput.Deployment != nil {
+103
View File
@@ -59,6 +59,19 @@ const (
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"
)
type SCUMTerminalResultStatus string
const (
SCUMTerminalResultSucceeded SCUMTerminalResultStatus = "succeeded"
SCUMTerminalResultFailed SCUMTerminalResultStatus = "failed"
SCUMTerminalResultCancelled SCUMTerminalResultStatus = "cancelled"
)
type SCUMSafeError struct {
@@ -93,6 +106,18 @@ 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 SCUMSchemaProbeRequest struct {
RequestID string
JobID string
@@ -101,6 +126,22 @@ type SCUMSchemaProbeRequest struct {
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 SCUMSchemaProbeDeclaration struct {
Capability string
TargetKey string
@@ -168,6 +209,28 @@ type SCUMSchemaProbeResult struct {
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 SCUMCapabilityRequirement struct {
Capability SCUMDataCapability
AdapterVersion string
@@ -260,6 +323,46 @@ func CopySCUMCapabilityEvidence(value SCUMCapabilityEvidence) SCUMCapabilityEvid
return value
}
func CopySCUMSQLiteTemplateRequestPtr(value *SCUMSQLiteTemplateRequest) *SCUMSQLiteTemplateRequest {
if value == nil {
return nil
}
copy := *value
copy.Parameters = CopySCUMValueMap(value.Parameters)
return &copy
}
func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResult {
if value == nil {
return nil
}
copy := *value
copy.Rows = CopySCUMRows(value.Rows)
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 {
+18 -2
View File
@@ -112,6 +112,7 @@ type RunJobExecutionInputBody struct {
Deployment *ServerDeploymentExecutionBody `json:"deployment,omitempty"`
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
SQLiteSchemaProbe *RunSQLiteSchemaProbeRequestBody `json:"sqliteSchemaProbe,omitempty"`
SQLiteTemplate *RunSQLiteTemplateRequestBody `json:"sqliteTemplate,omitempty"`
}
type RunSQLiteSchemaProbeRequestBody struct {
@@ -120,6 +121,20 @@ type RunSQLiteSchemaProbeRequestBody struct {
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 ServerDeploymentPlanBody struct {
SchemaVersion string `json:"schemaVersion"`
Operation string `json:"operation"`
@@ -197,6 +212,7 @@ type RunJobExecutionResultBody struct {
AuditSummary string `json:"auditSummary,omitempty"`
Content string `json:"content,omitempty"`
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
SQLiteTemplate *SCUMSQLiteTemplateResultDTO `json:"sqliteTemplate,omitempty"`
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"`
}
@@ -456,7 +472,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), 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, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrToDomain(request.ExecutionResult.SQLiteSchemaProbe), SQLiteTemplate: SCUMSQLiteTemplateResultPtrToDomain(request.ExecutionResult.SQLiteTemplate), ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
}
}
@@ -671,7 +687,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)},
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)},
LeaseToken: assignment.LeaseToken,
Attempt: assignment.Attempt,
FencingToken: assignment.FencingToken,
+43
View File
@@ -66,6 +66,49 @@ func TestRunJobResultRequestParsesSQLiteSchemaProbeEnvelope(t *testing.T) {
}
}
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 TestAIProviderFromDomainCopiesModels(t *testing.T) {
provider := domain.AIProvider{
ID: "ai.openai",
+102
View File
@@ -28,6 +28,14 @@ type SCUMSchemaProbeBoundsDTO struct {
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 SCUMSchemaProbeRequestDTO struct {
RequestID string `json:"requestId"`
JobID string `json:"jobId"`
@@ -36,6 +44,22 @@ type SCUMSchemaProbeRequestDTO struct {
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 SCUMSchemaProbeDispatchRequest struct {
IdempotencyKey string `json:"idempotencyKey"`
}
@@ -96,6 +120,28 @@ type SCUMSchemaProbeResultDTO struct {
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 SCUMCapabilityGateDTO struct {
Capability string `json:"capability"`
State string `json:"state"`
@@ -120,6 +166,22 @@ func SCUMSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *
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 runSQLiteSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *RunSQLiteSchemaProbeRequestBody {
if value == nil {
return nil
@@ -127,6 +189,13 @@ func runSQLiteSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeReque
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 SCUMSchemaProbeDispatchFromDomain(request domain.SCUMSchemaProbeRequest, queued domain.RemoteAdapterResult) SCUMSchemaProbeDispatchResponse {
return SCUMSchemaProbeDispatchResponse{ProbeRequest: SCUMSchemaProbeRequestFromDomain(request), QueuedJob: RemoteAdapterFromDomain(queued)}
}
@@ -159,6 +228,18 @@ func SCUMSchemaProbeResultPtrToDomain(value *SCUMSchemaProbeResultDTO) *domain.S
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 SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSchemaProbeResultDTO {
value = domain.CopySCUMSchemaProbeResult(value)
objects := make([]SCUMSchemaObjectEvidenceDTO, len(value.Objects))
@@ -188,6 +269,19 @@ func SCUMSchemaProbeResultPtrFromDomain(value *domain.SCUMSchemaProbeResult) *SC
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 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}
}
@@ -207,3 +301,11 @@ func scumProbeBoundsToDomain(value SCUMSchemaProbeBoundsDTO) domain.SCUMSchemaPr
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}
}
@@ -41,6 +41,26 @@ The payload must not include a host database path, DSN, socket, credential, raw
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.
## 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.
+5 -1
View File
@@ -26,7 +26,11 @@ Run executes the generic `remote.run.db.sqlite.probe` capability against the pac
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 verifies the packaged asset digest, enforces one approved read-only statement or introspection boundary, binds parameters, applies short busy/operation timeouts, row/result-byte limits, and returns a terminal envelope with safe rows matching the declared result schema. DDL, mutation, `ATTACH`, extension loading, write PRAGMAs, multi-statement input, and string-concatenated parameters are rejected.
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.
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.
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.
## Parsed Log Event Jobs
+23 -3
View File
@@ -346,6 +346,18 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
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.JobCapabilityClientManagerDeploy:
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" {
return validationError("client-manager deploy result type is invalid")
@@ -637,7 +649,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 {
if isProtectedRequestCapability(job.Capability) || job.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe || job.Capability == domain.JobCapabilityRemoteRunDBSQLiteQuery {
fencingToken = uint64(job.Attempt)
}
return domain.RunJobAssignment{
@@ -651,7 +663,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)},
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)},
LeaseToken: leaseToken,
Attempt: job.Attempt,
FencingToken: fencingToken,
@@ -729,7 +741,15 @@ func jobRetryBackoff(policy domain.JobRetryPolicy, attempt int) time.Duration {
}
func terminalFingerprint(result domain.RunJobResult) string {
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)
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)
}
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t|%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)
}
func terminalMessage(result domain.RunJobResult) string {
+57 -1
View File
@@ -3,6 +3,7 @@ package service
import (
"strings"
"testing"
"time"
"browser.local/platform/domain"
)
@@ -212,6 +213,41 @@ 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 TestCoreServiceRunJobReconcile(t *testing.T) {
svc, sessionToken := newRegisteredRunJobService(t)
createQueuedRunJob(t, svc, "job-1", "idem-1")
@@ -242,7 +278,7 @@ func newRegisteredRunJobService(t *testing.T) (*CoreService, string) {
t.Helper()
svc := newTestCoreService()
helloRequest := validRunControlHello()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start")
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start", domain.JobCapabilityRemoteRunDBSQLiteQuery)
helloRequest.CapabilityReport.Fingerprint = "cap-jobs"
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil {
@@ -251,6 +287,26 @@ 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 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}, DeclaredPermissions: []string{"server.remote.access"}, 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 createQueuedRunJob(t *testing.T, svc *CoreService, id string, idempotencyKey string) domain.Job {
t.Helper()
job, err := svc.CreateJob(domain.Job{
+24 -1
View File
@@ -9,7 +9,10 @@ import (
"browser.local/platform/validator"
)
const scumSchemaProbeExecutionKind = "sqlite.schema-probe"
const (
scumSchemaProbeExecutionKind = "sqlite.schema-probe"
scumSQLiteTemplateExecutionKind = "sqlite.template-query"
)
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
idempotencyKey = strings.TrimSpace(idempotencyKey)
@@ -149,6 +152,26 @@ func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchem
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 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
}
+5
View File
@@ -56,6 +56,11 @@ func ValidateRunJobResult(result domain.RunJobResult) error {
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())
}
}
return finish(violations)
}
+13
View File
@@ -1533,6 +1533,14 @@ 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")
}
}
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>")
@@ -1548,6 +1556,11 @@ func ValidateJob(job domain.Job) error {
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.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite {
if job.ServerInstanceID == "" {
violations = append(violations, "serverInstanceId is required for scoped file jobs")
+176 -1
View File
@@ -16,10 +16,15 @@ const (
maxSCUMProbeSamples = 3
maxSCUMProbeTimeoutMS = 10000
maxSCUMProbeResultBytes = 1024 * 1024
maxSCUMTemplateParameters = 64
maxSCUMTemplateRows = 1000
maxSCUMTemplateBusyTimeoutMS = 1000
maxSCUMTemplateValueBytes = 4096
)
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
@@ -30,6 +35,37 @@ func ValidateSCUMSchemaProbeRequest(request domain.SCUMSchemaProbeRequest) error
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 ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
var violations []string
violations = appendRequired(violations, "requestId", result.RequestID)
@@ -59,6 +95,65 @@ func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
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 ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error {
var violations []string
if !validSCUMDataCapability(evidence.Capability) {
@@ -213,6 +308,68 @@ func validateSCUMSchemaProbeBounds(prefix string, value domain.SCUMSchemaProbeBo
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 validateSCUMTemplateKey(prefix, value string) []string {
var violations []string
violations = appendRequired(violations, prefix, value)
if value != "" && (!scumTemplateKeyPattern.MatchString(value) || containsSCUMProtectedMaterial(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 {
if !scumTemplateKeyPattern.MatchString(key) || containsSCUMProtectedMaterial(key) || strings.Contains(key, "..") {
violations = append(violations, prefix+" key is unsafe")
}
field := prefix + ".value"
violations = append(violations, validateSCUMValue(field, value)...)
}
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 validateSCUMSchemaObjectEvidence(prefix string, value domain.SCUMSchemaObjectEvidence) []string {
var violations []string
if !validSCUMFingerprint(value.ObjectHash) {
@@ -283,6 +440,24 @@ func validSCUMDataCapability(value domain.SCUMDataCapability) bool {
}
}
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 validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
switch value {
case domain.SCUMTerminalResultSucceeded, domain.SCUMTerminalResultFailed, domain.SCUMTerminalResultCancelled:
return true
default:
return false
}
}
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
switch value {
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
@@ -303,7 +478,7 @@ func validSCUMSchemaProbeResultStatus(value domain.SCUMCapabilityEvidenceStatus)
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:
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:
return true
default:
return scumDataTargetSafeErrorPattern.MatchString(string(value))
+48
View File
@@ -85,6 +85,45 @@ func TestValidateSCUMSchemaProbeResultAcceptsRunTerminalStatuses(t *testing.T) {
}
}
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 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 {
@@ -100,3 +139,12 @@ func TestValidateSCUMCapabilityEvidenceRequiresSafeCurrentServiceIdentity(t *tes
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}}
}