Freeze SCUM typed RCON run contract
This commit is contained in:
@@ -437,6 +437,7 @@ func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
|
||||
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)
|
||||
return assignment
|
||||
}
|
||||
|
||||
|
||||
@@ -1158,6 +1158,7 @@ type JobExecutionInput struct {
|
||||
ServerDeploymentPlan *ServerDeploymentPlan
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeRequest
|
||||
SQLiteTemplate *SCUMSQLiteTemplateRequest
|
||||
RCONTemplate *SCUMTypedRCONTemplateRequest
|
||||
}
|
||||
|
||||
type ServerDeploymentPlan struct {
|
||||
@@ -1202,6 +1203,7 @@ type JobExecutionResult struct {
|
||||
Content string
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResult
|
||||
SQLiteTemplate *SCUMSQLiteTemplateResult
|
||||
RCONTemplate *SCUMTypedRCONTemplateResult
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidence
|
||||
DeploymentReceipt *ServerDeploymentExecutionReceipt
|
||||
}
|
||||
@@ -2048,8 +2050,10 @@ func CopyJob(job Job) Job {
|
||||
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.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
|
||||
job.ExecutionResult.SQLiteTemplate = CopySCUMSQLiteTemplateResultPtr(job.ExecutionResult.SQLiteTemplate)
|
||||
job.ExecutionResult.RCONTemplate = CopySCUMTypedRCONTemplateResultPtr(job.ExecutionResult.RCONTemplate)
|
||||
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
|
||||
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
|
||||
if job.ExecutionInput.Deployment != nil {
|
||||
|
||||
@@ -74,6 +74,14 @@ const (
|
||||
SCUMTerminalResultCancelled SCUMTerminalResultStatus = "cancelled"
|
||||
)
|
||||
|
||||
type SCUMRCONConfirmationStatus string
|
||||
|
||||
const (
|
||||
SCUMRCONConfirmationConfirmed SCUMRCONConfirmationStatus = "confirmed"
|
||||
SCUMRCONConfirmationFailed SCUMRCONConfirmationStatus = "failed"
|
||||
SCUMRCONConfirmationUnknown SCUMRCONConfirmationStatus = "unknown"
|
||||
)
|
||||
|
||||
type SCUMSafeError struct {
|
||||
Code SCUMSafeErrorCode
|
||||
Message string
|
||||
@@ -118,6 +126,17 @@ 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 SCUMSchemaProbeRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
@@ -142,6 +161,27 @@ type SCUMSQLiteTemplateRequest struct {
|
||||
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 SCUMSchemaProbeDeclaration struct {
|
||||
Capability string
|
||||
TargetKey string
|
||||
@@ -231,6 +271,31 @@ type SCUMSQLiteTemplateResult struct {
|
||||
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 SCUMCapabilityRequirement struct {
|
||||
Capability SCUMDataCapability
|
||||
AdapterVersion string
|
||||
@@ -332,6 +397,15 @@ func CopySCUMSQLiteTemplateRequestPtr(value *SCUMSQLiteTemplateRequest) *SCUMSQL
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMTypedRCONTemplateRequestPtr(value *SCUMTypedRCONTemplateRequest) *SCUMTypedRCONTemplateRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Payload = CopySCUMValueMap(value.Payload)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
@@ -341,6 +415,14 @@ func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLit
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMTypedRCONTemplateResultPtr(value *SCUMTypedRCONTemplateResult) *SCUMTypedRCONTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMValueMap(value map[string]any) map[string]any {
|
||||
if value == nil {
|
||||
return nil
|
||||
|
||||
@@ -113,6 +113,7 @@ type RunJobExecutionInputBody struct {
|
||||
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
|
||||
SQLiteSchemaProbe *RunSQLiteSchemaProbeRequestBody `json:"sqliteSchemaProbe,omitempty"`
|
||||
SQLiteTemplate *RunSQLiteTemplateRequestBody `json:"sqliteTemplate,omitempty"`
|
||||
RCONTemplate *RunTypedRCONTemplateRequestBody `json:"rconTemplate,omitempty"`
|
||||
}
|
||||
|
||||
type RunSQLiteSchemaProbeRequestBody struct {
|
||||
@@ -135,6 +136,25 @@ type RunSQLiteTemplateRequestBody struct {
|
||||
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 ServerDeploymentPlanBody struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Operation string `json:"operation"`
|
||||
@@ -213,6 +233,7 @@ type RunJobExecutionResultBody struct {
|
||||
Content string `json:"content,omitempty"`
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
|
||||
SQLiteTemplate *SCUMSQLiteTemplateResultDTO `json:"sqliteTemplate,omitempty"`
|
||||
RCONTemplate *SCUMTypedRCONTemplateResultDTO `json:"rconTemplate,omitempty"`
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
|
||||
DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"`
|
||||
}
|
||||
@@ -472,7 +493,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), 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), RCONTemplate: SCUMTypedRCONTemplateResultPtrToDomain(request.ExecutionResult.RCONTemplate), ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +708,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)},
|
||||
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)},
|
||||
LeaseToken: assignment.LeaseToken,
|
||||
Attempt: assignment.Attempt,
|
||||
FencingToken: assignment.FencingToken,
|
||||
|
||||
@@ -109,6 +109,53 @@ func TestRunJobResultRequestParsesSQLiteTemplateEnvelope(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 TestAIProviderFromDomainCopiesModels(t *testing.T) {
|
||||
provider := domain.AIProvider{
|
||||
ID: "ai.openai",
|
||||
|
||||
@@ -36,6 +36,13 @@ type SCUMSQLiteTemplateBoundsDTO struct {
|
||||
MaxResultBytes int `json:"maxResultBytes"`
|
||||
}
|
||||
|
||||
type SCUMTypedRCONTemplateBoundsDTO struct {
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
MaxResponseBytes int `json:"maxResponseBytes"`
|
||||
MaxConfirmRecords int `json:"maxConfirmRecords"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
@@ -60,6 +67,27 @@ type SCUMSQLiteTemplateRequestDTO struct {
|
||||
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 SCUMSchemaProbeDispatchRequest struct {
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
@@ -142,6 +170,31 @@ type SCUMSQLiteTemplateResultDTO struct {
|
||||
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 SCUMCapabilityGateDTO struct {
|
||||
Capability string `json:"capability"`
|
||||
State string `json:"state"`
|
||||
@@ -182,6 +235,22 @@ func SCUMSQLiteTemplateRequestPtrFromDomain(value *domain.SCUMSQLiteTemplateRequ
|
||||
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 runSQLiteSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *RunSQLiteSchemaProbeRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
@@ -196,6 +265,13 @@ func runSQLiteTemplateRequestPtrFromDomain(value *domain.SCUMSQLiteTemplateReque
|
||||
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 SCUMSchemaProbeDispatchFromDomain(request domain.SCUMSchemaProbeRequest, queued domain.RemoteAdapterResult) SCUMSchemaProbeDispatchResponse {
|
||||
return SCUMSchemaProbeDispatchResponse{ProbeRequest: SCUMSchemaProbeRequestFromDomain(request), QueuedJob: RemoteAdapterFromDomain(queued)}
|
||||
}
|
||||
@@ -240,6 +316,18 @@ func SCUMSQLiteTemplateResultPtrToDomain(value *SCUMSQLiteTemplateResultDTO) *do
|
||||
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 SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSchemaProbeResultDTO {
|
||||
value = domain.CopySCUMSchemaProbeResult(value)
|
||||
objects := make([]SCUMSchemaObjectEvidenceDTO, len(value.Objects))
|
||||
@@ -282,6 +370,18 @@ func SCUMSQLiteTemplateResultPtrFromDomain(value *domain.SCUMSQLiteTemplateResul
|
||||
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 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}
|
||||
}
|
||||
@@ -309,3 +409,11 @@ func scumSQLiteTemplateBoundsToDomain(value SCUMSQLiteTemplateBoundsDTO) domain.
|
||||
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}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,26 @@ The request never contains raw SQL, host/database paths, DSNs, sockets, credenti
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -42,7 +42,11 @@ Run and Platform discard raw IP addresses and other network identifiers before d
|
||||
|
||||
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.
|
||||
|
||||
Run never accepts browser command text or branches on SCUM command names. 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.
|
||||
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
|
||||
|
||||
|
||||
@@ -358,6 +358,21 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
|
||||
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.JobCapabilityClientManagerDeploy:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" {
|
||||
return validationError("client-manager deploy result type is invalid")
|
||||
@@ -663,7 +678,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)},
|
||||
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)},
|
||||
LeaseToken: leaseToken,
|
||||
Attempt: job.Attempt,
|
||||
FencingToken: fencingToken,
|
||||
@@ -749,7 +764,11 @@ func terminalFingerprint(result domain.RunJobResult) string {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t|%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)
|
||||
}
|
||||
|
||||
func terminalMessage(result domain.RunJobResult) string {
|
||||
|
||||
@@ -248,6 +248,42 @@ func TestCoreServiceRunJobSQLiteTemplateEnvelopeIsFencedToLease(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 TestCoreServiceRunJobReconcile(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
createQueuedRunJob(t, svc, "job-1", "idem-1")
|
||||
@@ -278,7 +314,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)
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start", domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON)
|
||||
helloRequest.CapabilityReport.Fingerprint = "cap-jobs"
|
||||
hello, err := svc.RegisterRunHello(helloRequest)
|
||||
if err != nil {
|
||||
@@ -293,9 +329,17 @@ 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 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}, DeclaredPermissions: []string{"server.remote.access"}, Permissions: domain.PluginPermissions{RemoteAccess: true}}); err != nil {
|
||||
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}, DeclaredPermissions: []string{"server.remote.access", "server.game-client.command"}, 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 {
|
||||
@@ -307,6 +351,10 @@ func scumTemplateTestResult(request domain.SCUMSQLiteTemplateRequest) domain.SCU
|
||||
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 createQueuedRunJob(t *testing.T, svc *CoreService, id string, idempotencyKey string) domain.Job {
|
||||
t.Helper()
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
const (
|
||||
scumSchemaProbeExecutionKind = "sqlite.schema-probe"
|
||||
scumSQLiteTemplateExecutionKind = "sqlite.template-query"
|
||||
scumRCONTemplateExecutionKind = "rcon.template-command"
|
||||
)
|
||||
|
||||
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
|
||||
@@ -172,6 +173,29 @@ func validateSCUMSQLiteTemplateResultForJob(job domain.Job, result domain.SCUMSQ
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ func ValidateRunJobResult(result domain.RunJobResult) error {
|
||||
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())
|
||||
}
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
|
||||
@@ -1541,6 +1541,17 @@ func ValidateJob(job domain.Job) error {
|
||||
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")
|
||||
}
|
||||
}
|
||||
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>")
|
||||
@@ -1561,6 +1572,11 @@ func ValidateJob(job domain.Job) error {
|
||||
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.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite {
|
||||
if job.ServerInstanceID == "" {
|
||||
violations = append(violations, "serverInstanceId is required for scoped file jobs")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -20,6 +21,9 @@ const (
|
||||
maxSCUMTemplateRows = 1000
|
||||
maxSCUMTemplateBusyTimeoutMS = 1000
|
||||
maxSCUMTemplateValueBytes = 4096
|
||||
maxSCUMRCONPayloadBytes = 4096
|
||||
maxSCUMRCONResponseBytes = 64 * 1024
|
||||
maxSCUMRCONConfirmRecords = 128
|
||||
)
|
||||
|
||||
var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`)
|
||||
@@ -66,6 +70,42 @@ func ValidateSCUMSQLiteTemplateRequest(request domain.SCUMSQLiteTemplateRequest)
|
||||
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 ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
@@ -154,6 +194,62 @@ func ValidateSCUMSQLiteTemplateResult(result domain.SCUMSQLiteTemplateResult) er
|
||||
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 ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error {
|
||||
var violations []string
|
||||
if !validSCUMDataCapability(evidence.Capability) {
|
||||
@@ -328,10 +424,27 @@ func validateSCUMSQLiteTemplateBounds(prefix string, value domain.SCUMSQLiteTemp
|
||||
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 validateSCUMTemplateKey(prefix, value string) []string {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, prefix, value)
|
||||
if value != "" && (!scumTemplateKeyPattern.MatchString(value) || containsSCUMProtectedMaterial(value) || strings.Contains(value, "..")) {
|
||||
if value != "" && (!scumTemplateKeyPattern.MatchString(value) || strings.Contains(value, "..")) {
|
||||
violations = append(violations, prefix+" is unsafe")
|
||||
}
|
||||
return violations
|
||||
@@ -343,7 +456,8 @@ func validateSCUMValueMap(prefix string, values map[string]any, maxItems int) []
|
||||
violations = append(violations, prefix+" exceeds declared limit")
|
||||
}
|
||||
for key, value := range values {
|
||||
if !scumTemplateKeyPattern.MatchString(key) || containsSCUMProtectedMaterial(key) || strings.Contains(key, "..") {
|
||||
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"
|
||||
@@ -370,6 +484,20 @@ func validateSCUMValue(prefix string, value any) []string {
|
||||
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) {
|
||||
@@ -449,6 +577,15 @@ func validSCUMReadCapability(value domain.SCUMDataCapability) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMRCONWriteCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMTerminalResultSucceeded, domain.SCUMTerminalResultFailed, domain.SCUMTerminalResultCancelled:
|
||||
@@ -458,6 +595,15 @@ func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMRCONConfirmationStatus(value domain.SCUMRCONConfirmationStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMRCONConfirmationConfirmed, domain.SCUMRCONConfirmationFailed, domain.SCUMRCONConfirmationUnknown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
|
||||
@@ -505,7 +651,7 @@ func containsSCUMProtectedMaterial(value string) bool {
|
||||
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) {
|
||||
if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\\\") || regexp.MustCompile(`[A-Za-z]:[\\/]`).MatchString(trimmed) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -124,6 +124,45 @@ func TestValidateSCUMSQLiteTemplateResultRejectsUnsafeRowsAndMismatchedCounts(t
|
||||
}
|
||||
}
|
||||
|
||||
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 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 {
|
||||
@@ -148,3 +187,12 @@ 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}}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user