Freeze SCUM guarded mutation run contract

This commit is contained in:
npc0-hue
2026-08-13 14:04:44 +08:00
parent ac6f5419fe
commit 41c109128d
17 changed files with 741 additions and 37 deletions
@@ -0,0 +1,32 @@
# Run Guarded SQLite/XML Mutation Execution Handoff (2026-08-13)
This is a browser-repository handoff for a separately authorized task in the independent Run repository `git@git.npc0.com:admin343/run.git`. It is not Run implementation or deployment evidence, and it does not enable SCUM database/XML write capabilities by itself.
## Positive prompt (正向提示词)
Implement generic plugin-owned guarded single-row SQLite/XML mutation execution for Run's protected SQL capability so Platform can dispatch reviewed SCUM profile-XML writes without sending raw SQL, raw XML, browser mutation text, host paths, sockets, credentials, table/column overrides, raw rows, `fieldKey=855`, or undeclared patch fields. Success means Run accepts only a typed leased `guardedMutation` request containing logical target/template keys, adapter/schema version, immutable asset digest, target identity digest, expected row/value/XML digests, patch digest, backup/offline/danger-confirmation evidence digests, readback expectation digest, idempotency key, bounded scalar payload, safe review reason, and strict limits; applies the packaged preserving patch inside a bounded transaction; rolls back on guard mismatch, malformed XML, zero/multiple affected rows, or failed readback; and returns a typed `sqlite.guarded-mutation` terminal envelope that Platform can validate against the original durable job.
## Directional prompt (方向提示词)
Work only in the independent Run repository. Preserve Run as a generic executor: resolve protected SQLite targets and packaged mutation templates from the generated Run workspace, verify active binding, adapter/schema fingerprint, asset/target/guard/patch/backup/offline/confirmation/readback digests, validate scalar payload against packaged schema, open only the package-declared SQLite target or fenced current-service snapshot as permitted by the generated plan, enforce timeout/busy/readback/payload limits, execute a bounded transaction, use a preserving XML patcher that changes only allowlisted named attributes or existing nodes, preserve unknown XML content, reject absent undeclared nodes instead of synthesizing them, update exactly one guarded row, and read back conclusive digest evidence before success.
Expected Run verification: focused protocol/runtime tests for valid single-row mutation, unknown template, digest mismatch, stale expected row/value/XML guard, missing backup/offline/danger confirmation, malformed XML, absent node, zero-row update rollback, multi-row rollback, timeout/cancellation, readback mismatch/unknown, result-size limit, duplicate/late terminal result behavior, and `go test ./...` from the Run repository root. After implementation, record the tested Run commit, distribution/deployment evidence for the active binding, and safe terminal-envelope evidence back in this browser-repository change before enabling guarded mutation gates or declaring production XML mutation assets.
## Boundary prompt (任务边界)
Do not edit or vendor Run source into this browser repository, add a `run/` tree here, accept raw SQL/XML/browser mutation text, expose host paths/credentials/sockets/raw XML/SQL/IPs/player identities in evidence, add SCUM-specific executor branches, branch on SCUM keys/tables/profile names/`855` semantics, synthesize missing XML nodes, rewrite XML from incomplete structs, update more than one row, skip same-instance backup/offline/danger-confirmation/readback guards, enable `855`, enable production guarded mutation assets, or treat this handoff as product acceptance evidence. Run must remain game-agnostic and execute only package-declared generic assets under the active signed binding and lease.
## Browser-side frozen contract
- Platform domain/DTO contracts define `SCUMGuardedMutationRequest`, `SCUMGuardedMutationResult`, guarded mutation bounds, terminal statuses, mutation readback statuses, and stable safe error codes for guard/readback/rollback failures.
- Job-channel DTOs expose leased `executionInput.guardedMutation` to Run and parse terminal `executionResult.guardedMutation` from Run.
- Validators reject unsafe template keys, protected material, raw XML, SQL/path-like material, `855` field payloads, missing schema/asset/identity/value/XML/patch/backup/offline/danger-confirmation/readback digests, loose affected-row bounds, unsafe review reasons, unsafe summaries, multi-row success, and missing conclusive readback.
- Service job completion accepts `sqlite.guarded-mutation` only for `remote.run.protected.sql` jobs carrying a typed guarded mutation request, requires the typed result on success, checks leased job/binding/template/schema/asset/target/guard/patch/backup/offline/confirmation/readback identity, and includes typed result digests in terminal idempotency fingerprints.
## Remaining evidence required before enabling mutations
- Tested Run commit and `go test ./...` output from the independent Run repository.
- Generated Run package carrying any future packaged mutation template, preserving XML patcher declaration, schema/payload/result/readback schemas, immutable asset digests, and logical SQLite target binding.
- Current-service evidence proving the exact source row/XML payload, named attribute mapping, preserving patch semantics, same-instance backup/restore evidence, offline/maintenance requirements, readback behavior, and safe failure outcomes.
- Active binding deployment evidence showing compatible Run executes `remote.run.protected.sql` through the typed guarded envelope without raw SQL/XML exposure.
- Platform acceptance evidence against isolated test data or an explicitly authorized test player proving permission, explicit confirmation, guards, backup/offline requirements, idempotency, readback, unknown-result handling, XML preservation, and no implicit respawn/death/kick activation.
@@ -122,11 +122,19 @@
- Added focused tests for DTO parsing, validator rejection of raw command-like payload keys, unsafe review reasons, loose bounds, unconfirmed success, unsafe summaries, service lease fencing, and payload digest mismatch rejection. Verification passed: `go test ./dto ./validator ./service -run 'Test(RunJobResultRequestParses(SQLite|TypedRCON)|ValidateSCUM(SQLiteTemplate|TypedRCON)|CoreServiceRunJob(SQLiteTemplate|TypedRCONTemplate)EnvelopeIsFencedToLease)'`.
- Recorded the separately rooted Run handoff prompt in `evidence/run-typed-rcon-template-execution-handoff-2026-08-13.md` with positive, directional, and boundary prompts. This is a contract handoff only; typed RCON templates, gift catalogs, write gates, and task 4.7 remain unchecked until tested Run commit/deployment/current-service command/readback evidence is recorded.
### Guarded SQLite/XML mutation contract freeze and Run handoff evidence (2026-08-13)
- Platform protocol docs now freeze the generic `guardedMutation` request and `sqlite.guarded-mutation` terminal envelope: logical target/template key, adapter/schema fingerprint, asset digest, target identity digest, expected row/value/XML digests, preserving patch digest, backup/offline/danger-confirmation evidence digests, readback expectation digest, idempotency key, bounded scalar payload, safe review reason, single-row limit, affected-row count, readback status, and stable safe result/error codes.
- Added Platform domain/DTO/job-channel/validator/service contracts for `SCUMGuardedMutationRequest` and `SCUMGuardedMutationResult`. Run assignments can carry only the typed guarded mutation request, not raw SQL/XML/browser mutation text; Run results can return only safe digests/status/summary; Platform verifies leased job identity, binding, template, schema, asset, target, guard, patch, backup, offline, danger confirmation, and readback digests before accepting success.
- Added focused tests for DTO parsing, validator rejection of raw XML/SQL/path-like material, `855` field payloads, missing backup/offline/danger-confirmation/readback digests, loose affected-row bounds, unsafe summaries, multi-row success, missing readback, service lease fencing, and patch digest mismatch rejection. Verification passed: `go test ./dto ./validator ./service -run 'Test(RunJobResultRequestParses(SQLite|TypedRCON|GuardedMutation)|ValidateSCUM(SQLiteTemplate|TypedRCON|GuardedMutation)|CoreServiceRunJob(SQLiteTemplate|TypedRCONTemplate|GuardedMutation)EnvelopeIsFencedToLease)'`.
- Recorded the separately rooted Run handoff prompt in `evidence/run-guarded-sqlite-xml-mutation-handoff-2026-08-13.md` with positive, directional, and boundary prompts. This is a contract handoff only; no guarded XML mutation asset, `855` preset, write gate, external Run implementation evidence, or real-service mutation acceptance is enabled by this task.
- Session verification also passed: `(cd platform && go test ./...)`, `scripts/check-structure.sh`, `openspec validate replace-scum-projections-with-real-data-management --strict`, and `git diff --check`.
- [x] 4.1 Add Platform protocol contracts under `platform/protocol`, API DTOs under `platform/dto`, validation under `platform/validator`, and plugin contracts/assets under `plugins/sdk` and `plugins/schemas`, plus contract documentation/mocks for probes, read-only template execution, typed RCON, guarded SQLite/XML mutation, parsed log events, and terminal result envelopes.
- [x] 4.2 Freeze the generic executor/result contract and hand off a separately authorized Run-repository task for packaged SQLite-template execution with query-only connections, bound parameters, one-statement validation, short busy/operation timeouts, cancellation, and row/result-byte limits.
- [x] 4.3 Require the independent Run task to return typed envelopes containing server/plugin binding, adapter/schema version, template key, asset digest, job identity, observed time, checksum, rows or affected-row count, and stable safe result/error codes.
- [x] 4.4 Require the independent Run task to implement generic plugin-owned typed RCON-template execution without accepting browser command text or adding branches for SCUM, SCUM keys, SCUM commands, or SCUM tables.
- [ ] 4.5 Require the independent Run task to implement generic guarded single-row SQLite/XML mutation execution with expected identity/value/checksum guards, a bounded transaction, preserving XML patching, rollback on zero/multiple affected rows, and read-after-write confirmation.
- [x] 4.5 Require the independent Run task to implement generic guarded single-row SQLite/XML mutation execution with expected identity/value/checksum guards, a bounded transaction, preserving XML patching, rollback on zero/multiple affected rows, and read-after-write confirmation.
- [ ] 4.6 Require the independent Run task to implement or extend generic plugin-declared log-source tailing so cursor persistence, rotation, truncate, restart, partial-line buffering, parser digest fencing, logical event fingerprinting, and replay remain independent of SCUM-specific source paths.
- [ ] 4.7 Verify from the independent Run task's acceptance evidence that control/job/log/artifact priorities, leases, fencing, acknowledgements, idempotency, and late/duplicate terminal-result handling remain intact for the new generic capabilities.
- [ ] 4.8 Add Platform-side capability negotiation so probe, player/squad/vehicle/flag/position reads, typed commands, gifts, and guarded mutations are gated independently for each active Run/plugin/adapter binding.
+1
View File
@@ -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
}
+4
View File
@@ -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 {
+131 -26
View File
@@ -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 &copy
}
func CopySCUMGuardedMutationRequestPtr(value *SCUMGuardedMutationRequest) *SCUMGuardedMutationRequest {
if value == nil {
return nil
}
copy := *value
copy.Payload = CopySCUMValueMap(value.Payload)
return &copy
}
func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResult {
if value == nil {
return nil
@@ -423,6 +520,14 @@ func CopySCUMTypedRCONTemplateResultPtr(value *SCUMTypedRCONTemplateResult) *SCU
return &copy
}
func CopySCUMGuardedMutationResultPtr(value *SCUMGuardedMutationResult) *SCUMGuardedMutationResult {
if value == nil {
return nil
}
copy := *value
return &copy
}
func CopySCUMValueMap(value map[string]any) map[string]any {
if value == nil {
return nil
+28 -2
View File
@@ -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,
+55
View File
@@ -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",
+122
View File
@@ -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
+21 -2
View File
@@ -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 {
+46 -2
View File
@@ -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{
+24 -3
View File
@@ -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
}
+5
View File
@@ -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)
}
+19
View File
@@ -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")
+166 -1
View File
@@ -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)
+54
View File
@@ -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}
}