Remove pre-1.0 bridge governance scaffolding
This commit is contained in:
@@ -59,6 +59,5 @@
|
||||
"runEndpoints": [],
|
||||
"jobs": [],
|
||||
"artifacts": [],
|
||||
"logStreams": [],
|
||||
"auditEvents": []
|
||||
}
|
||||
"logStreams": []
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestGameClientBridgeOperatorRoutes(t *testing.T) {
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall, domain.JobCapabilityRemoteRunDBSQLiteQuery)
|
||||
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "sqlite-db", Kind: "sqlite", TargetKey: "db/sqlite", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}}}
|
||||
plugin.RuntimeProfiles.ClientManagers = []domain.RuntimeClientManagerProfile{{Key: "scum-client", DisplayName: "SCUM Client", Version: "1.0.0", RepositoryURL: "https://github.com/example/scum-client.git", RevisionPolicy: "pinned", Revision: "0123456789abcdef", SupportedTargets: []domain.RuntimeTarget{{OS: "linux", Arch: "amd64"}}, BuildSystem: "go", EntryRef: "main.go", OutputArtifacts: []string{"scum-client"}, Deployment: domain.RuntimeClientManagerDeployment{Mode: "run-supervised", ExecutableRef: "scum-client", RequiredRunCapabilities: []string{domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall}}, Lifecycle: domain.RuntimeClientManagerLifecycle{Actions: []string{"start", "stop", "restart", "status", "update", "rollback", "uninstall"}, StartupTimeoutSeconds: 60, StopTimeoutSeconds: 30}, Health: domain.RuntimeClientManagerHealth{Mode: "component-heartbeat", IntervalSeconds: 15, DegradedAfterSeconds: 45, OfflineAfterSeconds: 120, RequiredCapabilities: []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"}}, Compatibility: domain.RuntimeClientManagerCompatibility{MinimumVersion: "1.0.0"}, UpdatePolicy: domain.RuntimeClientManagerUpdatePolicy{Strategy: "manual-staged", RequireApproval: true, HealthConfirmationSeconds: 60, RetainPrevious: true}}}
|
||||
plugin.GameClientBridge = domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", Title: "Diagnostic ping", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelNone, PayloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", ResultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", TimeoutSeconds: 3600, MaxPayloadBytes: 4096}}, QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{{Key: "player.lookup", Title: "Player lookup", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "sqlite-db", TargetKey: "db/sqlite", ParameterSchemaRef: "schemas/bridge/query/player-lookup.parameters.schema.json", ResultSchemaRef: "schemas/bridge/query/player-lookup.result.schema.json", MaxRows: 50, TimeoutSeconds: 10}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}
|
||||
plugin.GameClientBridge = domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", Title: "Diagnostic ping", Permission: "server.game-client.command", PayloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", ResultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", TimeoutSeconds: 3600, MaxPayloadBytes: 4096}}, QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{{Key: "player.lookup", Title: "Player lookup", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "sqlite-db", TargetKey: "db/sqlite", ParameterSchemaRef: "schemas/bridge/query/player-lookup.parameters.schema.json", ResultSchemaRef: "schemas/bridge/query/player-lookup.result.schema.json", MaxRows: 50, TimeoutSeconds: 10}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}
|
||||
if _, err := coreService.CreateGamePlugin(plugin); err != nil {
|
||||
t.Fatalf("create bridge plugin: %v", err)
|
||||
}
|
||||
|
||||
@@ -1519,7 +1519,7 @@ func TestPluginLifecycleAndAIConfigRoutesAreDurableAndRedacted(t *testing.T) {
|
||||
serverID := createRuntimeAPIFixtures(t, router, adminSession)
|
||||
createAIProviderFixture(t, router, adminSession)
|
||||
|
||||
lifecycleRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/plugin-lifecycles/server.runtime/actions", dto.PluginLifecycleActionRequest{ServerInstanceID: serverID, Operation: "install", TargetVersion: "1.0.0", IdempotencyKey: "api-plugin-install", Confirmed: false}, adminSession)
|
||||
lifecycleRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/plugin-lifecycles/server.runtime/actions", dto.PluginLifecycleActionRequest{ServerInstanceID: serverID, Operation: "install", TargetVersion: "1.0.0", IdempotencyKey: "api-plugin-install"}, adminSession)
|
||||
assertStatus(t, lifecycleRecorder, http.StatusAccepted)
|
||||
lifecycle := decodeBody[dto.PluginLifecycleActionResponse](t, lifecycleRecorder)
|
||||
if lifecycle.Status != "queued" || lifecycle.Job.ID == "" || lifecycle.Installation.ID == "" {
|
||||
@@ -1939,7 +1939,7 @@ func validGamePluginManifestRegistrationRequest() dto.GamePluginManifestRegistra
|
||||
},
|
||||
},
|
||||
AI: dto.GamePluginManifestAIBody{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"},
|
||||
ProductionLifecycle: dto.GamePluginProductionLifecycleBody{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional", ApprovalRequired: []string{"disable", "rollback", "retire"}},
|
||||
ProductionLifecycle: dto.GamePluginProductionLifecycleBody{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional"},
|
||||
RuntimeProfiles: dto.GamePluginRuntimeProfilesBody{LifecycleProfiles: []dto.RuntimeLifecycleProfileBody{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -14,28 +14,10 @@ const (
|
||||
GameClientBridgeCommandExpired GameClientBridgeCommandState = "expired"
|
||||
)
|
||||
|
||||
type GameClientBridgeApprovalState string
|
||||
|
||||
const (
|
||||
GameClientBridgeApprovalNotRequired GameClientBridgeApprovalState = "not_required"
|
||||
GameClientBridgeApprovalPending GameClientBridgeApprovalState = "pending"
|
||||
GameClientBridgeApprovalApproved GameClientBridgeApprovalState = "approved"
|
||||
GameClientBridgeApprovalRejected GameClientBridgeApprovalState = "rejected"
|
||||
)
|
||||
|
||||
type GameClientBridgeApprovalLevel string
|
||||
|
||||
const (
|
||||
GameClientBridgeApprovalLevelNone GameClientBridgeApprovalLevel = "none"
|
||||
GameClientBridgeApprovalLevelOperator GameClientBridgeApprovalLevel = "operator"
|
||||
GameClientBridgeApprovalLevelPlatformAdmin GameClientBridgeApprovalLevel = "platform-admin"
|
||||
)
|
||||
|
||||
type GameClientBridgeCommandDeclaration struct {
|
||||
Type string
|
||||
Title string
|
||||
Permission string
|
||||
ApprovalLevel GameClientBridgeApprovalLevel
|
||||
PayloadSchemaRef string
|
||||
ResultSchemaRef string
|
||||
TimeoutSeconds int
|
||||
@@ -113,57 +95,11 @@ type GameClientBridgeDataPackDeclaration struct {
|
||||
DataRefs []string
|
||||
}
|
||||
|
||||
type GameClientBridgeOperationKind string
|
||||
|
||||
const (
|
||||
GameClientBridgeOperationKindRCON GameClientBridgeOperationKind = "rcon"
|
||||
GameClientBridgeOperationKindSQLiteMutation GameClientBridgeOperationKind = "sqlite-mutation"
|
||||
)
|
||||
|
||||
type GameClientBridgeOperationTemplateDeclaration struct {
|
||||
Key string
|
||||
Title string
|
||||
Permission string
|
||||
ApprovalLevel GameClientBridgeApprovalLevel
|
||||
Kind GameClientBridgeOperationKind
|
||||
TransportKey string
|
||||
TargetKey string
|
||||
PayloadSchemaRef string
|
||||
ResultSchemaRef string
|
||||
ConfirmationSchemaRef string
|
||||
TimeoutSeconds int
|
||||
MaxPayloadBytes int
|
||||
MaxRowsAffected int
|
||||
Mutation GameClientBridgeOperationMutationDeclaration
|
||||
Safety GameClientBridgeOperationSafety
|
||||
}
|
||||
|
||||
type GameClientBridgeOperationMutationDeclaration struct {
|
||||
FieldKey string
|
||||
TableKey string
|
||||
IdentityKey string
|
||||
ValueKey string
|
||||
ConfirmationQueryKey string
|
||||
AllowedValueType string
|
||||
MinValue float64
|
||||
MaxValue float64
|
||||
}
|
||||
|
||||
type GameClientBridgeOperationSafety struct {
|
||||
RequiresApproval bool
|
||||
RequiresOfflinePlayer bool
|
||||
RequiresMaintenanceWindow bool
|
||||
RequiresBeforeValue bool
|
||||
RequiresConfirmation bool
|
||||
BackupRequired bool
|
||||
}
|
||||
|
||||
type GameClientBridgePageContract struct {
|
||||
PageKey string
|
||||
CommandTypes []string
|
||||
SnapshotTypes []string
|
||||
QueryTemplateKeys []string
|
||||
OperationKeys []string
|
||||
FeatureKeys []string
|
||||
}
|
||||
|
||||
@@ -192,16 +128,15 @@ type GameClientBridgeCompanionDeclaration struct {
|
||||
}
|
||||
|
||||
type GameClientBridgeManifest struct {
|
||||
Commands []GameClientBridgeCommandDeclaration
|
||||
Snapshots []GameClientBridgeSnapshotDeclaration
|
||||
QueryTemplates []GameClientBridgeQueryTemplateDeclaration
|
||||
LogProjections []GameClientBridgeLogProjectionDeclaration
|
||||
DataPacks []GameClientBridgeDataPackDeclaration
|
||||
OperationTemplates []GameClientBridgeOperationTemplateDeclaration
|
||||
Retention GameClientBridgeRetention
|
||||
Pages []GameClientBridgePageContract
|
||||
Features []GameClientBridgeFeatureDeclaration
|
||||
Companion GameClientBridgeCompanionDeclaration
|
||||
Commands []GameClientBridgeCommandDeclaration
|
||||
Snapshots []GameClientBridgeSnapshotDeclaration
|
||||
QueryTemplates []GameClientBridgeQueryTemplateDeclaration
|
||||
LogProjections []GameClientBridgeLogProjectionDeclaration
|
||||
DataPacks []GameClientBridgeDataPackDeclaration
|
||||
Retention GameClientBridgeRetention
|
||||
Pages []GameClientBridgePageContract
|
||||
Features []GameClientBridgeFeatureDeclaration
|
||||
Companion GameClientBridgeCompanionDeclaration
|
||||
}
|
||||
|
||||
type GameClientBridgeResultStatus string
|
||||
@@ -224,7 +159,6 @@ type GameClientBridgeCommand struct {
|
||||
IdempotencyKey string
|
||||
Priority int
|
||||
State GameClientBridgeCommandState
|
||||
ApprovalState GameClientBridgeApprovalState
|
||||
RequesterID string
|
||||
Claim GameClientBridgeClaim
|
||||
Cancellation GameClientBridgeCancellation
|
||||
@@ -489,14 +423,12 @@ func CopyGameClientBridgeManifest(value GameClientBridgeManifest) GameClientBrid
|
||||
value.DataPacks[index].ConfigMapRefs = CopyStringSlice(value.DataPacks[index].ConfigMapRefs)
|
||||
value.DataPacks[index].DataRefs = CopyStringSlice(value.DataPacks[index].DataRefs)
|
||||
}
|
||||
value.OperationTemplates = append([]GameClientBridgeOperationTemplateDeclaration(nil), value.OperationTemplates...)
|
||||
value.Pages = append([]GameClientBridgePageContract(nil), value.Pages...)
|
||||
value.Features = append([]GameClientBridgeFeatureDeclaration(nil), value.Features...)
|
||||
for index := range value.Pages {
|
||||
value.Pages[index].CommandTypes = CopyStringSlice(value.Pages[index].CommandTypes)
|
||||
value.Pages[index].SnapshotTypes = CopyStringSlice(value.Pages[index].SnapshotTypes)
|
||||
value.Pages[index].QueryTemplateKeys = CopyStringSlice(value.Pages[index].QueryTemplateKeys)
|
||||
value.Pages[index].OperationKeys = CopyStringSlice(value.Pages[index].OperationKeys)
|
||||
value.Pages[index].FeatureKeys = CopyStringSlice(value.Pages[index].FeatureKeys)
|
||||
}
|
||||
for index := range value.Features {
|
||||
|
||||
@@ -10,9 +10,8 @@ func TestCopyGameClientBridgeDeclarationsCopiesQueryTemplateSlices(t *testing.T)
|
||||
Target: GameClientBridgeLogProjectionTargetDeclaration{Collection: "users", UpsertKeys: []string{"steamId"}, CaptureMappings: map[string]string{"steamId": "steamId"}, FixedValues: map[string]string{"source": "stdout"}, ObservedAtField: "lastLoginAt"},
|
||||
Presence: &GameClientBridgeLogProjectionPresenceDeclaration{TimestampField: "lastLoginAt", ActiveWindowSeconds: 600, ActivityTarget: &GameClientBridgeLogProjectionTargetDeclaration{Collection: "activity", UpsertKeys: []string{"steamId"}, CaptureMappings: map[string]string{"steamId": "steamId"}}},
|
||||
}},
|
||||
DataPacks: []GameClientBridgeDataPackDeclaration{{Key: "db-v1", LogParserRefs: []string{"logs.json"}, ConfigMapRefs: []string{"config.json"}, DataRefs: []string{"data.json"}}},
|
||||
OperationTemplates: []GameClientBridgeOperationTemplateDeclaration{{Key: "player.fame.set"}},
|
||||
Pages: []GameClientBridgePageContract{{PageKey: "players", QueryTemplateKeys: []string{"player.lookup"}, OperationKeys: []string{"player.fame.set"}}},
|
||||
DataPacks: []GameClientBridgeDataPackDeclaration{{Key: "db-v1", LogParserRefs: []string{"logs.json"}, ConfigMapRefs: []string{"config.json"}, DataRefs: []string{"data.json"}}},
|
||||
Pages: []GameClientBridgePageContract{{PageKey: "players", QueryTemplateKeys: []string{"player.lookup"}}},
|
||||
}
|
||||
manifestCopy := CopyGameClientBridgeManifest(manifest)
|
||||
manifestCopy.QueryTemplates[0].Key = "mutated"
|
||||
@@ -22,10 +21,8 @@ func TestCopyGameClientBridgeDeclarationsCopiesQueryTemplateSlices(t *testing.T)
|
||||
manifestCopy.LogProjections[0].Presence.ActivityTarget.CaptureMappings["steamId"] = "mutated"
|
||||
manifestCopy.DataPacks[0].LogParserRefs[0] = "mutated"
|
||||
manifestCopy.DataPacks[0].DataRefs[0] = "mutated"
|
||||
manifestCopy.OperationTemplates[0].Key = "mutated"
|
||||
manifestCopy.Pages[0].QueryTemplateKeys[0] = "mutated"
|
||||
manifestCopy.Pages[0].OperationKeys[0] = "mutated"
|
||||
if manifest.QueryTemplates[0].Key != "player.lookup" || manifest.QueryTemplates[0].RowTarget.ColumnMappings["userId"] != "user_id" || manifest.LogProjections[0].StreamKeys[0] != "process.stdout" || manifest.LogProjections[0].Target.CaptureMappings["steamId"] != "steamId" || manifest.LogProjections[0].Presence.ActivityTarget.CaptureMappings["steamId"] != "steamId" || manifest.DataPacks[0].LogParserRefs[0] != "logs.json" || manifest.DataPacks[0].DataRefs[0] != "data.json" || manifest.OperationTemplates[0].Key != "player.fame.set" || manifest.Pages[0].QueryTemplateKeys[0] != "player.lookup" || manifest.Pages[0].OperationKeys[0] != "player.fame.set" {
|
||||
if manifest.QueryTemplates[0].Key != "player.lookup" || manifest.QueryTemplates[0].RowTarget.ColumnMappings["userId"] != "user_id" || manifest.LogProjections[0].StreamKeys[0] != "process.stdout" || manifest.LogProjections[0].Target.CaptureMappings["steamId"] != "steamId" || manifest.LogProjections[0].Presence.ActivityTarget.CaptureMappings["steamId"] != "steamId" || manifest.DataPacks[0].LogParserRefs[0] != "logs.json" || manifest.DataPacks[0].DataRefs[0] != "data.json" || manifest.Pages[0].QueryTemplateKeys[0] != "player.lookup" {
|
||||
t.Fatalf("manifest copy aliases query template declarations: source=%#v copy=%#v", manifest, manifestCopy)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ type PluginLifecycleRequest struct {
|
||||
Operation PluginLifecycleOperation
|
||||
TargetVersion string
|
||||
IdempotencyKey string
|
||||
Confirmed bool
|
||||
}
|
||||
|
||||
type PluginLifecycleResult struct {
|
||||
|
||||
@@ -372,7 +372,6 @@ type GamePluginManifestAI struct {
|
||||
type GamePluginProductionLifecycle struct {
|
||||
Operations []string
|
||||
DependencyPolicy string
|
||||
ApprovalRequired []string
|
||||
}
|
||||
|
||||
type GamePluginRemoteAccess struct {
|
||||
@@ -1068,7 +1067,6 @@ const (
|
||||
JobCapabilityRemoteRunDBSQLiteQuery = "remote.run.db.sqlite.query"
|
||||
JobCapabilityRemoteRunLogsTransfer = "remote.run.logs.transfer"
|
||||
JobCapabilityRemoteRunRCONCommand = "remote.run.rcon.command"
|
||||
JobCapabilityRemoteRunProtectedSQL = "remote.run.protected.sql"
|
||||
JobCapabilityRemoteRunProgram = "remote.run.program.command"
|
||||
JobCapabilityRunSelfUpdate = "run.self-update"
|
||||
JobCapabilityDistributionBuild = "distribution.build"
|
||||
@@ -1749,7 +1747,6 @@ func CopyPluginCreateFields(fields []PluginCreateField) []PluginCreateField {
|
||||
|
||||
func CopyGamePluginProductionLifecycle(lifecycle GamePluginProductionLifecycle) GamePluginProductionLifecycle {
|
||||
lifecycle.Operations = CopyStringSlice(lifecycle.Operations)
|
||||
lifecycle.ApprovalRequired = CopyStringSlice(lifecycle.ApprovalRequired)
|
||||
return lifecycle
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ type GameClientBridgeCommandResponse struct {
|
||||
RunJobID string `json:"runJobId,omitempty"`
|
||||
Priority int `json:"priority"`
|
||||
State string `json:"state"`
|
||||
ApprovalState string `json:"approvalState"`
|
||||
RequesterID string `json:"requesterId,omitempty"`
|
||||
ResultSummary string `json:"resultSummary,omitempty"`
|
||||
Result *GameClientBridgeCommandResultResponse `json:"result,omitempty"`
|
||||
@@ -244,7 +243,6 @@ func GameClientBridgeCommandFromDomain(value domain.GameClientBridgeCommand) Gam
|
||||
RunJobID: value.RunJobID,
|
||||
Priority: value.Priority,
|
||||
State: string(value.State),
|
||||
ApprovalState: string(value.ApprovalState),
|
||||
RequesterID: value.RequesterID,
|
||||
ResultSummary: value.Result.Summary,
|
||||
ExpiresAt: value.ExpiresAt,
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestGameClientBridgeBrowserProjectionsAreCompleteAndOmitInternalData(t *tes
|
||||
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
||||
command := domain.GameClientBridgeCommand{
|
||||
ID: "command-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", CommandType: "diagnostic.ping", Payload: map[string]any{"message": "internal command payload"}, IdempotencyKey: "internal-idempotency", Priority: 9,
|
||||
State: domain.GameClientBridgeCommandCancelled, ApprovalState: domain.GameClientBridgeApprovalApproved, RequesterID: "operator-1",
|
||||
State: domain.GameClientBridgeCommandCancelled, RequesterID: "operator-1",
|
||||
Claim: domain.GameClientBridgeClaim{SessionID: "internal-session-secret", InstallationID: "internal-installation", DeploymentGeneration: 9, FencingToken: 42, LeaseExpiresAt: now.Add(time.Minute)},
|
||||
Result: domain.GameClientBridgeResult{Status: domain.GameClientBridgeResultCancelled, Summary: "cancelled safely", Payload: map[string]any{"code": "cancelled"}, CompletedBy: "internal-completing-session", CompletedAt: now.Add(3 * time.Minute)},
|
||||
Cancellation: domain.GameClientBridgeCancellation{RequestedBy: "operator-2", Reason: "operator request", CancelledAt: now.Add(2 * time.Minute)},
|
||||
|
||||
@@ -11,7 +11,6 @@ type PluginLifecycleActionRequest struct {
|
||||
Operation string `json:"operation"`
|
||||
TargetVersion string `json:"targetVersion,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
}
|
||||
|
||||
type PluginLifecycleInstallationResponse struct {
|
||||
@@ -82,7 +81,7 @@ type AIConfigDiffApprovalResponse struct {
|
||||
}
|
||||
|
||||
func (request PluginLifecycleActionRequest) ToDomain(pluginID string) domain.PluginLifecycleRequest {
|
||||
return domain.PluginLifecycleRequest{PluginID: pluginID, ServerInstanceID: request.ServerInstanceID, Operation: domain.PluginLifecycleOperation(request.Operation), TargetVersion: request.TargetVersion, IdempotencyKey: request.IdempotencyKey, Confirmed: request.Confirmed}
|
||||
return domain.PluginLifecycleRequest{PluginID: pluginID, ServerInstanceID: request.ServerInstanceID, Operation: domain.PluginLifecycleOperation(request.Operation), TargetVersion: request.TargetVersion, IdempotencyKey: request.IdempotencyKey}
|
||||
}
|
||||
|
||||
func PluginLifecycleFromDomain(installation domain.PluginLifecycleInstallation) PluginLifecycleInstallationResponse {
|
||||
|
||||
+18
-68
@@ -250,7 +250,6 @@ type GamePluginManifestAIBody struct {
|
||||
type GamePluginProductionLifecycleBody struct {
|
||||
Operations []string `json:"operations"`
|
||||
DependencyPolicy string `json:"dependencyPolicy"`
|
||||
ApprovalRequired []string `json:"approvalRequired"`
|
||||
}
|
||||
|
||||
type GamePluginRemoteAccessBody struct {
|
||||
@@ -265,7 +264,6 @@ type GameClientBridgeCommandDeclarationBody struct {
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Permission string `json:"permission"`
|
||||
ApprovalLevel string `json:"approvalLevel"`
|
||||
PayloadSchemaRef string `json:"payloadSchemaRef"`
|
||||
ResultSchemaRef string `json:"resultSchemaRef,omitempty"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds"`
|
||||
@@ -339,50 +337,11 @@ type GameClientBridgeDataPackDeclarationBody struct {
|
||||
DataRefs []string `json:"dataRefs,omitempty"`
|
||||
}
|
||||
|
||||
type GameClientBridgeOperationSafetyBody struct {
|
||||
RequiresApproval bool `json:"requiresApproval,omitempty"`
|
||||
RequiresOfflinePlayer bool `json:"requiresOfflinePlayer,omitempty"`
|
||||
RequiresMaintenanceWindow bool `json:"requiresMaintenanceWindow,omitempty"`
|
||||
RequiresBeforeValue bool `json:"requiresBeforeValue,omitempty"`
|
||||
RequiresConfirmation bool `json:"requiresConfirmation,omitempty"`
|
||||
BackupRequired bool `json:"backupRequired,omitempty"`
|
||||
}
|
||||
|
||||
type GameClientBridgeOperationMutationDeclarationBody struct {
|
||||
FieldKey string `json:"fieldKey"`
|
||||
TableKey string `json:"tableKey"`
|
||||
IdentityKey string `json:"identityKey"`
|
||||
ValueKey string `json:"valueKey"`
|
||||
ConfirmationQueryKey string `json:"confirmationQueryKey"`
|
||||
AllowedValueType string `json:"allowedValueType"`
|
||||
MinValue float64 `json:"minValue,omitempty"`
|
||||
MaxValue float64 `json:"maxValue,omitempty"`
|
||||
}
|
||||
|
||||
type GameClientBridgeOperationTemplateDeclarationBody struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Permission string `json:"permission"`
|
||||
ApprovalLevel string `json:"approvalLevel"`
|
||||
Kind string `json:"kind"`
|
||||
TransportKey string `json:"transportKey"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
PayloadSchemaRef string `json:"payloadSchemaRef"`
|
||||
ResultSchemaRef string `json:"resultSchemaRef,omitempty"`
|
||||
ConfirmationSchemaRef string `json:"confirmationSchemaRef,omitempty"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds"`
|
||||
MaxPayloadBytes int `json:"maxPayloadBytes"`
|
||||
MaxRowsAffected int `json:"maxRowsAffected,omitempty"`
|
||||
Mutation GameClientBridgeOperationMutationDeclarationBody `json:"mutation,omitempty"`
|
||||
Safety GameClientBridgeOperationSafetyBody `json:"safety,omitempty"`
|
||||
}
|
||||
|
||||
type GameClientBridgePageContractBody struct {
|
||||
PageKey string `json:"pageKey"`
|
||||
CommandTypes []string `json:"commandTypes,omitempty"`
|
||||
SnapshotTypes []string `json:"snapshotTypes,omitempty"`
|
||||
QueryTemplateKeys []string `json:"queryTemplateKeys,omitempty"`
|
||||
OperationKeys []string `json:"operationKeys,omitempty"`
|
||||
FeatureKeys []string `json:"featureKeys,omitempty"`
|
||||
}
|
||||
|
||||
@@ -411,17 +370,16 @@ type GameClientBridgeCompanionDeclarationBody struct {
|
||||
}
|
||||
|
||||
type GameClientBridgeManifestBody struct {
|
||||
Commands []GameClientBridgeCommandDeclarationBody `json:"commands"`
|
||||
Snapshots []GameClientBridgeSnapshotDeclarationBody `json:"snapshots"`
|
||||
QueryTemplates []GameClientBridgeQueryTemplateDeclarationBody `json:"queryTemplates,omitempty"`
|
||||
LogProjections []GameClientBridgeLogProjectionDeclarationBody `json:"logProjections,omitempty"`
|
||||
DataPacks []GameClientBridgeDataPackDeclarationBody `json:"dataPacks,omitempty"`
|
||||
OperationTemplates []GameClientBridgeOperationTemplateDeclarationBody `json:"operationTemplates,omitempty"`
|
||||
CommandRetentionSeconds int `json:"commandRetentionSeconds"`
|
||||
MaxCommands int `json:"maxCommands"`
|
||||
Pages []GameClientBridgePageContractBody `json:"pages,omitempty"`
|
||||
Features []GameClientBridgeFeatureDeclarationBody `json:"features,omitempty"`
|
||||
Companion *GameClientBridgeCompanionDeclarationBody `json:"companion,omitempty"`
|
||||
Commands []GameClientBridgeCommandDeclarationBody `json:"commands"`
|
||||
Snapshots []GameClientBridgeSnapshotDeclarationBody `json:"snapshots"`
|
||||
QueryTemplates []GameClientBridgeQueryTemplateDeclarationBody `json:"queryTemplates,omitempty"`
|
||||
LogProjections []GameClientBridgeLogProjectionDeclarationBody `json:"logProjections,omitempty"`
|
||||
DataPacks []GameClientBridgeDataPackDeclarationBody `json:"dataPacks,omitempty"`
|
||||
CommandRetentionSeconds int `json:"commandRetentionSeconds"`
|
||||
MaxCommands int `json:"maxCommands"`
|
||||
Pages []GameClientBridgePageContractBody `json:"pages,omitempty"`
|
||||
Features []GameClientBridgeFeatureDeclarationBody `json:"features,omitempty"`
|
||||
Companion *GameClientBridgeCompanionDeclarationBody `json:"companion,omitempty"`
|
||||
}
|
||||
type GamePluginManifestBody struct {
|
||||
ID string `json:"id"`
|
||||
@@ -1163,7 +1121,7 @@ func (ai GamePluginManifestAIBody) ToDomain() domain.GamePluginManifestAI {
|
||||
}
|
||||
|
||||
func (lifecycle GamePluginProductionLifecycleBody) ToDomain() domain.GamePluginProductionLifecycle {
|
||||
return domain.GamePluginProductionLifecycle{Operations: domain.CopyStringSlice(lifecycle.Operations), DependencyPolicy: lifecycle.DependencyPolicy, ApprovalRequired: domain.CopyStringSlice(lifecycle.ApprovalRequired)}
|
||||
return domain.GamePluginProductionLifecycle{Operations: domain.CopyStringSlice(lifecycle.Operations), DependencyPolicy: lifecycle.DependencyPolicy}
|
||||
}
|
||||
|
||||
func (remote GamePluginRemoteAccessBody) ToDomain() domain.GamePluginRemoteAccess {
|
||||
@@ -1179,7 +1137,7 @@ func (remote GamePluginRemoteAccessBody) ToDomain() domain.GamePluginRemoteAcces
|
||||
func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManifest {
|
||||
commands := make([]domain.GameClientBridgeCommandDeclaration, len(body.Commands))
|
||||
for index, command := range body.Commands {
|
||||
commands[index] = domain.GameClientBridgeCommandDeclaration{Type: command.Type, Title: command.Title, Permission: command.Permission, ApprovalLevel: domain.GameClientBridgeApprovalLevel(command.ApprovalLevel), PayloadSchemaRef: command.PayloadSchemaRef, ResultSchemaRef: command.ResultSchemaRef, TimeoutSeconds: command.TimeoutSeconds, MaxPayloadBytes: command.MaxPayloadBytes}
|
||||
commands[index] = domain.GameClientBridgeCommandDeclaration{Type: command.Type, Title: command.Title, Permission: command.Permission, PayloadSchemaRef: command.PayloadSchemaRef, ResultSchemaRef: command.ResultSchemaRef, TimeoutSeconds: command.TimeoutSeconds, MaxPayloadBytes: command.MaxPayloadBytes}
|
||||
}
|
||||
snapshots := make([]domain.GameClientBridgeSnapshotDeclaration, len(body.Snapshots))
|
||||
for index, snapshot := range body.Snapshots {
|
||||
@@ -1202,13 +1160,9 @@ func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManif
|
||||
for index, dataPack := range body.DataPacks {
|
||||
dataPacks[index] = domain.GameClientBridgeDataPackDeclaration{Key: dataPack.Key, DatabaseUserVersion: dataPack.DatabaseUserVersion, LogParserRefs: domain.CopyStringSlice(dataPack.LogParserRefs), ConfigMapRefs: domain.CopyStringSlice(dataPack.ConfigMapRefs), DataRefs: domain.CopyStringSlice(dataPack.DataRefs)}
|
||||
}
|
||||
operationTemplates := make([]domain.GameClientBridgeOperationTemplateDeclaration, len(body.OperationTemplates))
|
||||
for index, template := range body.OperationTemplates {
|
||||
operationTemplates[index] = domain.GameClientBridgeOperationTemplateDeclaration{Key: template.Key, Title: template.Title, Permission: template.Permission, ApprovalLevel: domain.GameClientBridgeApprovalLevel(template.ApprovalLevel), Kind: domain.GameClientBridgeOperationKind(template.Kind), TransportKey: template.TransportKey, TargetKey: template.TargetKey, PayloadSchemaRef: template.PayloadSchemaRef, ResultSchemaRef: template.ResultSchemaRef, ConfirmationSchemaRef: template.ConfirmationSchemaRef, TimeoutSeconds: template.TimeoutSeconds, MaxPayloadBytes: template.MaxPayloadBytes, MaxRowsAffected: template.MaxRowsAffected, Mutation: domain.GameClientBridgeOperationMutationDeclaration{FieldKey: template.Mutation.FieldKey, TableKey: template.Mutation.TableKey, IdentityKey: template.Mutation.IdentityKey, ValueKey: template.Mutation.ValueKey, ConfirmationQueryKey: template.Mutation.ConfirmationQueryKey, AllowedValueType: template.Mutation.AllowedValueType, MinValue: template.Mutation.MinValue, MaxValue: template.Mutation.MaxValue}, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: template.Safety.RequiresApproval, RequiresOfflinePlayer: template.Safety.RequiresOfflinePlayer, RequiresMaintenanceWindow: template.Safety.RequiresMaintenanceWindow, RequiresBeforeValue: template.Safety.RequiresBeforeValue, RequiresConfirmation: template.Safety.RequiresConfirmation, BackupRequired: template.Safety.BackupRequired}}
|
||||
}
|
||||
pages := make([]domain.GameClientBridgePageContract, len(body.Pages))
|
||||
for index, page := range body.Pages {
|
||||
pages[index] = domain.GameClientBridgePageContract{PageKey: page.PageKey, CommandTypes: domain.CopyStringSlice(page.CommandTypes), SnapshotTypes: domain.CopyStringSlice(page.SnapshotTypes), QueryTemplateKeys: domain.CopyStringSlice(page.QueryTemplateKeys), OperationKeys: domain.CopyStringSlice(page.OperationKeys), FeatureKeys: domain.CopyStringSlice(page.FeatureKeys)}
|
||||
pages[index] = domain.GameClientBridgePageContract{PageKey: page.PageKey, CommandTypes: domain.CopyStringSlice(page.CommandTypes), SnapshotTypes: domain.CopyStringSlice(page.SnapshotTypes), QueryTemplateKeys: domain.CopyStringSlice(page.QueryTemplateKeys), FeatureKeys: domain.CopyStringSlice(page.FeatureKeys)}
|
||||
}
|
||||
features := make([]domain.GameClientBridgeFeatureDeclaration, len(body.Features))
|
||||
for index, feature := range body.Features {
|
||||
@@ -1218,7 +1172,7 @@ func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManif
|
||||
if body.Companion != nil {
|
||||
companion = domain.GameClientBridgeCompanionDeclaration{ProfileKey: body.Companion.ProfileKey, ConfigTemplateKey: body.Companion.ConfigTemplateKey, ConfigSchemaRef: body.Companion.ConfigSchemaRef, ConfigFormat: body.Companion.ConfigFormat, PlatformBaseURLSource: body.Companion.PlatformBaseURLSource, RegistrationProof: body.Companion.RegistrationProof, ProofMaterialSource: body.Companion.ProofMaterialSource, ProofMaterialEnv: body.Companion.ProofMaterialEnv, SessionMode: body.Companion.SessionMode, TLSPolicy: body.Companion.TLSPolicy, HeartbeatIntervalSeconds: body.Companion.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: body.Companion.CommandPollIntervalSeconds, RequestTimeoutSeconds: body.Companion.RequestTimeoutSeconds}
|
||||
}
|
||||
return domain.GameClientBridgeManifest{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, LogProjections: logProjections, DataPacks: dataPacks, OperationTemplates: operationTemplates, Retention: domain.GameClientBridgeRetention{KeepForSeconds: body.CommandRetentionSeconds, MaxRecords: body.MaxCommands}, Pages: pages, Features: features, Companion: companion}
|
||||
return domain.GameClientBridgeManifest{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, LogProjections: logProjections, DataPacks: dataPacks, Retention: domain.GameClientBridgeRetention{KeepForSeconds: body.CommandRetentionSeconds, MaxRecords: body.MaxCommands}, Pages: pages, Features: features, Companion: companion}
|
||||
}
|
||||
|
||||
func gameClientBridgeLogProjectionToDomain(value GameClientBridgeLogProjectionDeclarationBody) domain.GameClientBridgeLogProjectionDeclaration {
|
||||
@@ -1633,14 +1587,14 @@ func MarketplacePluginFromDomain(plugin domain.PluginMarketplacePlugin) Marketpl
|
||||
|
||||
func productionLifecycleFromDomain(lifecycle domain.GamePluginProductionLifecycle) GamePluginProductionLifecycleBody {
|
||||
lifecycle = domain.CopyGamePluginProductionLifecycle(lifecycle)
|
||||
return GamePluginProductionLifecycleBody{Operations: lifecycle.Operations, DependencyPolicy: lifecycle.DependencyPolicy, ApprovalRequired: lifecycle.ApprovalRequired}
|
||||
return GamePluginProductionLifecycleBody{Operations: lifecycle.Operations, DependencyPolicy: lifecycle.DependencyPolicy}
|
||||
}
|
||||
|
||||
func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) GameClientBridgeManifestBody {
|
||||
value = domain.CopyGameClientBridgeManifest(value)
|
||||
commands := make([]GameClientBridgeCommandDeclarationBody, len(value.Commands))
|
||||
for index, command := range value.Commands {
|
||||
commands[index] = GameClientBridgeCommandDeclarationBody{Type: command.Type, Title: command.Title, Permission: command.Permission, ApprovalLevel: string(command.ApprovalLevel), PayloadSchemaRef: command.PayloadSchemaRef, ResultSchemaRef: command.ResultSchemaRef, TimeoutSeconds: command.TimeoutSeconds, MaxPayloadBytes: command.MaxPayloadBytes}
|
||||
commands[index] = GameClientBridgeCommandDeclarationBody{Type: command.Type, Title: command.Title, Permission: command.Permission, PayloadSchemaRef: command.PayloadSchemaRef, ResultSchemaRef: command.ResultSchemaRef, TimeoutSeconds: command.TimeoutSeconds, MaxPayloadBytes: command.MaxPayloadBytes}
|
||||
}
|
||||
snapshots := make([]GameClientBridgeSnapshotDeclarationBody, len(value.Snapshots))
|
||||
for index, snapshot := range value.Snapshots {
|
||||
@@ -1662,13 +1616,9 @@ func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) G
|
||||
for index, dataPack := range value.DataPacks {
|
||||
dataPacks[index] = GameClientBridgeDataPackDeclarationBody{Key: dataPack.Key, DatabaseUserVersion: dataPack.DatabaseUserVersion, LogParserRefs: domain.CopyStringSlice(dataPack.LogParserRefs), ConfigMapRefs: domain.CopyStringSlice(dataPack.ConfigMapRefs), DataRefs: domain.CopyStringSlice(dataPack.DataRefs)}
|
||||
}
|
||||
operationTemplates := make([]GameClientBridgeOperationTemplateDeclarationBody, len(value.OperationTemplates))
|
||||
for index, template := range value.OperationTemplates {
|
||||
operationTemplates[index] = GameClientBridgeOperationTemplateDeclarationBody{Key: template.Key, Title: template.Title, Permission: template.Permission, ApprovalLevel: string(template.ApprovalLevel), Kind: string(template.Kind), TransportKey: template.TransportKey, TargetKey: template.TargetKey, PayloadSchemaRef: template.PayloadSchemaRef, ResultSchemaRef: template.ResultSchemaRef, ConfirmationSchemaRef: template.ConfirmationSchemaRef, TimeoutSeconds: template.TimeoutSeconds, MaxPayloadBytes: template.MaxPayloadBytes, MaxRowsAffected: template.MaxRowsAffected, Mutation: GameClientBridgeOperationMutationDeclarationBody{FieldKey: template.Mutation.FieldKey, TableKey: template.Mutation.TableKey, IdentityKey: template.Mutation.IdentityKey, ValueKey: template.Mutation.ValueKey, ConfirmationQueryKey: template.Mutation.ConfirmationQueryKey, AllowedValueType: template.Mutation.AllowedValueType, MinValue: template.Mutation.MinValue, MaxValue: template.Mutation.MaxValue}, Safety: GameClientBridgeOperationSafetyBody{RequiresApproval: template.Safety.RequiresApproval, RequiresOfflinePlayer: template.Safety.RequiresOfflinePlayer, RequiresMaintenanceWindow: template.Safety.RequiresMaintenanceWindow, RequiresBeforeValue: template.Safety.RequiresBeforeValue, RequiresConfirmation: template.Safety.RequiresConfirmation, BackupRequired: template.Safety.BackupRequired}}
|
||||
}
|
||||
pages := make([]GameClientBridgePageContractBody, len(value.Pages))
|
||||
for index, page := range value.Pages {
|
||||
pages[index] = GameClientBridgePageContractBody{PageKey: page.PageKey, CommandTypes: page.CommandTypes, SnapshotTypes: page.SnapshotTypes, QueryTemplateKeys: page.QueryTemplateKeys, OperationKeys: page.OperationKeys, FeatureKeys: page.FeatureKeys}
|
||||
pages[index] = GameClientBridgePageContractBody{PageKey: page.PageKey, CommandTypes: page.CommandTypes, SnapshotTypes: page.SnapshotTypes, QueryTemplateKeys: page.QueryTemplateKeys, FeatureKeys: page.FeatureKeys}
|
||||
}
|
||||
features := make([]GameClientBridgeFeatureDeclarationBody, len(value.Features))
|
||||
for index, feature := range value.Features {
|
||||
@@ -1678,7 +1628,7 @@ func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) G
|
||||
if value.Companion.ProfileKey != "" {
|
||||
companion = &GameClientBridgeCompanionDeclarationBody{ProfileKey: value.Companion.ProfileKey, ConfigTemplateKey: value.Companion.ConfigTemplateKey, ConfigSchemaRef: value.Companion.ConfigSchemaRef, ConfigFormat: value.Companion.ConfigFormat, PlatformBaseURLSource: value.Companion.PlatformBaseURLSource, RegistrationProof: value.Companion.RegistrationProof, ProofMaterialSource: value.Companion.ProofMaterialSource, ProofMaterialEnv: value.Companion.ProofMaterialEnv, SessionMode: value.Companion.SessionMode, TLSPolicy: value.Companion.TLSPolicy, HeartbeatIntervalSeconds: value.Companion.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: value.Companion.CommandPollIntervalSeconds, RequestTimeoutSeconds: value.Companion.RequestTimeoutSeconds}
|
||||
}
|
||||
return GameClientBridgeManifestBody{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, LogProjections: logProjections, DataPacks: dataPacks, OperationTemplates: operationTemplates, CommandRetentionSeconds: value.Retention.KeepForSeconds, MaxCommands: value.Retention.MaxRecords, Pages: pages, Features: features, Companion: companion}
|
||||
return GameClientBridgeManifestBody{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, LogProjections: logProjections, DataPacks: dataPacks, CommandRetentionSeconds: value.Retention.KeepForSeconds, MaxCommands: value.Retention.MaxRecords, Pages: pages, Features: features, Companion: companion}
|
||||
}
|
||||
|
||||
func gameClientBridgeLogProjectionFromDomain(value domain.GameClientBridgeLogProjectionDeclaration) GameClientBridgeLogProjectionDeclarationBody {
|
||||
|
||||
@@ -3,7 +3,6 @@ package dto
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
@@ -204,40 +203,3 @@ func TestGameClientBridgeQueryTemplateDeclarationRoundTripIsSafe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameClientBridgeOperationTemplateDeclarationRoundTripIsSafe(t *testing.T) {
|
||||
body := GameClientBridgeManifestBody{
|
||||
OperationTemplates: []GameClientBridgeOperationTemplateDeclarationBody{{
|
||||
Key: "player.attribute.855.set", Title: "Set player attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: "platform-admin", Kind: "sqlite-mutation", TransportKey: "scum-mutation-db", TargetKey: "scum-mutation-db",
|
||||
PayloadSchemaRef: "schemas/bridge/operations/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/operations/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/operations/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1,
|
||||
Safety: GameClientBridgeOperationSafetyBody{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true},
|
||||
}},
|
||||
CommandRetentionSeconds: 86400,
|
||||
MaxCommands: 1000,
|
||||
Pages: []GameClientBridgePageContractBody{{PageKey: "players", OperationKeys: []string{"player.attribute.855.set"}}},
|
||||
}
|
||||
|
||||
domainManifest := body.ToDomain()
|
||||
if len(domainManifest.OperationTemplates) != 1 || domainManifest.OperationTemplates[0].Kind != "sqlite-mutation" || domainManifest.OperationTemplates[0].MaxRowsAffected != 1 || !domainManifest.OperationTemplates[0].Safety.RequiresBeforeValue || domainManifest.Pages[0].OperationKeys[0] != "player.attribute.855.set" {
|
||||
t.Fatalf("operation template conversion lost declaration fields: %#v", domainManifest)
|
||||
}
|
||||
domainManifest.Pages[0].OperationKeys[0] = "mutated"
|
||||
if body.Pages[0].OperationKeys[0] != "player.attribute.855.set" {
|
||||
t.Fatal("operation template page keys alias request DTO data")
|
||||
}
|
||||
domainManifest.Pages[0].OperationKeys[0] = "player.attribute.855.set"
|
||||
|
||||
response := gameClientBridgeManifestFromDomain(domainManifest)
|
||||
response.Pages[0].OperationKeys[0] = "mutated"
|
||||
if domainManifest.Pages[0].OperationKeys[0] != "player.attribute.855.set" {
|
||||
t.Fatal("operation template page keys alias domain data")
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(response.OperationTemplates[0])
|
||||
if err != nil {
|
||||
t.Fatalf("marshal safe operation template projection: %v", err)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(string(encoded)), "sqltext") || strings.Contains(strings.ToLower(string(encoded)), "dsn") || strings.Contains(strings.ToLower(string(encoded)), "hostpath") || strings.Contains(strings.ToLower(string(encoded)), "socket") || strings.Contains(strings.ToLower(string(encoded)), "credential") {
|
||||
t.Fatalf("operation template projection leaked unsafe material: %s", encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ func TestGamePluginModelRoundTripCopiesSlices(t *testing.T) {
|
||||
ProductionLifecycle: domain.GamePluginProductionLifecycle{
|
||||
Operations: []string{"install", "upgrade", "rollback"},
|
||||
DependencyPolicy: "required",
|
||||
ApprovalRequired: []string{"rollback"},
|
||||
},
|
||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}}},
|
||||
Permissions: domain.PluginPermissions{
|
||||
@@ -69,7 +68,6 @@ func TestGamePluginModelRoundTripCopiesSlices(t *testing.T) {
|
||||
roundTrip.Tags[0] = "mutated"
|
||||
roundTrip.AIPurposes[0] = "config.suggest"
|
||||
roundTrip.ProductionLifecycle.Operations[0] = "retire"
|
||||
roundTrip.ProductionLifecycle.ApprovalRequired[0] = "disable"
|
||||
roundTrip.RuntimeProfiles.LifecycleProfiles[0].Capabilities[0] = "process.stop"
|
||||
|
||||
if source.RequiredRunCapabilities[0] != "process.start" {
|
||||
@@ -84,7 +82,7 @@ func TestGamePluginModelRoundTripCopiesSlices(t *testing.T) {
|
||||
if row.DeclaredPermissions[0] != "server.logs.read" || row.Pages[0].Permissions[0] != "server.logs.read" || row.Tags[0] != "survival" || row.AIPurposes[0] != "logs.diagnose" {
|
||||
t.Fatalf("expected model plugin registry metadata to remain unchanged, got %+v", row)
|
||||
}
|
||||
if source.ProductionLifecycle.Operations[0] != "install" || row.ProductionLifecycle.Operations[0] != "install" || source.ProductionLifecycle.ApprovalRequired[0] != "rollback" || row.ProductionLifecycle.ApprovalRequired[0] != "rollback" {
|
||||
if source.ProductionLifecycle.Operations[0] != "install" || row.ProductionLifecycle.Operations[0] != "install" {
|
||||
t.Fatalf("expected production lifecycle declaration to round-trip without aliasing, source=%+v row=%+v", source.ProductionLifecycle, row.ProductionLifecycle)
|
||||
}
|
||||
if source.RuntimeProfiles.LifecycleProfiles[0].Capabilities[0] != "process.start" || row.RuntimeProfiles.LifecycleProfiles[0].Capabilities[0] != "process.start" {
|
||||
|
||||
@@ -284,16 +284,6 @@ func (svc *CoreService) queueGameClientBridgeCommand(requesterID string, request
|
||||
if !request.ExpiresAt.After(stamp) {
|
||||
return domain.GameClientBridgeCommand{}, validationError("bridge command expiresAt must be in the future")
|
||||
}
|
||||
approvalState := domain.GameClientBridgeApprovalNotRequired
|
||||
if declaration.ApprovalLevel == domain.GameClientBridgeApprovalLevelOperator {
|
||||
approvalState = domain.GameClientBridgeApprovalApproved
|
||||
}
|
||||
if declaration.ApprovalLevel == domain.GameClientBridgeApprovalLevelPlatformAdmin {
|
||||
approvalState = domain.GameClientBridgeApprovalPending
|
||||
if requester, requesterErr := svc.store.Users().Get(requesterID); requesterErr == nil && isPlatformAdmin(requester) {
|
||||
approvalState = domain.GameClientBridgeApprovalApproved
|
||||
}
|
||||
}
|
||||
svc.bridgeSeq++
|
||||
command := domain.GameClientBridgeCommand{
|
||||
ID: fmt.Sprintf("bridge-command-%d-%d", stamp.UnixNano(), svc.bridgeSeq),
|
||||
@@ -305,7 +295,6 @@ func (svc *CoreService) queueGameClientBridgeCommand(requesterID string, request
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Priority: request.Priority,
|
||||
State: domain.GameClientBridgeCommandPending,
|
||||
ApprovalState: approvalState,
|
||||
RequesterID: requesterID,
|
||||
ExpiresAt: request.ExpiresAt,
|
||||
CreatedAt: stamp,
|
||||
@@ -351,9 +340,6 @@ func (svc *CoreService) claimGameClientBridgeCommands(component gameClientBridge
|
||||
if command.RunJobID != "" {
|
||||
continue
|
||||
}
|
||||
if command.ApprovalState != domain.GameClientBridgeApprovalNotRequired && command.ApprovalState != domain.GameClientBridgeApprovalApproved {
|
||||
continue
|
||||
}
|
||||
fencingToken := command.Claim.FencingToken + 1
|
||||
command.State = domain.GameClientBridgeCommandClaimed
|
||||
command.Claim = domain.GameClientBridgeClaim{SessionID: component.Session.ID, InstallationID: component.Installation.ID, DeploymentGeneration: component.Session.DeploymentGeneration, FencingToken: fencingToken, LeaseExpiresAt: gameClientBridgeClaimLeaseExpiry(stamp, command.ExpiresAt), ClaimedAt: stamp}
|
||||
|
||||
@@ -12,7 +12,7 @@ func newGameClientBridgeService(t *testing.T) (*CoreService, *time.Time) {
|
||||
t.Helper()
|
||||
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
||||
store := repo.NewMemoryStore()
|
||||
plugin := domain.GamePlugin{ID: "game.scum", RuntimeProfiles: domain.GamePluginRuntimeProfiles{ClientManagers: []domain.RuntimeClientManagerProfile{{Key: "scum-client", Health: domain.RuntimeClientManagerHealth{RequiredCapabilities: []string{gameClientBridgeCapability}}}}}, GameClientBridge: domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", ApprovalLevel: domain.GameClientBridgeApprovalLevelNone, TimeoutSeconds: 600, MaxPayloadBytes: 4096}}, Snapshots: []domain.GameClientBridgeSnapshotDeclaration{{Type: "players", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}, {Type: "health", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 60}}, {Type: "companion.health", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}}
|
||||
plugin := domain.GamePlugin{ID: "game.scum", RuntimeProfiles: domain.GamePluginRuntimeProfiles{ClientManagers: []domain.RuntimeClientManagerProfile{{Key: "scum-client", Health: domain.RuntimeClientManagerHealth{RequiredCapabilities: []string{gameClientBridgeCapability}}}}}, GameClientBridge: domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", TimeoutSeconds: 600, MaxPayloadBytes: 4096}}, Snapshots: []domain.GameClientBridgeSnapshotDeclaration{{Type: "players", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}, {Type: "health", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 60}}, {Type: "companion.health", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}}
|
||||
if err := store.GamePlugins().Create(plugin); err != nil {
|
||||
t.Fatalf("seed bridge plugin: %v", err)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestPluginLifecycleBridgeDispatchesBoundedJob(t *testing.T) {
|
||||
response, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
|
||||
RequestID: "bridge-plugin-install", PluginID: plugin.ID, RouteKey: "operations",
|
||||
ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionPluginLifecycle,
|
||||
Payload: map[string]string{"operation": "install", "targetVersion": plugin.Version, "idempotencyKey": "bridge-plugin-install-v1", "confirmed": "false"},
|
||||
Payload: map[string]string{"operation": "install", "targetVersion": plugin.Version, "idempotencyKey": "bridge-plugin-install-v1"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute lifecycle bridge: %v", err)
|
||||
|
||||
@@ -981,13 +981,7 @@ func (svc *CoreService) executeBridgeAIInvoke(sessionID string, base domain.Plug
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgePluginLifecycle(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
|
||||
confirmed, err := strconv.ParseBool(defaultBridgeValue(request.Payload["confirmed"], "false"))
|
||||
if err != nil {
|
||||
base.Status = "error"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "confirmed must be true or false"}
|
||||
return base
|
||||
}
|
||||
result, err := svc.RunPluginLifecycleForSession(sessionID, domain.PluginLifecycleRequest{PluginID: request.PluginID, ServerInstanceID: request.ServerInstanceID, Operation: domain.PluginLifecycleOperation(request.Payload["operation"]), TargetVersion: request.Payload["targetVersion"], IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID), Confirmed: confirmed})
|
||||
result, err := svc.RunPluginLifecycleForSession(sessionID, domain.PluginLifecycleRequest{PluginID: request.PluginID, ServerInstanceID: request.ServerInstanceID, Operation: domain.PluginLifecycleOperation(request.Payload["operation"]), TargetVersion: request.Payload["targetVersion"], IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID)})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
@@ -1579,9 +1573,6 @@ func normalizedProductionLifecycle(lifecycle domain.GamePluginProductionLifecycl
|
||||
if lifecycle.DependencyPolicy == "" {
|
||||
lifecycle.DependencyPolicy = "optional"
|
||||
}
|
||||
if len(lifecycle.ApprovalRequired) == 0 {
|
||||
lifecycle.ApprovalRequired = []string{"disable", "rollback", "retire"}
|
||||
}
|
||||
return lifecycle
|
||||
}
|
||||
|
||||
|
||||
@@ -1957,7 +1957,7 @@ func validPluginManifestRegistration() domain.GamePluginManifestRegistration {
|
||||
},
|
||||
},
|
||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"},
|
||||
ProductionLifecycle: domain.GamePluginProductionLifecycle{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional", ApprovalRequired: []string{"disable", "rollback", "retire"}},
|
||||
ProductionLifecycle: domain.GamePluginProductionLifecycle{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional"},
|
||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,9 +36,6 @@ func ValidatePluginLifecycleRequest(request domain.PluginLifecycleRequest) error
|
||||
if !validPluginLifecycleOperation(request.Operation) {
|
||||
violations = append(violations, "operation is invalid")
|
||||
}
|
||||
if (request.Operation == domain.PluginLifecycleOperationDisable || request.Operation == domain.PluginLifecycleOperationRollback || request.Operation == domain.PluginLifecycleOperationRetire) && !request.Confirmed {
|
||||
violations = append(violations, "confirmed is required for disruptive plugin lifecycle operation")
|
||||
}
|
||||
for _, value := range []fieldString{{field: "targetVersion", value: request.TargetVersion}, {field: "idempotencyKey", value: request.IdempotencyKey}} {
|
||||
if unsafeProductionText(value.value) {
|
||||
violations = append(violations, value.field+" is unsafe")
|
||||
|
||||
@@ -434,7 +434,7 @@ func ValidatePluginCreateInputs(fields []domain.PluginCreateField, inputs map[st
|
||||
|
||||
func validateGameClientBridgeManifest(field string, bridge domain.GameClientBridgeManifest, permissions []string, pages []domain.GamePluginPage, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
|
||||
companionPresent := bridge.Companion != (domain.GameClientBridgeCompanionDeclaration{})
|
||||
if len(bridge.Commands) == 0 && len(bridge.Snapshots) == 0 && len(bridge.QueryTemplates) == 0 && len(bridge.LogProjections) == 0 && len(bridge.DataPacks) == 0 && len(bridge.OperationTemplates) == 0 && len(bridge.Pages) == 0 && len(bridge.Features) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent {
|
||||
if len(bridge.Commands) == 0 && len(bridge.Snapshots) == 0 && len(bridge.QueryTemplates) == 0 && len(bridge.LogProjections) == 0 && len(bridge.DataPacks) == 0 && len(bridge.Pages) == 0 && len(bridge.Features) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent {
|
||||
return nil
|
||||
}
|
||||
var violations []string
|
||||
@@ -513,9 +513,6 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
if !containsString(permissions, command.Permission) {
|
||||
violations = append(violations, prefix+".permission must be declared by the plugin")
|
||||
}
|
||||
if command.ApprovalLevel != domain.GameClientBridgeApprovalLevelNone && command.ApprovalLevel != domain.GameClientBridgeApprovalLevelOperator && command.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin {
|
||||
violations = append(violations, prefix+".approvalLevel is invalid")
|
||||
}
|
||||
if !safeRelativeJSONRef(command.PayloadSchemaRef) || command.ResultSchemaRef != "" && !safeRelativeJSONRef(command.ResultSchemaRef) {
|
||||
violations = append(violations, prefix+" schema references must be safe relative JSON references")
|
||||
}
|
||||
@@ -647,72 +644,6 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
}
|
||||
}
|
||||
}
|
||||
operationTemplates := map[string]domain.GameClientBridgeOperationTemplateDeclaration{}
|
||||
for index, template := range bridge.OperationTemplates {
|
||||
prefix := fmt.Sprintf("%s.operationTemplates[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(template.Key) || unsafeGameClientBridgeCommandType(template.Key) {
|
||||
violations = append(violations, prefix+".key is invalid or unsafe")
|
||||
}
|
||||
if _, exists := operationTemplates[template.Key]; exists {
|
||||
violations = append(violations, prefix+".key is duplicated")
|
||||
}
|
||||
operationTemplates[template.Key] = template
|
||||
if strings.TrimSpace(template.Title) == "" || len([]rune(template.Title)) > 80 {
|
||||
violations = append(violations, prefix+".title is invalid")
|
||||
}
|
||||
if !containsString(permissions, template.Permission) {
|
||||
violations = append(violations, prefix+".permission must be declared by the plugin")
|
||||
}
|
||||
if template.ApprovalLevel != domain.GameClientBridgeApprovalLevelNone && template.ApprovalLevel != domain.GameClientBridgeApprovalLevelOperator && template.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin {
|
||||
violations = append(violations, prefix+".approvalLevel is invalid")
|
||||
}
|
||||
if template.Kind != domain.GameClientBridgeOperationKindRCON && template.Kind != domain.GameClientBridgeOperationKindSQLiteMutation {
|
||||
violations = append(violations, prefix+".kind is invalid")
|
||||
}
|
||||
if !safeRelativeJSONRef(template.PayloadSchemaRef) || template.ResultSchemaRef != "" && !safeRelativeJSONRef(template.ResultSchemaRef) || template.ConfirmationSchemaRef != "" && !safeRelativeJSONRef(template.ConfirmationSchemaRef) {
|
||||
violations = append(violations, prefix+" schema references must be safe relative JSON references")
|
||||
}
|
||||
if template.TimeoutSeconds < 1 || template.TimeoutSeconds > 3600 {
|
||||
violations = append(violations, prefix+".timeoutSeconds is invalid")
|
||||
}
|
||||
if template.MaxPayloadBytes < 1 || template.MaxPayloadBytes > maxGameClientBridgePayloadSize {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is invalid")
|
||||
}
|
||||
transport, exists := transports[template.TransportKey]
|
||||
if !exists {
|
||||
violations = append(violations, prefix+".transportKey must reference a declared runtime transport profile")
|
||||
continue
|
||||
}
|
||||
if transport.TargetKey != template.TargetKey || strings.TrimSpace(template.TargetKey) == "" {
|
||||
violations = append(violations, prefix+".targetKey must match the declared runtime transport profile")
|
||||
}
|
||||
switch template.Kind {
|
||||
case domain.GameClientBridgeOperationKindRCON:
|
||||
if transport.Kind != "rcon" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunRCONCommand) {
|
||||
violations = append(violations, prefix+" transport must be rcon with remote.run.rcon.command capability")
|
||||
}
|
||||
if template.MaxRowsAffected != 0 {
|
||||
violations = append(violations, prefix+".maxRowsAffected is only valid for sqlite-mutation")
|
||||
}
|
||||
if !emptyGameClientBridgeOperationMutation(template.Mutation) {
|
||||
violations = append(violations, prefix+".mutation is only valid for sqlite-mutation")
|
||||
}
|
||||
case domain.GameClientBridgeOperationKindSQLiteMutation:
|
||||
if transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunProtectedSQL) {
|
||||
violations = append(violations, prefix+" transport must be sqlite with remote.run.protected.sql capability")
|
||||
}
|
||||
if template.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin {
|
||||
violations = append(violations, prefix+".approvalLevel must require platform-admin approval for sqlite-mutation")
|
||||
}
|
||||
if template.MaxRowsAffected < 1 || template.MaxRowsAffected > 10 {
|
||||
violations = append(violations, prefix+".maxRowsAffected is invalid")
|
||||
}
|
||||
if !template.Safety.RequiresBeforeValue || !template.Safety.RequiresConfirmation || (!template.Safety.RequiresOfflinePlayer && !template.Safety.RequiresMaintenanceWindow) {
|
||||
violations = append(violations, prefix+".safety must require before value, confirmation, and offline or maintenance protection")
|
||||
}
|
||||
violations = append(violations, validateGameClientBridgeOperationMutation(prefix+".mutation", template.Mutation, queryTemplates)...)
|
||||
}
|
||||
}
|
||||
pageDeclarations := map[string]domain.GamePluginPage{}
|
||||
for _, page := range pages {
|
||||
pageDeclarations[page.Key] = page
|
||||
@@ -797,16 +728,6 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
violations = append(violations, prefix+" must declare remote.access.request for query templates")
|
||||
}
|
||||
}
|
||||
for _, operationKey := range page.OperationKeys {
|
||||
operation, exists := operationTemplates[operationKey]
|
||||
if !exists {
|
||||
violations = append(violations, prefix+" references undeclared operation template "+operationKey)
|
||||
continue
|
||||
}
|
||||
if !containsString(pageDeclaration.Permissions, operation.Permission) {
|
||||
violations = append(violations, prefix+" must declare operation template permission "+operation.Permission)
|
||||
}
|
||||
}
|
||||
for _, featureKey := range page.FeatureKeys {
|
||||
feature, exists := features[featureKey]
|
||||
if !exists {
|
||||
@@ -994,31 +915,6 @@ func unsafeGameClientBridgeCommandType(value string) bool {
|
||||
return has("shell", "powershell", "script", "terminal", "execute", "exec", "eval") || has("command", "cmd", "process", "system", "os", "executor") && has("run")
|
||||
}
|
||||
|
||||
func emptyGameClientBridgeOperationMutation(value domain.GameClientBridgeOperationMutationDeclaration) bool {
|
||||
return value.FieldKey == "" && value.TableKey == "" && value.IdentityKey == "" && value.ValueKey == "" && value.ConfirmationQueryKey == "" && value.AllowedValueType == "" && value.MinValue == 0 && value.MaxValue == 0
|
||||
}
|
||||
|
||||
func validateGameClientBridgeOperationMutation(prefix string, value domain.GameClientBridgeOperationMutationDeclaration, queryTemplates map[string]domain.GameClientBridgeQueryTemplateDeclaration) []string {
|
||||
var violations []string
|
||||
for field, item := range map[string]string{"fieldKey": value.FieldKey, "tableKey": value.TableKey, "identityKey": value.IdentityKey, "valueKey": value.ValueKey, "confirmationQueryKey": value.ConfirmationQueryKey} {
|
||||
if !validDistributionLogicalKey(item) || unsafeGameClientBridgePayloadKey(item) {
|
||||
violations = append(violations, prefix+"."+field+" must be a safe logical key")
|
||||
}
|
||||
}
|
||||
if !oneOf(value.AllowedValueType, "integer", "number", "string", "boolean") {
|
||||
violations = append(violations, prefix+".allowedValueType is invalid")
|
||||
}
|
||||
if value.MaxValue != 0 && value.MinValue > value.MaxValue {
|
||||
violations = append(violations, prefix+".minValue must not exceed maxValue")
|
||||
}
|
||||
if value.ConfirmationQueryKey != "" {
|
||||
if _, exists := queryTemplates[value.ConfirmationQueryKey]; !exists {
|
||||
violations = append(violations, prefix+".confirmationQueryKey must reference a declared query template")
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func ValidatePluginBridgeAuthorizeRequest(request domain.PluginBridgeAuthorizeRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "pluginId", request.PluginID)
|
||||
@@ -2078,7 +1974,6 @@ func pluginSafeStrings(plugin domain.GamePlugin) []fieldString {
|
||||
values = appendStringSliceFields(values, "tags", plugin.Tags)
|
||||
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
|
||||
values = appendStringSliceFields(values, "productionLifecycle.operations", plugin.ProductionLifecycle.Operations)
|
||||
values = appendStringSliceFields(values, "productionLifecycle.approvalRequired", plugin.ProductionLifecycle.ApprovalRequired)
|
||||
values = append(values, fieldString{field: "productionLifecycle.dependencyPolicy", value: plugin.ProductionLifecycle.DependencyPolicy})
|
||||
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
|
||||
values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods)
|
||||
@@ -2125,7 +2020,6 @@ func manifestSafeStrings(registration domain.GamePluginManifestRegistration) []f
|
||||
values = appendStringSliceFields(values, "ai.purposes", manifest.AI.Purposes)
|
||||
values = append(values, fieldString{field: "ai.mediation", value: manifest.AI.Mediation}, fieldString{field: "ai.configWritePolicy", value: manifest.AI.ConfigWritePolicy}, fieldString{field: "productionLifecycle.dependencyPolicy", value: manifest.ProductionLifecycle.DependencyPolicy})
|
||||
values = appendStringSliceFields(values, "productionLifecycle.operations", manifest.ProductionLifecycle.Operations)
|
||||
values = appendStringSliceFields(values, "productionLifecycle.approvalRequired", manifest.ProductionLifecycle.ApprovalRequired)
|
||||
values = appendStringSliceFields(values, "remoteAccess.methods", manifest.RemoteAccess.Methods)
|
||||
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", manifest.RemoteAccess.RunCapabilities)
|
||||
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", manifest.RemoteAccess.DatabaseEngines)
|
||||
@@ -2339,7 +2233,7 @@ func validPluginRunCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||
domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProgram,
|
||||
domain.JobCapabilityRemoteRunProgram,
|
||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||
domain.JobCapabilityDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd,
|
||||
domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate,
|
||||
@@ -2372,7 +2266,7 @@ func remoteCapabilityRequiresInputRef(capability string) bool {
|
||||
domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery,
|
||||
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProgram:
|
||||
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProgram:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -2516,16 +2410,6 @@ func validateProductionLifecycle(field string, lifecycle domain.GamePluginProduc
|
||||
if lifecycle.DependencyPolicy != "required" && lifecycle.DependencyPolicy != "optional" {
|
||||
violations = append(violations, field+".dependencyPolicy must be required or optional")
|
||||
}
|
||||
for i, operation := range lifecycle.ApprovalRequired {
|
||||
if operation != string(domain.PluginLifecycleOperationDisable) && operation != string(domain.PluginLifecycleOperationRollback) && operation != string(domain.PluginLifecycleOperationRetire) {
|
||||
violations = append(violations, fmt.Sprintf("%s.approvalRequired[%d] is invalid", field, i))
|
||||
}
|
||||
}
|
||||
for _, operation := range []string{string(domain.PluginLifecycleOperationDisable), string(domain.PluginLifecycleOperationRollback), string(domain.PluginLifecycleOperationRetire)} {
|
||||
if containsString(lifecycle.Operations, operation) && !containsString(lifecycle.ApprovalRequired, operation) {
|
||||
violations = append(violations, field+".approvalRequired must include "+operation)
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
|
||||
@@ -130,24 +130,19 @@ func TestValidateGamePluginManifestRegistrationValidatesRuntimeProfiles(t *testi
|
||||
func TestValidateGamePluginManifestRegistrationValidatesGameClientBridgeCatalog(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
registration.Manifest.Permissions = append(registration.Manifest.Permissions, "server.game-client.read", "server.game-client.command", "server.game-client.maintenance", "server.remote.access")
|
||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL)
|
||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunRCONCommand)
|
||||
registration.Manifest.Pages[0].Permissions = append(registration.Manifest.Pages[0].Permissions, "server.game-client.read", "server.game-client.command", "server.game-client.maintenance", "server.remote.access")
|
||||
registration.Manifest.Pages[0].BridgeActions = append(registration.Manifest.Pages[0].BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest))
|
||||
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{
|
||||
{Key: "sqlite-db", Kind: "sqlite", TargetKey: "db/sqlite", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}},
|
||||
{Key: "scum-rcon", Kind: "rcon", TargetKey: "scum-rcon", Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}},
|
||||
{Key: "scum-mutation-db", Kind: "sqlite", TargetKey: "scum-mutation-db", Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}},
|
||||
}
|
||||
registration.Manifest.GameClientBridge = domain.GameClientBridgeManifest{
|
||||
Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", Title: "Diagnostic ping", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelNone, PayloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", ResultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 4096}},
|
||||
Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", Title: "Diagnostic ping", Permission: "server.game-client.command", PayloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", ResultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 4096}},
|
||||
Snapshots: []domain.GameClientBridgeSnapshotDeclaration{{Type: "players", SchemaVersion: "1", SchemaRef: "schemas/bridge/players.schema.json", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}},
|
||||
QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{{Key: "player.lookup", Title: "Player lookup", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "sqlite-db", TargetKey: "db/sqlite", ParameterSchemaRef: "schemas/bridge/query/player-lookup.parameters.schema.json", ResultSchemaRef: "schemas/bridge/query/player-lookup.result.schema.json", MaxRows: 50, TimeoutSeconds: 10}},
|
||||
OperationTemplates: []domain.GameClientBridgeOperationTemplateDeclaration{
|
||||
{Key: "player.fame.set", Title: "Set player fame", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelNone, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "scum-rcon", TargetKey: "scum-rcon", PayloadSchemaRef: "schemas/bridge/operations/player-fame-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/operations/player-fame-set.result.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048},
|
||||
{Key: "player.attribute.855.set", Title: "Set player attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindSQLiteMutation, TransportKey: "scum-mutation-db", TargetKey: "scum-mutation-db", PayloadSchemaRef: "schemas/bridge/operations/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/operations/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/operations/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1, Mutation: domain.GameClientBridgeOperationMutationDeclaration{FieldKey: "855", TableKey: "prisoner", IdentityKey: "user_profile_id", ValueKey: "value", ConfirmationQueryKey: "player.lookup", AllowedValueType: "integer", MinValue: 0, MaxValue: 100000}, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true}},
|
||||
},
|
||||
Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000},
|
||||
Pages: []domain.GameClientBridgePageContract{{PageKey: "logs", CommandTypes: []string{"diagnostic.ping"}, SnapshotTypes: []string{"players"}, QueryTemplateKeys: []string{"player.lookup"}, OperationKeys: []string{"player.fame.set", "player.attribute.855.set"}}},
|
||||
Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000},
|
||||
Pages: []domain.GameClientBridgePageContract{{PageKey: "logs", CommandTypes: []string{"diagnostic.ping"}, SnapshotTypes: []string{"players"}, QueryTemplateKeys: []string{"player.lookup"}}},
|
||||
}
|
||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||
t.Fatalf("expected bridge catalog to validate, got %v", err)
|
||||
@@ -168,14 +163,13 @@ func TestValidateGamePluginManifestRegistrationValidatesGameClientBridgeCatalog(
|
||||
unsafe := registration
|
||||
unsafe.Manifest.GameClientBridge = domain.CopyGameClientBridgeManifest(registration.Manifest.GameClientBridge)
|
||||
unsafe.Manifest.GameClientBridge.Commands[0].Type = "shell.execute"
|
||||
unsafe.Manifest.GameClientBridge.Commands[0].ApprovalLevel = ""
|
||||
unsafe.Manifest.GameClientBridge.Commands[0].PayloadSchemaRef = "/etc/command.json"
|
||||
unsafe.Manifest.GameClientBridge.Pages[0].CommandTypes = []string{"undeclared.command"}
|
||||
err := ValidateGamePluginManifestRegistration(unsafe)
|
||||
if err == nil {
|
||||
t.Fatal("expected unsafe bridge catalog rejection")
|
||||
}
|
||||
for _, expected := range []string{"type is invalid or unsafe", "approvalLevel is invalid", "schema references", "undeclared command"} {
|
||||
for _, expected := range []string{"type is invalid or unsafe", "schema references", "undeclared command"} {
|
||||
if !strings.Contains(err.Error(), expected) {
|
||||
t.Fatalf("expected %q in validation error: %v", expected, err)
|
||||
}
|
||||
@@ -241,54 +235,6 @@ func TestValidateGamePluginManifestRegistrationValidatesGameClientBridgeCatalog(
|
||||
})
|
||||
}
|
||||
|
||||
operationTemplateTests := []struct {
|
||||
name string
|
||||
expected string
|
||||
mutate func(*domain.GamePluginManifestRegistration)
|
||||
}{
|
||||
{name: "duplicate key", expected: "key is duplicated", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.OperationTemplates = append(value.Manifest.GameClientBridge.OperationTemplates, value.Manifest.GameClientBridge.OperationTemplates[0])
|
||||
}},
|
||||
{name: "unsafe key", expected: "key is invalid or unsafe", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.OperationTemplates[0].Key = "raw.sql.execute"
|
||||
}},
|
||||
{name: "mutation missing approval metadata", expected: "approvalLevel must require", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.OperationTemplates[1].ApprovalLevel = domain.GameClientBridgeApprovalLevelNone
|
||||
}},
|
||||
{name: "unsafe schema", expected: "schema references", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.OperationTemplates[0].PayloadSchemaRef = "/etc/operation.json"
|
||||
}},
|
||||
{name: "rcon wrong transport", expected: "transport must be rcon", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.OperationTemplates[0].TransportKey = "sqlite-db"
|
||||
value.Manifest.GameClientBridge.OperationTemplates[0].TargetKey = "db/sqlite"
|
||||
}},
|
||||
{name: "mutation wrong transport", expected: "transport must be sqlite", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.OperationTemplates[1].TransportKey = "scum-rcon"
|
||||
value.Manifest.GameClientBridge.OperationTemplates[1].TargetKey = "scum-rcon"
|
||||
}},
|
||||
{name: "mutation row bound", expected: "maxRowsAffected is invalid", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.OperationTemplates[1].MaxRowsAffected = 0
|
||||
}},
|
||||
{name: "mutation missing safety", expected: "safety must require", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.OperationTemplates[1].Safety.RequiresBeforeValue = false
|
||||
}},
|
||||
{name: "undeclared page operation", expected: "undeclared operation template", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.GameClientBridge.Pages[0].OperationKeys = []string{"missing.operation"}
|
||||
}},
|
||||
{name: "page missing operation permission", expected: "must declare operation template permission", mutate: func(value *domain.GamePluginManifestRegistration) {
|
||||
value.Manifest.Pages[0].Permissions = []string{"server.game-client.read", "server.remote.access"}
|
||||
}},
|
||||
}
|
||||
for _, test := range operationTemplateTests {
|
||||
t.Run("operation template "+test.name, func(t *testing.T) {
|
||||
invalid := domain.CopyGamePluginManifestRegistration(registration)
|
||||
test.mutate(&invalid)
|
||||
err := ValidateGamePluginManifestRegistration(invalid)
|
||||
if err == nil || !strings.Contains(err.Error(), test.expected) {
|
||||
t.Fatalf("expected %q rejection, got %v", test.expected, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermissions(t *testing.T) {
|
||||
@@ -452,7 +398,7 @@ func validGamePluginManifestRegistration() domain.GamePluginManifestRegistration
|
||||
{Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}},
|
||||
},
|
||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"},
|
||||
ProductionLifecycle: domain.GamePluginProductionLifecycle{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional", ApprovalRequired: []string{"disable", "rollback", "retire"}},
|
||||
ProductionLifecycle: domain.GamePluginProductionLifecycle{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user