feat(scum): wire platform schema probe jobs

This commit is contained in:
npc0-hue
2026-08-12 11:06:14 +08:00
parent 6d4319187f
commit 2638eda55b
15 changed files with 405 additions and 13 deletions
@@ -13,6 +13,12 @@
- The executor advertises `remote.run.db.sqlite.probe`, accepts only package-scoped logical `databases/...` SQLite targets, applies query-only fixed introspection plus binding/job/fence and output bounds, and returns SHA-256-fingerprinted redacted envelopes without SCUM-specific branches or raw database content. - The executor advertises `remote.run.db.sqlite.probe`, accepts only package-scoped logical `databases/...` SQLite targets, applies query-only fixed introspection plus binding/job/fence and output bounds, and returns SHA-256-fingerprinted redacted envelopes without SCUM-specific branches or raw database content.
- The active binding `server-run-server-scum-1785923898033` has not yet been rebuilt and deployed from that commit, and its current capability report does not include `remote.run.db.sqlite.probe`. Keep every probe/read/write gate disabled and do not submit a current-service probe until deployment compatibility is verified. - The active binding `server-run-server-scum-1785923898033` has not yet been rebuilt and deployed from that commit, and its current capability report does not include `remote.run.db.sqlite.probe`. Keep every probe/read/write gate disabled and do not submit a current-service probe until deployment compatibility is verified.
### Server-management diagnostic evidence (2026-08-12)
- Personal server-management MCP inventory found `青火网络` (`OqXCYpgMSCZyslYA`) as the matching candidate server, and `test_connection` succeeded.
- Bounded SSH diagnostics checked only process/service metadata, emitted no command lines, paths, SQL, database content, credentials, or SCUM rows, and found no independent process for active endpoint `server-run-server-scum-1785923898033`; a generic `run` process/service name was also absent.
- Because the active Windows Run deployment still cannot be proven to advertise `remote.run.db.sqlite.probe`, no Platform durable schema probe was submitted and current-service evidence tasks remain blocked.
- [x] 2.1 Add a release gate that keeps every database-backed SCUM read and write capability disabled until capability-specific current-service evidence matches a versioned plugin adapter; do not add production SQL or mutation assets before this group is complete. - [x] 2.1 Add a release gate that keeps every database-backed SCUM read and write capability disabled until capability-specific current-service evidence matches a versioned plugin adapter; do not add production SQL or mutation assets before this group is complete.
- [x] 2.2 Define the minimal generic schema-probe request/result contract, safe error model, binding identity, bounds, and redacted evidence DTO needed by Platform and the plugin without embedding SCUM table names or host paths in Run-facing generic code. - [x] 2.2 Define the minimal generic schema-probe request/result contract, safe error model, binding identity, bounds, and redacted evidence DTO needed by Platform and the plugin without embedding SCUM table names or host paths in Run-facing generic code.
- [ ] 2.3 If the active binding lacks the minimal bounded query-only probe executor, create/hand off a separately authorized task rooted in the independent Run repository, wait for its tests/commit/deployment evidence, and record that evidence here; do not edit or vendor Run source from this change. - [ ] 2.3 If the active binding lacks the minimal bounded query-only probe executor, create/hand off a separately authorized task rooted in the independent Run repository, wait for its tests/commit/deployment evidence, and record that evidence here; do not edit or vendor Run source from this change.
@@ -41,6 +47,12 @@
## 4. Generic External Run Execution and Result Contracts ## 4. Generic External Run Execution and Result Contracts
### Platform probe wiring evidence (2026-08-12)
- Platform now has an internal-only `RequestSCUMSchemaProbeForSession` path that builds a durable `remote.run.db.sqlite.probe` job from the SCUM plugin's manifest declaration, active runtime binding, logical target key, adapter version, and bounded probe limits; public remote-adapter and plugin-page requests for the probe capability are denied.
- Run terminal results may carry `executionResult.sqliteSchemaProbe`; Platform DTO/domain/validator/job-channel code validates the typed redacted envelope, job/request identity, and binding fence before persisting it on the durable job.
- Focused evidence: `go test ./dto ./service -run 'Test(RunJobResultRequestParsesSQLiteSchemaProbeEnvelope|SCUMSchemaProbeDispatchIsPlatformScheduledAndFenced|RemoteAdapterRequestPropagatesTypedInputsToRunJob)'` and `(cd platform && go test ./...)` passed locally. These tests do not prove the active Windows Run deployment or current SCUM schema, so tasks 2.3-2.9 and 4.2-4.9 remain unchecked.
- [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.
- [ ] 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. - [ ] 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.
- [ ] 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.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.
+10 -9
View File
@@ -87,15 +87,16 @@ type RemoteAdapterDeclaration struct {
} }
type RemoteAdapterRequest struct { type RemoteAdapterRequest struct {
ServerInstanceID string ServerInstanceID string
DeclarationKey string DeclarationKey string
TargetKey string TargetKey string
Capability string Capability string
TimeoutSeconds int TimeoutSeconds int
MaxAttempts int MaxAttempts int
IdempotencyKey string IdempotencyKey string
InputRef string InputRef string
Inputs map[string]string Inputs map[string]string
PlatformScheduled bool
} }
type RemoteAdapterResult struct { type RemoteAdapterResult struct {
+2
View File
@@ -1185,6 +1185,7 @@ type JobExecutionResult struct {
SizeBytes int64 SizeBytes int64
AuditSummary string AuditSummary string
Content string Content string
SQLiteSchemaProbe *SCUMSchemaProbeResult
ServerDeploymentEvidence *ServerDeploymentEvidence ServerDeploymentEvidence *ServerDeploymentEvidence
DeploymentReceipt *ServerDeploymentExecutionReceipt DeploymentReceipt *ServerDeploymentExecutionReceipt
} }
@@ -2025,6 +2026,7 @@ func CopyJob(job Job) Job {
job.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...) job.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...)
job.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON) job.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON)
job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan) job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)
job.ExecutionResult.SQLiteSchemaProbe = CopySCUMSchemaProbeResultPtr(job.ExecutionResult.SQLiteSchemaProbe)
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 {
+8
View File
@@ -262,6 +262,14 @@ func CopySCUMSchemaProbeResult(value SCUMSchemaProbeResult) SCUMSchemaProbeResul
return value return value
} }
func CopySCUMSchemaProbeResultPtr(value *SCUMSchemaProbeResult) *SCUMSchemaProbeResult {
if value == nil {
return nil
}
copy := CopySCUMSchemaProbeResult(*value)
return &copy
}
func CopySCUMLiveDataManifest(value SCUMLiveDataManifest) SCUMLiveDataManifest { func CopySCUMLiveDataManifest(value SCUMLiveDataManifest) SCUMLiveDataManifest {
value.CapabilityGates = append([]SCUMLiveDataCapabilityGateDeclaration(nil), value.CapabilityGates...) value.CapabilityGates = append([]SCUMLiveDataCapabilityGateDeclaration(nil), value.CapabilityGates...)
for index := range value.CapabilityGates { for index := range value.CapabilityGates {
+2 -1
View File
@@ -189,6 +189,7 @@ type RunJobExecutionResultBody struct {
SizeBytes int64 `json:"sizeBytes,omitempty"` SizeBytes int64 `json:"sizeBytes,omitempty"`
AuditSummary string `json:"auditSummary,omitempty"` AuditSummary string `json:"auditSummary,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"` ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"` DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"`
} }
@@ -448,7 +449,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, 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), ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
} }
} }
+2 -1
View File
@@ -884,6 +884,7 @@ type JobExecutionResultResponse struct {
Checksum string `json:"checksum,omitempty"` Checksum string `json:"checksum,omitempty"`
SizeBytes int64 `json:"sizeBytes,omitempty"` SizeBytes int64 `json:"sizeBytes,omitempty"`
AuditSummary string `json:"auditSummary,omitempty"` AuditSummary string `json:"auditSummary,omitempty"`
SQLiteSchemaProbe *SCUMSchemaProbeResultDTO `json:"sqliteSchemaProbe,omitempty"`
ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"` ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"`
} }
@@ -1930,7 +1931,7 @@ func JobFromDomain(job domain.Job) JobResponse {
State: job.State, State: job.State,
Progress: progressFromDomain(job.Progress), Progress: progressFromDomain(job.Progress),
ResultRef: job.ResultRef, ResultRef: job.ResultRef,
ExecutionResult: JobExecutionResultResponse{Kind: job.ExecutionResult.Kind, ProcessState: job.ExecutionResult.ProcessState, ExitClassification: job.ExecutionResult.ExitClassification, ExitCode: job.ExecutionResult.ExitCode, Version: job.ExecutionResult.Version, Checksum: job.ExecutionResult.Checksum, SizeBytes: job.ExecutionResult.SizeBytes, AuditSummary: job.ExecutionResult.AuditSummary, ServerDeploymentEvidence: serverDeploymentEvidenceFromDomain(job.ExecutionResult.ServerDeploymentEvidence)}, ExecutionResult: JobExecutionResultResponse{Kind: job.ExecutionResult.Kind, ProcessState: job.ExecutionResult.ProcessState, ExitClassification: job.ExecutionResult.ExitClassification, ExitCode: job.ExecutionResult.ExitCode, Version: job.ExecutionResult.Version, Checksum: job.ExecutionResult.Checksum, SizeBytes: job.ExecutionResult.SizeBytes, AuditSummary: job.ExecutionResult.AuditSummary, SQLiteSchemaProbe: SCUMSchemaProbeResultPtrFromDomain(job.ExecutionResult.SQLiteSchemaProbe), ServerDeploymentEvidence: serverDeploymentEvidenceFromDomain(job.ExecutionResult.ServerDeploymentEvidence)},
RetryPolicy: JobRetryPolicyResponse{ RetryPolicy: JobRetryPolicyResponse{
MaxAttempts: job.RetryPolicy.MaxAttempts, MaxAttempts: job.RetryPolicy.MaxAttempts,
InitialBackoffSeconds: job.RetryPolicy.InitialBackoffSeconds, InitialBackoffSeconds: job.RetryPolicy.InitialBackoffSeconds,
+34
View File
@@ -31,6 +31,40 @@ func TestAIProviderResponseExposesOnlySecretPresence(t *testing.T) {
} }
} }
func TestRunJobResultRequestParsesSQLiteSchemaProbeEnvelope(t *testing.T) {
payload := `{
"runEndpointId":"run-local",
"sessionToken":"run-session",
"jobId":"job-probe",
"leaseToken":"lease-probe",
"attempt":1,
"state":"succeeded",
"progress":{"percent":100,"message":"done"},
"executionResult":{
"kind":"sqlite.schema.probe",
"sqliteSchemaProbe":{
"requestId":"job-probe",
"jobId":"job-probe",
"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":"logical:scum-database"},
"status":"compatible",
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
"observedAt":"2026-08-12T00:00:00Z",
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
"limits":{"maxObjects":256,"maxColumnsPerObject":128,"maxIndexesPerObject":64,"maxForeignKeys":64,"maxCardinalityReads":64,"maxSampleRows":3,"timeoutMs":5000,"maxResultBytes":524288}
}
}
}`
var request RunJobResultRequest
if err := json.Unmarshal([]byte(payload), &request); err != nil {
t.Fatalf("unmarshal Run job result: %v", err)
}
domainRequest := request.ToDomain()
probe := domainRequest.ExecutionResult.SQLiteSchemaProbe
if probe == nil || probe.JobID != "job-probe" || probe.Binding.DatabaseIdentity != "logical:scum-database" || probe.ResultDigest != "sha256:"+strings.Repeat("b", 64) {
t.Fatalf("sqliteSchemaProbe envelope did not parse: %+v", probe)
}
}
func TestAIProviderFromDomainCopiesModels(t *testing.T) { func TestAIProviderFromDomainCopiesModels(t *testing.T) {
provider := domain.AIProvider{ provider := domain.AIProvider{
ID: "ai.openai", ID: "ai.openai",
+36
View File
@@ -98,6 +98,34 @@ func SCUMSchemaProbeRequestToDomain(value SCUMSchemaProbeRequestDTO) domain.SCUM
return domain.SCUMSchemaProbeRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Bounds: scumProbeBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt} return domain.SCUMSchemaProbeRequest{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Bounds: scumProbeBoundsToDomain(value.Bounds), RequestedAt: value.RequestedAt}
} }
func SCUMSchemaProbeResultToDomain(value SCUMSchemaProbeResultDTO) domain.SCUMSchemaProbeResult {
objects := make([]domain.SCUMSchemaObjectEvidence, len(value.Objects))
for index, object := range value.Objects {
columns := make([]domain.SCUMSchemaColumnEvidence, len(object.DeclaredColumns))
for i, column := range object.DeclaredColumns {
columns[i] = domain.SCUMSchemaColumnEvidence{NameFingerprint: column.NameFingerprint, DeclaredType: column.DeclaredType, Nullable: column.Nullable, PrimaryKey: column.PrimaryKey, Ordinal: column.Ordinal}
}
indexes := make([]domain.SCUMSchemaIndexEvidence, len(object.Indexes))
for i, item := range object.Indexes {
indexes[i] = domain.SCUMSchemaIndexEvidence{NameFingerprint: item.NameFingerprint, Unique: item.Unique, ColumnHashes: append([]string(nil), item.ColumnHashes...)}
}
foreignKeys := make([]domain.SCUMSchemaForeignKeyEvidence, len(object.ForeignKeys))
for i, item := range object.ForeignKeys {
foreignKeys[i] = domain.SCUMSchemaForeignKeyEvidence{FromColumnHash: item.FromColumnHash, ToObjectHash: item.ToObjectHash, ToColumnHash: item.ToColumnHash}
}
objects[index] = domain.SCUMSchemaObjectEvidence{ObjectHash: object.ObjectHash, Kind: object.Kind, NameFingerprint: object.NameFingerprint, DeclaredColumns: columns, Indexes: indexes, ForeignKeys: foreignKeys, ApproximateRows: object.ApproximateRows, SampleFingerprints: append([]string(nil), object.SampleFingerprints...)}
}
return domain.SCUMSchemaProbeResult{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityToDomain(value.Binding), Status: domain.SCUMCapabilityEvidenceStatus(value.Status), SchemaFingerprint: value.SchemaFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, Objects: objects, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorCode(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumProbeBoundsToDomain(value.Limits)}
}
func SCUMSchemaProbeResultPtrToDomain(value *SCUMSchemaProbeResultDTO) *domain.SCUMSchemaProbeResult {
if value == nil {
return nil
}
result := SCUMSchemaProbeResultToDomain(*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))
@@ -119,6 +147,14 @@ func SCUMSchemaProbeResultFromDomain(value domain.SCUMSchemaProbeResult) SCUMSch
return SCUMSchemaProbeResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), SchemaFingerprint: value.SchemaFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, Objects: objects, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumProbeBoundsFromDomain(value.Limits)} return SCUMSchemaProbeResultDTO{RequestID: value.RequestID, JobID: value.JobID, Binding: scumBindingIdentityFromDomain(value.Binding), Status: string(value.Status), SchemaFingerprint: value.SchemaFingerprint, ObservedAt: value.ObservedAt, ResultDigest: value.ResultDigest, Objects: objects, SafeError: SCUMSafeErrorDTO{Code: string(value.SafeError.Code), Message: value.SafeError.Message, Retryable: value.SafeError.Retryable}, Limits: scumProbeBoundsFromDomain(value.Limits)}
} }
func SCUMSchemaProbeResultPtrFromDomain(value *domain.SCUMSchemaProbeResult) *SCUMSchemaProbeResultDTO {
if value == nil {
return nil
}
result := SCUMSchemaProbeResultFromDomain(*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}
} }
+12
View File
@@ -334,6 +334,18 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "run.update.staged" { if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "run.update.staged" {
return validationError("Run self-update result type is invalid") return validationError("Run self-update result type is invalid")
} }
case domain.JobCapabilityRemoteRunDBSQLiteProbe:
if result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != scumSchemaProbeExecutionKind {
return validationError("SQLite schema probe result type is invalid")
}
if result.State == domain.JobStateSucceeded {
if result.ExecutionResult.SQLiteSchemaProbe == nil {
return validationError("SQLite schema probe terminal result is required")
}
if err := validateSCUMSchemaProbeResultForJob(job, *result.ExecutionResult.SQLiteSchemaProbe); 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")
+5 -2
View File
@@ -44,6 +44,9 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
if err := validator.ValidateRemoteAdapterRequest(request); err != nil { if err := validator.ValidateRemoteAdapterRequest(request); err != nil {
return domain.RemoteAdapterResult{}, err return domain.RemoteAdapterResult{}, err
} }
if request.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe && !request.PlatformScheduled {
return domain.RemoteAdapterResult{}, forbiddenError("schema probe is scheduled by Platform and is not a direct remote-adapter request")
}
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID) instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
if err != nil { if err != nil {
return domain.RemoteAdapterResult{}, err return domain.RemoteAdapterResult{}, err
@@ -129,7 +132,7 @@ func isRemoteAdapterCapability(capability string) bool {
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite, domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite, domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop, domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery,
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand: domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand:
return true return true
default: default:
@@ -166,7 +169,7 @@ func remoteAdapterKindForCapability(capability string) domain.RemoteAdapterKind
return domain.RemoteAdapterRunFile return domain.RemoteAdapterRunFile
case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop: case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop:
return domain.RemoteAdapterRunProcess return domain.RemoteAdapterRunProcess
case domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery: case domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery:
return domain.RemoteAdapterDatabase return domain.RemoteAdapterDatabase
case domain.JobCapabilityRemoteRunRCONCommand: case domain.JobCapabilityRemoteRunRCONCommand:
return domain.RemoteAdapterRCON return domain.RemoteAdapterRCON
+95
View File
@@ -2,6 +2,7 @@ package service
import ( import (
"encoding/json" "encoding/json"
"strings"
"testing" "testing"
"browser.local/platform/domain" "browser.local/platform/domain"
@@ -86,3 +87,97 @@ func TestRemoteAdapterRequestPropagatesTypedInputsToRunJob(t *testing.T) {
t.Fatal("real Run claim aliases persisted remote inputs") t.Fatal("real Run claim aliases persisted remote inputs")
} }
} }
func TestSCUMSchemaProbeDispatchIsPlatformScheduledAndFenced(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
plugin.Permissions.RemoteAccess = true
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
plugin.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, DatabaseEngines: []string{"sqlite"}}
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}})
plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys = append(plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys, "scum-database")
plugin.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}}
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatal(err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatal(err)
}
session := createServiceUserAndLogin(t, svc, domain.User{ID: "user-scum-probe-owner", DisplayName: "SCUM Probe Owner", Email: "scum-probe-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "server-scum-probe", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Probe"})
if err != nil {
t.Fatalf("create server instance: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
_, err = svc.RequestRemoteAdapterForSession(session, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: "scum-database", TargetKey: "scum-database", Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, IdempotencyKey: "direct-probe-denied", InputRef: "input://scum-schema-probe/direct-probe-denied"})
if err == nil || !strings.Contains(err.Error(), "scheduled by Platform") {
t.Fatalf("expected public probe request denial, got %v", err)
}
endpoint.Capabilities = withoutCapability(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatal(err)
}
if _, _, err = svc.RequestSCUMSchemaProbeForSession(session, instance.ID, "probe-missing-run-capability"); err == nil || !strings.Contains(err.Error(), "does not expose") {
t.Fatalf("expected missing active Run capability, got %v", err)
}
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if err != nil || len(jobs) != 0 {
t.Fatalf("missing probe executor must not create jobs: len=%d err=%v", len(jobs), err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatal(err)
}
probeRequest, queued, err := svc.RequestSCUMSchemaProbeForSession(session, instance.ID, "probe-current-schema")
if err != nil {
t.Fatalf("queue SCUM schema probe: %v", err)
}
job, err := svc.store.Jobs().Get(queued.RequestID)
if err != nil {
t.Fatalf("get probe job: %v", err)
}
if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || job.TargetKey != "scum-database" || job.ExecutionInput.RemoteAdapterKind != string(domain.RemoteAdapterDatabase) || job.ExecutionInput.Inputs["databaseIdentity"] != "logical:scum-database" {
t.Fatalf("unexpected probe job envelope: %+v", job)
}
if job.ExecutionInput.Inputs["jobId"] != job.ID || job.ExecutionInput.Inputs["requestId"] != probeRequest.RequestID || job.ExecutionInput.Inputs["maxResultBytes"] != "524288" {
t.Fatalf("probe inputs are not fenced and bounded: %+v", job.ExecutionInput.Inputs)
}
helloRequest := validRunControlHello()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
helloRequest.CapabilityReport.Fingerprint = "cap-scum-probe"
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil {
t.Fatalf("register Run hello: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil {
t.Fatalf("claim probe job: %v", err)
}
if !claim.HasJob || claim.Job == nil || claim.Job.ExecutionInput.Inputs["adapterVersion"] != "scum-live-data-v0" || claim.Job.ExecutionInput.Inputs["targetKey"] != "scum-database" {
t.Fatalf("claimed probe job lost typed inputs: %+v", claim.Job)
}
badBinding := probeRequest.Binding
badBinding.RunBindingID = "runtime-binding-other"
badProbe := domain.SCUMSchemaProbeResult{RequestID: probeRequest.RequestID, JobID: probeRequest.JobID, Binding: badBinding, Status: domain.SCUMCapabilityEvidenceCompatible, SchemaFingerprint: "sha256:" + strings.Repeat("a", 64), ObservedAt: fixedTime, ResultDigest: "sha256:" + strings.Repeat("b", 64), Limits: probeRequest.Bounds}
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "probe complete"}, Message: "probe complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &badProbe, AuditSummary: "redacted schema probe"}})
if err == nil || !strings.Contains(err.Error(), "binding identity") {
t.Fatalf("expected binding mismatch rejection, got %v", err)
}
goodProbe := badProbe
goodProbe.Binding = probeRequest.Binding
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "probe complete"}, Message: "probe complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &goodProbe, AuditSummary: "redacted schema probe"}}); err != nil {
t.Fatalf("complete fenced probe job: %v", err)
}
stored, err := svc.store.Jobs().Get(job.ID)
if err != nil {
t.Fatalf("get completed probe job: %v", err)
}
if stored.ExecutionResult.SQLiteSchemaProbe == nil || stored.ExecutionResult.SQLiteSchemaProbe.ResultDigest != goodProbe.ResultDigest || stored.ExecutionResult.SQLiteSchemaProbe.Binding.RunBindingID != probeRequest.Binding.RunBindingID {
t.Fatalf("typed probe result was not persisted safely: %+v", stored.ExecutionResult.SQLiteSchemaProbe)
}
}
+6
View File
@@ -124,6 +124,7 @@ type Core interface {
ListBackupsForSession(string, domain.BackupFilter) ([]domain.BackupRecord, error) ListBackupsForSession(string, domain.BackupFilter) ([]domain.BackupRecord, error)
ListRemoteAdapterDeclarationsForSession(string, string) ([]domain.RemoteAdapterDeclaration, error) ListRemoteAdapterDeclarationsForSession(string, string) ([]domain.RemoteAdapterDeclaration, error)
RequestRemoteAdapterForSession(string, domain.RemoteAdapterRequest) (domain.RemoteAdapterResult, error) RequestRemoteAdapterForSession(string, domain.RemoteAdapterRequest) (domain.RemoteAdapterResult, error)
RequestSCUMSchemaProbeForSession(string, string, string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error)
GetServerConfigForSession(string, string) (domain.ServerConfig, error) GetServerConfigForSession(string, string) (domain.ServerConfig, error)
GetDeclaredFileReadSnapshotForSession(string, string, string) (domain.DeclaredFileReadSnapshot, error) GetDeclaredFileReadSnapshotForSession(string, string, string) (domain.DeclaredFileReadSnapshot, error)
PreviewServerConfigWriteForSession(string, domain.ServerConfigDiffRequest) (domain.ServerConfigDiffPreview, error) PreviewServerConfigWriteForSession(string, domain.ServerConfigDiffRequest) (domain.ServerConfigDiffPreview, error)
@@ -1167,6 +1168,11 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "requested remote capability is not declared by plugin"} base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "requested remote capability is not declared by plugin"}
return base return base
} }
if capability == domain.JobCapabilityRemoteRunDBSQLiteProbe {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "schema probe is scheduled by Platform and is not a plugin page action"}
return base
}
declarationKey := strings.TrimSpace(payload["declarationKey"]) declarationKey := strings.TrimSpace(payload["declarationKey"])
if declarationKey == "" { if declarationKey == "" {
for _, profile := range plugin.RuntimeProfiles.TransportProfiles { for _, profile := range plugin.RuntimeProfiles.TransportProfiles {
+171
View File
@@ -0,0 +1,171 @@
package service
import (
"errors"
"strconv"
"strings"
"browser.local/platform/domain"
"browser.local/platform/repo"
"browser.local/platform/validator"
)
const scumSchemaProbeExecutionKind = "sqlite.schema.probe"
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
idempotencyKey = strings.TrimSpace(idempotencyKey)
if idempotencyKey == "" {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("idempotencyKey is required")
}
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
probe := plugin.SCUMLiveData.Probe
if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin")
}
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run is not online for SCUM schema probe")
}
if !containsString(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run does not expose the generic SQLite schema-probe executor")
}
binding, err := svc.runtimeBindingForServer(instance.ID)
if errors.Is(err, repo.ErrNotFound) {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("runtime binding is required before SCUM schema probe")
}
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData)
if adapterVersion == "" {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema-probe adapter version is not declared")
}
bounds := probe.Bounds
if bounds.MaxObjects == 0 {
bounds = domain.DefaultSCUMSchemaProbeBounds()
}
jobID := jobIDFromParts("job-remote-adapter", instance.ID, idempotencyKey)
request := domain.SCUMSchemaProbeRequest{
RequestID: jobID,
JobID: jobID,
Binding: domain.SCUMBindingIdentity{
ServerInstanceID: instance.ID,
RunBindingID: binding.ID,
RunEndpointID: instance.RunEndpointID,
PluginID: plugin.ID,
PluginVersion: plugin.Version,
AdapterVersion: adapterVersion,
DatabaseIdentity: "logical:" + probe.TargetKey,
},
Bounds: bounds,
RequestedAt: svc.now(),
}
if err := validator.ValidateSCUMSchemaProbeRequest(request); err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{
ServerInstanceID: instance.ID,
DeclarationKey: probe.TargetKey,
TargetKey: probe.TargetKey,
Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe,
TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds),
MaxAttempts: 1,
IdempotencyKey: idempotencyKey,
InputRef: "input://scum-schema-probe/" + instance.ID + "/" + idempotencyKey,
Inputs: scumSchemaProbeInputs(request, probe.TargetKey),
PlatformScheduled: true,
})
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
return request, result, nil
}
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
for _, gate := range manifest.CapabilityGates {
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
return strings.TrimSpace(gate.AdapterVersion)
}
}
for _, gate := range manifest.CapabilityGates {
if strings.TrimSpace(gate.AdapterVersion) != "" {
return strings.TrimSpace(gate.AdapterVersion)
}
}
return ""
}
func scumSchemaProbeTimeoutSeconds(bounds domain.SCUMSchemaProbeBounds) int {
if bounds.TimeoutMS <= 0 {
return 1
}
seconds := (bounds.TimeoutMS + 999) / 1000
if seconds <= 0 {
return 1
}
return seconds
}
func scumSchemaProbeInputs(request domain.SCUMSchemaProbeRequest, targetKey string) map[string]string {
return map[string]string{
"requestId": request.RequestID,
"jobId": request.JobID,
"targetKey": targetKey,
"serverInstanceId": request.Binding.ServerInstanceID,
"runBindingId": request.Binding.RunBindingID,
"runEndpointId": request.Binding.RunEndpointID,
"pluginId": request.Binding.PluginID,
"pluginVersion": request.Binding.PluginVersion,
"adapterVersion": request.Binding.AdapterVersion,
"gameVersion": request.Binding.GameVersion,
"databaseIdentity": request.Binding.DatabaseIdentity,
"maxObjects": strconv.Itoa(request.Bounds.MaxObjects),
"maxColumnsPerObject": strconv.Itoa(request.Bounds.MaxColumnsPerObject),
"maxIndexesPerObject": strconv.Itoa(request.Bounds.MaxIndexesPerObject),
"maxForeignKeys": strconv.Itoa(request.Bounds.MaxForeignKeys),
"maxCardinalityReads": strconv.Itoa(request.Bounds.MaxCardinalityReads),
"maxSampleRows": strconv.Itoa(request.Bounds.MaxSampleRows),
"timeoutMs": strconv.Itoa(request.Bounds.TimeoutMS),
"maxResultBytes": strconv.Itoa(request.Bounds.MaxResultBytes),
}
}
func scumSchemaProbeBindingFromInputs(inputs map[string]string) domain.SCUMBindingIdentity {
return domain.SCUMBindingIdentity{
ServerInstanceID: inputs["serverInstanceId"],
RunBindingID: inputs["runBindingId"],
RunEndpointID: inputs["runEndpointId"],
PluginID: inputs["pluginId"],
PluginVersion: inputs["pluginVersion"],
AdapterVersion: inputs["adapterVersion"],
GameVersion: inputs["gameVersion"],
DatabaseIdentity: inputs["databaseIdentity"],
}
}
func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error {
if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil {
return err
}
if result.JobID != job.ID || result.JobID != job.ExecutionInput.Inputs["jobId"] || result.RequestID != job.ExecutionInput.Inputs["requestId"] {
return validationError("SQLite schema probe result does not match leased job identity")
}
if !sameSCUMSchemaProbeBinding(result.Binding, scumSchemaProbeBindingFromInputs(job.ExecutionInput.Inputs)) {
return validationError("SQLite schema probe result does not match leased binding identity")
}
return nil
}
func sameSCUMSchemaProbeBinding(a, b domain.SCUMBindingIdentity) bool {
return a.ServerInstanceID == b.ServerInstanceID && a.RunBindingID == b.RunBindingID && a.RunEndpointID == b.RunEndpointID && a.PluginID == b.PluginID && a.PluginVersion == b.PluginVersion && a.AdapterVersion == b.AdapterVersion && a.GameVersion == b.GameVersion && a.DatabaseIdentity == b.DatabaseIdentity
}
+5
View File
@@ -51,6 +51,11 @@ func ValidateRunJobResult(result domain.RunJobResult) error {
if result.ExecutionResult.Checksum != "" && !validSHA256Checksum(result.ExecutionResult.Checksum) { if result.ExecutionResult.Checksum != "" && !validSHA256Checksum(result.ExecutionResult.Checksum) {
violations = append(violations, "executionResult.checksum must be sha256:<hex>") violations = append(violations, "executionResult.checksum must be sha256:<hex>")
} }
if result.ExecutionResult.SQLiteSchemaProbe != nil {
if err := ValidateSCUMSchemaProbeResult(*result.ExecutionResult.SQLiteSchemaProbe); err != nil {
violations = append(violations, "executionResult.sqliteSchemaProbe: "+err.Error())
}
}
return finish(violations) return finish(violations)
} }
+5
View File
@@ -1543,6 +1543,11 @@ func ValidateJob(job domain.Job) error {
if len(job.ExecutionResult.AuditSummary) > maxAuditSummaryLength { if len(job.ExecutionResult.AuditSummary) > maxAuditSummaryLength {
violations = append(violations, "executionResult.auditSummary is too long") violations = append(violations, "executionResult.auditSummary is too long")
} }
if job.ExecutionResult.SQLiteSchemaProbe != nil {
if err := ValidateSCUMSchemaProbeResult(*job.ExecutionResult.SQLiteSchemaProbe); err != nil {
violations = append(violations, "executionResult.sqliteSchemaProbe: "+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")