Freeze SCUM guarded mutation run contract
This commit is contained in:
@@ -438,6 +438,7 @@ func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
|
||||
assignment.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(assignment.ExecutionInput.SQLiteSchemaProbe)
|
||||
assignment.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(assignment.ExecutionInput.SQLiteTemplate)
|
||||
assignment.ExecutionInput.RCONTemplate = CopySCUMTypedRCONTemplateRequestPtr(assignment.ExecutionInput.RCONTemplate)
|
||||
assignment.ExecutionInput.GuardedMutation = CopySCUMGuardedMutationRequestPtr(assignment.ExecutionInput.GuardedMutation)
|
||||
return assignment
|
||||
}
|
||||
|
||||
|
||||
@@ -1159,6 +1159,7 @@ type JobExecutionInput struct {
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeRequest
|
||||
SQLiteTemplate *SCUMSQLiteTemplateRequest
|
||||
RCONTemplate *SCUMTypedRCONTemplateRequest
|
||||
GuardedMutation *SCUMGuardedMutationRequest
|
||||
}
|
||||
|
||||
type ServerDeploymentPlan struct {
|
||||
@@ -1204,6 +1205,7 @@ type JobExecutionResult struct {
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResult
|
||||
SQLiteTemplate *SCUMSQLiteTemplateResult
|
||||
RCONTemplate *SCUMTypedRCONTemplateResult
|
||||
GuardedMutation *SCUMGuardedMutationResult
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidence
|
||||
DeploymentReceipt *ServerDeploymentExecutionReceipt
|
||||
}
|
||||
@@ -2051,9 +2053,11 @@ func CopyJob(job Job) Job {
|
||||
job.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe)
|
||||
job.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate)
|
||||
job.ExecutionInput.RCONTemplate = CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate)
|
||||
job.ExecutionInput.GuardedMutation = CopySCUMGuardedMutationRequestPtr(job.ExecutionInput.GuardedMutation)
|
||||
job.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
|
||||
job.ExecutionResult.SQLiteTemplate = CopySCUMSQLiteTemplateResultPtr(job.ExecutionResult.SQLiteTemplate)
|
||||
job.ExecutionResult.RCONTemplate = CopySCUMTypedRCONTemplateResultPtr(job.ExecutionResult.RCONTemplate)
|
||||
job.ExecutionResult.GuardedMutation = CopySCUMGuardedMutationResultPtr(job.ExecutionResult.GuardedMutation)
|
||||
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
|
||||
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
|
||||
if job.ExecutionInput.Deployment != nil {
|
||||
|
||||
@@ -38,32 +38,40 @@ const (
|
||||
type SCUMSafeErrorCode string
|
||||
|
||||
const (
|
||||
SCUMSafeErrorNone SCUMSafeErrorCode = "none"
|
||||
SCUMSafeErrorProbeExecutorAbsent SCUMSafeErrorCode = "probe_executor_absent"
|
||||
SCUMSafeErrorProbeMissing SCUMSafeErrorCode = "probe_missing"
|
||||
SCUMSafeErrorProbeFailed SCUMSafeErrorCode = "probe_failed"
|
||||
SCUMSafeErrorSchemaIncompatible SCUMSafeErrorCode = "schema_incompatible"
|
||||
SCUMSafeErrorBindingMismatch SCUMSafeErrorCode = "binding_mismatch"
|
||||
SCUMSafeErrorAdapterMismatch SCUMSafeErrorCode = "adapter_mismatch"
|
||||
SCUMSafeErrorFingerprintMismatch SCUMSafeErrorCode = "fingerprint_mismatch"
|
||||
SCUMSafeErrorDigestMismatch SCUMSafeErrorCode = "digest_mismatch"
|
||||
SCUMSafeErrorEvidenceExpired SCUMSafeErrorCode = "evidence_expired"
|
||||
SCUMSafeErrorInvalidProbePayload SCUMSafeErrorCode = "invalid_probe_payload"
|
||||
SCUMSafeErrorInvalidRequest SCUMSafeErrorCode = "invalid_request"
|
||||
SCUMSafeErrorTargetUnavailable SCUMSafeErrorCode = "target_unavailable"
|
||||
SCUMSafeErrorSourceUnavailable SCUMSafeErrorCode = "source_unavailable"
|
||||
SCUMSafeErrorSQLiteOpenFailed SCUMSafeErrorCode = "sqlite_open_failed"
|
||||
SCUMSafeErrorSQLiteReadFailed SCUMSafeErrorCode = "sqlite_read_failed"
|
||||
SCUMSafeErrorDatabaseBusy SCUMSafeErrorCode = "database_busy"
|
||||
SCUMSafeErrorTimeout SCUMSafeErrorCode = "timeout"
|
||||
SCUMSafeErrorCancelled SCUMSafeErrorCode = "cancelled"
|
||||
SCUMSafeErrorSourceChanged SCUMSafeErrorCode = "source_changed"
|
||||
SCUMSafeErrorResultLimitExceeded SCUMSafeErrorCode = "result_limit_exceeded"
|
||||
SCUMSafeErrorTemplateMissing SCUMSafeErrorCode = "template_missing"
|
||||
SCUMSafeErrorTemplateMismatch SCUMSafeErrorCode = "template_digest_mismatch"
|
||||
SCUMSafeErrorParameterInvalid SCUMSafeErrorCode = "parameter_schema_invalid"
|
||||
SCUMSafeErrorRowLimitExceeded SCUMSafeErrorCode = "row_limit_exceeded"
|
||||
SCUMSafeErrorResultSchemaInvalid SCUMSafeErrorCode = "result_schema_invalid"
|
||||
SCUMSafeErrorNone SCUMSafeErrorCode = "none"
|
||||
SCUMSafeErrorProbeExecutorAbsent SCUMSafeErrorCode = "probe_executor_absent"
|
||||
SCUMSafeErrorProbeMissing SCUMSafeErrorCode = "probe_missing"
|
||||
SCUMSafeErrorProbeFailed SCUMSafeErrorCode = "probe_failed"
|
||||
SCUMSafeErrorSchemaIncompatible SCUMSafeErrorCode = "schema_incompatible"
|
||||
SCUMSafeErrorBindingMismatch SCUMSafeErrorCode = "binding_mismatch"
|
||||
SCUMSafeErrorAdapterMismatch SCUMSafeErrorCode = "adapter_mismatch"
|
||||
SCUMSafeErrorFingerprintMismatch SCUMSafeErrorCode = "fingerprint_mismatch"
|
||||
SCUMSafeErrorDigestMismatch SCUMSafeErrorCode = "digest_mismatch"
|
||||
SCUMSafeErrorEvidenceExpired SCUMSafeErrorCode = "evidence_expired"
|
||||
SCUMSafeErrorInvalidProbePayload SCUMSafeErrorCode = "invalid_probe_payload"
|
||||
SCUMSafeErrorInvalidRequest SCUMSafeErrorCode = "invalid_request"
|
||||
SCUMSafeErrorTargetUnavailable SCUMSafeErrorCode = "target_unavailable"
|
||||
SCUMSafeErrorSourceUnavailable SCUMSafeErrorCode = "source_unavailable"
|
||||
SCUMSafeErrorSQLiteOpenFailed SCUMSafeErrorCode = "sqlite_open_failed"
|
||||
SCUMSafeErrorSQLiteReadFailed SCUMSafeErrorCode = "sqlite_read_failed"
|
||||
SCUMSafeErrorDatabaseBusy SCUMSafeErrorCode = "database_busy"
|
||||
SCUMSafeErrorTimeout SCUMSafeErrorCode = "timeout"
|
||||
SCUMSafeErrorCancelled SCUMSafeErrorCode = "cancelled"
|
||||
SCUMSafeErrorSourceChanged SCUMSafeErrorCode = "source_changed"
|
||||
SCUMSafeErrorResultLimitExceeded SCUMSafeErrorCode = "result_limit_exceeded"
|
||||
SCUMSafeErrorTemplateMissing SCUMSafeErrorCode = "template_missing"
|
||||
SCUMSafeErrorTemplateMismatch SCUMSafeErrorCode = "template_digest_mismatch"
|
||||
SCUMSafeErrorParameterInvalid SCUMSafeErrorCode = "parameter_schema_invalid"
|
||||
SCUMSafeErrorRowLimitExceeded SCUMSafeErrorCode = "row_limit_exceeded"
|
||||
SCUMSafeErrorResultSchemaInvalid SCUMSafeErrorCode = "result_schema_invalid"
|
||||
SCUMSafeErrorMutationGuardMismatch SCUMSafeErrorCode = "mutation_guard_mismatch"
|
||||
SCUMSafeErrorMutationBackupUnavailable SCUMSafeErrorCode = "mutation_backup_unavailable"
|
||||
SCUMSafeErrorMutationOfflineRequired SCUMSafeErrorCode = "mutation_offline_required"
|
||||
SCUMSafeErrorMutationConfirmationMissing SCUMSafeErrorCode = "mutation_confirmation_missing"
|
||||
SCUMSafeErrorMutationPatchInvalid SCUMSafeErrorCode = "mutation_patch_invalid"
|
||||
SCUMSafeErrorAffectedRowsMismatch SCUMSafeErrorCode = "affected_rows_mismatch"
|
||||
SCUMSafeErrorReadbackMismatch SCUMSafeErrorCode = "readback_mismatch"
|
||||
SCUMSafeErrorRollbackFailed SCUMSafeErrorCode = "rollback_failed"
|
||||
)
|
||||
|
||||
type SCUMTerminalResultStatus string
|
||||
@@ -82,6 +90,15 @@ const (
|
||||
SCUMRCONConfirmationUnknown SCUMRCONConfirmationStatus = "unknown"
|
||||
)
|
||||
|
||||
type SCUMMutationReadbackStatus string
|
||||
|
||||
const (
|
||||
SCUMMutationReadbackConfirmed SCUMMutationReadbackStatus = "confirmed"
|
||||
SCUMMutationReadbackFailed SCUMMutationReadbackStatus = "failed"
|
||||
SCUMMutationReadbackConflict SCUMMutationReadbackStatus = "conflict"
|
||||
SCUMMutationReadbackUnknown SCUMMutationReadbackStatus = "unknown"
|
||||
)
|
||||
|
||||
type SCUMSafeError struct {
|
||||
Code SCUMSafeErrorCode
|
||||
Message string
|
||||
@@ -137,6 +154,18 @@ func DefaultSCUMTypedRCONTemplateBounds() SCUMTypedRCONTemplateBounds {
|
||||
return SCUMTypedRCONTemplateBounds{MaxPayloadBytes: 2048, TimeoutMS: 5000, MaxResponseBytes: 16 * 1024, MaxConfirmRecords: 16}
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationBounds struct {
|
||||
MaxPayloadBytes int
|
||||
TimeoutMS int
|
||||
BusyTimeoutMS int
|
||||
MaxReadbackBytes int
|
||||
MaxAffectedRows int
|
||||
}
|
||||
|
||||
func DefaultSCUMGuardedMutationBounds() SCUMGuardedMutationBounds {
|
||||
return SCUMGuardedMutationBounds{MaxPayloadBytes: 4096, TimeoutMS: 5000, BusyTimeoutMS: 250, MaxReadbackBytes: 16 * 1024, MaxAffectedRows: 1}
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
@@ -182,6 +211,32 @@ type SCUMTypedRCONTemplateRequest struct {
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationRequest struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
RequiredSchemaFingerprint string
|
||||
AssetDigest string
|
||||
TargetIdentityDigest string
|
||||
ExpectedRowDigest string
|
||||
ExpectedValueDigest string
|
||||
ExpectedXMLDigest string
|
||||
PatchDigest string
|
||||
BackupEvidenceDigest string
|
||||
OfflineEvidenceDigest string
|
||||
DangerConfirmationDigest string
|
||||
ReadbackExpectationDigest string
|
||||
IdempotencyKey string
|
||||
Payload map[string]any
|
||||
ReviewReason string
|
||||
Bounds SCUMGuardedMutationBounds
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDeclaration struct {
|
||||
Capability string
|
||||
TargetKey string
|
||||
@@ -296,6 +351,39 @@ type SCUMTypedRCONTemplateResult struct {
|
||||
Limits SCUMTypedRCONTemplateBounds
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationResult struct {
|
||||
RequestID string
|
||||
JobID string
|
||||
Binding SCUMBindingIdentity
|
||||
Status SCUMTerminalResultStatus
|
||||
Capability SCUMDataCapability
|
||||
TargetKey string
|
||||
TemplateKey string
|
||||
AdapterVersion string
|
||||
SchemaFingerprint string
|
||||
AssetDigest string
|
||||
SourceFingerprint string
|
||||
TargetIdentityDigest string
|
||||
ExpectedRowDigest string
|
||||
ExpectedValueDigest string
|
||||
ExpectedXMLDigest string
|
||||
PatchDigest string
|
||||
BackupEvidenceDigest string
|
||||
OfflineEvidenceDigest string
|
||||
DangerConfirmationDigest string
|
||||
ReadbackExpectationDigest string
|
||||
ObservedAt time.Time
|
||||
ResultDigest string
|
||||
BeforeDigest string
|
||||
AfterDigest string
|
||||
ReadbackDigest string
|
||||
AffectedRows int
|
||||
ReadbackStatus SCUMMutationReadbackStatus
|
||||
SafeSummary string
|
||||
SafeError SCUMSafeError
|
||||
Limits SCUMGuardedMutationBounds
|
||||
}
|
||||
|
||||
type SCUMCapabilityRequirement struct {
|
||||
Capability SCUMDataCapability
|
||||
AdapterVersion string
|
||||
@@ -406,6 +494,15 @@ func CopySCUMTypedRCONTemplateRequestPtr(value *SCUMTypedRCONTemplateRequest) *S
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMGuardedMutationRequestPtr(value *SCUMGuardedMutationRequest) *SCUMGuardedMutationRequest {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Payload = CopySCUMValueMap(value.Payload)
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
@@ -423,6 +520,14 @@ func CopySCUMTypedRCONTemplateResultPtr(value *SCUMTypedRCONTemplateResult) *SCU
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMGuardedMutationResultPtr(value *SCUMGuardedMutationResult) *SCUMGuardedMutationResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func CopySCUMValueMap(value map[string]any) map[string]any {
|
||||
if value == nil {
|
||||
return nil
|
||||
|
||||
@@ -114,6 +114,7 @@ type RunJobExecutionInputBody struct {
|
||||
SQLiteSchemaProbe *RunSQLiteSchemaProbeRequestBody `json:"sqliteSchemaProbe,omitempty"`
|
||||
SQLiteTemplate *RunSQLiteTemplateRequestBody `json:"sqliteTemplate,omitempty"`
|
||||
RCONTemplate *RunTypedRCONTemplateRequestBody `json:"rconTemplate,omitempty"`
|
||||
GuardedMutation *RunGuardedMutationRequestBody `json:"guardedMutation,omitempty"`
|
||||
}
|
||||
|
||||
type RunSQLiteSchemaProbeRequestBody struct {
|
||||
@@ -155,6 +156,30 @@ type RunTypedRCONTemplateRequestBody struct {
|
||||
Limits SCUMTypedRCONTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type RunGuardedMutationRequestBody struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Limits SCUMGuardedMutationBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type ServerDeploymentPlanBody struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Operation string `json:"operation"`
|
||||
@@ -234,6 +259,7 @@ type RunJobExecutionResultBody struct {
|
||||
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
|
||||
SQLiteTemplate *SCUMSQLiteTemplateResultDTO `json:"sqliteTemplate,omitempty"`
|
||||
RCONTemplate *SCUMTypedRCONTemplateResultDTO `json:"rconTemplate,omitempty"`
|
||||
GuardedMutation *SCUMGuardedMutationResultDTO `json:"guardedMutation,omitempty"`
|
||||
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
|
||||
DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"`
|
||||
}
|
||||
@@ -493,7 +519,7 @@ func (request RunJobResultRequest) ToDomain() domain.RunJobResult {
|
||||
Message: request.Message,
|
||||
ErrorCode: request.ErrorCode,
|
||||
Retryable: request.Retryable,
|
||||
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrToDomain(request.ExecutionResult.SQLiteSchemaProbe), SQLiteTemplate: SCUMSQLiteTemplateResultPtrToDomain(request.ExecutionResult.SQLiteTemplate), RCONTemplate: SCUMTypedRCONTemplateResultPtrToDomain(request.ExecutionResult.RCONTemplate), 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), GuardedMutation: SCUMGuardedMutationResultPtrToDomain(request.ExecutionResult.GuardedMutation), ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,7 +734,7 @@ func RunJobAssignmentFromDomain(assignment domain.RunJobAssignment) RunJobAssign
|
||||
State: assignment.State,
|
||||
Progress: progressReportFromDomain(assignment.Progress),
|
||||
ResultRef: assignment.ResultRef,
|
||||
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: runSQLiteSchemaProbeRequestPtrFromDomain(assignment.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: runSQLiteTemplateRequestPtrFromDomain(assignment.ExecutionInput.SQLiteTemplate), RCONTemplate: runTypedRCONTemplateRequestPtrFromDomain(assignment.ExecutionInput.RCONTemplate)},
|
||||
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: runSQLiteSchemaProbeRequestPtrFromDomain(assignment.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: runSQLiteTemplateRequestPtrFromDomain(assignment.ExecutionInput.SQLiteTemplate), RCONTemplate: runTypedRCONTemplateRequestPtrFromDomain(assignment.ExecutionInput.RCONTemplate), GuardedMutation: runGuardedMutationRequestPtrFromDomain(assignment.ExecutionInput.GuardedMutation)},
|
||||
LeaseToken: assignment.LeaseToken,
|
||||
Attempt: assignment.Attempt,
|
||||
FencingToken: assignment.FencingToken,
|
||||
|
||||
@@ -156,6 +156,61 @@ func TestRunJobResultRequestParsesTypedRCONTemplateEnvelope(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunJobResultRequestParsesGuardedMutationEnvelope(t *testing.T) {
|
||||
payload := `{
|
||||
"runEndpointId":"run-local",
|
||||
"sessionToken":"run-session",
|
||||
"jobId":"job-mutation",
|
||||
"leaseToken":"lease-mutation",
|
||||
"attempt":1,
|
||||
"state":"succeeded",
|
||||
"progress":{"percent":100,"message":"done"},
|
||||
"executionResult":{
|
||||
"kind":"sqlite.guarded-mutation",
|
||||
"guardedMutation":{
|
||||
"requestId":"request-mutation",
|
||||
"jobId":"job-mutation",
|
||||
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
||||
"status":"succeeded",
|
||||
"capability":"profile-xml.write",
|
||||
"targetKey":"scum-mutation-db",
|
||||
"templateKey":"profile.attributes.patch.v1",
|
||||
"adapterVersion":"scum-live-data-v0",
|
||||
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
||||
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"targetIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `",
|
||||
"expectedRowDigest":"sha256:` + strings.Repeat("2", 64) + `",
|
||||
"expectedValueDigest":"sha256:` + strings.Repeat("3", 64) + `",
|
||||
"expectedXmlDigest":"sha256:` + strings.Repeat("4", 64) + `",
|
||||
"patchDigest":"sha256:` + strings.Repeat("5", 64) + `",
|
||||
"backupEvidenceDigest":"sha256:` + strings.Repeat("6", 64) + `",
|
||||
"offlineEvidenceDigest":"sha256:` + strings.Repeat("7", 64) + `",
|
||||
"dangerConfirmationDigest":"sha256:` + strings.Repeat("8", 64) + `",
|
||||
"readbackExpectationDigest":"sha256:` + strings.Repeat("9", 64) + `",
|
||||
"observedAt":"2026-08-13T00:00:00Z",
|
||||
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"beforeDigest":"sha256:` + strings.Repeat("a", 64) + `",
|
||||
"afterDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
||||
"readbackDigest":"sha256:` + strings.Repeat("c", 64) + `",
|
||||
"affectedRows":1,
|
||||
"readbackStatus":"confirmed",
|
||||
"safeSummary":"confirmed by declared readback",
|
||||
"limits":{"maxPayloadBytes":4096,"timeoutMs":5000,"busyTimeoutMs":250,"maxReadbackBytes":16384,"maxAffectedRows":1}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var request RunJobResultRequest
|
||||
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
||||
t.Fatalf("unmarshal Run job result: %v", err)
|
||||
}
|
||||
domainRequest := request.ToDomain()
|
||||
result := domainRequest.ExecutionResult.GuardedMutation
|
||||
if result == nil || result.TemplateKey != "profile.attributes.patch.v1" || result.PatchDigest != "sha256:"+strings.Repeat("5", 64) || result.AffectedRows != 1 || result.ReadbackStatus != domain.SCUMMutationReadbackConfirmed {
|
||||
t.Fatalf("guardedMutation envelope did not parse: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIProviderFromDomainCopiesModels(t *testing.T) {
|
||||
provider := domain.AIProvider{
|
||||
ID: "ai.openai",
|
||||
|
||||
@@ -43,6 +43,14 @@ type SCUMTypedRCONTemplateBoundsDTO struct {
|
||||
MaxConfirmRecords int `json:"maxConfirmRecords"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationBoundsDTO struct {
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
BusyTimeoutMS int `json:"busyTimeoutMs"`
|
||||
MaxReadbackBytes int `json:"maxReadbackBytes"`
|
||||
MaxAffectedRows int `json:"maxAffectedRows"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
@@ -88,6 +96,32 @@ type SCUMTypedRCONTemplateRequestDTO struct {
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationRequestDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ReviewReason string `json:"reviewReason"`
|
||||
Bounds SCUMGuardedMutationBoundsDTO `json:"bounds"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
}
|
||||
|
||||
type SCUMSchemaProbeDispatchRequest struct {
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
@@ -195,6 +229,39 @@ type SCUMTypedRCONTemplateResultDTO struct {
|
||||
Limits SCUMTypedRCONTemplateBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMGuardedMutationResultDTO struct {
|
||||
RequestID string `json:"requestId"`
|
||||
JobID string `json:"jobId"`
|
||||
Binding SCUMBindingIdentityDTO `json:"binding"`
|
||||
Status string `json:"status"`
|
||||
Capability string `json:"capability"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
AdapterVersion string `json:"adapterVersion"`
|
||||
SchemaFingerprint string `json:"schemaFingerprint"`
|
||||
AssetDigest string `json:"assetDigest"`
|
||||
SourceFingerprint string `json:"sourceFingerprint,omitempty"`
|
||||
TargetIdentityDigest string `json:"targetIdentityDigest"`
|
||||
ExpectedRowDigest string `json:"expectedRowDigest"`
|
||||
ExpectedValueDigest string `json:"expectedValueDigest"`
|
||||
ExpectedXMLDigest string `json:"expectedXmlDigest"`
|
||||
PatchDigest string `json:"patchDigest"`
|
||||
BackupEvidenceDigest string `json:"backupEvidenceDigest"`
|
||||
OfflineEvidenceDigest string `json:"offlineEvidenceDigest"`
|
||||
DangerConfirmationDigest string `json:"dangerConfirmationDigest"`
|
||||
ReadbackExpectationDigest string `json:"readbackExpectationDigest"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
ResultDigest string `json:"resultDigest"`
|
||||
BeforeDigest string `json:"beforeDigest,omitempty"`
|
||||
AfterDigest string `json:"afterDigest,omitempty"`
|
||||
ReadbackDigest string `json:"readbackDigest,omitempty"`
|
||||
AffectedRows int `json:"affectedRows"`
|
||||
ReadbackStatus string `json:"readbackStatus"`
|
||||
SafeSummary string `json:"safeSummary,omitempty"`
|
||||
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
|
||||
Limits SCUMGuardedMutationBoundsDTO `json:"limits"`
|
||||
}
|
||||
|
||||
type SCUMCapabilityGateDTO struct {
|
||||
Capability string `json:"capability"`
|
||||
State string `json:"state"`
|
||||
@@ -251,6 +318,22 @@ func SCUMTypedRCONTemplateRequestPtrFromDomain(value *domain.SCUMTypedRCONTempla
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestToDomain(value SCUMGuardedMutationRequestDTO) domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumGuardedMutationBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestFromDomain(value domain.SCUMGuardedMutationRequest) SCUMGuardedMutationRequestDTO {
|
||||
return SCUMGuardedMutationRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumGuardedMutationBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationRequestPtrFromDomain(value *domain.SCUMGuardedMutationRequest) *SCUMGuardedMutationRequestDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationRequestFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func runSQLiteSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *RunSQLiteSchemaProbeRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
@@ -272,6 +355,13 @@ func runTypedRCONTemplateRequestPtrFromDomain(value *domain.SCUMTypedRCONTemplat
|
||||
return &RunTypedRCONTemplateRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Limits: scumTypedRCONTemplateBoundsFromDomain(value.Bounds)}
|
||||
}
|
||||
|
||||
func runGuardedMutationRequestPtrFromDomain(value *domain.SCUMGuardedMutationRequest) *RunGuardedMutationRequestBody {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &RunGuardedMutationRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Limits: scumGuardedMutationBoundsFromDomain(value.Bounds)}
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeDispatchFromDomain(request domain.SCUMSchemaProbeRequest, queued domain.RemoteAdapterResult) SCUMSchemaProbeDispatchResponse {
|
||||
return SCUMSchemaProbeDispatchResponse{ProbeRequest: SCUMSchemaProbeRequestFromDomain(request), QueuedJob: RemoteAdapterFromDomain(queued)}
|
||||
}
|
||||
@@ -328,6 +418,18 @@ func SCUMTypedRCONTemplateResultPtrToDomain(value *SCUMTypedRCONTemplateResultDT
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultToDomain(value SCUMGuardedMutationResultDTO) domain.SCUMGuardedMutationResult {
|
||||
return domain.SCUMGuardedMutationResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), Capability: domain.SCUMDataCapability(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, SourceFingerprint: value.SourceFingerprint, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, BeforeDigest: value.BeforeDigest, AfterDigest: value.AfterDigest, ReadbackDigest: value.ReadbackDigest, AffectedRows: value.AffectedRows, ReadbackStatus: domain.SCUMMutationReadbackStatus(value.ReadbackStatus), SafeSummary: value.SafeSummary, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumGuardedMutationBoundsToDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultPtrToDomain(value *SCUMGuardedMutationResultDTO) *domain.SCUMGuardedMutationResult {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationResultToDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSchemaProbeResultDTO {
|
||||
value = domain.CopySCUMSchemaProbeResult(value)
|
||||
objects := make([]SCUMSchemaObjectEvidenceDTO, len(value.Objects))
|
||||
@@ -382,6 +484,18 @@ func SCUMTypedRCONTemplateResultPtrFromDomain(value *domain.SCUMTypedRCONTemplat
|
||||
return &result
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultFromDomain(value domain.SCUMGuardedMutationResult) SCUMGuardedMutationResultDTO {
|
||||
return SCUMGuardedMutationResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, SourceFingerprint: value.SourceFingerprint, TargetIdentityDigest: value.TargetIdentityDigest, ExpectedRowDigest: value.ExpectedRowDigest, ExpectedValueDigest: value.ExpectedValueDigest, ExpectedXMLDigest: value.ExpectedXMLDigest, PatchDigest: value.PatchDigest, BackupEvidenceDigest: value.BackupEvidenceDigest, OfflineEvidenceDigest: value.OfflineEvidenceDigest, DangerConfirmationDigest: value.DangerConfirmationDigest, ReadbackExpectationDigest: value.ReadbackExpectationDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, BeforeDigest: value.BeforeDigest, AfterDigest: value.AfterDigest, ReadbackDigest: value.ReadbackDigest, AffectedRows: value.AffectedRows, ReadbackStatus: string(value.ReadbackStatus), SafeSummary: value.SafeSummary, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumGuardedMutationBoundsFromDomain(value.Limits)}
|
||||
}
|
||||
|
||||
func SCUMGuardedMutationResultPtrFromDomain(value *domain.SCUMGuardedMutationResult) *SCUMGuardedMutationResultDTO {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := SCUMGuardedMutationResultFromDomain(*value)
|
||||
return &result
|
||||
}
|
||||
|
||||
func 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}
|
||||
}
|
||||
@@ -417,3 +531,11 @@ func scumTypedRCONTemplateBoundsToDomain(value SCUMTypedRCONTemplateBoundsDTO) d
|
||||
func scumTypedRCONTemplateBoundsFromDomain(value domain.SCUMTypedRCONTemplateBounds) SCUMTypedRCONTemplateBoundsDTO {
|
||||
return SCUMTypedRCONTemplateBoundsDTO{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, MaxResponseBytes: value.MaxResponseBytes, MaxConfirmRecords: value.MaxConfirmRecords}
|
||||
}
|
||||
|
||||
func scumGuardedMutationBoundsToDomain(value SCUMGuardedMutationBoundsDTO) domain.SCUMGuardedMutationBounds {
|
||||
return domain.SCUMGuardedMutationBounds{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxReadbackBytes: value.MaxReadbackBytes, MaxAffectedRows: value.MaxAffectedRows}
|
||||
}
|
||||
|
||||
func scumGuardedMutationBoundsFromDomain(value domain.SCUMGuardedMutationBounds) SCUMGuardedMutationBoundsDTO {
|
||||
return SCUMGuardedMutationBoundsDTO{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxReadbackBytes: value.MaxReadbackBytes, MaxAffectedRows: value.MaxAffectedRows}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,26 @@ The request never contains browser command text, raw RCON, SQL, XML, host/databa
|
||||
|
||||
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.
|
||||
|
||||
## Guarded mutation request
|
||||
|
||||
`SCUMGuardedMutationRequest` is the Platform durable-job payload for plugin-owned single-row SQLite/XML mutation templates after the mutation capability is proven, reviewed, and explicitly confirmed. Required fields are:
|
||||
|
||||
- `requestId`, `jobId`;
|
||||
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
|
||||
- `capability`, limited to guarded database/XML write capabilities such as `profile-xml.write`;
|
||||
- logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, `targetIdentityDigest`, `expectedRowDigest`, `expectedValueDigest`, `expectedXmlDigest`, `patchDigest`, `backupEvidenceDigest`, `offlineEvidenceDigest`, `dangerConfirmationDigest`, and `readbackExpectationDigest`;
|
||||
- `idempotencyKey`, scalar `payload` validated against the plugin-declared payload schema, and safe `reviewReason`;
|
||||
- `bounds`: `maxPayloadBytes`, `timeoutMs`, `busyTimeoutMs`, `maxReadbackBytes`, and `maxAffectedRows`, which must equal `1`;
|
||||
- `requestedAt`.
|
||||
|
||||
The request never contains raw SQL, raw XML, browser mutation text, host/database paths, DSNs, sockets, credentials, table/column overrides, raw row payloads, `fieldKey=855`, `prisoner.value`, or undeclared patch fields. Run resolves the logical target and packaged preserving patch template inside the generated Run package.
|
||||
|
||||
## Guarded mutation result
|
||||
|
||||
`SCUMGuardedMutationResult` is the terminal envelope for `sqlite.guarded-mutation` results. Required fields are request/job/binding identity, status (`succeeded`, `failed`, or `cancelled`), write capability, target/template key, adapter version, schema fingerprint, asset digest, source fingerprint when succeeded, target identity digest, expected row/value/XML digests, patch digest, backup/offline/danger-confirmation digests, readback expectation digest, observed time, result digest, before/after/readback digests when succeeded, affected-row count, readback status, safe summary, safe error, and limits actually applied.
|
||||
|
||||
Platform accepts mutation success only when the terminal envelope matches the leased durable job and the declared binding/template/schema/asset/target/guard/patch/backup/offline/confirmation/readback digests, `affectedRows` is exactly `1`, and readback is `confirmed`. Zero rows, multiple rows, stale expected values, schema or source changes, malformed XML, absent named nodes, rollback, missing backup/offline/danger confirmation, missing readback, or unsafe summaries remain failed/conflict/unknown outcomes and must not update local verified facts.
|
||||
|
||||
## Release behavior
|
||||
|
||||
The first-party SCUM plugin declares `scumLiveData` with `remote.run.db.sqlite.probe` and per-capability gates. Until current-service evidence exists, all gates remain `disabled` with `evidenceStatus: missing`. Query assets, RCON templates, XML mutations, map transforms, and gift transports may be added only after current-service probe evidence proves their adapter requirements; unsupported or ambiguous capabilities stay disabled independently.
|
||||
|
||||
@@ -54,6 +54,10 @@ Database/XML writes are disabled until current-service evidence proves the sourc
|
||||
|
||||
Platform dispatches guarded mutations only with effective `server.game-client.maintenance`, explicit danger confirmation, target identity, expected before values/checksum, same-instance backup evidence, idempotency key, reason, adapter/digest, and declared safety requirements. Run performs one bounded transaction, updates exactly one guarded row, preserves untargeted XML content, rolls back on zero/multiple affected rows or conflicts, and performs read-after-write confirmation before any success result.
|
||||
|
||||
The leased Run assignment carries a typed `guardedMutation` request only. Required fields are `requestId`, server/plugin binding, `profile-xml.write` capability, logical `targetKey`, `templateKey`, `adapterVersion`, `requiredSchemaFingerprint`, immutable `assetDigest`, `targetIdentityDigest`, `expectedRowDigest`, `expectedValueDigest`, `expectedXmlDigest`, `patchDigest`, `backupEvidenceDigest`, `offlineEvidenceDigest`, `dangerConfirmationDigest`, `readbackExpectationDigest`, idempotency key, bounded scalar payload, review reason, and limits containing `maxPayloadBytes`, `timeoutMs`, `busyTimeoutMs`, `maxReadbackBytes`, and `maxAffectedRows=1`. The payload carries no raw SQL, raw XML, database path, table/column override, `855` field key, browser mutation text, host path, socket, credential, or undeclared patch field.
|
||||
|
||||
The terminal `sqlite.guarded-mutation` envelope contains `requestId`, `jobId`, binding, status (`succeeded`, `failed`, or `cancelled`), capability, target/template key, adapter/schema fingerprint, asset digest, source fingerprint, target identity digest, expected row/value/XML digests, patch digest, backup/offline/danger-confirmation digests, readback expectation digest, observed time, result digest, before/after/readback digests, affected-row count, readback status, safe summary, safe error, and applied limits. Platform accepts success only when the envelope matches the leased job and binding, `affectedRows` is exactly `1`, and `readbackStatus` is `confirmed`; zero/multiple rows, guard mismatches, malformed XML, missing backup/offline/danger confirmation, missing readback, or stale schema remain safe failed/conflict/unknown results.
|
||||
|
||||
Saving attributes must never implicitly kill, respawn, kick, or otherwise activate destructive game behavior. Any verified required activation is a separate permission-checked and explicitly confirmed typed command.
|
||||
|
||||
## Terminal Result Envelope
|
||||
|
||||
@@ -373,6 +373,21 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityRemoteRunProtectedSQL:
|
||||
if job.ExecutionInput.GuardedMutation == nil {
|
||||
break
|
||||
}
|
||||
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumGuardedMutationExecutionKind {
|
||||
return validationError("guarded mutation result type is invalid")
|
||||
}
|
||||
if result.State == domain.JobStateSucceeded {
|
||||
if result.ExecutionResult.GuardedMutation == nil {
|
||||
return validationError("guarded mutation terminal result is required")
|
||||
}
|
||||
if err := validateSCUMGuardedMutationResultForJob(job, *result.ExecutionResult.GuardedMutation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.JobCapabilityClientManagerDeploy:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" {
|
||||
return validationError("client-manager deploy result type is invalid")
|
||||
@@ -678,7 +693,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
|
||||
State: job.State,
|
||||
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
|
||||
ResultRef: job.ResultRef,
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: domain.CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate), RCONTemplate: domain.CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate)},
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: domain.CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate), RCONTemplate: domain.CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate), GuardedMutation: domain.CopySCUMGuardedMutationRequestPtr(job.ExecutionInput.GuardedMutation)},
|
||||
LeaseToken: leaseToken,
|
||||
Attempt: job.Attempt,
|
||||
FencingToken: fencingToken,
|
||||
@@ -768,7 +783,11 @@ func terminalFingerprint(result domain.RunJobResult) string {
|
||||
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)
|
||||
guardedMutationFingerprint := ""
|
||||
if result.ExecutionResult.GuardedMutation != nil {
|
||||
guardedMutationFingerprint = fmt.Sprintf("%s|%s|%s|%d|%s", result.ExecutionResult.GuardedMutation.ResultDigest, result.ExecutionResult.GuardedMutation.AssetDigest, result.ExecutionResult.GuardedMutation.PatchDigest, result.ExecutionResult.GuardedMutation.AffectedRows, result.ExecutionResult.GuardedMutation.ReadbackStatus)
|
||||
}
|
||||
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t|%s|%s|%s|%s|%s|%s", result.State, result.Progress.Percent, result.ResultRef, result.Message, result.ErrorCode, result.Progress.Message, result.Retryable, result.ExecutionResult.Kind, result.ExecutionResult.Checksum, schemaProbeFingerprint, sqliteTemplateFingerprint, rconTemplateFingerprint, guardedMutationFingerprint)
|
||||
}
|
||||
|
||||
func terminalMessage(result domain.RunJobResult) string {
|
||||
|
||||
@@ -284,6 +284,42 @@ func TestCoreServiceRunJobTypedRCONTemplateEnvelopeIsFencedToLease(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobGuardedMutationEnvelopeIsFencedToLease(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
request := scumGuardedMutationTestRequest()
|
||||
createSCUMTemplateServerFixture(t, svc, request.Binding)
|
||||
job, err := svc.CreateJob(domain.Job{ID: request.JobID, ServerInstanceID: request.Binding.ServerInstanceID, RunEndpointID: request.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedSQL, TargetKey: request.TargetKey, InputRef: "input://guarded-mutation/request-mutation", IdempotencyKey: request.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-sql", RemoteAdapterKey: request.TargetKey, GuardedMutation: &request}})
|
||||
if err != nil {
|
||||
t.Fatalf("create guarded mutation job: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.GuardedMutation == nil || claim.Job.ExecutionInput.GuardedMutation.Payload["attributeKey"].(string) != "Strength" {
|
||||
t.Fatalf("claim guarded mutation job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
result := scumGuardedMutationTestResult(request)
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "mutation complete"}, Message: "mutation complete", ExecutionResult: domain.JobExecutionResult{Kind: scumGuardedMutationExecutionKind, GuardedMutation: &result, AuditSummary: "redacted guarded mutation"}}); err != nil {
|
||||
t.Fatalf("complete matching guarded mutation job: %v", err)
|
||||
}
|
||||
|
||||
badRequest := request
|
||||
badRequest.RequestID = "request-mutation-bad"
|
||||
badRequest.JobID = "job-mutation-bad"
|
||||
badRequest.IdempotencyKey = "idem-mutation-bad"
|
||||
if _, err := svc.CreateJob(domain.Job{ID: badRequest.JobID, ServerInstanceID: badRequest.Binding.ServerInstanceID, RunEndpointID: badRequest.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedSQL, TargetKey: badRequest.TargetKey, InputRef: "input://guarded-mutation/request-mutation-bad", IdempotencyKey: badRequest.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-sql", RemoteAdapterKey: badRequest.TargetKey, GuardedMutation: &badRequest}}); err != nil {
|
||||
t.Fatalf("create bad guarded mutation job: %v", err)
|
||||
}
|
||||
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||
if err != nil {
|
||||
t.Fatalf("claim bad guarded mutation job: %v", err)
|
||||
}
|
||||
badResult := scumGuardedMutationTestResult(badRequest)
|
||||
badResult.PatchDigest = "sha256:" + strings.Repeat("b", 64)
|
||||
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "mutation complete"}, Message: "mutation complete", ExecutionResult: domain.JobExecutionResult{Kind: scumGuardedMutationExecutionKind, GuardedMutation: &badResult, AuditSummary: "redacted guarded mutation"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "digest") {
|
||||
t.Fatalf("expected digest-fenced guarded mutation result rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRunJobReconcile(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredRunJobService(t)
|
||||
createQueuedRunJob(t, svc, "job-1", "idem-1")
|
||||
@@ -314,7 +350,7 @@ func newRegisteredRunJobService(t *testing.T) (*CoreService, string) {
|
||||
t.Helper()
|
||||
svc := newTestCoreService()
|
||||
helloRequest := validRunControlHello()
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start", domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON)
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start", domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
helloRequest.CapabilityReport.Fingerprint = "cap-jobs"
|
||||
hello, err := svc.RegisterRunHello(helloRequest)
|
||||
if err != nil {
|
||||
@@ -333,13 +369,17 @@ func scumTypedRCONTemplateTestRequest() domain.SCUMTypedRCONTemplateRequest {
|
||||
return domain.SCUMTypedRCONTemplateRequest{RequestID: "request-rcon", JobID: "job-rcon", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityEconomyCommand, TransportKey: "scum-rcon", TargetKey: "scum-rcon", TemplateKey: "economy.fame.set.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), PayloadDigest: scumTemplateTestHash(), ConfirmationDigest: scumTemplateTestHash(), TargetIdentityDigest: scumTemplateTestHash(), IdempotencyKey: "idem-rcon", Payload: map[string]any{"externalPlayerId": "player-redacted", "absoluteValue": 100.0}, ReviewReason: "operator reviewed absolute fame update", Bounds: domain.DefaultSCUMTypedRCONTemplateBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func scumGuardedMutationTestRequest() domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: "request-mutation", JobID: "job-mutation", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityProfileXMLWrite, TargetKey: "scum-mutation-db", TemplateKey: "profile.attributes.patch.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), TargetIdentityDigest: scumTemplateTestHash(), ExpectedRowDigest: scumTemplateTestHash(), ExpectedValueDigest: scumTemplateTestHash(), ExpectedXMLDigest: scumTemplateTestHash(), PatchDigest: scumTemplateTestHash(), BackupEvidenceDigest: scumTemplateTestHash(), OfflineEvidenceDigest: scumTemplateTestHash(), DangerConfirmationDigest: scumTemplateTestHash(), ReadbackExpectationDigest: scumTemplateTestHash(), IdempotencyKey: "idem-mutation", Payload: map[string]any{"attributeKey": "Strength", "absoluteValue": 8.5}, ReviewReason: "operator confirmed offline profile attribute patch", Bounds: domain.DefaultSCUMGuardedMutationBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func scumTypedRCONSourcePlan() *domain.RuntimeSourceRCONPlan {
|
||||
return &domain.RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/scum-rcon/release.json", Port: 27015}
|
||||
}
|
||||
|
||||
func createSCUMTemplateServerFixture(t *testing.T, svc *CoreService, binding domain.SCUMBindingIdentity) {
|
||||
t.Helper()
|
||||
if _, err := svc.CreateGamePlugin(domain.GamePlugin{ID: binding.PluginID, Name: "SCUM", Version: binding.PluginVersion, ServerType: "scum", ManifestRef: "artifact://manifests/game.scum/0.1.6", CreateFormSchemaRef: "artifact://schemas/game.scum/create-form/0.1.6", RequiredRunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON}, DeclaredPermissions: []string{"server.remote.access", "server.game-client.command"}, 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, domain.JobCapabilityRemoteRunProtectedSQL}, DeclaredPermissions: []string{"server.remote.access", "server.game-client.command", "server.game-client.maintenance"}, 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 {
|
||||
@@ -355,6 +395,10 @@ func scumTypedRCONTemplateTestResult(request domain.SCUMTypedRCONTemplateRequest
|
||||
return domain.SCUMTypedRCONTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TransportKey: request.TransportKey, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, PayloadDigest: request.PayloadDigest, ConfirmationDigest: request.ConfirmationDigest, TargetIdentityDigest: request.TargetIdentityDigest, ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), ResponseDigest: scumTemplateTestHash(), ConfirmationStatus: domain.SCUMRCONConfirmationConfirmed, ConfirmationDigestID: scumTemplateTestHash(), SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func scumGuardedMutationTestResult(request domain.SCUMGuardedMutationRequest) domain.SCUMGuardedMutationResult {
|
||||
return domain.SCUMGuardedMutationResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, SourceFingerprint: scumTemplateTestHash(), TargetIdentityDigest: request.TargetIdentityDigest, ExpectedRowDigest: request.ExpectedRowDigest, ExpectedValueDigest: request.ExpectedValueDigest, ExpectedXMLDigest: request.ExpectedXMLDigest, PatchDigest: request.PatchDigest, BackupEvidenceDigest: request.BackupEvidenceDigest, OfflineEvidenceDigest: request.OfflineEvidenceDigest, DangerConfirmationDigest: request.DangerConfirmationDigest, ReadbackExpectationDigest: request.ReadbackExpectationDigest, ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), BeforeDigest: scumTemplateTestHash(), AfterDigest: scumTemplateTestHash(), ReadbackDigest: scumTemplateTestHash(), AffectedRows: 1, ReadbackStatus: domain.SCUMMutationReadbackConfirmed, SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func createQueuedRunJob(t *testing.T, svc *CoreService, id string, idempotencyKey string) domain.Job {
|
||||
t.Helper()
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
scumSchemaProbeExecutionKind = "sqlite.schema-probe"
|
||||
scumSQLiteTemplateExecutionKind = "sqlite.template-query"
|
||||
scumRCONTemplateExecutionKind = "rcon.template-command"
|
||||
scumSchemaProbeExecutionKind = "sqlite.schema-probe"
|
||||
scumSQLiteTemplateExecutionKind = "sqlite.template-query"
|
||||
scumRCONTemplateExecutionKind = "rcon.template-command"
|
||||
scumGuardedMutationExecutionKind = "sqlite.guarded-mutation"
|
||||
)
|
||||
|
||||
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
|
||||
@@ -196,6 +197,26 @@ func validateSCUMTypedRCONTemplateResultForJob(job domain.Job, result domain.SCU
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationResultForJob(job domain.Job, result domain.SCUMGuardedMutationResult) error {
|
||||
if err := validator.ValidateSCUMGuardedMutationResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := job.ExecutionInput.GuardedMutation
|
||||
if expected == nil {
|
||||
return validationError("guarded mutation request is missing from leased job")
|
||||
}
|
||||
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
|
||||
return validationError("guarded mutation result does not match leased job identity")
|
||||
}
|
||||
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
|
||||
return validationError("guarded mutation result does not match leased binding identity")
|
||||
}
|
||||
if result.Capability != expected.Capability || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.SchemaFingerprint != expected.RequiredSchemaFingerprint || result.AssetDigest != expected.AssetDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest || result.ExpectedRowDigest != expected.ExpectedRowDigest || result.ExpectedValueDigest != expected.ExpectedValueDigest || result.ExpectedXMLDigest != expected.ExpectedXMLDigest || result.PatchDigest != expected.PatchDigest || result.BackupEvidenceDigest != expected.BackupEvidenceDigest || result.OfflineEvidenceDigest != expected.OfflineEvidenceDigest || result.DangerConfirmationDigest != expected.DangerConfirmationDigest || result.ReadbackExpectationDigest != expected.ReadbackExpectationDigest {
|
||||
return validationError("guarded mutation result does not match leased template, target, guard, digest, or readback identity")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func 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
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ func ValidateRunJobResult(result domain.RunJobResult) error {
|
||||
violations = append(violations, "executionResult.rconTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if result.ExecutionResult.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationResult(*result.ExecutionResult.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionResult.guardedMutation: "+err.Error())
|
||||
}
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
|
||||
@@ -1552,6 +1552,20 @@ func ValidateJob(job domain.Job) error {
|
||||
violations = append(violations, "executionInput.rconTemplate requires a protected rcon transport plan")
|
||||
}
|
||||
}
|
||||
if job.ExecutionInput.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationRequest(*job.ExecutionInput.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionInput.guardedMutation: "+err.Error())
|
||||
}
|
||||
if job.Capability != domain.JobCapabilityRemoteRunProtectedSQL {
|
||||
violations = append(violations, "executionInput.guardedMutation is allowed only for protected sql jobs")
|
||||
}
|
||||
if job.ExecutionInput.RemoteAdapterKind != "protected-sql" {
|
||||
violations = append(violations, "executionInput.guardedMutation requires a protected sql transport plan")
|
||||
}
|
||||
if len(job.ExecutionInput.Inputs) != 0 || job.ExecutionInput.Content != "" {
|
||||
violations = append(violations, "executionInput.guardedMutation must not persist raw adapter inputs")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...)
|
||||
if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) {
|
||||
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
|
||||
@@ -1577,6 +1591,11 @@ func ValidateJob(job domain.Job) error {
|
||||
violations = append(violations, "executionResult.rconTemplate: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.ExecutionResult.GuardedMutation != nil {
|
||||
if err := ValidateSCUMGuardedMutationResult(*job.ExecutionResult.GuardedMutation); err != nil {
|
||||
violations = append(violations, "executionResult.guardedMutation: "+err.Error())
|
||||
}
|
||||
}
|
||||
if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite {
|
||||
if job.ServerInstanceID == "" {
|
||||
violations = append(violations, "serverInstanceId is required for scoped file jobs")
|
||||
|
||||
@@ -24,6 +24,8 @@ const (
|
||||
maxSCUMRCONPayloadBytes = 4096
|
||||
maxSCUMRCONResponseBytes = 64 * 1024
|
||||
maxSCUMRCONConfirmRecords = 128
|
||||
maxSCUMMutationPayloadBytes = 4096
|
||||
maxSCUMMutationReadbackBytes = 64 * 1024
|
||||
)
|
||||
|
||||
var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`)
|
||||
@@ -106,6 +108,41 @@ func ValidateSCUMTypedRCONTemplateRequest(request domain.SCUMTypedRCONTemplateRe
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMGuardedMutationRequest(request domain.SCUMGuardedMutationRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
if !validSCUMGuardedMutationCapability(request.Capability) {
|
||||
violations = append(violations, "capability must be a guarded database/XML write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
|
||||
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range scumGuardedMutationRequestDigests(request) {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("idempotencyKey", request.IdempotencyKey)...)
|
||||
if strings.TrimSpace(request.ReviewReason) == "" || len(request.ReviewReason) > 320 || containsSCUMProtectedMaterial(request.ReviewReason) || containsSCUMRawXML(request.ReviewReason) {
|
||||
violations = append(violations, "reviewReason is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMGuardedMutationBounds("bounds", request.Bounds)...)
|
||||
violations = append(violations, validateSCUMGuardedMutationPayload("payload", request.Payload)...)
|
||||
violations = append(violations, validateSCUMJSONSize("payload", request.Payload, request.Bounds.MaxPayloadBytes)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
@@ -250,6 +287,75 @@ func ValidateSCUMTypedRCONTemplateResult(result domain.SCUMTypedRCONTemplateResu
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMGuardedMutationResult(result domain.SCUMGuardedMutationResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
violations = appendRequired(violations, "jobId", result.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
|
||||
if !validSCUMTerminalResultStatus(result.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validSCUMGuardedMutationCapability(result.Capability) {
|
||||
violations = append(violations, "capability must be a guarded database/XML write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
|
||||
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
if result.SourceFingerprint != "" && !validSCUMFingerprint(result.SourceFingerprint) {
|
||||
violations = append(violations, "sourceFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range scumGuardedMutationResultDigests(result) {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
if !validSCUMMutationReadbackStatus(result.ReadbackStatus) {
|
||||
violations = append(violations, "readbackStatus is invalid")
|
||||
}
|
||||
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) || containsSCUMRawXML(result.SafeSummary) {
|
||||
violations = append(violations, "safeSummary is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMGuardedMutationBounds("limits", result.Limits)...)
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.AffectedRows != 1 || result.Limits.MaxAffectedRows != 1 {
|
||||
violations = append(violations, "succeeded result requires exactly one affected row")
|
||||
}
|
||||
if result.ReadbackStatus != domain.SCUMMutationReadbackConfirmed || result.SourceFingerprint == "" || result.BeforeDigest == "" || result.AfterDigest == "" || result.ReadbackDigest == "" {
|
||||
violations = append(violations, "succeeded result requires conclusive before/after/readback digests")
|
||||
}
|
||||
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "succeeded result must not carry an error code")
|
||||
}
|
||||
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
|
||||
violations = append(violations, "non-succeeded result requires a safe error code")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func scumGuardedMutationRequestDigests(request domain.SCUMGuardedMutationRequest) []struct{ name, value string } {
|
||||
return []struct{ name, value string }{{"assetDigest", request.AssetDigest}, {"targetIdentityDigest", request.TargetIdentityDigest}, {"expectedRowDigest", request.ExpectedRowDigest}, {"expectedValueDigest", request.ExpectedValueDigest}, {"expectedXmlDigest", request.ExpectedXMLDigest}, {"patchDigest", request.PatchDigest}, {"backupEvidenceDigest", request.BackupEvidenceDigest}, {"offlineEvidenceDigest", request.OfflineEvidenceDigest}, {"dangerConfirmationDigest", request.DangerConfirmationDigest}, {"readbackExpectationDigest", request.ReadbackExpectationDigest}}
|
||||
}
|
||||
|
||||
func scumGuardedMutationResultDigests(result domain.SCUMGuardedMutationResult) []struct{ name, value string } {
|
||||
items := []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"targetIdentityDigest", result.TargetIdentityDigest}, {"expectedRowDigest", result.ExpectedRowDigest}, {"expectedValueDigest", result.ExpectedValueDigest}, {"expectedXmlDigest", result.ExpectedXMLDigest}, {"patchDigest", result.PatchDigest}, {"backupEvidenceDigest", result.BackupEvidenceDigest}, {"offlineEvidenceDigest", result.OfflineEvidenceDigest}, {"dangerConfirmationDigest", result.DangerConfirmationDigest}, {"readbackExpectationDigest", result.ReadbackExpectationDigest}, {"resultDigest", result.ResultDigest}}
|
||||
for _, item := range []struct{ name, value string }{{"beforeDigest", result.BeforeDigest}, {"afterDigest", result.AfterDigest}, {"readbackDigest", result.ReadbackDigest}} {
|
||||
if item.value != "" {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error {
|
||||
var violations []string
|
||||
if !validSCUMDataCapability(evidence.Capability) {
|
||||
@@ -441,6 +547,26 @@ func validateSCUMTypedRCONTemplateBounds(prefix string, value domain.SCUMTypedRC
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationBounds(prefix string, value domain.SCUMGuardedMutationBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMMutationPayloadBytes {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.BusyTimeoutMS < 0 || value.BusyTimeoutMS > maxSCUMTemplateBusyTimeoutMS {
|
||||
violations = append(violations, prefix+".busyTimeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxReadbackBytes < 1 || value.MaxReadbackBytes > maxSCUMMutationReadbackBytes {
|
||||
violations = append(violations, prefix+".maxReadbackBytes is out of bounds")
|
||||
}
|
||||
if value.MaxAffectedRows != 1 {
|
||||
violations = append(violations, prefix+".maxAffectedRows must be 1")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMTemplateKey(prefix, value string) []string {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, prefix, value)
|
||||
@@ -466,6 +592,23 @@ func validateSCUMValueMap(prefix string, values map[string]any, maxItems int) []
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMGuardedMutationPayload(prefix string, values map[string]any) []string {
|
||||
violations := validateSCUMValueMap(prefix, values, 64)
|
||||
for key, value := range values {
|
||||
loweredKey := strings.ToLower(key)
|
||||
if strings.Contains(loweredKey, "sql") || strings.Contains(loweredKey, "xml") || strings.Contains(loweredKey, "path") || strings.Contains(loweredKey, "table") || strings.Contains(loweredKey, "column") || strings.Contains(loweredKey, "query") || strings.Contains(loweredKey, "raw") || strings.Contains(loweredKey, "855") {
|
||||
violations = append(violations, prefix+" key is unsafe")
|
||||
}
|
||||
if text, ok := value.(string); ok {
|
||||
trimmed := strings.TrimSpace(strings.ToLower(text))
|
||||
if containsSCUMRawXML(text) || trimmed == "855" || strings.Contains(trimmed, "fieldkey=855") || strings.Contains(trimmed, "prisoner.value") {
|
||||
violations = append(violations, prefix+" value is unsafe")
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMValue(prefix string, value any) []string {
|
||||
var violations []string
|
||||
switch item := value.(type) {
|
||||
@@ -586,6 +729,15 @@ func validSCUMRCONWriteCapability(value domain.SCUMDataCapability) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMGuardedMutationCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityProfileXMLWrite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMTerminalResultSucceeded, domain.SCUMTerminalResultFailed, domain.SCUMTerminalResultCancelled:
|
||||
@@ -604,6 +756,15 @@ func validSCUMRCONConfirmationStatus(value domain.SCUMRCONConfirmationStatus) bo
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMMutationReadbackStatus(value domain.SCUMMutationReadbackStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMMutationReadbackConfirmed, domain.SCUMMutationReadbackFailed, domain.SCUMMutationReadbackConflict, domain.SCUMMutationReadbackUnknown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
|
||||
@@ -624,13 +785,17 @@ 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, domain.SCUMSafeErrorTemplateMissing, domain.SCUMSafeErrorTemplateMismatch, domain.SCUMSafeErrorParameterInvalid, domain.SCUMSafeErrorRowLimitExceeded, domain.SCUMSafeErrorResultSchemaInvalid:
|
||||
case "", domain.SCUMSafeErrorNone, domain.SCUMSafeErrorProbeExecutorAbsent, domain.SCUMSafeErrorProbeMissing, domain.SCUMSafeErrorProbeFailed, domain.SCUMSafeErrorSchemaIncompatible, domain.SCUMSafeErrorBindingMismatch, domain.SCUMSafeErrorAdapterMismatch, domain.SCUMSafeErrorFingerprintMismatch, domain.SCUMSafeErrorDigestMismatch, domain.SCUMSafeErrorEvidenceExpired, domain.SCUMSafeErrorInvalidProbePayload, domain.SCUMSafeErrorInvalidRequest, domain.SCUMSafeErrorTargetUnavailable, domain.SCUMSafeErrorSourceUnavailable, domain.SCUMSafeErrorSQLiteOpenFailed, domain.SCUMSafeErrorSQLiteReadFailed, domain.SCUMSafeErrorDatabaseBusy, domain.SCUMSafeErrorTimeout, domain.SCUMSafeErrorCancelled, domain.SCUMSafeErrorSourceChanged, domain.SCUMSafeErrorResultLimitExceeded, domain.SCUMSafeErrorTemplateMissing, domain.SCUMSafeErrorTemplateMismatch, domain.SCUMSafeErrorParameterInvalid, domain.SCUMSafeErrorRowLimitExceeded, domain.SCUMSafeErrorResultSchemaInvalid, domain.SCUMSafeErrorMutationGuardMismatch, domain.SCUMSafeErrorMutationBackupUnavailable, domain.SCUMSafeErrorMutationOfflineRequired, domain.SCUMSafeErrorMutationConfirmationMissing, domain.SCUMSafeErrorMutationPatchInvalid, domain.SCUMSafeErrorAffectedRowsMismatch, domain.SCUMSafeErrorReadbackMismatch, domain.SCUMSafeErrorRollbackFailed:
|
||||
return true
|
||||
default:
|
||||
return scumDataTargetSafeErrorPattern.MatchString(string(value))
|
||||
}
|
||||
}
|
||||
|
||||
func containsSCUMRawXML(value string) bool {
|
||||
return regexp.MustCompile(`<\s*/?\s*[A-Za-z][^>]*>`).MatchString(value)
|
||||
}
|
||||
|
||||
func validSCUMFingerprint(value string) bool { return scumHashPattern.MatchString(value) }
|
||||
func validSCUMDigest(value string) bool {
|
||||
return regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$`).MatchString(value)
|
||||
|
||||
@@ -163,6 +163,51 @@ func TestValidateSCUMTypedRCONTemplateResultRejectsUnconfirmedSuccessAndUnsafeSu
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationRequestAllowsBoundedGenericTemplate(t *testing.T) {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
|
||||
if err := ValidateSCUMGuardedMutationRequest(request); err != nil {
|
||||
t.Fatalf("expected valid guarded mutation request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationRequestRejectsRawXMLSQL855AndMissingGuards(t *testing.T) {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
request.Payload = map[string]any{"rawXml": "<CharacterTemplate><Attribute name=\"Strength\" value=\"9\" /></CharacterTemplate>", "fieldKey855": "855"}
|
||||
request.BackupEvidenceDigest = ""
|
||||
request.OfflineEvidenceDigest = ""
|
||||
request.DangerConfirmationDigest = ""
|
||||
request.ReadbackExpectationDigest = ""
|
||||
request.ReviewReason = "update sqlite:///private/tmp/SCUM.db directly"
|
||||
request.Bounds.MaxAffectedRows = 2
|
||||
|
||||
err := ValidateSCUMGuardedMutationRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "payload key is unsafe") || !strings.Contains(err.Error(), "payload value is unsafe") || !strings.Contains(err.Error(), "backupEvidenceDigest") || !strings.Contains(err.Error(), "offlineEvidenceDigest") || !strings.Contains(err.Error(), "dangerConfirmationDigest") || !strings.Contains(err.Error(), "readbackExpectationDigest") || !strings.Contains(err.Error(), "maxAffectedRows") || !strings.Contains(err.Error(), "reviewReason") {
|
||||
t.Fatalf("expected guarded mutation safety violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationResultAcceptsConfirmedSingleRowEnvelope(t *testing.T) {
|
||||
result := validatorSCUMGuardedMutationResult()
|
||||
|
||||
if err := ValidateSCUMGuardedMutationResult(result); err != nil {
|
||||
t.Fatalf("expected valid guarded mutation result, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMGuardedMutationResultRejectsMultiRowMissingReadbackAndUnsafeSummary(t *testing.T) {
|
||||
result := validatorSCUMGuardedMutationResult()
|
||||
result.AffectedRows = 2
|
||||
result.ReadbackStatus = domain.SCUMMutationReadbackUnknown
|
||||
result.ReadbackDigest = ""
|
||||
result.SafeSummary = "raw <CharacterTemplate /> leaked"
|
||||
|
||||
err := ValidateSCUMGuardedMutationResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "exactly one affected row") || !strings.Contains(err.Error(), "readback") || !strings.Contains(err.Error(), "safeSummary") {
|
||||
t.Fatalf("expected affected-row/readback/summary violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func 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 {
|
||||
@@ -196,3 +241,12 @@ func validatorSCUMTypedRCONTemplateResult() domain.SCUMTypedRCONTemplateResult {
|
||||
request := validatorSCUMTypedRCONTemplateRequest()
|
||||
return domain.SCUMTypedRCONTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TransportKey: request.TransportKey, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, PayloadDigest: request.PayloadDigest, ConfirmationDigest: request.ConfirmationDigest, TargetIdentityDigest: request.TargetIdentityDigest, ObservedAt: time.Now(), ResultDigest: scumProbeHash, ResponseDigest: scumProbeHash, ConfirmationStatus: domain.SCUMRCONConfirmationConfirmed, ConfirmationDigestID: scumProbeHash, SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
}
|
||||
|
||||
func validatorSCUMGuardedMutationRequest() domain.SCUMGuardedMutationRequest {
|
||||
return domain.SCUMGuardedMutationRequest{RequestID: "request-mutation-1", JobID: "job-mutation-1", Binding: validatorSCUMBinding(), Capability: domain.SCUMDataCapabilityProfileXMLWrite, TargetKey: "scum-mutation-db", TemplateKey: "profile.attributes.patch.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumProbeHash, AssetDigest: scumProbeHash, TargetIdentityDigest: scumProbeHash, ExpectedRowDigest: scumProbeHash, ExpectedValueDigest: scumProbeHash, ExpectedXMLDigest: scumProbeHash, PatchDigest: scumProbeHash, BackupEvidenceDigest: scumProbeHash, OfflineEvidenceDigest: scumProbeHash, DangerConfirmationDigest: scumProbeHash, ReadbackExpectationDigest: scumProbeHash, IdempotencyKey: "idem-mutation-1", Payload: map[string]any{"attributeKey": "Strength", "absoluteValue": 8.5}, ReviewReason: "operator confirmed offline profile attribute patch", Bounds: domain.DefaultSCUMGuardedMutationBounds(), RequestedAt: time.Now()}
|
||||
}
|
||||
|
||||
func validatorSCUMGuardedMutationResult() domain.SCUMGuardedMutationResult {
|
||||
request := validatorSCUMGuardedMutationRequest()
|
||||
return domain.SCUMGuardedMutationResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, SourceFingerprint: scumProbeHash, TargetIdentityDigest: request.TargetIdentityDigest, ExpectedRowDigest: request.ExpectedRowDigest, ExpectedValueDigest: request.ExpectedValueDigest, ExpectedXMLDigest: request.ExpectedXMLDigest, PatchDigest: request.PatchDigest, BackupEvidenceDigest: request.BackupEvidenceDigest, OfflineEvidenceDigest: request.OfflineEvidenceDigest, DangerConfirmationDigest: request.DangerConfirmationDigest, ReadbackExpectationDigest: request.ReadbackExpectationDigest, ObservedAt: time.Now(), ResultDigest: scumProbeHash, BeforeDigest: scumProbeHash, AfterDigest: scumProbeHash, ReadbackDigest: scumProbeHash, AffectedRows: 1, ReadbackStatus: domain.SCUMMutationReadbackConfirmed, SafeSummary: "confirmed by declared readback", SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}, Limits: request.Bounds}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user