Freeze SCUM typed RCON run contract

This commit is contained in:
npc0-hue
2026-08-13 13:37:37 +08:00
parent 467d5c3a8c
commit ac6f5419fe
17 changed files with 643 additions and 11 deletions
@@ -0,0 +1,32 @@
# Run Typed RCON Template 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 write capabilities by itself.
## Positive prompt (正向提示词)
Implement generic plugin-owned typed RCON-template execution for Run's protected RCON capability so Platform can dispatch reviewed SCUM command writes without sending browser command text, raw RCON, SQL, XML, host paths, sockets, credentials, or undeclared command keys. Success means Run accepts only a typed leased `rconTemplate` request containing logical transport/target keys, template key, adapter/schema version, immutable asset digest, canonical payload digest, confirmation digest, target identity digest, idempotency key, bounded scalar payload, safe review reason, and strict limits; renders only the packaged template; executes through generic protected RCON; performs the declared confirmation path; and returns a typed `rcon.template-command` 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 RCON transports and packaged command templates from the generated Run workspace, verify asset/payload/confirmation digests and active binding, validate scalar payload against packaged schema, render only the packaged template with bound values, enforce payload/response/confirmation limits, honor timeout and cancellation, redact rendered command text from all result envelopes, and return stable safe error codes. The terminal envelope must contain request/job/binding identity, capability, transport/target/template key, adapter version, schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, observed time, result digest, response digest, confirmation status, confirmation digest id, safe summary, safe error, and applied limits.
Expected Run verification: focused protocol/runtime tests for valid template execution, unknown template, digest mismatch, payload schema rejection, missing protected transport, timeout/cancellation, response limits, confirmation success/failure/unknown, 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 typed command or gift gates.
## Boundary prompt (任务边界)
Do not edit or vendor Run source into this browser repository, add a `run/` tree here, accept browser command text, expose rendered RCON text, expose host paths/credentials/sockets/raw XML/SQL/IPs/player identities in evidence, add SCUM-specific executor branches, branch on SCUM keys/commands/tables/gift/economy semantics, enable typed RCON templates or gift catalogs in the production SCUM manifest, enable write gates before current-service command/readback evidence and tested Run evidence are recorded, 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 `SCUMTypedRCONTemplateRequest`, `SCUMTypedRCONTemplateResult`, typed RCON bounds, terminal statuses, and confirmation statuses.
- Job-channel DTOs expose leased `executionInput.rconTemplate` to Run and parse terminal `executionResult.rconTemplate` from Run.
- Validators reject unsafe template keys, protected material, raw command-like payload keys, invalid digests, loose bounds, unsafe review reasons, unsafe summaries, unconfirmed success, and mismatched adapter/binding versions.
- Service job completion accepts `rcon.template-command` only for `remote.run.protected.rcon` jobs carrying a typed template request, requires the typed result on success, checks leased job/binding/transport/template/schema/asset/payload/confirmation/target identity, and includes typed result digests in terminal idempotency fingerprints.
## Remaining evidence required before enabling writes
- Tested Run commit and `go test ./...` output from the independent Run repository.
- Generated Run package carrying any future packaged RCON templates and immutable digests.
- Current-service evidence proving supported command semantics, confirmation/readback behavior, item aliases/transports where relevant, and safe failure outcomes.
- Active binding deployment evidence showing compatible Run executes `remote.run.protected.rcon` through the typed envelope without raw command exposure.
- Platform acceptance evidence for at least one safe typed command job against isolated test data or an explicitly authorized test player.
@@ -115,10 +115,17 @@
- Added focused tests for DTO parsing, safe validator rejection of raw SQL/path-like material and loose bounds, typed row/result validation, service lease fencing, and digest mismatch rejection. Verification passed: `go test ./dto ./validator ./service -run 'Test(RunJobResultRequestParsesSQLite|ValidateSCUMSQLiteTemplate|CoreServiceRunJobSQLiteTemplateEnvelopeIsFencedToLease)'`. - Added focused tests for DTO parsing, safe validator rejection of raw SQL/path-like material and loose bounds, typed row/result validation, service lease fencing, and digest mismatch rejection. Verification passed: `go test ./dto ./validator ./service -run 'Test(RunJobResultRequestParsesSQLite|ValidateSCUMSQLiteTemplate|CoreServiceRunJobSQLiteTemplateEnvelopeIsFencedToLease)'`.
- Recorded the separately rooted Run handoff prompt in `evidence/run-sqlite-template-execution-handoff-2026-08-13.md` with positive, directional, and boundary prompts. This is a contract handoff only; tasks 4.7, 4.9, and DB-backed read gates remain unchecked until tested Run commit/deployment/terminal-envelope evidence is recorded. - Recorded the separately rooted Run handoff prompt in `evidence/run-sqlite-template-execution-handoff-2026-08-13.md` with positive, directional, and boundary prompts. This is a contract handoff only; tasks 4.7, 4.9, and DB-backed read gates remain unchecked until tested Run commit/deployment/terminal-envelope evidence is recorded.
### Typed RCON-template contract freeze and Run handoff evidence (2026-08-13)
- Platform protocol docs now freeze the generic `rconTemplate` request and `rcon.template-command` terminal envelope: logical transport/target/template keys, adapter/schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, idempotency key, bounded scalar payload, safe review reason, response/confirmation limits, conclusive confirmation status, and stable safe result/error codes.
- Added Platform domain/DTO/job-channel/validator/service contracts for `SCUMTypedRCONTemplateRequest` and `SCUMTypedRCONTemplateResult`. Run assignments can carry only the typed template request, not browser command text; Run results can return only safe digests/status/summary; Platform verifies leased job identity, binding, transport, template, schema, asset, payload, confirmation, and target digests before accepting success.
- 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.
- [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.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.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.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.
- [ ] 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. - [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. - [ ] 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.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.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.
+1
View File
@@ -437,6 +437,7 @@ func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
assignment.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(assignment.ExecutionInput.SourceRCON) assignment.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(assignment.ExecutionInput.SourceRCON)
assignment.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(assignment.ExecutionInput.SQLiteSchemaProbe) assignment.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(assignment.ExecutionInput.SQLiteSchemaProbe)
assignment.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(assignment.ExecutionInput.SQLiteTemplate) assignment.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(assignment.ExecutionInput.SQLiteTemplate)
assignment.ExecutionInput.RCONTemplate = CopySCUMTypedRCONTemplateRequestPtr(assignment.ExecutionInput.RCONTemplate)
return assignment return assignment
} }
+4
View File
@@ -1158,6 +1158,7 @@ type JobExecutionInput struct {
ServerDeploymentPlan *ServerDeploymentPlan ServerDeploymentPlan *ServerDeploymentPlan
SQLiteSchemaProbe *SCUMSchemaProbeRequest SQLiteSchemaProbe *SCUMSchemaProbeRequest
SQLiteTemplate *SCUMSQLiteTemplateRequest SQLiteTemplate *SCUMSQLiteTemplateRequest
RCONTemplate *SCUMTypedRCONTemplateRequest
} }
type ServerDeploymentPlan struct { type ServerDeploymentPlan struct {
@@ -1202,6 +1203,7 @@ type JobExecutionResult struct {
Content string Content string
SQLiteSchemaProbe *SCUMSchemaProbeResult SQLiteSchemaProbe *SCUMSchemaProbeResult
SQLiteTemplate *SCUMSQLiteTemplateResult SQLiteTemplate *SCUMSQLiteTemplateResult
RCONTemplate *SCUMTypedRCONTemplateResult
ServerDeploymentEvidence *ServerDeploymentEvidence ServerDeploymentEvidence *ServerDeploymentEvidence
DeploymentReceipt *ServerDeploymentExecutionReceipt DeploymentReceipt *ServerDeploymentExecutionReceipt
} }
@@ -2048,8 +2050,10 @@ func CopyJob(job Job) Job {
job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan) job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)
job.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe) job.ExecutionInput.SQLiteSchemaProbe = CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe)
job.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate) job.ExecutionInput.SQLiteTemplate = CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate)
job.ExecutionInput.RCONTemplate = CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate)
job.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe) job.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
job.ExecutionResult.SQLiteTemplate = CopySCUMSQLiteTemplateResultPtr(job.ExecutionResult.SQLiteTemplate) job.ExecutionResult.SQLiteTemplate = CopySCUMSQLiteTemplateResultPtr(job.ExecutionResult.SQLiteTemplate)
job.ExecutionResult.RCONTemplate = CopySCUMTypedRCONTemplateResultPtr(job.ExecutionResult.RCONTemplate)
job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence) job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence)
job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt) job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt)
if job.ExecutionInput.Deployment != nil { if job.ExecutionInput.Deployment != nil {
+82
View File
@@ -74,6 +74,14 @@ const (
SCUMTerminalResultCancelled SCUMTerminalResultStatus = "cancelled" SCUMTerminalResultCancelled SCUMTerminalResultStatus = "cancelled"
) )
type SCUMRCONConfirmationStatus string
const (
SCUMRCONConfirmationConfirmed SCUMRCONConfirmationStatus = "confirmed"
SCUMRCONConfirmationFailed SCUMRCONConfirmationStatus = "failed"
SCUMRCONConfirmationUnknown SCUMRCONConfirmationStatus = "unknown"
)
type SCUMSafeError struct { type SCUMSafeError struct {
Code SCUMSafeErrorCode Code SCUMSafeErrorCode
Message string Message string
@@ -118,6 +126,17 @@ func DefaultSCUMSQLiteTemplateBounds() SCUMSQLiteTemplateBounds {
return SCUMSQLiteTemplateBounds{MaxParameters: 64, MaxRows: 500, TimeoutMS: 5000, BusyTimeoutMS: 250, MaxResultBytes: 1024 * 1024} return SCUMSQLiteTemplateBounds{MaxParameters: 64, MaxRows: 500, TimeoutMS: 5000, BusyTimeoutMS: 250, MaxResultBytes: 1024 * 1024}
} }
type SCUMTypedRCONTemplateBounds struct {
MaxPayloadBytes int
TimeoutMS int
MaxResponseBytes int
MaxConfirmRecords int
}
func DefaultSCUMTypedRCONTemplateBounds() SCUMTypedRCONTemplateBounds {
return SCUMTypedRCONTemplateBounds{MaxPayloadBytes: 2048, TimeoutMS: 5000, MaxResponseBytes: 16 * 1024, MaxConfirmRecords: 16}
}
type SCUMSchemaProbeRequest struct { type SCUMSchemaProbeRequest struct {
RequestID string RequestID string
JobID string JobID string
@@ -142,6 +161,27 @@ type SCUMSQLiteTemplateRequest struct {
RequestedAt time.Time RequestedAt time.Time
} }
type SCUMTypedRCONTemplateRequest struct {
RequestID string
JobID string
Binding SCUMBindingIdentity
Capability SCUMDataCapability
TransportKey string
TargetKey string
TemplateKey string
AdapterVersion string
RequiredSchemaFingerprint string
AssetDigest string
PayloadDigest string
ConfirmationDigest string
TargetIdentityDigest string
IdempotencyKey string
Payload map[string]any
ReviewReason string
Bounds SCUMTypedRCONTemplateBounds
RequestedAt time.Time
}
type SCUMSchemaProbeDeclaration struct { type SCUMSchemaProbeDeclaration struct {
Capability string Capability string
TargetKey string TargetKey string
@@ -231,6 +271,31 @@ type SCUMSQLiteTemplateResult struct {
Limits SCUMSQLiteTemplateBounds Limits SCUMSQLiteTemplateBounds
} }
type SCUMTypedRCONTemplateResult struct {
RequestID string
JobID string
Binding SCUMBindingIdentity
Status SCUMTerminalResultStatus
Capability SCUMDataCapability
TransportKey string
TargetKey string
TemplateKey string
AdapterVersion string
SchemaFingerprint string
AssetDigest string
PayloadDigest string
ConfirmationDigest string
TargetIdentityDigest string
ObservedAt time.Time
ResultDigest string
ResponseDigest string
ConfirmationStatus SCUMRCONConfirmationStatus
ConfirmationDigestID string
SafeSummary string
SafeError SCUMSafeError
Limits SCUMTypedRCONTemplateBounds
}
type SCUMCapabilityRequirement struct { type SCUMCapabilityRequirement struct {
Capability SCUMDataCapability Capability SCUMDataCapability
AdapterVersion string AdapterVersion string
@@ -332,6 +397,15 @@ func CopySCUMSQLiteTemplateRequestPtr(value *SCUMSQLiteTemplateRequest) *SCUMSQL
return &copy return &copy
} }
func CopySCUMTypedRCONTemplateRequestPtr(value *SCUMTypedRCONTemplateRequest) *SCUMTypedRCONTemplateRequest {
if value == nil {
return nil
}
copy := *value
copy.Payload = CopySCUMValueMap(value.Payload)
return &copy
}
func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResult { func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLiteTemplateResult {
if value == nil { if value == nil {
return nil return nil
@@ -341,6 +415,14 @@ func CopySCUMSQLiteTemplateResultPtr(value *SCUMSQLiteTemplateResult) *SCUMSQLit
return &copy return &copy
} }
func CopySCUMTypedRCONTemplateResultPtr(value *SCUMTypedRCONTemplateResult) *SCUMTypedRCONTemplateResult {
if value == nil {
return nil
}
copy := *value
return &copy
}
func CopySCUMValueMap(value map[string]any) map[string]any { func CopySCUMValueMap(value map[string]any) map[string]any {
if value == nil { if value == nil {
return nil return nil
+23 -2
View File
@@ -113,6 +113,7 @@ type RunJobExecutionInputBody struct {
ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"` ServerDeploymentPlan *ServerDeploymentPlanBody `json:"serverDeploymentPlan,omitempty"`
SQLiteSchemaProbe *RunSQLiteSchemaProbeRequestBody `json:"sqliteSchemaProbe,omitempty"` SQLiteSchemaProbe *RunSQLiteSchemaProbeRequestBody `json:"sqliteSchemaProbe,omitempty"`
SQLiteTemplate *RunSQLiteTemplateRequestBody `json:"sqliteTemplate,omitempty"` SQLiteTemplate *RunSQLiteTemplateRequestBody `json:"sqliteTemplate,omitempty"`
RCONTemplate *RunTypedRCONTemplateRequestBody `json:"rconTemplate,omitempty"`
} }
type RunSQLiteSchemaProbeRequestBody struct { type RunSQLiteSchemaProbeRequestBody struct {
@@ -135,6 +136,25 @@ type RunSQLiteTemplateRequestBody struct {
Limits SCUMSQLiteTemplateBoundsDTO `json:"limits"` Limits SCUMSQLiteTemplateBoundsDTO `json:"limits"`
} }
type RunTypedRCONTemplateRequestBody struct {
RequestID string `json:"requestId"`
Binding SCUMBindingIdentityDTO `json:"binding"`
Capability string `json:"capability"`
TransportKey string `json:"transportKey"`
TargetKey string `json:"targetKey"`
TemplateKey string `json:"templateKey"`
AdapterVersion string `json:"adapterVersion"`
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
AssetDigest string `json:"assetDigest"`
PayloadDigest string `json:"payloadDigest"`
ConfirmationDigest string `json:"confirmationDigest"`
TargetIdentityDigest string `json:"targetIdentityDigest"`
IdempotencyKey string `json:"idempotencyKey"`
Payload map[string]any `json:"payload,omitempty"`
ReviewReason string `json:"reviewReason"`
Limits SCUMTypedRCONTemplateBoundsDTO `json:"limits"`
}
type ServerDeploymentPlanBody struct { type ServerDeploymentPlanBody struct {
SchemaVersion string `json:"schemaVersion"` SchemaVersion string `json:"schemaVersion"`
Operation string `json:"operation"` Operation string `json:"operation"`
@@ -213,6 +233,7 @@ type RunJobExecutionResultBody struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"` SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
SQLiteTemplate *SCUMSQLiteTemplateResultDTO `json:"sqliteTemplate,omitempty"` SQLiteTemplate *SCUMSQLiteTemplateResultDTO `json:"sqliteTemplate,omitempty"`
RCONTemplate *SCUMTypedRCONTemplateResultDTO `json:"rconTemplate,omitempty"`
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"` ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"` DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"`
} }
@@ -472,7 +493,7 @@ func (request RunJobResultRequest) ToDomain() domain.RunJobResult {
Message: request.Message, Message: request.Message,
ErrorCode: request.ErrorCode, ErrorCode: request.ErrorCode,
Retryable: request.Retryable, Retryable: request.Retryable,
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrToDomain(request.ExecutionResult.SQLiteSchemaProbe), SQLiteTemplate: SCUMSQLiteTemplateResultPtrToDomain(request.ExecutionResult.SQLiteTemplate), ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)}, ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrToDomain(request.ExecutionResult.SQLiteSchemaProbe), SQLiteTemplate: SCUMSQLiteTemplateResultPtrToDomain(request.ExecutionResult.SQLiteTemplate), RCONTemplate: SCUMTypedRCONTemplateResultPtrToDomain(request.ExecutionResult.RCONTemplate), ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
} }
} }
@@ -687,7 +708,7 @@ func RunJobAssignmentFromDomain(assignment domain.RunJobAssignment) RunJobAssign
State: assignment.State, State: assignment.State,
Progress: progressReportFromDomain(assignment.Progress), Progress: progressReportFromDomain(assignment.Progress),
ResultRef: assignment.ResultRef, ResultRef: assignment.ResultRef,
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: runSQLiteSchemaProbeRequestPtrFromDomain(assignment.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: runSQLiteTemplateRequestPtrFromDomain(assignment.ExecutionInput.SQLiteTemplate)}, ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: runSQLiteSchemaProbeRequestPtrFromDomain(assignment.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: runSQLiteTemplateRequestPtrFromDomain(assignment.ExecutionInput.SQLiteTemplate), RCONTemplate: runTypedRCONTemplateRequestPtrFromDomain(assignment.ExecutionInput.RCONTemplate)},
LeaseToken: assignment.LeaseToken, LeaseToken: assignment.LeaseToken,
Attempt: assignment.Attempt, Attempt: assignment.Attempt,
FencingToken: assignment.FencingToken, FencingToken: assignment.FencingToken,
+47
View File
@@ -109,6 +109,53 @@ func TestRunJobResultRequestParsesSQLiteTemplateEnvelope(t *testing.T) {
} }
} }
func TestRunJobResultRequestParsesTypedRCONTemplateEnvelope(t *testing.T) {
payload := `{
"runEndpointId":"run-local",
"sessionToken":"run-session",
"jobId":"job-rcon",
"leaseToken":"lease-rcon",
"attempt":1,
"state":"succeeded",
"progress":{"percent":100,"message":"done"},
"executionResult":{
"kind":"rcon.template-command",
"rconTemplate":{
"requestId":"request-rcon",
"jobId":"job-rcon",
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
"status":"succeeded",
"capability":"economy-command.write",
"transportKey":"scum-rcon",
"targetKey":"scum-rcon",
"templateKey":"economy.fame.set.v1",
"adapterVersion":"scum-live-data-v0",
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
"payloadDigest":"sha256:` + strings.Repeat("e", 64) + `",
"confirmationDigest":"sha256:` + strings.Repeat("f", 64) + `",
"targetIdentityDigest":"sha256:` + strings.Repeat("c", 64) + `",
"observedAt":"2026-08-13T00:00:00Z",
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
"responseDigest":"sha256:` + strings.Repeat("1", 64) + `",
"confirmationStatus":"confirmed",
"confirmationDigestId":"sha256:` + strings.Repeat("2", 64) + `",
"safeSummary":"confirmed by declared readback",
"limits":{"maxPayloadBytes":2048,"timeoutMs":5000,"maxResponseBytes":16384,"maxConfirmRecords":16}
}
}
}`
var request RunJobResultRequest
if err := json.Unmarshal([]byte(payload), &request); err != nil {
t.Fatalf("unmarshal Run job result: %v", err)
}
domainRequest := request.ToDomain()
result := domainRequest.ExecutionResult.RCONTemplate
if result == nil || result.TemplateKey != "economy.fame.set.v1" || result.PayloadDigest != "sha256:"+strings.Repeat("e", 64) || result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed {
t.Fatalf("rconTemplate envelope did not parse: %+v", result)
}
}
func TestAIProviderFromDomainCopiesModels(t *testing.T) { func TestAIProviderFromDomainCopiesModels(t *testing.T) {
provider := domain.AIProvider{ provider := domain.AIProvider{
ID: "ai.openai", ID: "ai.openai",
+108
View File
@@ -36,6 +36,13 @@ type SCUMSQLiteTemplateBoundsDTO struct {
MaxResultBytes int `json:"maxResultBytes"` MaxResultBytes int `json:"maxResultBytes"`
} }
type SCUMTypedRCONTemplateBoundsDTO struct {
MaxPayloadBytes int `json:"maxPayloadBytes"`
TimeoutMS int `json:"timeoutMs"`
MaxResponseBytes int `json:"maxResponseBytes"`
MaxConfirmRecords int `json:"maxConfirmRecords"`
}
type SCUMSchemaProbeRequestDTO struct { type SCUMSchemaProbeRequestDTO struct {
RequestID string `json:"requestId"` RequestID string `json:"requestId"`
JobID string `json:"jobId"` JobID string `json:"jobId"`
@@ -60,6 +67,27 @@ type SCUMSQLiteTemplateRequestDTO struct {
RequestedAt time.Time `json:"requestedAt"` RequestedAt time.Time `json:"requestedAt"`
} }
type SCUMTypedRCONTemplateRequestDTO struct {
RequestID string `json:"requestId"`
JobID string `json:"jobId"`
Binding SCUMBindingIdentityDTO `json:"binding"`
Capability string `json:"capability"`
TransportKey string `json:"transportKey"`
TargetKey string `json:"targetKey"`
TemplateKey string `json:"templateKey"`
AdapterVersion string `json:"adapterVersion"`
RequiredSchemaFingerprint string `json:"requiredSchemaFingerprint,omitempty"`
AssetDigest string `json:"assetDigest"`
PayloadDigest string `json:"payloadDigest"`
ConfirmationDigest string `json:"confirmationDigest"`
TargetIdentityDigest string `json:"targetIdentityDigest"`
IdempotencyKey string `json:"idempotencyKey"`
Payload map[string]any `json:"payload,omitempty"`
ReviewReason string `json:"reviewReason"`
Bounds SCUMTypedRCONTemplateBoundsDTO `json:"bounds"`
RequestedAt time.Time `json:"requestedAt"`
}
type SCUMSchemaProbeDispatchRequest struct { type SCUMSchemaProbeDispatchRequest struct {
IdempotencyKey string `json:"idempotencyKey"` IdempotencyKey string `json:"idempotencyKey"`
} }
@@ -142,6 +170,31 @@ type SCUMSQLiteTemplateResultDTO struct {
Limits SCUMSQLiteTemplateBoundsDTO `json:"limits"` Limits SCUMSQLiteTemplateBoundsDTO `json:"limits"`
} }
type SCUMTypedRCONTemplateResultDTO struct {
RequestID string `json:"requestId"`
JobID string `json:"jobId"`
Binding SCUMBindingIdentityDTO `json:"binding"`
Status string `json:"status"`
Capability string `json:"capability"`
TransportKey string `json:"transportKey"`
TargetKey string `json:"targetKey"`
TemplateKey string `json:"templateKey"`
AdapterVersion string `json:"adapterVersion"`
SchemaFingerprint string `json:"schemaFingerprint,omitempty"`
AssetDigest string `json:"assetDigest"`
PayloadDigest string `json:"payloadDigest"`
ConfirmationDigest string `json:"confirmationDigest"`
TargetIdentityDigest string `json:"targetIdentityDigest"`
ObservedAt time.Time `json:"observedAt"`
ResultDigest string `json:"resultDigest"`
ResponseDigest string `json:"responseDigest,omitempty"`
ConfirmationStatus string `json:"confirmationStatus"`
ConfirmationDigestID string `json:"confirmationDigestId,omitempty"`
SafeSummary string `json:"safeSummary,omitempty"`
SafeError SCUMSafeErrorDTO `json:"safeError,omitempty"`
Limits SCUMTypedRCONTemplateBoundsDTO `json:"limits"`
}
type SCUMCapabilityGateDTO struct { type SCUMCapabilityGateDTO struct {
Capability string `json:"capability"` Capability string `json:"capability"`
State string `json:"state"` State string `json:"state"`
@@ -182,6 +235,22 @@ func SCUMSQLiteTemplateRequestPtrFromDomain(value *domain.SCUMSQLiteTemplateRequ
return &result return &result
} }
func SCUMTypedRCONTemplateRequestToDomain(value SCUMTypedRCONTemplateRequestDTO) domain.SCUMTypedRCONTemplateRequest {
return domain.SCUMTypedRCONTemplateRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Capability: domain.SCUMDataCapability(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumTypedRCONTemplateBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
}
func SCUMTypedRCONTemplateRequestFromDomain(value domain.SCUMTypedRCONTemplateRequest) SCUMTypedRCONTemplateRequestDTO {
return SCUMTypedRCONTemplateRequestDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Bounds: scumTypedRCONTemplateBoundsFromDomain(value.Bounds), RequestedAt: value.RequestedAt}
}
func SCUMTypedRCONTemplateRequestPtrFromDomain(value *domain.SCUMTypedRCONTemplateRequest) *SCUMTypedRCONTemplateRequestDTO {
if value == nil {
return nil
}
result := SCUMTypedRCONTemplateRequestFromDomain(*value)
return &result
}
func runSQLiteSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *RunSQLiteSchemaProbeRequestBody { func runSQLiteSchemaProbeRequestPtrFromDomain(value *domain.SCUMSchemaProbeRequest) *RunSQLiteSchemaProbeRequestBody {
if value == nil { if value == nil {
return nil return nil
@@ -196,6 +265,13 @@ func runSQLiteTemplateRequestPtrFromDomain(value *domain.SCUMSQLiteTemplateReque
return &RunSQLiteTemplateRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, Parameters: domain.CopySCUMValueMap(value.Parameters), Limits: scumSQLiteTemplateBoundsFromDomain(value.Bounds)} return &RunSQLiteTemplateRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, ParameterDigest: value.ParameterDigest, Parameters: domain.CopySCUMValueMap(value.Parameters), Limits: scumSQLiteTemplateBoundsFromDomain(value.Bounds)}
} }
func runTypedRCONTemplateRequestPtrFromDomain(value *domain.SCUMTypedRCONTemplateRequest) *RunTypedRCONTemplateRequestBody {
if value == nil {
return nil
}
return &RunTypedRCONTemplateRequestBody{RequestID: value.RequestID, Binding: scumBindingIdentityFromDomain(value.Binding), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, RequiredSchemaFingerprint: value.RequiredSchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, IdempotencyKey: value.IdempotencyKey, Payload: domain.CopySCUMValueMap(value.Payload), ReviewReason: value.ReviewReason, Limits: scumTypedRCONTemplateBoundsFromDomain(value.Bounds)}
}
func SCUMSchemaProbeDispatchFromDomain(request domain.SCUMSchemaProbeRequest, queued domain.RemoteAdapterResult) SCUMSchemaProbeDispatchResponse { func SCUMSchemaProbeDispatchFromDomain(request domain.SCUMSchemaProbeRequest, queued domain.RemoteAdapterResult) SCUMSchemaProbeDispatchResponse {
return SCUMSchemaProbeDispatchResponse{ProbeRequest: SCUMSchemaProbeRequestFromDomain(request), QueuedJob: RemoteAdapterFromDomain(queued)} return SCUMSchemaProbeDispatchResponse{ProbeRequest: SCUMSchemaProbeRequestFromDomain(request), QueuedJob: RemoteAdapterFromDomain(queued)}
} }
@@ -240,6 +316,18 @@ func SCUMSQLiteTemplateResultPtrToDomain(value *SCUMSQLiteTemplateResultDTO) *do
return &result return &result
} }
func SCUMTypedRCONTemplateResultToDomain(value SCUMTypedRCONTemplateResultDTO) domain.SCUMTypedRCONTemplateResult {
return domain.SCUMTypedRCONTemplateResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMTerminalResultStatus(value.Status), Capability: domain.SCUMDataCapability(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, ResponseDigest: value.ResponseDigest, ConfirmationStatus: domain.SCUMRCONConfirmationStatus(value.ConfirmationStatus), ConfirmationDigestID: value.ConfirmationDigestID, SafeSummary: value.SafeSummary, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumTypedRCONTemplateBoundsToDomain(value.Limits)}
}
func SCUMTypedRCONTemplateResultPtrToDomain(value *SCUMTypedRCONTemplateResultDTO) *domain.SCUMTypedRCONTemplateResult {
if value == nil {
return nil
}
result := SCUMTypedRCONTemplateResultToDomain(*value)
return &result
}
func SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSchemaProbeResultDTO { func SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSchemaProbeResultDTO {
value = domain.CopySCUMSchemaProbeResult(value) value = domain.CopySCUMSchemaProbeResult(value)
objects := make([]SCUMSchemaObjectEvidenceDTO, len(value.Objects)) objects := make([]SCUMSchemaObjectEvidenceDTO, len(value.Objects))
@@ -282,6 +370,18 @@ func SCUMSQLiteTemplateResultPtrFromDomain(value *domain.SCUMSQLiteTemplateResul
return &result return &result
} }
func SCUMTypedRCONTemplateResultFromDomain(value domain.SCUMTypedRCONTemplateResult) SCUMTypedRCONTemplateResultDTO {
return SCUMTypedRCONTemplateResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), Capability: string(value.Capability), TransportKey: value.TransportKey, TargetKey: value.TargetKey, TemplateKey: value.TemplateKey, AdapterVersion: value.AdapterVersion, SchemaFingerprint: value.SchemaFingerprint, AssetDigest: value.AssetDigest, PayloadDigest: value.PayloadDigest, ConfirmationDigest: value.ConfirmationDigest, TargetIdentityDigest: value.TargetIdentityDigest, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, ResponseDigest: value.ResponseDigest, ConfirmationStatus: string(value.ConfirmationStatus), ConfirmationDigestID: value.ConfirmationDigestID, SafeSummary: value.SafeSummary, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumTypedRCONTemplateBoundsFromDomain(value.Limits)}
}
func SCUMTypedRCONTemplateResultPtrFromDomain(value *domain.SCUMTypedRCONTemplateResult) *SCUMTypedRCONTemplateResultDTO {
if value == nil {
return nil
}
result := SCUMTypedRCONTemplateResultFromDomain(*value)
return &result
}
func SCUMCapabilityGateFromDomain(value domain.SCUMCapabilityGate) SCUMCapabilityGateDTO { 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} return SCUMCapabilityGateDTO{Capability: string(value.Capability), State: string(value.State), Enabled: value.Enabled, ReasonCode: string(value.ReasonCode), Reason: value.Reason}
} }
@@ -309,3 +409,11 @@ func scumSQLiteTemplateBoundsToDomain(value SCUMSQLiteTemplateBoundsDTO) domain.
func scumSQLiteTemplateBoundsFromDomain(value domain.SCUMSQLiteTemplateBounds) SCUMSQLiteTemplateBoundsDTO { func scumSQLiteTemplateBoundsFromDomain(value domain.SCUMSQLiteTemplateBounds) SCUMSQLiteTemplateBoundsDTO {
return SCUMSQLiteTemplateBoundsDTO{MaxParameters: value.MaxParameters, MaxRows: value.MaxRows, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxResultBytes: value.MaxResultBytes} return SCUMSQLiteTemplateBoundsDTO{MaxParameters: value.MaxParameters, MaxRows: value.MaxRows, TimeoutMS: value.TimeoutMS, BusyTimeoutMS: value.BusyTimeoutMS, MaxResultBytes: value.MaxResultBytes}
} }
func scumTypedRCONTemplateBoundsToDomain(value SCUMTypedRCONTemplateBoundsDTO) domain.SCUMTypedRCONTemplateBounds {
return domain.SCUMTypedRCONTemplateBounds{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, MaxResponseBytes: value.MaxResponseBytes, MaxConfirmRecords: value.MaxConfirmRecords}
}
func scumTypedRCONTemplateBoundsFromDomain(value domain.SCUMTypedRCONTemplateBounds) SCUMTypedRCONTemplateBoundsDTO {
return SCUMTypedRCONTemplateBoundsDTO{MaxPayloadBytes: value.MaxPayloadBytes, TimeoutMS: value.TimeoutMS, MaxResponseBytes: value.MaxResponseBytes, MaxConfirmRecords: value.MaxConfirmRecords}
}
@@ -61,6 +61,26 @@ The request never contains raw SQL, host/database paths, DSNs, sockets, credenti
Platform accepts rows only when the terminal envelope matches the leased durable job's binding, template key, adapter/schema fingerprint, asset digest, and parameter digest. Late, duplicate, mismatched, stale, unsafe, over-limit, or schema-invalid results remain safe terminal failures and must not be converted into empty successful generations. Platform accepts rows only when the terminal envelope matches the leased durable job's binding, template key, adapter/schema fingerprint, asset digest, and parameter digest. Late, duplicate, mismatched, stale, unsafe, over-limit, or schema-invalid results remain safe terminal failures and must not be converted into empty successful generations.
## Typed RCON template request
`SCUMTypedRCONTemplateRequest` is the Platform durable-job payload for plugin-owned command templates after a write capability is proven and reviewed. Required fields are:
- `requestId`, `jobId`;
- `binding`: `serverInstanceId`, `runBindingId`, `runEndpointId`, `pluginId`, `pluginVersion`, `adapterVersion`, `gameVersion`, `databaseIdentity`;
- `capability`, limited to verified typed RCON write capabilities such as economy-command or gift-command writes;
- logical `transportKey`, `targetKey`, `templateKey`, `adapterVersion`, optional `requiredSchemaFingerprint`, immutable `assetDigest`, canonical `payloadDigest`, `confirmationDigest`, and `targetIdentityDigest`;
- `idempotencyKey`, scalar `payload` validated against the plugin-declared payload schema, and safe `reviewReason`;
- `bounds`: `maxPayloadBytes`, `timeoutMs`, `maxResponseBytes`, and `maxConfirmRecords`;
- `requestedAt`.
The request never contains browser command text, raw RCON, SQL, XML, host/database paths, DSNs, sockets, credentials, or undeclared command keys. Run resolves the template and protected RCON transport inside the generated Run package.
## Typed RCON template result
`SCUMTypedRCONTemplateResult` is the terminal envelope for `rcon.template-command` results. Required fields are request/job/binding identity, status (`succeeded`, `failed`, or `cancelled`), write capability, transport/target/template key, adapter version, schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, observed time, result digest, response digest, confirmation status, confirmation digest id, safe summary, safe error, and limits actually applied.
Platform accepts write success only when the envelope matches the leased durable job and the declared confirmation status is conclusive. Missing, mismatched, stale, unsafe, partial, timed-out, cancelled, or schema-invalid confirmations remain failed or unknown outcomes; they must not update local verified facts or trigger automatic redelivery.
## Release behavior ## 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. The first-party SCUM plugin declares `scumLiveData` with `remote.run.db.sqlite.probe` and per-capability gates. Until current-service evidence exists, all gates remain `disabled` with `evidenceStatus: missing`. Query assets, RCON templates, XML mutations, map transforms, and gift transports may be added only after current-service probe evidence proves their adapter requirements; unsupported or ambiguous capabilities stay disabled independently.
+5 -1
View File
@@ -42,7 +42,11 @@ Run and Platform discard raw IP addresses and other network identifiers before d
SCUM command writes use only plugin-owned typed command templates. Platform dispatches a template key, adapter version, digest, target identity, idempotency key, validated parameters, and review reason through the durable job channel. SCUM command writes use only plugin-owned typed command templates. Platform dispatches a template key, adapter version, digest, target identity, idempotency key, validated parameters, and review reason through the durable job channel.
Run never accepts browser command text or branches on SCUM command names. A write is successful only after the declared confirmation path returns schema-valid conclusive evidence; missing, partial, mismatched, cancelled, or timed-out confirmation is reported as failed, partial, or unknown rather than success. The leased Run assignment carries a typed `rconTemplate` request only. Required fields are `requestId`, server/plugin binding, write capability, logical `transportKey`, logical `targetKey`, `templateKey`, `adapterVersion`, optional `requiredSchemaFingerprint`, immutable `assetDigest`, canonical `payloadDigest`, `confirmationDigest`, `targetIdentityDigest`, idempotency key, bounded scalar payload, review reason, and limits containing `maxPayloadBytes`, `timeoutMs`, `maxResponseBytes`, and `maxConfirmRecords`. The payload carries no browser command text, raw RCON, SQL, XML, host path, socket, credential, or undeclared command key.
Run resolves the packaged command template and protected RCON transport from the generated Run package, verifies the asset/payload/confirmation digests and active binding, renders only the packaged template with bound scalar payload values, executes through generic protected RCON, and performs only the declared confirmation path. Run never accepts browser command text, exposes the rendered command in result envelopes, or branches on SCUM command names, SCUM keys, SCUM commands, SCUM tables, or gift/economy semantics.
The terminal `rcon.template-command` envelope contains `requestId`, `jobId`, binding, status (`succeeded`, `failed`, or `cancelled`), capability, transport/target/template key, adapter version, schema fingerprint when required, asset digest, payload digest, confirmation digest, target identity digest, observed time, result digest, response digest, confirmation status, confirmation digest id, safe summary, safe error, and applied limits. A write is successful only after the declared confirmation path returns schema-valid conclusive evidence; missing, partial, mismatched, cancelled, or timed-out confirmation is reported as failed, partial, or unknown rather than success.
## Guarded SQLite/XML Mutation Jobs ## Guarded SQLite/XML Mutation Jobs
+21 -2
View File
@@ -358,6 +358,21 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
return err return err
} }
} }
case domain.JobCapabilityRemoteRunProtectedRCON:
if job.ExecutionInput.RCONTemplate == nil {
break
}
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumRCONTemplateExecutionKind {
return validationError("typed RCON template result type is invalid")
}
if result.State == domain.JobStateSucceeded {
if result.ExecutionResult.RCONTemplate == nil {
return validationError("typed RCON template terminal result is required")
}
if err := validateSCUMTypedRCONTemplateResultForJob(job, *result.ExecutionResult.RCONTemplate); err != nil {
return err
}
}
case domain.JobCapabilityClientManagerDeploy: case domain.JobCapabilityClientManagerDeploy:
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" { if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" {
return validationError("client-manager deploy result type is invalid") return validationError("client-manager deploy result type is invalid")
@@ -663,7 +678,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
State: job.State, State: job.State,
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message}, Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
ResultRef: job.ResultRef, ResultRef: job.ResultRef,
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: domain.CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate)}, ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(job.ExecutionInput.SQLiteSchemaProbe), SQLiteTemplate: domain.CopySCUMSQLiteTemplateRequestPtr(job.ExecutionInput.SQLiteTemplate), RCONTemplate: domain.CopySCUMTypedRCONTemplateRequestPtr(job.ExecutionInput.RCONTemplate)},
LeaseToken: leaseToken, LeaseToken: leaseToken,
Attempt: job.Attempt, Attempt: job.Attempt,
FencingToken: fencingToken, FencingToken: fencingToken,
@@ -749,7 +764,11 @@ func terminalFingerprint(result domain.RunJobResult) string {
if result.ExecutionResult.SQLiteTemplate != nil { if result.ExecutionResult.SQLiteTemplate != nil {
sqliteTemplateFingerprint = fmt.Sprintf("%s|%s|%s|%d", result.ExecutionResult.SQLiteTemplate.ResultDigest, result.ExecutionResult.SQLiteTemplate.AssetDigest, result.ExecutionResult.SQLiteTemplate.ParameterDigest, result.ExecutionResult.SQLiteTemplate.RowCount) sqliteTemplateFingerprint = fmt.Sprintf("%s|%s|%s|%d", result.ExecutionResult.SQLiteTemplate.ResultDigest, result.ExecutionResult.SQLiteTemplate.AssetDigest, result.ExecutionResult.SQLiteTemplate.ParameterDigest, result.ExecutionResult.SQLiteTemplate.RowCount)
} }
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t|%s|%s|%s|%s", result.State, result.Progress.Percent, result.ResultRef, result.Message, result.ErrorCode, result.Progress.Message, result.Retryable, result.ExecutionResult.Kind, result.ExecutionResult.Checksum, schemaProbeFingerprint, sqliteTemplateFingerprint) rconTemplateFingerprint := ""
if result.ExecutionResult.RCONTemplate != nil {
rconTemplateFingerprint = fmt.Sprintf("%s|%s|%s|%s|%s", result.ExecutionResult.RCONTemplate.ResultDigest, result.ExecutionResult.RCONTemplate.AssetDigest, result.ExecutionResult.RCONTemplate.PayloadDigest, result.ExecutionResult.RCONTemplate.ConfirmationDigest, result.ExecutionResult.RCONTemplate.ConfirmationStatus)
}
return fmt.Sprintf("%s|%d|%s|%s|%s|%s|%t|%s|%s|%s|%s|%s", result.State, result.Progress.Percent, result.ResultRef, result.Message, result.ErrorCode, result.Progress.Message, result.Retryable, result.ExecutionResult.Kind, result.ExecutionResult.Checksum, schemaProbeFingerprint, sqliteTemplateFingerprint, rconTemplateFingerprint)
} }
func terminalMessage(result domain.RunJobResult) string { func terminalMessage(result domain.RunJobResult) string {
+50 -2
View File
@@ -248,6 +248,42 @@ func TestCoreServiceRunJobSQLiteTemplateEnvelopeIsFencedToLease(t *testing.T) {
} }
} }
func TestCoreServiceRunJobTypedRCONTemplateEnvelopeIsFencedToLease(t *testing.T) {
svc, sessionToken := newRegisteredRunJobService(t)
request := scumTypedRCONTemplateTestRequest()
createSCUMTemplateServerFixture(t, svc, request.Binding)
job, err := svc.CreateJob(domain.Job{ID: request.JobID, ServerInstanceID: request.Binding.ServerInstanceID, RunEndpointID: request.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedRCON, TargetKey: request.TargetKey, InputRef: "input://rcon-template/request-rcon", IdempotencyKey: request.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-rcon", SourceRCON: scumTypedRCONSourcePlan(), RCONTemplate: &request}})
if err != nil {
t.Fatalf("create typed RCON job: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedRCON}, Capacity: domain.RunCapacity{MaxJobs: 4}})
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.RCONTemplate == nil || claim.Job.ExecutionInput.RCONTemplate.Payload["absoluteValue"].(float64) != 100.0 {
t.Fatalf("claim typed RCON job: claim=%+v err=%v", claim, err)
}
result := scumTypedRCONTemplateTestResult(request)
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: request.Binding.RunEndpointID, SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "rcon complete"}, Message: "rcon complete", ExecutionResult: domain.JobExecutionResult{Kind: scumRCONTemplateExecutionKind, RCONTemplate: &result, AuditSummary: "redacted typed RCON template command"}}); err != nil {
t.Fatalf("complete matching typed RCON job: %v", err)
}
badRequest := request
badRequest.RequestID = "request-rcon-bad"
badRequest.JobID = "job-rcon-bad"
badRequest.IdempotencyKey = "idem-rcon-bad"
if _, err := svc.CreateJob(domain.Job{ID: badRequest.JobID, ServerInstanceID: badRequest.Binding.ServerInstanceID, RunEndpointID: badRequest.Binding.RunEndpointID, Capability: domain.JobCapabilityRemoteRunProtectedRCON, TargetKey: badRequest.TargetKey, InputRef: "input://rcon-template/request-rcon-bad", IdempotencyKey: badRequest.IdempotencyKey, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{RemoteAdapterKind: "protected-rcon", SourceRCON: scumTypedRCONSourcePlan(), RCONTemplate: &badRequest}}); err != nil {
t.Fatalf("create bad typed RCON job: %v", err)
}
badClaim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedRCON}, Capacity: domain.RunCapacity{MaxJobs: 4}})
if err != nil {
t.Fatalf("claim bad typed RCON job: %v", err)
}
badResult := scumTypedRCONTemplateTestResult(badRequest)
badResult.PayloadDigest = "sha256:" + strings.Repeat("b", 64)
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: badRequest.Binding.RunEndpointID, SessionToken: sessionToken, JobID: badClaim.Job.JobID, LeaseToken: badClaim.Job.LeaseToken, Attempt: badClaim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "rcon complete"}, Message: "rcon complete", ExecutionResult: domain.JobExecutionResult{Kind: scumRCONTemplateExecutionKind, RCONTemplate: &badResult, AuditSummary: "redacted typed RCON template command"}})
if err == nil || !strings.Contains(err.Error(), "digest") {
t.Fatalf("expected digest-fenced typed RCON result rejection, got %v", err)
}
}
func TestCoreServiceRunJobReconcile(t *testing.T) { func TestCoreServiceRunJobReconcile(t *testing.T) {
svc, sessionToken := newRegisteredRunJobService(t) svc, sessionToken := newRegisteredRunJobService(t)
createQueuedRunJob(t, svc, "job-1", "idem-1") createQueuedRunJob(t, svc, "job-1", "idem-1")
@@ -278,7 +314,7 @@ func newRegisteredRunJobService(t *testing.T) (*CoreService, string) {
t.Helper() t.Helper()
svc := newTestCoreService() svc := newTestCoreService()
helloRequest := validRunControlHello() helloRequest := validRunControlHello()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start", domain.JobCapabilityRemoteRunDBSQLiteQuery) helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.start", domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON)
helloRequest.CapabilityReport.Fingerprint = "cap-jobs" helloRequest.CapabilityReport.Fingerprint = "cap-jobs"
hello, err := svc.RegisterRunHello(helloRequest) hello, err := svc.RegisterRunHello(helloRequest)
if err != nil { if err != nil {
@@ -293,9 +329,17 @@ func scumTemplateTestBinding() domain.SCUMBindingIdentity {
return domain.SCUMBindingIdentity{ServerInstanceID: "server-scum", RunBindingID: "binding-scum", RunEndpointID: "run-local", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "scum-database"} return domain.SCUMBindingIdentity{ServerInstanceID: "server-scum", RunBindingID: "binding-scum", RunEndpointID: "run-local", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "scum-database"}
} }
func scumTypedRCONTemplateTestRequest() domain.SCUMTypedRCONTemplateRequest {
return domain.SCUMTypedRCONTemplateRequest{RequestID: "request-rcon", JobID: "job-rcon", Binding: scumTemplateTestBinding(), Capability: domain.SCUMDataCapabilityEconomyCommand, TransportKey: "scum-rcon", TargetKey: "scum-rcon", TemplateKey: "economy.fame.set.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumTemplateTestHash(), AssetDigest: scumTemplateTestHash(), PayloadDigest: scumTemplateTestHash(), ConfirmationDigest: scumTemplateTestHash(), TargetIdentityDigest: scumTemplateTestHash(), IdempotencyKey: "idem-rcon", Payload: map[string]any{"externalPlayerId": "player-redacted", "absoluteValue": 100.0}, ReviewReason: "operator reviewed absolute fame update", Bounds: domain.DefaultSCUMTypedRCONTemplateBounds(), RequestedAt: time.Now()}
}
func scumTypedRCONSourcePlan() *domain.RuntimeSourceRCONPlan {
return &domain.RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/scum-rcon/release.json", Port: 27015}
}
func createSCUMTemplateServerFixture(t *testing.T, svc *CoreService, binding domain.SCUMBindingIdentity) { func createSCUMTemplateServerFixture(t *testing.T, svc *CoreService, binding domain.SCUMBindingIdentity) {
t.Helper() t.Helper()
if _, err := svc.CreateGamePlugin(domain.GamePlugin{ID: binding.PluginID, Name: "SCUM", Version: binding.PluginVersion, ServerType: "scum", ManifestRef: "artifact://manifests/game.scum/0.1.6", CreateFormSchemaRef: "artifact://schemas/game.scum/create-form/0.1.6", RequiredRunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}, DeclaredPermissions: []string{"server.remote.access"}, Permissions: domain.PluginPermissions{RemoteAccess: true}}); err != nil { if _, err := svc.CreateGamePlugin(domain.GamePlugin{ID: binding.PluginID, Name: "SCUM", Version: binding.PluginVersion, ServerType: "scum", ManifestRef: "artifact://manifests/game.scum/0.1.6", CreateFormSchemaRef: "artifact://schemas/game.scum/create-form/0.1.6", RequiredRunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunProtectedRCON}, DeclaredPermissions: []string{"server.remote.access", "server.game-client.command"}, Permissions: domain.PluginPermissions{RemoteAccess: true}}); err != nil {
t.Fatalf("create SCUM plugin fixture: %v", err) 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 { if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: binding.ServerInstanceID, PluginID: binding.PluginID, RunEndpointID: binding.RunEndpointID, Name: "SCUM"}); err != nil {
@@ -307,6 +351,10 @@ func scumTemplateTestResult(request domain.SCUMSQLiteTemplateRequest) domain.SCU
return domain.SCUMSQLiteTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, ParameterDigest: request.ParameterDigest, SourceFingerprint: scumTemplateTestHash(), ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), RowCount: 1, Rows: []map[string]any{{"externalPlayerId": "player-redacted", "displayName": "Known Player"}}, Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}} return domain.SCUMSQLiteTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, ParameterDigest: request.ParameterDigest, SourceFingerprint: scumTemplateTestHash(), ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), RowCount: 1, Rows: []map[string]any{{"externalPlayerId": "player-redacted", "displayName": "Known Player"}}, Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
} }
func scumTypedRCONTemplateTestResult(request domain.SCUMTypedRCONTemplateRequest) domain.SCUMTypedRCONTemplateResult {
return domain.SCUMTypedRCONTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TransportKey: request.TransportKey, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, PayloadDigest: request.PayloadDigest, ConfirmationDigest: request.ConfirmationDigest, TargetIdentityDigest: request.TargetIdentityDigest, ObservedAt: time.Now(), ResultDigest: scumTemplateTestHash(), ResponseDigest: scumTemplateTestHash(), ConfirmationStatus: domain.SCUMRCONConfirmationConfirmed, ConfirmationDigestID: scumTemplateTestHash(), SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
}
func createQueuedRunJob(t *testing.T, svc *CoreService, id string, idempotencyKey string) domain.Job { func createQueuedRunJob(t *testing.T, svc *CoreService, id string, idempotencyKey string) domain.Job {
t.Helper() t.Helper()
job, err := svc.CreateJob(domain.Job{ job, err := svc.CreateJob(domain.Job{
+24
View File
@@ -12,6 +12,7 @@ import (
const ( const (
scumSchemaProbeExecutionKind = "sqlite.schema-probe" scumSchemaProbeExecutionKind = "sqlite.schema-probe"
scumSQLiteTemplateExecutionKind = "sqlite.template-query" scumSQLiteTemplateExecutionKind = "sqlite.template-query"
scumRCONTemplateExecutionKind = "rcon.template-command"
) )
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) { func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
@@ -172,6 +173,29 @@ func validateSCUMSQLiteTemplateResultForJob(job domain.Job, result domain.SCUMSQ
return nil return nil
} }
func validateSCUMTypedRCONTemplateResultForJob(job domain.Job, result domain.SCUMTypedRCONTemplateResult) error {
if err := validator.ValidateSCUMTypedRCONTemplateResult(result); err != nil {
return err
}
expected := job.ExecutionInput.RCONTemplate
if expected == nil {
return validationError("typed RCON template request is missing from leased job")
}
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
return validationError("typed RCON template result does not match leased job identity")
}
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
return validationError("typed RCON template result does not match leased binding identity")
}
if result.Capability != expected.Capability || result.TransportKey != expected.TransportKey || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.AssetDigest != expected.AssetDigest || result.PayloadDigest != expected.PayloadDigest || result.ConfirmationDigest != expected.ConfirmationDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest {
return validationError("typed RCON template result does not match leased template, target, digest, or payload identity")
}
if expected.RequiredSchemaFingerprint != "" && result.SchemaFingerprint != expected.RequiredSchemaFingerprint {
return validationError("typed RCON template result does not match leased schema fingerprint")
}
return nil
}
func sameSCUMSchemaProbeBinding(a, b domain.SCUMBindingIdentity) bool { 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 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
@@ -61,6 +61,11 @@ func ValidateRunJobResult(result domain.RunJobResult) error {
violations = append(violations, "executionResult.sqliteTemplate: "+err.Error()) violations = append(violations, "executionResult.sqliteTemplate: "+err.Error())
} }
} }
if result.ExecutionResult.RCONTemplate != nil {
if err := ValidateSCUMTypedRCONTemplateResult(*result.ExecutionResult.RCONTemplate); err != nil {
violations = append(violations, "executionResult.rconTemplate: "+err.Error())
}
}
return finish(violations) return finish(violations)
} }
+16
View File
@@ -1541,6 +1541,17 @@ func ValidateJob(job domain.Job) error {
violations = append(violations, "executionInput.sqliteTemplate is allowed only for sqlite query jobs") violations = append(violations, "executionInput.sqliteTemplate is allowed only for sqlite query jobs")
} }
} }
if job.ExecutionInput.RCONTemplate != nil {
if err := ValidateSCUMTypedRCONTemplateRequest(*job.ExecutionInput.RCONTemplate); err != nil {
violations = append(violations, "executionInput.rconTemplate: "+err.Error())
}
if job.Capability != domain.JobCapabilityRemoteRunProtectedRCON {
violations = append(violations, "executionInput.rconTemplate is allowed only for protected rcon jobs")
}
if job.ExecutionInput.SourceRCON == nil || job.ExecutionInput.RemoteAdapterKind != "protected-rcon" {
violations = append(violations, "executionInput.rconTemplate requires a protected rcon transport plan")
}
}
violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...) violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...)
if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) { if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) {
violations = append(violations, "executionResult.checksum must be sha256:<hex>") violations = append(violations, "executionResult.checksum must be sha256:<hex>")
@@ -1561,6 +1572,11 @@ func ValidateJob(job domain.Job) error {
violations = append(violations, "executionResult.sqliteTemplate: "+err.Error()) violations = append(violations, "executionResult.sqliteTemplate: "+err.Error())
} }
} }
if job.ExecutionResult.RCONTemplate != nil {
if err := ValidateSCUMTypedRCONTemplateResult(*job.ExecutionResult.RCONTemplate); err != nil {
violations = append(violations, "executionResult.rconTemplate: "+err.Error())
}
}
if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite { if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite {
if job.ServerInstanceID == "" { if job.ServerInstanceID == "" {
violations = append(violations, "serverInstanceId is required for scoped file jobs") violations = append(violations, "serverInstanceId is required for scoped file jobs")
+149 -3
View File
@@ -1,6 +1,7 @@
package validator package validator
import ( import (
"encoding/json"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@@ -20,6 +21,9 @@ const (
maxSCUMTemplateRows = 1000 maxSCUMTemplateRows = 1000
maxSCUMTemplateBusyTimeoutMS = 1000 maxSCUMTemplateBusyTimeoutMS = 1000
maxSCUMTemplateValueBytes = 4096 maxSCUMTemplateValueBytes = 4096
maxSCUMRCONPayloadBytes = 4096
maxSCUMRCONResponseBytes = 64 * 1024
maxSCUMRCONConfirmRecords = 128
) )
var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`) var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`)
@@ -66,6 +70,42 @@ func ValidateSCUMSQLiteTemplateRequest(request domain.SCUMSQLiteTemplateRequest)
return finish(violations) return finish(violations)
} }
func ValidateSCUMTypedRCONTemplateRequest(request domain.SCUMTypedRCONTemplateRequest) error {
var violations []string
violations = appendRequired(violations, "requestId", request.RequestID)
violations = appendRequired(violations, "jobId", request.JobID)
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
if !validSCUMRCONWriteCapability(request.Capability) {
violations = append(violations, "capability must be a typed RCON write capability")
}
violations = append(violations, validateSCUMTemplateKey("transportKey", request.TransportKey)...)
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
violations = append(violations, "adapterVersion contains protected material")
}
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
violations = append(violations, "adapterVersion must match binding.adapterVersion")
}
if request.RequiredSchemaFingerprint != "" && !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
}
for _, item := range []struct{ name, value string }{{"assetDigest", request.AssetDigest}, {"payloadDigest", request.PayloadDigest}, {"confirmationDigest", request.ConfirmationDigest}, {"targetIdentityDigest", request.TargetIdentityDigest}} {
if !validSCUMDigest(item.value) {
violations = append(violations, item.name+" must be sha256 digest")
}
}
violations = append(violations, validateSCUMTemplateKey("idempotencyKey", request.IdempotencyKey)...)
if strings.TrimSpace(request.ReviewReason) == "" || len(request.ReviewReason) > 320 || containsSCUMProtectedMaterial(request.ReviewReason) {
violations = append(violations, "reviewReason is unsafe")
}
violations = append(violations, validateSCUMTypedRCONTemplateBounds("bounds", request.Bounds)...)
violations = append(violations, validateSCUMValueMap("payload", request.Payload, 64)...)
violations = append(violations, validateSCUMJSONSize("payload", request.Payload, request.Bounds.MaxPayloadBytes)...)
return finish(violations)
}
func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error { func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
var violations []string var violations []string
violations = appendRequired(violations, "requestId", result.RequestID) violations = appendRequired(violations, "requestId", result.RequestID)
@@ -154,6 +194,62 @@ func ValidateSCUMSQLiteTemplateResult(result domain.SCUMSQLiteTemplateResult) er
return finish(violations) return finish(violations)
} }
func ValidateSCUMTypedRCONTemplateResult(result domain.SCUMTypedRCONTemplateResult) error {
var violations []string
violations = appendRequired(violations, "requestId", result.RequestID)
violations = appendRequired(violations, "jobId", result.JobID)
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
if !validSCUMTerminalResultStatus(result.Status) {
violations = append(violations, "status is invalid")
}
if !validSCUMRCONWriteCapability(result.Capability) {
violations = append(violations, "capability must be a typed RCON write capability")
}
violations = append(violations, validateSCUMTemplateKey("transportKey", result.TransportKey)...)
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
violations = append(violations, "adapterVersion contains protected material")
}
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
violations = append(violations, "adapterVersion must match binding.adapterVersion")
}
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
}
for _, item := range []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"payloadDigest", result.PayloadDigest}, {"confirmationDigest", result.ConfirmationDigest}, {"targetIdentityDigest", result.TargetIdentityDigest}, {"resultDigest", result.ResultDigest}} {
if !validSCUMDigest(item.value) {
violations = append(violations, item.name+" must be sha256 digest")
}
}
if result.ResponseDigest != "" && !validSCUMDigest(result.ResponseDigest) {
violations = append(violations, "responseDigest must be sha256 digest")
}
if result.ConfirmationDigestID != "" && !validSCUMDigest(result.ConfirmationDigestID) {
violations = append(violations, "confirmationDigestId must be sha256 digest")
}
if !validSCUMRCONConfirmationStatus(result.ConfirmationStatus) {
violations = append(violations, "confirmationStatus is invalid")
}
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) {
violations = append(violations, "safeSummary is unsafe")
}
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
violations = append(violations, validateSCUMTypedRCONTemplateBounds("limits", result.Limits)...)
if result.Status == domain.SCUMTerminalResultSucceeded {
if result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed || result.ResponseDigest == "" || result.ConfirmationDigestID == "" {
violations = append(violations, "succeeded result requires confirmed response and confirmation digests")
}
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
violations = append(violations, "succeeded result must not carry an error code")
}
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
violations = append(violations, "non-succeeded result requires a safe error code")
}
return finish(violations)
}
func ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error { func ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error {
var violations []string var violations []string
if !validSCUMDataCapability(evidence.Capability) { if !validSCUMDataCapability(evidence.Capability) {
@@ -328,10 +424,27 @@ func validateSCUMSQLiteTemplateBounds(prefix string, value domain.SCUMSQLiteTemp
return violations return violations
} }
func validateSCUMTypedRCONTemplateBounds(prefix string, value domain.SCUMTypedRCONTemplateBounds) []string {
var violations []string
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMRCONPayloadBytes {
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
}
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
violations = append(violations, prefix+".timeoutMs is out of bounds")
}
if value.MaxResponseBytes < 1 || value.MaxResponseBytes > maxSCUMRCONResponseBytes {
violations = append(violations, prefix+".maxResponseBytes is out of bounds")
}
if value.MaxConfirmRecords < 1 || value.MaxConfirmRecords > maxSCUMRCONConfirmRecords {
violations = append(violations, prefix+".maxConfirmRecords is out of bounds")
}
return violations
}
func validateSCUMTemplateKey(prefix, value string) []string { func validateSCUMTemplateKey(prefix, value string) []string {
var violations []string var violations []string
violations = appendRequired(violations, prefix, value) violations = appendRequired(violations, prefix, value)
if value != "" && (!scumTemplateKeyPattern.MatchString(value) || containsSCUMProtectedMaterial(value) || strings.Contains(value, "..")) { if value != "" && (!scumTemplateKeyPattern.MatchString(value) || strings.Contains(value, "..")) {
violations = append(violations, prefix+" is unsafe") violations = append(violations, prefix+" is unsafe")
} }
return violations return violations
@@ -343,7 +456,8 @@ func validateSCUMValueMap(prefix string, values map[string]any, maxItems int) []
violations = append(violations, prefix+" exceeds declared limit") violations = append(violations, prefix+" exceeds declared limit")
} }
for key, value := range values { for key, value := range values {
if !scumTemplateKeyPattern.MatchString(key) || containsSCUMProtectedMaterial(key) || strings.Contains(key, "..") { loweredKey := strings.ToLower(key)
if !scumTemplateKeyPattern.MatchString(key) || containsSCUMProtectedMaterial(key) || strings.Contains(key, "..") || strings.Contains(loweredKey, "command") || strings.Contains(loweredKey, "rcon") {
violations = append(violations, prefix+" key is unsafe") violations = append(violations, prefix+" key is unsafe")
} }
field := prefix + ".value" field := prefix + ".value"
@@ -370,6 +484,20 @@ func validateSCUMValue(prefix string, value any) []string {
return violations return violations
} }
func validateSCUMJSONSize(prefix string, value any, maxBytes int) []string {
if maxBytes <= 0 {
return []string{prefix + " max byte limit is required"}
}
payload, err := json.Marshal(value)
if err != nil {
return []string{prefix + " must be JSON serializable"}
}
if len(payload) > maxBytes {
return []string{prefix + " exceeds declared byte limit"}
}
return nil
}
func validateSCUMSchemaObjectEvidence(prefix string, value domain.SCUMSchemaObjectEvidence) []string { func validateSCUMSchemaObjectEvidence(prefix string, value domain.SCUMSchemaObjectEvidence) []string {
var violations []string var violations []string
if !validSCUMFingerprint(value.ObjectHash) { if !validSCUMFingerprint(value.ObjectHash) {
@@ -449,6 +577,15 @@ func validSCUMReadCapability(value domain.SCUMDataCapability) bool {
} }
} }
func validSCUMRCONWriteCapability(value domain.SCUMDataCapability) bool {
switch value {
case domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
return true
default:
return false
}
}
func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool { func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
switch value { switch value {
case domain.SCUMTerminalResultSucceeded, domain.SCUMTerminalResultFailed, domain.SCUMTerminalResultCancelled: case domain.SCUMTerminalResultSucceeded, domain.SCUMTerminalResultFailed, domain.SCUMTerminalResultCancelled:
@@ -458,6 +595,15 @@ func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
} }
} }
func validSCUMRCONConfirmationStatus(value domain.SCUMRCONConfirmationStatus) bool {
switch value {
case domain.SCUMRCONConfirmationConfirmed, domain.SCUMRCONConfirmationFailed, domain.SCUMRCONConfirmationUnknown:
return true
default:
return false
}
}
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool { func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
switch value { switch value {
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed: case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
@@ -505,7 +651,7 @@ func containsSCUMProtectedMaterial(value string) bool {
if strings.HasPrefix(lowered, "sqlite://") || strings.HasPrefix(lowered, "mysql://") || strings.HasPrefix(lowered, "file://") || strings.HasPrefix(lowered, "tcp://") || strings.HasPrefix(lowered, "unix://") { if strings.HasPrefix(lowered, "sqlite://") || strings.HasPrefix(lowered, "mysql://") || strings.HasPrefix(lowered, "file://") || strings.HasPrefix(lowered, "tcp://") || strings.HasPrefix(lowered, "unix://") {
return true return true
} }
if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\\\") || regexp.MustCompile(`^[A-Za-z]:[\\/]`).MatchString(trimmed) { if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\\\") || regexp.MustCompile(`[A-Za-z]:[\\/]`).MatchString(trimmed) {
return true return true
} }
return false return false
+48
View File
@@ -124,6 +124,45 @@ func TestValidateSCUMSQLiteTemplateResultRejectsUnsafeRowsAndMismatchedCounts(t
} }
} }
func TestValidateSCUMTypedRCONTemplateRequestAllowsBoundedGenericTemplate(t *testing.T) {
request := validatorSCUMTypedRCONTemplateRequest()
if err := ValidateSCUMTypedRCONTemplateRequest(request); err != nil {
t.Fatalf("expected valid typed RCON template request, got %v", err)
}
}
func TestValidateSCUMTypedRCONTemplateRequestRejectsRawCommandAndLooseBounds(t *testing.T) {
request := validatorSCUMTypedRCONTemplateRequest()
request.Payload = map[string]any{"rawCommand": "#SetFamePoints 7 100"}
request.ReviewReason = `use C:\SCUM\secret.txt`
request.Bounds.MaxPayloadBytes = 100000
err := ValidateSCUMTypedRCONTemplateRequest(request)
if err == nil || !strings.Contains(err.Error(), "payload key is unsafe") || !strings.Contains(err.Error(), "reviewReason is unsafe") || !strings.Contains(err.Error(), "maxPayloadBytes") {
t.Fatalf("expected raw command/reason/bounds violations, got %v", err)
}
}
func TestValidateSCUMTypedRCONTemplateResultAcceptsConfirmedEnvelope(t *testing.T) {
result := validatorSCUMTypedRCONTemplateResult()
if err := ValidateSCUMTypedRCONTemplateResult(result); err != nil {
t.Fatalf("expected valid typed RCON template result, got %v", err)
}
}
func TestValidateSCUMTypedRCONTemplateResultRejectsUnconfirmedSuccessAndUnsafeSummary(t *testing.T) {
result := validatorSCUMTypedRCONTemplateResult()
result.ConfirmationStatus = domain.SCUMRCONConfirmationUnknown
result.SafeSummary = "rcon password leaked"
err := ValidateSCUMTypedRCONTemplateResult(result)
if err == nil || !strings.Contains(err.Error(), "confirmed") || !strings.Contains(err.Error(), "safeSummary") {
t.Fatalf("expected confirmation and summary violations, got %v", err)
}
}
func TestValidateSCUMCapabilityEvidenceRequiresSafeCurrentServiceIdentity(t *testing.T) { 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}} 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 { if err := ValidateSCUMCapabilityEvidence(evidence); err != nil {
@@ -148,3 +187,12 @@ func validatorSCUMSQLiteTemplateResult() domain.SCUMSQLiteTemplateResult {
request := validatorSCUMSQLiteTemplateRequest() request := validatorSCUMSQLiteTemplateRequest()
return domain.SCUMSQLiteTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, ParameterDigest: request.ParameterDigest, SourceFingerprint: scumProbeHash, ObservedAt: time.Now(), ResultDigest: scumProbeHash, RowCount: 1, Rows: []map[string]any{{"externalPlayerId": "player-redacted", "displayName": "Known Player", "fame": 12.5, "online": true, "squadId": nil}}, Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}} return domain.SCUMSQLiteTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, ParameterDigest: request.ParameterDigest, SourceFingerprint: scumProbeHash, ObservedAt: time.Now(), ResultDigest: scumProbeHash, RowCount: 1, Rows: []map[string]any{{"externalPlayerId": "player-redacted", "displayName": "Known Player", "fame": 12.5, "online": true, "squadId": nil}}, Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
} }
func validatorSCUMTypedRCONTemplateRequest() domain.SCUMTypedRCONTemplateRequest {
return domain.SCUMTypedRCONTemplateRequest{RequestID: "request-rcon-1", JobID: "job-rcon-1", Binding: validatorSCUMBinding(), Capability: domain.SCUMDataCapabilityEconomyCommand, TransportKey: "scum-rcon", TargetKey: "scum-rcon", TemplateKey: "economy.fame.set.v1", AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumProbeHash, AssetDigest: scumProbeHash, PayloadDigest: scumProbeHash, ConfirmationDigest: scumProbeHash, TargetIdentityDigest: scumProbeHash, IdempotencyKey: "idem-rcon-1", Payload: map[string]any{"externalPlayerId": "player-redacted", "absoluteValue": 100.0}, ReviewReason: "operator reviewed absolute fame update", Bounds: domain.DefaultSCUMTypedRCONTemplateBounds(), RequestedAt: time.Now()}
}
func validatorSCUMTypedRCONTemplateResult() domain.SCUMTypedRCONTemplateResult {
request := validatorSCUMTypedRCONTemplateRequest()
return domain.SCUMTypedRCONTemplateResult{RequestID: request.RequestID, JobID: request.JobID, Binding: request.Binding, Status: domain.SCUMTerminalResultSucceeded, Capability: request.Capability, TransportKey: request.TransportKey, TargetKey: request.TargetKey, TemplateKey: request.TemplateKey, AdapterVersion: request.AdapterVersion, SchemaFingerprint: request.RequiredSchemaFingerprint, AssetDigest: request.AssetDigest, PayloadDigest: request.PayloadDigest, ConfirmationDigest: request.ConfirmationDigest, TargetIdentityDigest: request.TargetIdentityDigest, ObservedAt: time.Now(), ResultDigest: scumProbeHash, ResponseDigest: scumProbeHash, ConfirmationStatus: domain.SCUMRCONConfirmationConfirmed, ConfirmationDigestID: scumProbeHash, SafeSummary: "confirmed by declared readback", Limits: request.Bounds, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
}