feat(scum): wire platform schema probe jobs
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 ©
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -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)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user